The Gaudi Framework  v41r0 (bdbe7744)
Loading...
Searching...
No Matches
GaudiTesting.GaudiExeTest.GaudiExeTest Class Reference
Inheritance diagram for GaudiTesting.GaudiExeTest.GaudiExeTest:
Collaboration diagram for GaudiTesting.GaudiExeTest.GaudiExeTest:

Public Member Functions

None validate_with_reference (cls, bytes data, str key, dict reference, Callable[[str, str], None] record_property)
 validate_json_with_reference (cls, str output_file, str reference_file, detailed=True)
 find_reference_block (cls, str reference_block, Callable preprocessor=None, str signature=None, int signature_offset=0)
 test_count_messages (self, reference, stdout, record_property)
None test_stdout (self, bytes stdout, Callable record_property, dict reference)
None test_ttrees (self, bytes stdout, Callable record_property, dict reference)
None test_histos (self, bytes stdout, Callable record_property, dict reference)
None test_stderr (self, bytes stderr, Callable record_property, dict reference)
 test_record_options (self, Callable record_property)
Public Member Functions inherited from GaudiTesting.SubprocessBaseTest.SubprocessBaseTest
Path|None cwd (self)
str resolve_path (cls, Path|str path)
None update_env (cls, dict[str, str] env)
FixtureResult run_program (cls, tmp_path=None)
 run_program_for_dbg (cls, tmp_path)
None test_fixture_setup (self, Callable[[str, str], None] record_property, FixtureResult fixture_result, Path|None reference_path)
None test_returncode (self, int returncode)

Static Public Attributes

Callable options = None
 options_code = None
Callable preprocessor = normalizeTestSuite
Static Public Attributes inherited from GaudiTesting.SubprocessBaseTest.SubprocessBaseTest
list command = None
str reference = None
list environment = None
int timeout = 600
int returncode = 0
dict popen_kwargs = {}
list env_to_record

Protected Member Functions

list[str] _prepare_command (cls, tmp_path=Path())
Protected Member Functions inherited from GaudiTesting.SubprocessBaseTest.SubprocessBaseTest
dict[str, str] _prepare_environment (cls)
str _determine_program (cls, str prog)
str _handle_timeout (cls, subprocess.Popen proc)
 _prepare_execution (cls, tmp_path=None)

Static Protected Member Functions

str _output_diff (dict reference_data, str output, Callable[[str], str] preprocessor=lambda x:x)
Static Protected Member Functions inherited from GaudiTesting.SubprocessBaseTest.SubprocessBaseTest
str _collect_stack_trace (subprocess.Popen proc)
None _terminate_process (subprocess.Popen proc)

Additional Inherited Members

Static Public Member Functions inherited from GaudiTesting.SubprocessBaseTest.SubprocessBaseTest
str expand_vars_from (str value, dict[str, str] env)
None unset_vars (dict[str, str] env, list[str] vars_to_unset)

Detailed Description

An extension of SubprocessBaseTest tailored to the Gaudi/LHCb workflow.
It includes additional functionalities for handling options,
preprocessing output, and validating against platform-specific reference files.

Definition at line 35 of file GaudiExeTest.py.

Member Function Documentation

◆ _output_diff()

str GaudiTesting.GaudiExeTest.GaudiExeTest._output_diff ( dict reference_data,
str output,
Callable[[str], str] preprocessor = lambda x: x )
staticprotected
Compute the difference between the reference data and the current output.

Definition at line 95 of file GaudiExeTest.py.

99 ) -> str:
100 """
101 Compute the difference between the reference data and the current output.
102 """
103 expected_output = (
104 reference_data.splitlines()
105 if hasattr(reference_data, "splitlines")
106 else reference_data
107 )
108 actual_output = preprocessor(output).splitlines()
109 return "\n".join(
110 difflib.unified_diff(
111 expected_output,
112 actual_output,
113 fromfile="expected",
114 tofile="actual",
115 lineterm="",
116 )
117 )
118

◆ _prepare_command()

list[str] GaudiTesting.GaudiExeTest.GaudiExeTest._prepare_command ( cls,
tmp_path = Path() )
protected
Override the base class to include options.

Reimplemented from GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.

Definition at line 47 of file GaudiExeTest.py.

47 def _prepare_command(cls, tmp_path=Path()) -> list[str]:
48 """
49 Override the base class to include options.
50 """
51 command = super()._prepare_command(tmp_path=tmp_path)
52
53 def generate_unique_options_filename(extension, directory):
54 timestamp = time.strftime("%Y%m%d_%H%M%S")
55 return directory / f"options_{timestamp}.{extension}"
56
57 if hasattr(cls, "options") and cls.options is not None:
58 options = cls.options
59 filename = None
60
61 # Check if options is a function
62 if callable(options):
63 source_lines = inspect.getsource(options).splitlines()
64 clean_source = dedent(
65 "\n".join(source_lines[1:])
66 ) # Skip the first line (def options():)
67 filename = generate_unique_options_filename("py", tmp_path)
68 with open(filename, "w") as file:
69 file.write(clean_source)
70 cls.options_code = CodeWrapper(clean_source, "python")
71
72 # Check if options is a dictionary
73 elif isinstance(options, dict):
74 filename = generate_unique_options_filename("json", tmp_path)
75 with open(filename, "w") as file:
76 json.dump(options, file, indent=4)
77 cls.options_code = CodeWrapper(options, "json")
78
79 # Check if options is a string
80 elif isinstance(options, str):
81 options = dedent(options)
82 filename = generate_unique_options_filename("opts", tmp_path)
83 with open(filename, "w") as file:
84 file.write(options)
85 cls.options_code = CodeWrapper(options, "cpp")
86
87 else:
88 raise ValueError(f"invalid options type '{type(options).__name__}'")
89
90 if filename:
91 command.append(str(filename))
92 return command
93

