From 2183d741bbed3d94004534d8a5b4ec8b25472366 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Wed, 29 Apr 2020 04:32:36 +0200 Subject: [PATCH] Add global session uuid (fixes #524) --- README.rst | 35 +++++++++++++++++++++++++++++++++++ changelog/524.feature | 2 ++ src/xdist/plugin.py | 22 +++++++++++++++++++++- src/xdist/remote.py | 3 +++ src/xdist/workermanage.py | 5 +++++ testing/acceptance_test.py | 23 +++++++++++++++++++++++ testing/test_newhooks.py | 16 ++++++++++------ testing/test_plugin.py | 12 ++++++++++++ testing/test_remote.py | 3 +++ 9 files changed, 114 insertions(+), 7 deletions(-) create mode 100644 changelog/524.feature diff --git a/README.rst b/README.rst index 25bd148..6167148 100644 --- a/README.rst +++ b/README.rst @@ -276,6 +276,41 @@ defined: The information about the worker_id in a test is stored in the ``TestReport`` as well, under the ``worker_id`` attribute. + +Uniquely identifying the current test run +----------------------------------------- + +*New in version 1.32.* + +If you need to globally distinguish one test run from others in your +workers, you can use the ``testrun_uid`` fixture. For instance, let's say you +wanted to create a separate database for each test run: + +.. code-block:: python + + import pytest + from posix_ipc import Semaphore, O_CREAT + + @pytest.fixture(scope="session", autouse=True) + def create_unique_database(testrun_uid): + """ create a unique database for this particular test run """ + database_url = f"psql://myapp-{testrun_uid}" + + with Semaphore(f"/{testrun_uid}-lock", flags=O_CREAT, initial_value=1): + if not database_exists(database_url): + create_database(database_url) + + @pytest.fixture() + def db(testrun_uid): + """ retrieve unique database """ + database_url = f"psql://myapp-{testrun_uid}" + return database_get_instance(database_url) + + +Additionally, during a test run, the following environment variable is defined: + +* ``PYTEST_XDIST_TESTRUNUID`: the unique id of the test run + Acessing ``sys.argv`` from the master node in workers ----------------------------------------------------- diff --git a/changelog/524.feature b/changelog/524.feature new file mode 100644 index 0000000..90adf05 --- /dev/null +++ b/changelog/524.feature @@ -0,0 +1,2 @@ +Add `testrun_uid` fixture. This is a shared value that uniquely identifies a test run among all workers. +This also adds a `PYTEST_XDIST_TESTRUNUID` environment variable that is accessible within a test as well as a command line option `--testrunuid` to manually set the value from outside. diff --git a/src/xdist/plugin.py b/src/xdist/plugin.py index 4a0488e..5165126 100644 --- a/src/xdist/plugin.py +++ b/src/xdist/plugin.py @@ -1,4 +1,5 @@ import os +import uuid import py import pytest @@ -122,12 +123,22 @@ def pytest_addoption(parser): metavar="GLOB", help="add expression for ignores when rsyncing to remote tx nodes.", ) - group.addoption( "--boxed", action="store_true", help="backward compatibility alias for pytest-forked --forked", ) + group.addoption( + "--testrunuid", + action="store", + help=( + "provide an identifier shared amongst all workers as the value of " + "the 'testrun_uid' fixture,\n\n," + "if not provided, 'testrun_uid' is filled with a new unique string " + "on every test run." + ), + ) + parser.addini( "rsyncdirs", "list of (relative) paths to be rsynced for remote distributed testing.", @@ -214,3 +225,12 @@ def worker_id(request): return request.config.workerinput["workerid"] else: return "master" + + +@pytest.fixture(scope="session") +def testrun_uid(request): + """Return the unique id of the current test.""" + if hasattr(request.config, "workerinput"): + return request.config.workerinput["testrunuid"] + else: + return uuid.uuid4().hex diff --git a/src/xdist/remote.py b/src/xdist/remote.py index 94cebd6..28991c0 100644 --- a/src/xdist/remote.py +++ b/src/xdist/remote.py @@ -22,6 +22,7 @@ class WorkerInteractor(object): def __init__(self, config, channel): self.config = config self.workerid = config.workerinput.get("workerid", "?") + self.testrunuid = config.workerinput["testrunuid"] self.log = py.log.Producer("worker-%s" % self.workerid) if not config.option.debug: py.log.setconsumer(self.log._keywords, None) @@ -112,6 +113,7 @@ class WorkerInteractor(object): ) data["item_index"] = self.item_index data["worker_id"] = self.workerid + data["testrun_uid"] = self.testrunuid assert self.session.items[self.item_index].nodeid == report.nodeid self.sendevent("testreport", data=data) @@ -238,6 +240,7 @@ if __name__ == "__channelexec__": importpath + os.pathsep + os.environ.get("PYTHONPATH", "") ) + os.environ["PYTEST_XDIST_TESTRUNUID"] = workerinput["testrunuid"] os.environ["PYTEST_XDIST_WORKER"] = workerinput["workerid"] os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"]) diff --git a/src/xdist/workermanage.py b/src/xdist/workermanage.py index af5241f..60b95f3 100644 --- a/src/xdist/workermanage.py +++ b/src/xdist/workermanage.py @@ -3,6 +3,7 @@ import fnmatch import os import re import sys +import uuid import py import pytest @@ -35,6 +36,9 @@ class NodeManager(object): def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): self.config = config self.trace = self.config.trace.get("nodemanager") + self.testrunuid = self.config.getoption("testrunuid") + if self.testrunuid is None: + self.testrunuid = uuid.uuid4().hex self.group = execnet.Group() if specs is None: specs = self._getxspecs() @@ -222,6 +226,7 @@ class WorkerController(object): "workercount": len(nodemanager.specs), "slaveid": gateway.id, "slavecount": len(nodemanager.specs), + "testrunuid": nodemanager.testrunuid, "mainargv": sys.argv, } # TODO: deprecated name, backward compatibility only. Remove it in future diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d93f08e..9ab8187 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1066,6 +1066,29 @@ def test_worker_id_fixture(testdir, n): assert worker_ids == {"gw0", "gw1"} +@pytest.mark.parametrize("n", [0, 2]) +def test_testrun_uid_fixture(testdir, n): + import glob + + f = testdir.makepyfile( + """ + import pytest + @pytest.mark.parametrize("run_num", range(2)) + def test_testrun_uid1(testrun_uid, run_num): + with open("testrun_uid%s.txt" % run_num, "w") as f: + f.write(testrun_uid) + """ + ) + result = testdir.runpytest(f, "-n%d" % n) + result.stdout.fnmatch_lines("* 2 passed in *") + testrun_uids = set() + for fname in glob.glob(str(testdir.tmpdir.join("*.txt"))): + with open(fname) as f: + testrun_uids.add(f.read().strip()) + assert len(testrun_uids) == 1 + assert len(testrun_uids.pop()) == 32 + + @pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"]) def test_error_report_styles(testdir, tb): testdir.makepyfile( diff --git a/testing/test_newhooks.py b/testing/test_newhooks.py index 22928fe..741e64f 100644 --- a/testing/test_newhooks.py +++ b/testing/test_newhooks.py @@ -15,7 +15,7 @@ class TestHooks: def test_runtest_logreport(self, testdir): """Test that log reports from pytest_runtest_logreport when running - with xdist contain "node", "nodeid" and "worker_id" attributes. (#8) + with xdist contain "node", "nodeid", "worker_id", and "testrun_uid" attributes. (#8) """ testdir.makeconftest( """ @@ -23,20 +23,24 @@ class TestHooks: if hasattr(report, 'node'): if report.when == "call": workerid = report.node.workerinput['workerid'] + testrunuid = report.node.workerinput['testrunuid'] if workerid != report.worker_id: print("HOOK: Worker id mismatch: %s %s" % (workerid, report.worker_id)) + elif testrunuid != report.testrun_uid: + print("HOOK: Testrun uid mismatch: %s %s" + % (testrunuid, report.testrun_uid)) else: - print("HOOK: %s %s" - % (report.nodeid, report.worker_id)) + print("HOOK: %s %s %s" + % (report.nodeid, report.worker_id, report.testrun_uid)) """ ) res = testdir.runpytest("-n1", "-s") res.stdout.fnmatch_lines( [ - "*HOOK: test_runtest_logreport.py::test_a gw0*", - "*HOOK: test_runtest_logreport.py::test_b gw0*", - "*HOOK: test_runtest_logreport.py::test_c gw0*", + "*HOOK: test_runtest_logreport.py::test_a gw0 *", + "*HOOK: test_runtest_logreport.py::test_b gw0 *", + "*HOOK: test_runtest_logreport.py::test_c gw0 *", "*3 passed*", ] ) diff --git a/testing/test_plugin.py b/testing/test_plugin.py index dc0fc51..ca2cc2c 100644 --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -95,6 +95,18 @@ def test_dsession_with_collect_only(testdir): assert not config.pluginmanager.hasplugin("dsession") +def test_testrunuid_provided(testdir): + config = testdir.parseconfigure("--testrunuid", "test123", "--tx=popen") + nm = NodeManager(config) + assert nm.testrunuid == "test123" + + +def test_testrunuid_generated(testdir): + config = testdir.parseconfigure("--tx=popen") + nm = NodeManager(config) + assert len(nm.testrunuid) == 32 + + class TestDistOptions: def test_getxspecs(self, testdir): config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz") diff --git a/testing/test_remote.py b/testing/test_remote.py index 81d8153..e223a66 100644 --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -2,6 +2,7 @@ import py import pprint import pytest import sys +import uuid from xdist.workermanage import WorkerController import execnet @@ -44,6 +45,7 @@ class WorkerSetup: putevent = self.use_callback and self.events.put or None class DummyMananger: + testrunuid = uuid.uuid4().hex specs = [0, 1] self.slp = WorkerController(DummyMananger, self.gateway, config, putevent) @@ -220,6 +222,7 @@ def test_remote_env_vars(testdir): """ import os def test(): + assert len(os.environ['PYTEST_XDIST_TESTRUNUID']) == 32 assert os.environ['PYTEST_XDIST_WORKER'] in ('gw0', 'gw1') assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2' """