The Gaudi Framework  master (b9786168)
Loading...
Searching...
No Matches
fixtures.py
Go to the documentation of this file.
11import inspect
12import os
13import subprocess
14from collections import defaultdict
15from pathlib import Path
16from typing import Callable, Generator, Optional
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) -> Optional[str]:
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[Optional[Path], 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 path = cls.reference
195 if hasattr(cls, "resolve_path"):
196 path = cls.resolve_path(path)
197
198 path = expand_reference_file_name(path)
199 yield Path(path) if path else None
200
201 elif isinstance(cls.reference, dict):
202 yield None
203
204 else:
205 raise ValueError(
206 f"Invalid reference type {type(cls.reference)}, only str, PathLike or dict are allowed"
207 )
208
209
210@pytest.fixture(scope="class")
211def reference(request, reference_path: Optional[Path]) -> Generator[dict, None, None]:
212 cls = request.cls
213 original_reference_data = None
214 current_reference_data = None
215
216 if reference_path:
217 if reference_path.exists() and reference_path.stat().st_size > 0:
218 with open(reference_path, "r") as f:
219 original_reference_data = yaml.safe_load(f)
220 else:
221 # if the file does not exist we may have a relative path, so
222 # we have to resolve it wrt the file containing the test class
223 reference_path = file_path_for_class(cls).parent / reference_path
224 original_reference_data = AlwaysTrueDefaultDict(lambda: None)
225
226 current_reference_data = original_reference_data.copy()
227
228 elif hasattr(cls, "reference") and isinstance(cls.reference, dict):
229 original_reference_data = cls.reference
230 current_reference_data = original_reference_data.copy()
231
232 yield current_reference_data
233
234 if current_reference_data == original_reference_data:
235 return
236
237 if reference_path:
238 cnt = 0
239 newrefname = f"{reference_path}.new"
240 while os.path.exists(newrefname):
241 cnt += 1
242 newrefname = f"{reference_path}.~{cnt}~.new"
243
244 if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
245 # when we ignore stdout validation we just overwrite the reference file
246 newrefname = reference_path
247
248 os.makedirs(os.path.dirname(newrefname), exist_ok=True)
249 with open(newrefname, "w") as f:
250 yaml.safe_dump(current_reference_data, f, sort_keys=False)
Generator[Optional[Path], None, None] reference_path(request)
Definition fixtures.py:187
None capture_class_docstring(pytest.FixtureRequest request)
Definition fixtures.py:180
Generator[bytes, None, None] stderr(subprocess.CompletedProcess completed_process)
Definition fixtures.py:149
Generator[Path, None, None] cwd(FixtureResult fixture_result)
Definition fixtures.py:161
Generator[bytes, None, None] stdout(subprocess.CompletedProcess completed_process)
Definition fixtures.py:142
Optional[str] _get_shared_cwd_id(type cls)
Definition fixtures.py:86
Generator[FixtureResult, None, None] fixture_result(pytest.FixtureRequest request, Callable tmp_path_factory)
Definition fixtures.py:114
Path _path_for_shared_cwd(pytest.Config config, str cwd_id)
Definition fixtures.py:102
None check_for_exceptions(pytest.FixtureRequest request, FixtureResult fixture_result)
Definition fixtures.py:168
pytest_configure(config)
Definition fixtures.py:58
Generator[subprocess.CompletedProcess, None, None] completed_process(FixtureResult fixture_result)
Definition fixtures.py:135
pytest_collection_modifyitems(config, items)
Definition fixtures.py:69
Generator[int, None, None] returncode(subprocess.CompletedProcess completed_process)
Definition fixtures.py:156