Skip to content

Commit 2c6bc13

Browse files
authored
Add an example on how to share session fixture data to README (#483)
Add an example on how to share session fixture data to README
2 parents 79dd52b + 0c53761 commit 2c6bc13

File tree

2 files changed

+57
-4
lines changed

2 files changed

+57
-4
lines changed

README.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,59 @@ any guaranteed order, but you can control this with these options:
9595
in version ``1.21``.
9696

9797

98+
Making session-scoped fixtures execute only once
99+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
100+
101+
``pytest-xdist`` is designed so that each worker process will perform its own collection and execute
102+
a subset of all tests. This means that tests in different processes requesting a high-level
103+
scoped fixture (for example ``session``) will execute the fixture code more than once, which
104+
breaks expectations and might be undesired in certain situations.
105+
106+
While ``pytest-xdist`` does not have a builtin support for ensuring a session-scoped fixture is
107+
executed exactly once, this can be achieved by using a lock file for inter-process communication.
108+
109+
The example below needs to execute the fixture ``session_data`` only once (because it is
110+
resource intensive, or needs to execute only once to define configuration options, etc), so it makes
111+
use of a `FileLock <https://pypi.org/project/filelock/>`_ to produce the fixture data only once
112+
when the first process requests the fixture, while the other processes will then read
113+
the data from a file.
114+
115+
Here is the code:
116+
117+
.. code-block:: python
118+
119+
import json
120+
121+
import pytest
122+
from filelock import FileLock
123+
124+
125+
@pytest.fixture(scope="session")
126+
def session_data(tmp_path_factory, worker_id):
127+
if not worker_id:
128+
# not executing in with multiple workers, just produce the data and let
129+
# pytest's fixture caching do its job
130+
return produce_expensive_data()
131+
132+
# get the temp directory shared for by all workers
133+
root_tmp_dir = tmp_path_factory.getbasetemp().parent
134+
135+
fn = root_tmp_dir / "data.json"
136+
with FileLock(str(fn) + ".lock"):
137+
if fn.is_file():
138+
data = json.loads(fn.read_text())
139+
else:
140+
data = produce_expensive_data()
141+
fn.write_text(json.dumps(data))
142+
return data
143+
144+
145+
The example above can also be use in cases a fixture needs to execute exactly once per test session, like
146+
initializing a database service and populating initial tables.
147+
148+
This technique might not work for every case, but should be a starting point for many situations
149+
where executing a high-scope fixture exactly once is important.
150+
98151
Running tests in a Python subprocess
99152
------------------------------------
100153

testing/acceptance_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -784,7 +784,7 @@ def test_func(request):
784784
)
785785
)
786786
result = testdir.runpytest(n)
787-
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warnings*"])
787+
result.stdout.fnmatch_lines(["*this is a warning*", "*1 passed, 1 warning*"])
788788

789789
@pytest.mark.parametrize("n", ["-n0", "-n1"])
790790
def test_custom_subclass(self, testdir, n):
@@ -808,7 +808,7 @@ def test_func(request):
808808
)
809809
testdir.syspathinsert()
810810
result = testdir.runpytest(n)
811-
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warnings*"])
811+
result.stdout.fnmatch_lines(["*MyWarning*", "*1 passed, 1 warning*"])
812812

813813
@pytest.mark.parametrize("n", ["-n0", "-n1"])
814814
def test_unserializable_arguments(self, testdir, n):
@@ -825,7 +825,7 @@ def test_func(tmpdir):
825825
)
826826
testdir.syspathinsert()
827827
result = testdir.runpytest(n)
828-
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warnings*"])
828+
result.stdout.fnmatch_lines(["*UserWarning*foo.txt*", "*1 passed, 1 warning*"])
829829

830830
@pytest.mark.parametrize("n", ["-n0", "-n1"])
831831
def test_unserializable_warning_details(self, testdir, n):
@@ -857,7 +857,7 @@ def test_func(tmpdir):
857857
testdir.syspathinsert()
858858
result = testdir.runpytest(n)
859859
result.stdout.fnmatch_lines(
860-
["*ResourceWarning*unclosed*", "*1 passed, 1 warnings*"]
860+
["*ResourceWarning*unclosed*", "*1 passed, 1 warning*"]
861861
)
862862

863863

0 commit comments

Comments
 (0)