The Gaudi Framework master (1fbcc22a)
Loading...
Searching...
No Matches
fixtures.py
Go to the documentation of this file.
11import inspect
12import os
13import subprocess
14from collections import defaultdict
15from collections.abc import Callable, Generator
16from pathlib import Path
17
18import pytest
19import yaml
20from GaudiTesting.FixtureResult import FixtureResult
21from GaudiTesting.pytest.ctest_measurements_reporter import results
22from GaudiTesting.SubprocessBaseTest import SubprocessBaseTest
23from GaudiTesting.utils import (
24 CodeWrapper,
25 expand_reference_file_name,
26 file_path_for_class,
27 get_platform,
28 str_representer,
29)
30
31
32class AlwaysTrueDefaultDict(defaultdict):
33 def __contains__(self, key):
34 return True
35
36
37yaml.representer.SafeRepresenter.add_representer(str, str_representer)
38yaml.representer.SafeRepresenter.add_representer(
39 AlwaysTrueDefaultDict, yaml.representer.SafeRepresenter.represent_dict
40)
41
42# This plugin provides a set of pytest fixtures for the Gaudi testing framework.
43# These fixtures facilitate the execution of tests and capture various aspects
44# of the test results.
45
46# The fixtures include:
47# - fixture_result: Runs the program and yields the result.
48# - completed_process: Yields the completed subprocess process.
49# - stdout: Captures the standard output of the subprocess.
50# - stderr: Captures the standard error of the subprocess.
51# - returncode: Captures the return code of the subprocess.
52# - cwd: Captures the directory in which the program was executed.
53# - check_for_exceptions: Skips the test if exceptions are found in the result.
54# - capture_class_docstring: Captures the docstring of the test class.
55# - reference: Creates a .new file if the output data is different from the reference.
56
57
58def pytest_configure(config):
59 config.addinivalue_line(
60 "markers",
61 "shared_cwd(id): make SubprocessBaseTest tests share a working directory",
62 )
63 config.addinivalue_line(
64 "markers",
65 "do_not_collect_source: flag the test code as not to be collected",
66 )
67
68
70 """
71 Record source code of tests.
72 """
73 for item in items:
74 if isinstance(item, pytest.Function) and not item.get_closest_marker(
75 "do_not_collect_source"
76 ):
77 name = (
78 f"{item.parent.name}.{item.originalname}"
79 if isinstance(item.parent, pytest.Class)
80 else item.originalname
81 )
82 source_code = CodeWrapper(inspect.getsource(item.function), "python")
83 results[f"{name}.source_code"] = source_code
84
85
86def _get_shared_cwd_id(cls: type) -> str | None:
87 """
88 Extract the id of the shared cwd directory needed by the class, if any.
89
90 If the class is marked with shared_cwd multiple times only the last one
91 is taken into account.
92
93 Return the id or None.
94 """
95 if hasattr(cls, "pytestmark"):
96 for mark in cls.pytestmark:
97 if mark.name == "shared_cwd":
98 return mark.args[0]
99 return None
100
101
102def _path_for_shared_cwd(config: pytest.Config, cwd_id: str) -> Path:
103 """
104 Return the path to the shared directory identified by id.
105 """
106 name = f"gaudi.{get_platform()}.{cwd_id}".replace("/", "_")
107 return config.cache.mkdir(name)
108
109
110@pytest.fixture(scope="class")
112 request: pytest.FixtureRequest,
113 tmp_path_factory: Callable,
114) -> Generator[FixtureResult, None, None]:
115 cls = request.cls
116 result = None
117 if cls and issubclass(cls, SubprocessBaseTest):
118 if hasattr(cls, "popen_kwargs") and "cwd" not in cls.popen_kwargs:
119 cwd_id = _get_shared_cwd_id(cls)
120 cls.popen_kwargs["cwd"] = (
121 _path_for_shared_cwd(request.config, cwd_id)
122 if cwd_id
123 else tmp_path_factory.mktemp("workdir")
124 )
125 result = cls.run_program(
126 tmp_path=tmp_path_factory.mktemp("tmp-", numbered=True)
127 )
128
129 yield result
130
131
132@pytest.fixture(scope="class")
134 fixture_result: FixtureResult,
135) -> Generator[subprocess.CompletedProcess, None, None]:
136 yield fixture_result.completed_process if fixture_result else None
137
138
139@pytest.fixture(scope="class")
141 completed_process: subprocess.CompletedProcess,
142) -> Generator[bytes, None, None]:
143 yield completed_process.stdout if completed_process else None
144
145
146@pytest.fixture(scope="class")
148 completed_process: subprocess.CompletedProcess,
149) -> Generator[bytes, None, None]:
150 yield completed_process.stderr if completed_process else None
151
152
153@pytest.fixture(scope="class")
155 completed_process: subprocess.CompletedProcess,
156) -> Generator[int, None, None]:
157 yield completed_process.returncode if completed_process else None
158
159
160@pytest.fixture(scope="class")
161def cwd(fixture_result: FixtureResult) -> Generator[Path, None, None]:
162 yield Path(fixture_result.cwd) if fixture_result else None
163
164
165@pytest.fixture(autouse=True)
167 request: pytest.FixtureRequest, fixture_result: FixtureResult
168) -> None:
169 if (
170 fixture_result
171 and fixture_result.run_exception is not None
172 and "test_fixture_setup" not in request.keywords
173 ):
174 pytest.skip(f"{fixture_result.run_exception}")
175
176
177@pytest.fixture(scope="class", autouse=True)
179 request: pytest.FixtureRequest,
180) -> None:
181 cls = request.cls
182 if cls and cls.__doc__:
183 results[f"{cls.__name__}.doc"] = inspect.getdoc(cls)
184
185
186@pytest.fixture(scope="class")
187def reference_path(request) -> Generator[Path | None, None, None]:
188 cls = request.cls
189
190 if not hasattr(cls, "reference") or cls.reference is None:
191 yield None
192
193 elif isinstance(cls.reference, (str, os.PathLike)):
194 reference = file_path_for_class(cls).parent / os.path.expandvars(cls.reference)
195 path = expand_reference_file_name(reference)
196 yield Path(path)
197
198 elif isinstance(cls.reference, dict):
199 yield None
200
201 else:
202 raise ValueError(
203 f"Invalid reference type {type(cls.reference)}, only str, PathLike or dict are allowed"
204 )
205
206
207@pytest.fixture(scope="class")
208def reference(request, reference_path: Path | None) -> Generator[dict, None, None]:
209 cls = request.cls
210 original_reference_data = None
211 current_reference_data = None
212
213 if reference_path:
214 if reference_path.exists() and reference_path.stat().st_size > 0:
215 with open(reference_path) as f:
216 original_reference_data = yaml.safe_load(f)
217 else:
218 original_reference_data = AlwaysTrueDefaultDict(lambda: None)
219
220 current_reference_data = original_reference_data.copy()
221
222 elif hasattr(cls, "reference") and isinstance(cls.reference, dict):
223 original_reference_data = cls.reference
224 current_reference_data = original_reference_data.copy()
225
226 yield current_reference_data
227
228 if current_reference_data == original_reference_data:
229 return
230
231 if reference_path:
232 cnt = 0
233 newrefname = f"{reference_path}.new"
234 while os.path.exists(newrefname):
235 cnt += 1
236 newrefname = f"{reference_path}.~{cnt}~.new"
237
238 if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
239 # when we ignore stdout validation we just overwrite the reference file
240 newrefname = reference_path
241
242 os.makedirs(os.path.dirname(newrefname), exist_ok=True)
243 with open(newrefname, "w") as f:
244 yaml.safe_dump(current_reference_data, f, sort_keys=False)
Generator[FixtureResult, None, None] fixture_result(pytest.FixtureRequest request, Callable tmp_path_factory)
Definition fixtures.py:114
pytest_collection_modifyitems(config, items)
Definition fixtures.py:69
Generator[Path, None, None] cwd(FixtureResult fixture_result)
Definition fixtures.py:161
Generator[subprocess.CompletedProcess, None, None] completed_process(FixtureResult fixture_result)
Definition fixtures.py:135
pytest_configure(config)
Definition fixtures.py:58
str|None _get_shared_cwd_id(type cls)
Definition fixtures.py:86
Generator[bytes, None, None] stderr(subprocess.CompletedProcess completed_process)
Definition fixtures.py:149
None check_for_exceptions(pytest.FixtureRequest request, FixtureResult fixture_result)
Definition fixtures.py:168
Generator[Path|None, None, None] reference_path(request)
Definition fixtures.py:187
Generator[bytes, None, None] stdout(subprocess.CompletedProcess completed_process)
Definition fixtures.py:142
None capture_class_docstring(pytest.FixtureRequest request)
Definition fixtures.py:180
Path _path_for_shared_cwd(pytest.Config config, str cwd_id)
Definition fixtures.py:102
Generator[int, None, None] returncode(subprocess.CompletedProcess completed_process)
Definition fixtures.py:156