17 from pathlib
import Path
18 from textwrap
import dedent
19 from typing
import Callable, Dict, List
28 find_histos_summaries,
32 NO_ERROR_MESSAGES = {
"ERROR": 0,
"FATAL": 0}
37 An extension of SubprocessBaseTest tailored to the Gaudi/LHCb workflow.
38 It includes additional functionalities for handling options,
39 preprocessing output, and validating against platform-specific reference files.
42 options: Callable =
None
44 preprocessor: Callable = normalizeTestSuite
49 Override the base class to include options.
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}"
57 if hasattr(cls,
"options")
and cls.options
is not None:
63 source_lines = inspect.getsource(options).splitlines()
64 clean_source = dedent(
65 "\n".join(source_lines[1:])
67 filename = generate_unique_options_filename(
"py", tmp_path)
68 with open(filename,
"w")
as file:
69 file.write(clean_source)
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)
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:
88 raise ValueError(f
"invalid options type '{type(options).__name__}'")
91 command.append(
str(filename))
98 preprocessor: Callable[[str], str] =
lambda x: x,
101 Compute the difference between the reference data and the current output.
104 reference_data.splitlines()
105 if hasattr(reference_data,
"splitlines")
108 actual_output = preprocessor(output).splitlines()
110 difflib.unified_diff(
125 record_property: Callable[[str, str],
None],
128 Validate the given data against a reference file for the specified key.
141 if len(data) < 100000:
142 assert data == reference[key]
144 same_as_reference = data == reference[key]
145 assert same_as_reference,
"data is different from reference"
146 except AssertionError:
150 cls.
_output_diff(reference[key]
or "", data, cls.preprocessor),
154 reference[key] = data
155 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
156 pytest.xfail(
"Ignoring stdout validation")
159 pytest.skip(
"No reference file provided")
163 cls, output_file: str, reference_file: str, detailed=
True
166 Validate the JSON output against a reference JSON file.
168 assert os.path.isfile(output_file)
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}")
177 assert lreference,
"reference file not set"
178 assert os.path.isfile(lreference)
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}")
187 assert output == expected
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
196 reference_block: str,
197 preprocessor: Callable =
None,
198 signature: str =
None,
199 signature_offset: int = 0,
205 preprocessor=preprocessor,
207 signature_offset=signature_offset,
210 preprocessor(stdout.decode(
"utf-8"))
212 else stdout.decode(
"utf-8")
214 stdout_lines = processed_stdout.strip().split(
"\n")
215 reference_lines = dedent(reference_block).strip().split(
"\n")
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]
223 start_index = stdout_lines.index(signature)
224 end_index = start_index + len(reference_lines)
225 observed_block = stdout_lines[start_index:end_index]
227 if observed_block != reference_lines:
229 difflib.unified_diff(
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."
242 raise AssertionError(
243 f
"Signature '{signature}' not found in the output."
246 return assert_function
248 @pytest.mark.do_not_collect_source
251 Test the count of error messages in the stdout against expected values.
253 expected_messages = (
254 None if reference
is None else reference.get(
"messages_count")
256 if expected_messages
is None:
259 if not isinstance(expected_messages, dict):
260 raise ValueError(
"reference['messages_count'] must be a dict")
261 if not expected_messages:
264 expected_messages = NO_ERROR_MESSAGES
265 reference[
"messages_count"] = expected_messages
267 outlines = self.preprocessor(
268 stdout.decode(
"utf-8", errors=
"backslashreplace")
271 messages = {key: []
for key
in expected_messages}
272 for n, line
in enumerate(outlines, 1):
274 if len(words) >= 2
and words[1]
in messages:
275 messages[words[1]].append((n, line.rstrip()))
277 messages_count = {key: len(value)
for key, value
in messages.items()}
279 assert messages_count == expected_messages
280 except AssertionError:
281 reference[
"messages_count"] = messages_count
282 record_property(
"unexpected_messages_count", messages)
285 @pytest.mark.do_not_collect_source
287 self, stdout: bytes, record_property: Callable, reference: Dict
290 Test the standard output against the reference.
292 out = self.preprocessor(stdout.decode(
"utf-8", errors=
"backslashreplace"))
295 @pytest.mark.do_not_collect_source
297 self, stdout: bytes, record_property: Callable, reference: Dict
300 Test the TTree summaries against the reference.
302 if not self.reference
or reference.get(
"ttrees")
is None:
307 re.compile(
r"Basket|.*size|Compression"),
310 assert ttrees == reference[
"ttrees"]
311 except AssertionError:
312 reference[
"ttrees"] = ttrees
313 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
314 pytest.xfail(
"Ignoring stdout validation")
317 @pytest.mark.do_not_collect_source
319 self, stdout: bytes, record_property: Callable, reference: Dict
322 Test the histogram summaries against the reference.
324 if not self.reference
or reference.get(
"histos")
is None:
329 re.compile(
r"Basket|.*size|Compression"),
332 assert histos == reference[
"histos"]
333 except AssertionError:
334 reference[
"histos"] = histos
335 if os.environ.get(
"GAUDI_TEST_IGNORE_STDOUT_VALIDATION") ==
"1":
336 pytest.xfail(
"Ignoring stdout validation")
339 @pytest.mark.do_not_collect_source
341 self, stderr: bytes, record_property: Callable, reference: Dict
344 Test the standard error output against the reference.
346 err = self.preprocessor(stderr.decode(
"utf-8", errors=
"backslashreplace"))
347 if self.reference
and reference.get(
"stderr")
is not None:
351 assert not err.strip(),
"Expected no standard error output, but got some."
353 @pytest.mark.do_not_collect_source