◆ find_reference_block()

GaudiTesting.GaudiExeTest.GaudiExeTest.find_reference_block ( cls,
str reference_block,
Callable preprocessor = None,
str signature = None,
int signature_offset = 0 )

Definition at line 194 of file GaudiExeTest.py.

200 ):
201 def assert_function(
202 cls,
203 stdout,
204 record_property,
205 preprocessor=preprocessor,
206 signature=signature,
207 signature_offset=signature_offset,
208 ):
209 processed_stdout = (
210 preprocessor(stdout.decode("utf-8"))
211 if preprocessor
212 else stdout.decode("utf-8")
213 )
214 stdout_lines = processed_stdout.strip().split("\n")
215 reference_lines = dedent(reference_block).strip().split("\n")
216
217 if signature is None and signature_offset is not None:
218 if signature_offset < 0:
219 signature_offset = len(reference_lines) + signature_offset
220 signature = reference_lines[signature_offset]
221
222 try:
223 start_index = stdout_lines.index(signature)
224 end_index = start_index + len(reference_lines)
225 observed_block = stdout_lines[start_index:end_index]
226
227 if observed_block != reference_lines:
228 diff = list(
229 difflib.unified_diff(
230 reference_lines,
231 observed_block,
232 fromfile="expected",
233 tofile="actual",
234 )
235 )
236 diff_text = "\n".join(diff)
237 record_property("block_diff", CodeWrapper(diff_text, "diff"))
238 raise AssertionError(
239 "The observed block does not match the reference."
240 )
241 except ValueError:
242 raise AssertionError(
243 f"Signature '{signature}' not found in the output."
244 )
245
246 return assert_function
247

◆ test_count_messages()

GaudiTesting.GaudiExeTest.GaudiExeTest.test_count_messages ( self,
reference,
stdout,
record_property )
Test the count of error messages in the stdout against expected values.

Definition at line 249 of file GaudiExeTest.py.

249 def test_count_messages(self, reference, stdout, record_property):
250 """
251 Test the count of error messages in the stdout against expected values.
252 """
253 expected_messages = (
254 None if reference is None else reference.get("messages_count")
255 )
256 if expected_messages is None:
257 pytest.skip()
258
259 if not isinstance(expected_messages, dict):
260 raise ValueError("reference['messages_count'] must be a dict")
261 if not expected_messages:
262 # an empty dict doesn't make sense, let's assume we are bootstrapping the reference
263 # and start from the default
264 expected_messages = NO_ERROR_MESSAGES
265 reference["messages_count"] = expected_messages
266
267 outlines = self.preprocessor(
268 stdout.decode("utf-8", errors="backslashreplace")
269 ).splitlines()
270
271 messages = {key: [] for key in expected_messages}
272 for n, line in enumerate(outlines, 1):
273 words = line.split()
274 if len(words) >= 2 and words[1] in messages:
275 messages[words[1]].append((n, line.rstrip()))
276
277 messages_count = {key: len(value) for key, value in messages.items()}
278 try:
279 assert messages_count == expected_messages
280 except AssertionError:
281 reference["messages_count"] = messages_count
282 record_property("unexpected_messages_count", messages)
283 raise
284

◆ test_histos()

None GaudiTesting.GaudiExeTest.GaudiExeTest.test_histos ( self,
bytes stdout,
Callable record_property,
dict reference )
Test the histogram summaries against the reference.

Definition at line 321 of file GaudiExeTest.py.

323 ) -> None:
324 """
325 Test the histogram summaries against the reference.
326 """
327 if not self.reference or "histos" not in reference:
328 pytest.skip()
329
330 histos = filter_dict(
331 find_histos_summaries(stdout.decode()),
332 re.compile(r"Basket|.*size|Compression"),
333 )
334 try:
335 assert histos == reference["histos"]
336 except AssertionError:
337 reference["histos"] = histos
338 if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
339 pytest.xfail("Ignoring stdout validation")
340 raise
341

◆ test_record_options()

GaudiTesting.GaudiExeTest.GaudiExeTest.test_record_options ( self,
Callable record_property )

Definition at line 357 of file GaudiExeTest.py.

357 def test_record_options(self, record_property: Callable):
358 if self.options_code:
359 record_property("options", self.options_code)

◆ test_stderr()

None GaudiTesting.GaudiExeTest.GaudiExeTest.test_stderr ( self,
bytes stderr,
Callable record_property,
dict reference )
Test the standard error output against the reference.

