16 from pathlib
import Path
17 from textwrap
import dedent
18 from typing
import Callable, Dict, List
27 find_histos_summaries,
31 NO_ERROR_MESSAGES = {
"ERROR": 0,
"FATAL": 0}
36 An extension of SubprocessBaseTest tailored to the Gaudi/LHCb workflow.
37 It includes additional functionalities for handling options,
38 preprocessing output, and validating against platform-specific reference files.
41 options: Callable =
None
43 preprocessor: Callable = normalizeTestSuite
48 Override the base class to include options.
52 if hasattr(cls,
"options")
and cls.options
is not None:
58 source_lines = inspect.getsource(options).splitlines()
59 clean_source = dedent(
60 "\n".join(source_lines[1:])
62 filename = tmp_path /
"options.py"
63 with open(filename,
"w")
as file:
64 file.write(clean_source)
68 elif isinstance(options, dict):
69 filename = tmp_path /
"options.json"
70 with open(filename,
"w")
as file:
71 json.dump(options, file, indent=4)
75 elif isinstance(options, str):
76 options = dedent(options)
77 filename = tmp_path /
"options.opts"
78 with open(filename,
"w")
as file:
83 raise ValueError(f
"invalid options type '{type(options).__name__}'")
86 command.append(
str(filename))
93 preprocessor: Callable[[str], str] =
lambda x: x,
96 Compute the difference between the reference data and the current output.
99 reference_data.splitlines()
100 if hasattr(reference_data,
"splitlines")
103 actual_output = preprocessor(output).splitlines()
105 difflib.unified_diff(
120 record_property: Callable[[str, str],
None],
123 Validate the given data against a reference file for the specified key.
128 assert data == reference[key]
129 except AssertionError:
133 cls.
_output_diff(reference[key]
or "", data, cls.preprocessor),
137 reference[key] = data
138 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
139 pytest.xfail(
"Ignoring stdout validation")
142 pytest.skip(
"No reference file provided")
146 cls, output_file: str, reference_file: str, detailed=
True
149 Validate the JSON output against a reference JSON file.
151 assert os.path.isfile(output_file)
154 with open(output_file)
as f:
155 output = json.load(f)
156 except json.JSONDecodeError
as err:
157 pytest.fail(f
"json parser error in {output_file}: {err}")
160 assert lreference,
"reference file not set"
161 assert os.path.isfile(lreference)
164 with open(lreference)
as f:
165 expected = json.load(f)
166 except json.JSONDecodeError
as err:
167 pytest.fail(f
"JSON parser error in {lreference}: {err}")
170 assert output == expected
172 expected = sorted(expected, key=
lambda item: (item[
"component"], item[
"name"]))
173 output = sorted(output, key=
lambda item: (item[
"component"], item[
"name"]))
174 assert output == expected
179 reference_block: str,
180 preprocessor: Callable =
None,
181 signature: str =
None,
182 signature_offset: int = 0,
188 preprocessor=preprocessor,
190 signature_offset=signature_offset,
193 preprocessor(stdout.decode(
"utf-8"))
195 else stdout.decode(
"utf-8")
197 stdout_lines = processed_stdout.strip().split(
"\n")
198 reference_lines = dedent(reference_block).strip().split(
"\n")
200 if signature
is None and signature_offset
is not None:
201 if signature_offset < 0:
202 signature_offset = len(reference_lines) + signature_offset
203 signature = reference_lines[signature_offset]
206 start_index = stdout_lines.index(signature)
207 end_index = start_index + len(reference_lines)
208 observed_block = stdout_lines[start_index:end_index]
210 if observed_block != reference_lines:
212 difflib.unified_diff(
219 diff_text =
"\n".join(diff)
220 record_property(
"block_diff",
CodeWrapper(diff_text,
"diff"))
221 raise AssertionError(
222 "The observed block does not match the reference."
225 raise AssertionError(
226 f
"Signature '{signature}' not found in the output."
229 return assert_function
231 @pytest.mark.do_not_collect_source
234 Test the count of error messages in the stdout against expected values.
236 expected_messages = (
237 None if reference
is None else reference.get(
"messages_count")
239 if expected_messages
is None:
242 if not isinstance(expected_messages, dict):
243 raise ValueError(
"reference['messages_count'] must be a dict")
244 if not expected_messages:
247 expected_messages = NO_ERROR_MESSAGES
248 reference[
"messages_count"] = expected_messages
250 outlines = self.preprocessor(
251 stdout.decode(
"utf-8", errors=
"backslashreplace")
254 messages = {key: []
for key
in expected_messages}
255 for n, line
in enumerate(outlines, 1):
257 if len(words) >= 2
and words[1]
in messages:
258 messages[words[1]].append((n, line.rstrip()))
260 messages_count = {key: len(value)
for key, value
in messages.items()}
262 assert messages_count == expected_messages
263 except AssertionError:
264 reference[
"messages_count"] = messages_count
265 record_property(
"unexpected_messages_count", messages)
268 @pytest.mark.do_not_collect_source
270 self, stdout: bytes, record_property: Callable, reference: Dict
273 Test the standard output against the reference.
275 out = self.preprocessor(stdout.decode(
"utf-8", errors=
"backslashreplace"))
278 @pytest.mark.do_not_collect_source
280 self, stdout: bytes, record_property: Callable, reference: Dict
283 Test the TTree summaries against the reference.
285 if not self.reference
or reference.get(
"ttrees")
is None:
290 re.compile(
r"Basket|.*size|Compression"),
293 assert ttrees == reference[
"ttrees"]
294 except AssertionError:
295 reference[
"ttrees"] = ttrees
296 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
297 pytest.xfail(
"Ignoring stdout validation")
300 @pytest.mark.do_not_collect_source
302 self, stdout: bytes, record_property: Callable, reference: Dict
305 Test the histogram summaries against the reference.
307 if not self.reference
or reference.get(
"histos")
is None:
312 re.compile(
r"Basket|.*size|Compression"),
315 assert histos == reference[
"histos"]
316 except AssertionError:
317 reference[
"histos"] = histos
318 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
319 pytest.xfail(
"Ignoring stdout validation")
322 @pytest.mark.do_not_collect_source
324 self, stderr: bytes, record_property: Callable, reference: Dict
327 Test the standard error output against the reference.
329 err = self.preprocessor(stderr.decode(
"utf-8", errors=
"backslashreplace"))
330 if self.reference
and reference.get(
"stderr")
is not None:
334 assert not err.strip(),
"Expected no standard error output, but got some."
336 @pytest.mark.do_not_collect_source