Add global session uuid (fixes #524)
This commit is contained in:
35
README.rst
35
README.rst
@@ -276,6 +276,41 @@ defined:
|
|||||||
The information about the worker_id in a test is stored in the ``TestReport`` as
|
The information about the worker_id in a test is stored in the ``TestReport`` as
|
||||||
well, under the ``worker_id`` attribute.
|
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
|
Acessing ``sys.argv`` from the master node in workers
|
||||||
-----------------------------------------------------
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
2
changelog/524.feature
Normal file
2
changelog/524.feature
Normal file
@@ -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.
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
@@ -122,12 +123,22 @@ def pytest_addoption(parser):
|
|||||||
metavar="GLOB",
|
metavar="GLOB",
|
||||||
help="add expression for ignores when rsyncing to remote tx nodes.",
|
help="add expression for ignores when rsyncing to remote tx nodes.",
|
||||||
)
|
)
|
||||||
|
|
||||||
group.addoption(
|
group.addoption(
|
||||||
"--boxed",
|
"--boxed",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="backward compatibility alias for pytest-forked --forked",
|
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(
|
parser.addini(
|
||||||
"rsyncdirs",
|
"rsyncdirs",
|
||||||
"list of (relative) paths to be rsynced for remote distributed testing.",
|
"list of (relative) paths to be rsynced for remote distributed testing.",
|
||||||
@@ -214,3 +225,12 @@ def worker_id(request):
|
|||||||
return request.config.workerinput["workerid"]
|
return request.config.workerinput["workerid"]
|
||||||
else:
|
else:
|
||||||
return "master"
|
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
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class WorkerInteractor(object):
|
|||||||
def __init__(self, config, channel):
|
def __init__(self, config, channel):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.workerid = config.workerinput.get("workerid", "?")
|
self.workerid = config.workerinput.get("workerid", "?")
|
||||||
|
self.testrunuid = config.workerinput["testrunuid"]
|
||||||
self.log = py.log.Producer("worker-%s" % self.workerid)
|
self.log = py.log.Producer("worker-%s" % self.workerid)
|
||||||
if not config.option.debug:
|
if not config.option.debug:
|
||||||
py.log.setconsumer(self.log._keywords, None)
|
py.log.setconsumer(self.log._keywords, None)
|
||||||
@@ -112,6 +113,7 @@ class WorkerInteractor(object):
|
|||||||
)
|
)
|
||||||
data["item_index"] = self.item_index
|
data["item_index"] = self.item_index
|
||||||
data["worker_id"] = self.workerid
|
data["worker_id"] = self.workerid
|
||||||
|
data["testrun_uid"] = self.testrunuid
|
||||||
assert self.session.items[self.item_index].nodeid == report.nodeid
|
assert self.session.items[self.item_index].nodeid == report.nodeid
|
||||||
self.sendevent("testreport", data=data)
|
self.sendevent("testreport", data=data)
|
||||||
|
|
||||||
@@ -238,6 +240,7 @@ if __name__ == "__channelexec__":
|
|||||||
importpath + os.pathsep + os.environ.get("PYTHONPATH", "")
|
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"] = workerinput["workerid"]
|
||||||
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
|
os.environ["PYTEST_XDIST_WORKER_COUNT"] = str(workerinput["workercount"])
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import fnmatch
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
@@ -35,6 +36,9 @@ class NodeManager(object):
|
|||||||
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
|
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.trace = self.config.trace.get("nodemanager")
|
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()
|
self.group = execnet.Group()
|
||||||
if specs is None:
|
if specs is None:
|
||||||
specs = self._getxspecs()
|
specs = self._getxspecs()
|
||||||
@@ -222,6 +226,7 @@ class WorkerController(object):
|
|||||||
"workercount": len(nodemanager.specs),
|
"workercount": len(nodemanager.specs),
|
||||||
"slaveid": gateway.id,
|
"slaveid": gateway.id,
|
||||||
"slavecount": len(nodemanager.specs),
|
"slavecount": len(nodemanager.specs),
|
||||||
|
"testrunuid": nodemanager.testrunuid,
|
||||||
"mainargv": sys.argv,
|
"mainargv": sys.argv,
|
||||||
}
|
}
|
||||||
# TODO: deprecated name, backward compatibility only. Remove it in future
|
# TODO: deprecated name, backward compatibility only. Remove it in future
|
||||||
|
|||||||
@@ -1066,6 +1066,29 @@ def test_worker_id_fixture(testdir, n):
|
|||||||
assert worker_ids == {"gw0", "gw1"}
|
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"])
|
@pytest.mark.parametrize("tb", ["auto", "long", "short", "no", "line", "native"])
|
||||||
def test_error_report_styles(testdir, tb):
|
def test_error_report_styles(testdir, tb):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class TestHooks:
|
|||||||
|
|
||||||
def test_runtest_logreport(self, testdir):
|
def test_runtest_logreport(self, testdir):
|
||||||
"""Test that log reports from pytest_runtest_logreport when running
|
"""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(
|
testdir.makeconftest(
|
||||||
"""
|
"""
|
||||||
@@ -23,20 +23,24 @@ class TestHooks:
|
|||||||
if hasattr(report, 'node'):
|
if hasattr(report, 'node'):
|
||||||
if report.when == "call":
|
if report.when == "call":
|
||||||
workerid = report.node.workerinput['workerid']
|
workerid = report.node.workerinput['workerid']
|
||||||
|
testrunuid = report.node.workerinput['testrunuid']
|
||||||
if workerid != report.worker_id:
|
if workerid != report.worker_id:
|
||||||
print("HOOK: Worker id mismatch: %s %s"
|
print("HOOK: Worker id mismatch: %s %s"
|
||||||
% (workerid, report.worker_id))
|
% (workerid, report.worker_id))
|
||||||
|
elif testrunuid != report.testrun_uid:
|
||||||
|
print("HOOK: Testrun uid mismatch: %s %s"
|
||||||
|
% (testrunuid, report.testrun_uid))
|
||||||
else:
|
else:
|
||||||
print("HOOK: %s %s"
|
print("HOOK: %s %s %s"
|
||||||
% (report.nodeid, report.worker_id))
|
% (report.nodeid, report.worker_id, report.testrun_uid))
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
res = testdir.runpytest("-n1", "-s")
|
res = testdir.runpytest("-n1", "-s")
|
||||||
res.stdout.fnmatch_lines(
|
res.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*HOOK: test_runtest_logreport.py::test_a gw0*",
|
"*HOOK: test_runtest_logreport.py::test_a gw0 *",
|
||||||
"*HOOK: test_runtest_logreport.py::test_b gw0*",
|
"*HOOK: test_runtest_logreport.py::test_b gw0 *",
|
||||||
"*HOOK: test_runtest_logreport.py::test_c gw0*",
|
"*HOOK: test_runtest_logreport.py::test_c gw0 *",
|
||||||
"*3 passed*",
|
"*3 passed*",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -95,6 +95,18 @@ def test_dsession_with_collect_only(testdir):
|
|||||||
assert not config.pluginmanager.hasplugin("dsession")
|
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:
|
class TestDistOptions:
|
||||||
def test_getxspecs(self, testdir):
|
def test_getxspecs(self, testdir):
|
||||||
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
|
config = testdir.parseconfigure("--tx=popen", "--tx", "ssh=xyz")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import py
|
|||||||
import pprint
|
import pprint
|
||||||
import pytest
|
import pytest
|
||||||
import sys
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
from xdist.workermanage import WorkerController
|
from xdist.workermanage import WorkerController
|
||||||
import execnet
|
import execnet
|
||||||
@@ -44,6 +45,7 @@ class WorkerSetup:
|
|||||||
putevent = self.use_callback and self.events.put or None
|
putevent = self.use_callback and self.events.put or None
|
||||||
|
|
||||||
class DummyMananger:
|
class DummyMananger:
|
||||||
|
testrunuid = uuid.uuid4().hex
|
||||||
specs = [0, 1]
|
specs = [0, 1]
|
||||||
|
|
||||||
self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
|
self.slp = WorkerController(DummyMananger, self.gateway, config, putevent)
|
||||||
@@ -220,6 +222,7 @@ def test_remote_env_vars(testdir):
|
|||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
def test():
|
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'] in ('gw0', 'gw1')
|
||||||
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
|
assert os.environ['PYTEST_XDIST_WORKER_COUNT'] == '2'
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user