Definition at line 343 of file GaudiExeTest.py.

345 ) -> None:
346 """
347 Test the standard error output against the reference.
348 """
349 err = self.preprocessor(stderr.decode("utf-8", errors="backslashreplace"))
350 if self.reference and "stderr" in reference:
351 self.validate_with_reference(err, "stderr", reference, record_property)
352
353 else:
354 assert not err.strip(), "Expected no standard error output, but got some."
355

◆ test_stdout()

None GaudiTesting.GaudiExeTest.GaudiExeTest.test_stdout ( self,
bytes stdout,
Callable record_property,
dict reference )
Test the standard output against the reference.

Definition at line 286 of file GaudiExeTest.py.

288 ) -> None:
289 """
290 Test the standard output against the reference.
291 """
292 if not self.reference or "stdout" not in reference:
293 pytest.skip("No stdout reference")
294
295 out = self.preprocessor(stdout.decode("utf-8", errors="backslashreplace"))
296 self.validate_with_reference(out, "stdout", reference, record_property)
297

◆ test_ttrees()

None GaudiTesting.GaudiExeTest.GaudiExeTest.test_ttrees ( self,
bytes stdout,
Callable record_property,
dict reference )
Test the TTree summaries against the reference.

Definition at line 299 of file GaudiExeTest.py.

301 ) -> None:
302 """
303 Test the TTree summaries against the reference.
304 """
305 if not self.reference or "ttrees" not in reference:
306 pytest.skip()
307
308 ttrees = filter_dict(
309 find_ttree_summaries(stdout.decode()),
310 re.compile(r"Basket|.*size|Compression"),
311 )
312 try:
313 assert ttrees == reference["ttrees"]
314 except AssertionError:
315 reference["ttrees"] = ttrees
316 if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
317 pytest.xfail("Ignoring stdout validation")
318 raise
319

◆ validate_json_with_reference()

GaudiTesting.GaudiExeTest.GaudiExeTest.validate_json_with_reference ( cls,
str output_file,
str reference_file,
detailed = True )
Validate the JSON output against a reference JSON file.

Definition at line 162 of file GaudiExeTest.py.

164 ):
165 """
166 Validate the JSON output against a reference JSON file.
167 """
168 assert os.path.isfile(output_file)
169
170 try:
171 with open(output_file) as f:
172 output = json.load(f)
173 except json.JSONDecodeError as err:
174 pytest.fail(f"json parser error in {output_file}: {err}")
175
176 lreference = cls.resolve_path(reference_file)
177 assert lreference, "reference file not set"
178 assert os.path.isfile(lreference)
179
180 try:
181 with open(lreference) as f:
182 expected = json.load(f)
183 except json.JSONDecodeError as err:
184 pytest.fail(f"JSON parser error in {lreference}: {err}")
185
186 if not detailed:
187 assert output == expected
188
189 expected = sorted(expected, key=lambda item: (item["component"], item["name"]))
190 output = sorted(output, key=lambda item: (item["component"], item["name"]))
191 assert output == expected
192

◆ validate_with_reference()

None GaudiTesting.GaudiExeTest.GaudiExeTest.validate_with_reference ( cls,
bytes data,
str key,
dict reference,
Callable[[str, str], None] record_property )
Validate the given data against a reference file for the specified key.

Definition at line 120 of file GaudiExeTest.py.

126 ) -> None:
127 """
128 Validate the given data against a reference file for the specified key.
129 """
130 if cls.reference:
131 try:
132 if key in reference:
133 # If the data is less than 100k, we can compare it directly
134 # and let pytest generate a nice diff, otherwise we hide the values
135 # to pytest to avoid that the test (practically) hangs.
136 #
137 # See
138 # - https://gitlab.cern.ch/lhcb/LHCb/-/issues/252
139 # - https://gitlab.cern.ch/gaudi/Gaudi/-/merge_requests/1375
140 # - https://gitlab.cern.ch/lhcb/LHCb/-/merge_requests/4773#note_9075367
141 if len(data) < 100000:
142 assert data == reference[key]
143 else:
144 same_as_reference = data == reference[key]
145 assert same_as_reference, "data is different from reference"
146 except AssertionError:
147 record_property(
148 f"{key}_diff",
149 CodeWrapper(
150 cls._output_diff(reference[key] or "", data, cls.preprocessor),
151 "diff",
152 ),
153 )
154 reference[key] = data
155 if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
156 pytest.xfail("Ignoring stdout validation")
157 raise
158 else:
159 pytest.skip("No reference file provided")
160

Member Data Documentation

◆ options

Callable GaudiTesting.GaudiExeTest.GaudiExeTest.options = None
static

Definition at line 42 of file GaudiExeTest.py.

◆ options_code

GaudiTesting.GaudiExeTest.GaudiExeTest.options_code = None
static

Definition at line 43 of file GaudiExeTest.py.

◆ preprocessor

GaudiTesting.GaudiExeTest.GaudiExeTest.preprocessor = normalizeTestSuite
static

Definition at line 44 of file GaudiExeTest.py.


The documentation for this class was generated from the following file: