16 from math
import log10
17 from pathlib
import Path
18 from textwrap
import dedent
19 from typing
import Callable, Dict, List
28 find_histos_summaries,
35 An extension of SubprocessBaseTest tailored to the Gaudi/LHCb workflow.
36 It includes additional functionalities for handling options,
37 preprocessing output, and validating against platform-specific reference files.
40 options: Callable =
None
42 preprocessor: Callable = normalizeTestSuite
47 Override the base class to include options.
51 if hasattr(cls,
"options")
and cls.options
is not None:
57 source_lines = inspect.getsource(options).splitlines()
58 clean_source = dedent(
59 "\n".join(source_lines[1:])
61 filename = tmp_path /
"options.py"
62 with open(filename,
"w")
as file:
63 file.write(clean_source)
67 elif isinstance(options, dict):
68 filename = tmp_path /
"options.json"
69 with open(filename,
"w")
as file:
70 json.dump(options, file, indent=4)
74 elif isinstance(options, str):
75 options = dedent(options)
76 filename = tmp_path /
"options.opts"
77 with open(filename,
"w")
as file:
82 raise ValueError(f
"invalid options type '{type(options).__name__}'")
85 command.append(
str(filename))
92 preprocessor: Callable[[str], str] =
lambda x: x,
95 Compute the difference between the reference data and the current output.
98 reference_data.splitlines()
99 if hasattr(reference_data,
"splitlines")
102 actual_output = preprocessor(output).splitlines()
104 difflib.unified_diff(
119 record_property: Callable[[str, str],
None],
122 Validate the given data against a reference file for the specified key.
127 assert data == reference[key]
128 except AssertionError:
132 cls.
_output_diff(reference[key]
or "", data, cls.preprocessor),
136 reference[key] = data
137 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
138 pytest.xfail(
"Ignoring stdout validation")
141 pytest.skip(
"No reference file provided")
145 cls, output_file: str, reference_file: str, detailed=
True
148 Validate the JSON output against a reference JSON file.
150 assert os.path.isfile(output_file)
153 with open(output_file)
as f:
154 output = json.load(f)
155 except json.JSONDecodeError
as err:
156 pytest.fail(f
"json parser error in {output_file}: {err}")
159 assert lreference,
"reference file not set"
160 assert os.path.isfile(lreference)
163 with open(lreference)
as f:
164 expected = json.load(f)
165 except json.JSONDecodeError
as err:
166 pytest.fail(f
"JSON parser error in {lreference}: {err}")
169 assert output == expected
171 expected = sorted(expected, key=
lambda item: (item[
"component"], item[
"name"]))
172 output = sorted(output, key=
lambda item: (item[
"component"], item[
"name"]))
173 assert output == expected
178 reference_block: str,
179 preprocessor: Callable =
None,
180 signature: str =
None,
181 signature_offset: int = 0,
187 preprocessor=preprocessor,
189 signature_offset=signature_offset,
192 preprocessor(stdout.decode(
"utf-8"))
194 else stdout.decode(
"utf-8")
196 stdout_lines = processed_stdout.strip().split(
"\n")
197 reference_lines = dedent(reference_block).strip().split(
"\n")
199 if signature
is None and signature_offset
is not None:
200 if signature_offset < 0:
201 signature_offset = len(reference_lines) + signature_offset
202 signature = reference_lines[signature_offset]
205 start_index = stdout_lines.index(signature)
206 end_index = start_index + len(reference_lines)
207 observed_block = stdout_lines[start_index:end_index]
209 if observed_block != reference_lines:
211 difflib.unified_diff(
218 diff_text =
"\n".join(diff)
219 record_property(
"block_diff",
CodeWrapper(diff_text,
"diff"))
220 raise AssertionError(
221 "The observed block does not match the reference."
224 raise AssertionError(
225 f
"Signature '{signature}' not found in the output."
228 return assert_function
232 expected: Dict = {
"ERROR": 0,
"FATAL": 0}, stdout: str =
None
238 outlines = stdout.splitlines()
240 fmt =
"%%%dd - %%s" % (
int(log10(len(outlines) + 1)))
246 if len(words) >= 2
and words[1]
in errors:
247 errors[words[1]].append(fmt % (linecount, l.rstrip()))
250 assert len(errors[e]) == expected[e]
252 @pytest.mark.do_not_collect_source
254 self, stdout: bytes, record_property: Callable, reference: Dict
257 Test the standard output against the reference.
259 out = self.preprocessor(stdout.decode(
"utf-8", errors=
"backslashreplace"))
262 @pytest.mark.do_not_collect_source
264 self, stdout: bytes, record_property: Callable, reference: Dict
267 Test the TTree summaries against the reference.
269 if not self.reference
or reference.get(
"ttrees")
is None:
274 re.compile(
r"Basket|.*size|Compression"),
277 assert ttrees == reference[
"ttrees"]
278 except AssertionError:
279 reference[
"ttrees"] = ttrees
280 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
281 pytest.xfail(
"Ignoring stdout validation")
284 @pytest.mark.do_not_collect_source
286 self, stdout: bytes, record_property: Callable, reference: Dict
289 Test the histogram summaries against the reference.
291 if not self.reference
or reference.get(
"histos")
is None:
296 re.compile(
r"Basket|.*size|Compression"),
299 assert histos == reference[
"histos"]
300 except AssertionError:
301 reference[
"histos"] = histos
302 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
303 pytest.xfail(
"Ignoring stdout validation")
306 @pytest.mark.do_not_collect_source
308 self, stderr: bytes, record_property: Callable, reference: Dict
311 Test the standard error output against the reference.
313 err = self.preprocessor(stderr.decode(
"utf-8", errors=
"backslashreplace"))
314 if self.reference
and reference.get(
"stderr")
is not None:
318 assert not err.strip(),
"Expected no standard error output, but got some."
320 @pytest.mark.do_not_collect_source