The Gaudi Framework  master (37c0b60a)
fixtures.py
Go to the documentation of this file.
1 
11 import inspect
12 import os
13 import subprocess
14 from collections import defaultdict
15 from pathlib import Path
16 from typing import Callable, Generator, Optional
17 
18 import pytest
19 import yaml
20 from GaudiTesting.FixtureResult import FixtureResult
21 from GaudiTesting.pytest.ctest_measurements_reporter import results
22 from GaudiTesting.SubprocessBaseTest import SubprocessBaseTest
23 from GaudiTesting.utils import (
24  CodeWrapper,
25  expand_reference_file_name,
26  file_path_for_class,
27  get_platform,
28  str_representer,
29 )
30 
31 
32 class AlwaysTrueDefaultDict(defaultdict):
33  def __contains__(self, key):
34  return True
35 
36 
37 yaml.representer.SafeRepresenter.add_representer(str, str_representer)
38 yaml.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 
58 def 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 
69 def pytest_collection_modifyitems(config, items):
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 
86 def _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 
102 def _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")
140 def stdout(
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")
147 def stderr(
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")
161 def 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")
187 def 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")
211 def 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)
ReadAndWriteWhiteBoard.Path
Path
Definition: ReadAndWriteWhiteBoard.py:58
GaudiTesting.utils.file_path_for_class
def file_path_for_class(cls)
Definition: utils.py:396
fixtures._get_shared_cwd_id
Optional[str] _get_shared_cwd_id(type cls)
Definition: fixtures.py:86
fixtures.fixture_result
Generator[FixtureResult, None, None] fixture_result(pytest.FixtureRequest request, Callable tmp_path_factory)
Definition: fixtures.py:111
fixtures.returncode
Generator[int, None, None] returncode(subprocess.CompletedProcess completed_process)
Definition: fixtures.py:154
fixtures.capture_class_docstring
None capture_class_docstring(pytest.FixtureRequest request)
Definition: fixtures.py:178
fixtures._path_for_shared_cwd
Path _path_for_shared_cwd(pytest.Config config, str cwd_id)
Definition: fixtures.py:102
GaudiTesting.FixtureResult
Definition: FixtureResult.py:1
fixtures.stderr
Generator[bytes, None, None] stderr(subprocess.CompletedProcess completed_process)
Definition: fixtures.py:147
GaudiTesting.utils
Definition: utils.py:1
fixtures.AlwaysTrueDefaultDict
Definition: fixtures.py:32
fixtures.AlwaysTrueDefaultDict.__contains__
def __contains__(self, key)
Definition: fixtures.py:33
fixtures.completed_process
Generator[subprocess.CompletedProcess, None, None] completed_process(FixtureResult fixture_result)
Definition: fixtures.py:133
GaudiTesting.SubprocessBaseTest
Definition: SubprocessBaseTest.py:1
fixtures.reference_path
Generator[Optional[Path], None, None] reference_path(request)
Definition: fixtures.py:187
fixtures.check_for_exceptions
None check_for_exceptions(pytest.FixtureRequest request, FixtureResult fixture_result)
Definition: fixtures.py:166
GaudiTesting.utils.CodeWrapper
Definition: utils.py:22
fixtures.pytest_configure
def pytest_configure(config)
Definition: fixtures.py:58
fixtures.reference
Generator[dict, None, None] reference(request, Optional[Path] reference_path)
Definition: fixtures.py:211
fixtures.pytest_collection_modifyitems
def pytest_collection_modifyitems(config, items)
Definition: fixtures.py:69
fixtures.cwd
Generator[Path, None, None] cwd(FixtureResult fixture_result)
Definition: fixtures.py:161
GaudiTesting.utils.expand_reference_file_name
def expand_reference_file_name(reference)
Definition: utils.py:114
fixtures.stdout
Generator[bytes, None, None] stdout(subprocess.CompletedProcess completed_process)
Definition: fixtures.py:140