The Gaudi Framework  v39r0 (5b8b5eda)
fixtures.py
Go to the documentation of this file.
1 
11 import inspect
12 import os
13 import subprocess
14 import time
15 from collections import defaultdict
16 from pathlib import Path
17 from typing import Callable, Generator, Optional
18 
19 import pytest
20 import yaml
21 from GaudiTesting.FixtureResult import FixtureResult
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_validation_time: Captures the validation time for each test function.
55 # - capture_class_docstring: Captures the docstring of the test class.
56 # - reference: Creates a .new file if the output data is different from the reference.
57 
58 
59 def pytest_configure(config):
60  config.addinivalue_line(
61  "markers",
62  "shared_cwd(id): make SubprocessBaseTest tests share a working directory",
63  )
64  config.addinivalue_line(
65  "markers",
66  "do_not_collect_source: flag the test code as not to be collected",
67  )
68 
69 
70 def pytest_sessionstart(session):
71  session.sources = {}
72  session.docstrings = {}
73 
74 
75 def pytest_collection_modifyitems(config, items):
76  """
77  Record source code of tests.
78  """
79  for item in items:
80  if isinstance(item, pytest.Function) and not item.get_closest_marker(
81  "do_not_collect_source"
82  ):
83  name = (
84  f"{item.parent.name}.{item.originalname}"
85  if isinstance(item.parent, pytest.Class)
86  else item.originalname
87  )
88  source_code = CodeWrapper(inspect.getsource(item.function), "python")
89  item.session.sources[name] = source_code
90 
91 
92 def _get_shared_cwd_id(cls: type) -> Optional[str]:
93  """
94  Extract the id of the shared cwd directory needed by the class, if any.
95 
96  If the class is marked with shared_cwd multiple times only the last one
97  is taken into account.
98 
99  Return the id or None.
100  """
101  if hasattr(cls, "pytestmark"):
102  for mark in cls.pytestmark:
103  if mark.name == "shared_cwd":
104  return mark.args[0]
105  return None
106 
107 
108 def _path_for_shared_cwd(config: pytest.Config, cwd_id: str) -> Path:
109  """
110  Return the path to the shared directory identified by id.
111  """
112  name = f"gaudi.{get_platform()}.{cwd_id}".replace("/", "_")
113  return config.cache.mkdir(name)
114 
115 
116 @pytest.fixture(scope="class")
118  request: pytest.FixtureRequest,
119  tmp_path_factory: Callable,
120 ) -> Generator[FixtureResult, None, None]:
121  cls = request.cls
122  result = None
123  if cls and issubclass(cls, SubprocessBaseTest):
124  if hasattr(cls, "popen_kwargs") and "cwd" not in cls.popen_kwargs:
125  cwd_id = _get_shared_cwd_id(cls)
126  cls.popen_kwargs["cwd"] = (
127  _path_for_shared_cwd(request.config, cwd_id)
128  if cwd_id
129  else tmp_path_factory.mktemp("workdir")
130  )
131  result = cls.run_program(
132  tmp_path=tmp_path_factory.mktemp("tmp-", numbered=True)
133  )
134 
135  yield result
136 
137 
138 @pytest.fixture(scope="class")
140  fixture_result: FixtureResult,
141 ) -> Generator[subprocess.CompletedProcess, None, None]:
142  yield fixture_result.completed_process if fixture_result else None
143 
144 
145 @pytest.fixture(scope="class")
146 def stdout(
147  completed_process: subprocess.CompletedProcess,
148 ) -> Generator[bytes, None, None]:
149  yield completed_process.stdout if completed_process else None
150 
151 
152 @pytest.fixture(scope="class")
153 def stderr(
154  completed_process: subprocess.CompletedProcess,
155 ) -> Generator[bytes, None, None]:
156  yield completed_process.stderr if completed_process else None
157 
158 
159 @pytest.fixture(scope="class")
161  completed_process: subprocess.CompletedProcess,
162 ) -> Generator[int, None, None]:
163  yield completed_process.returncode if completed_process else None
164 
165 
166 @pytest.fixture(scope="class")
167 def cwd(fixture_result: FixtureResult) -> Generator[Path, None, None]:
168  yield Path(fixture_result.cwd) if fixture_result else None
169 
170 
171 @pytest.fixture(autouse=True)
173  request: pytest.FixtureRequest, fixture_result: FixtureResult
174 ) -> None:
175  if (
176  fixture_result
177  and fixture_result.failure is not None
178  and "test_fixture_setup" not in request.keywords
179  ):
180  pytest.skip(f"{fixture_result.failure}")
181 
182 
183 @pytest.fixture(scope="function", autouse=True)
185  record_property: Callable[[str, str], None],
186 ) -> Generator[None, None, None]:
187  val_start_time = time.perf_counter()
188  yield
189  record_property("validate_time", round(time.perf_counter() - val_start_time, 2))
190 
191 
192 @pytest.fixture(scope="class", autouse=True)
194  request: pytest.FixtureRequest,
195 ) -> None:
196  cls = request.cls
197  if cls and cls.__doc__:
198  request.session.docstrings[cls.__name__] = inspect.getdoc(cls)
199 
200 
201 @pytest.fixture(scope="class")
202 def reference_path(request) -> Generator[Optional[Path], None, None]:
203  cls = request.cls
204  path = None
205  if hasattr(cls, "reference") and cls.reference:
206  path = cls.reference
207  if hasattr(cls, "resolve_path"):
208  path = cls.resolve_path(path)
209 
210  path = expand_reference_file_name(path)
211  yield Path(path) if path else None
212 
213 
214 @pytest.fixture(scope="class")
215 def reference(request, reference_path: Optional[Path]):
216  cls = request.cls
217  original_reference_data = None
218  current_reference_data = None
219 
220  if reference_path:
221  if reference_path.exists() and reference_path.stat().st_size > 0:
222  with open(reference_path, "r") as f:
223  original_reference_data = yaml.safe_load(f)
224  else:
225  # if the file does not exist we may have a relative path, so
226  # we have to resolve it wrt the file containing the test class
227  reference_path = file_path_for_class(cls).parent / reference_path
228  original_reference_data = AlwaysTrueDefaultDict(lambda: None)
229 
230  current_reference_data = original_reference_data.copy()
231 
232  yield current_reference_data
233 
234  if current_reference_data != original_reference_data:
235  cnt = 0
236  newrefname = f"{reference_path}.new"
237  while os.path.exists(newrefname):
238  cnt += 1
239  newrefname = f"{reference_path}.~{cnt}~.new"
240 
241  if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
242  # when we ignore stdout validation we just overwrite the reference file
243  newrefname = reference_path
244 
245  os.makedirs(os.path.dirname(newrefname), exist_ok=True)
246  with open(newrefname, "w") as f:
247  yaml.safe_dump(current_reference_data, f, sort_keys=False)
fixtures.pytest_sessionstart
def pytest_sessionstart(session)
Definition: fixtures.py:70
ReadAndWriteWhiteBoard.Path
Path
Definition: ReadAndWriteWhiteBoard.py:58
GaudiTesting.utils.file_path_for_class
def file_path_for_class(cls)
Definition: utils.py:394
fixtures._get_shared_cwd_id
Optional[str] _get_shared_cwd_id(type cls)
Definition: fixtures.py:92
fixtures.fixture_result
Generator[FixtureResult, None, None] fixture_result(pytest.FixtureRequest request, Callable tmp_path_factory)
Definition: fixtures.py:117
fixtures.returncode
Generator[int, None, None] returncode(subprocess.CompletedProcess completed_process)
Definition: fixtures.py:160
fixtures.reference
def reference(request, Optional[Path] reference_path)
Definition: fixtures.py:215
fixtures.capture_class_docstring
None capture_class_docstring(pytest.FixtureRequest request)
Definition: fixtures.py:193
fixtures._path_for_shared_cwd
Path _path_for_shared_cwd(pytest.Config config, str cwd_id)
Definition: fixtures.py:108
GaudiTesting.FixtureResult
Definition: FixtureResult.py:1
fixtures.stderr
Generator[bytes, None, None] stderr(subprocess.CompletedProcess completed_process)
Definition: fixtures.py:153
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:139
GaudiTesting.SubprocessBaseTest
Definition: SubprocessBaseTest.py:1
fixtures.reference_path
Generator[Optional[Path], None, None] reference_path(request)
Definition: fixtures.py:202
fixtures.check_for_exceptions
None check_for_exceptions(pytest.FixtureRequest request, FixtureResult fixture_result)
Definition: fixtures.py:172
GaudiTesting.utils.CodeWrapper
Definition: utils.py:22
fixtures.pytest_configure
def pytest_configure(config)
Definition: fixtures.py:59
fixtures.pytest_collection_modifyitems
def pytest_collection_modifyitems(config, items)
Definition: fixtures.py:75
fixtures.capture_validation_time
Generator[None, None, None] capture_validation_time(Callable[[str, str], None] record_property)
Definition: fixtures.py:184
fixtures.cwd
Generator[Path, None, None] cwd(FixtureResult fixture_result)
Definition: fixtures.py:167
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:146