The Gaudi Framework  master (37c0b60a)
GaudiExeTest.py
Go to the documentation of this file.
1 
11 import difflib
12 import inspect
13 import json
14 import os
15 import re
16 from pathlib import Path
17 from textwrap import dedent
18 from typing import Callable, Dict, List
19 
20 import pytest
21 
22 from GaudiTesting.preprocessors import normalizeTestSuite
23 from GaudiTesting.SubprocessBaseTest import SubprocessBaseTest
24 from GaudiTesting.utils import (
25  CodeWrapper,
26  filter_dict,
27  find_histos_summaries,
28  find_ttree_summaries,
29 )
30 
31 NO_ERROR_MESSAGES = {"ERROR": 0, "FATAL": 0}
32 
33 
35  """
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.
39  """
40 
41  options: Callable = None
42  options_code = None
43  preprocessor: Callable = normalizeTestSuite
44 
45  @classmethod
46  def _prepare_command(cls, tmp_path=Path()) -> List[str]:
47  """
48  Override the base class to include options.
49  """
50  command = super()._prepare_command(tmp_path=tmp_path)
51 
52  if hasattr(cls, "options") and cls.options is not None:
53  options = cls.options
54  filename = None
55 
56  # Check if options is a function
57  if callable(options):
58  source_lines = inspect.getsource(options).splitlines()
59  clean_source = dedent(
60  "\n".join(source_lines[1:])
61  ) # Skip the first line (def options():)
62  filename = tmp_path / "options.py"
63  with open(filename, "w") as file:
64  file.write(clean_source)
65  cls.options_code = CodeWrapper(clean_source, "python")
66 
67  # Check if options is a dictionary
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)
72  cls.options_code = CodeWrapper(options, "json")
73 
74  # Check if options is a string
75  elif isinstance(options, str):
76  options = dedent(options)
77  filename = tmp_path / "options.opts"
78  with open(filename, "w") as file:
79  file.write(options)
80  cls.options_code = CodeWrapper(options, "cpp")
81 
82  else:
83  raise ValueError(f"invalid options type '{type(options).__name__}'")
84 
85  if filename:
86  command.append(str(filename))
87  return command
88 
89  @staticmethod
91  reference_data: dict,
92  output: str,
93  preprocessor: Callable[[str], str] = lambda x: x,
94  ) -> str:
95  """
96  Compute the difference between the reference data and the current output.
97  """
98  expected_output = (
99  reference_data.splitlines()
100  if hasattr(reference_data, "splitlines")
101  else reference_data
102  )
103  actual_output = preprocessor(output).splitlines()
104  return "\n".join(
105  difflib.unified_diff(
106  expected_output,
107  actual_output,
108  fromfile="expected",
109  tofile="actual",
110  lineterm="",
111  )
112  )
113 
114  @classmethod
116  cls,
117  data: bytes,
118  key: str,
119  reference: Dict,
120  record_property: Callable[[str, str], None],
121  ) -> None:
122  """
123  Validate the given data against a reference file for the specified key.
124  """
125  if cls.reference:
126  try:
127  if key in reference:
128  assert data == reference[key]
129  except AssertionError:
130  record_property(
131  f"{key}_diff",
132  CodeWrapper(
133  cls._output_diff(reference[key] or "", data, cls.preprocessor),
134  "diff",
135  ),
136  )
137  reference[key] = data
138  if os.environ.get("GAUDI_TEST_IGNORE_STDOUT_VALIDATION") == "1":
139  pytest.xfail("Ignoring stdout validation")
140  raise
141  else:
142  pytest.skip("No reference file provided")
143 
144  @classmethod
146  cls, output_file: str, reference_file: str, detailed=True
147  ):
148  """
149  Validate the JSON output against a reference JSON file.
150  """
151  assert os.path.isfile(output_file)
152 
153  try:
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}")
158 
159  lreference = cls.resolve_path(reference_file)
160  assert lreference, "reference file not set"
161  assert os.path.isfile(lreference)
162 
163  try:
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}")
168 
169  if not detailed:
170  assert output == expected
171 
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
175 
176  @classmethod
178  cls,
179  reference_block: str,
180  preprocessor: Callable = None,
181  signature: str = None,
182  signature_offset: int = 0,
183  ):
184  def assert_function(
185  cls,
186  stdout,
187  record_property,
188  preprocessor=preprocessor,
189  signature=signature,
190  signature_offset=signature_offset,
191  ):
192  processed_stdout = (
193  preprocessor(stdout.decode("utf-8"))
194  if preprocessor
195  else stdout.decode("utf-8")
196  )
197  stdout_lines = processed_stdout.strip().split("\n")
198  reference_lines = dedent(reference_block).strip().split("\n")
199 
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]
204 
205  try:
206  start_index = stdout_lines.index(signature)
207  end_index = start_index + len(reference_lines)
208  observed_block = stdout_lines[start_index:end_index]
209 
210  if observed_block != reference_lines:
211  diff = list(
212  difflib.unified_diff(
213  reference_lines,
214  observed_block,
215  fromfile="expected",
216  tofile="actual",
217  )
218  )
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."
223  )
224  except ValueError:
225  raise AssertionError(
226  f"Signature '{signature}' not found in the output."
227  )
228 
229  return assert_function
230 
231  @pytest.mark.do_not_collect_source
232  def test_count_messages(self, reference, stdout, record_property):
233  """
234  Test the count of error messages in the stdout against expected values.
235  """
236  expected_messages = (
237  None if reference is None else reference.get("messages_count")
238  )
239  if expected_messages is None:
240  pytest.skip()
241 
242  if not isinstance(expected_messages, dict):
243  raise ValueError("reference['messages_count'] must be a dict")
244  if not expected_messages:
245  # an empty dict doesn't make sense, let's assume we are bootstrapping the reference
246  # and start from the default
247  expected_messages = NO_ERROR_MESSAGES
248  reference["messages_count"] = expected_messages
249 
250  outlines = self.preprocessor(
251  stdout.decode("utf-8", errors="backslashreplace")
252  ).splitlines()
253 
254  messages = {key: [] for key in expected_messages}
255  for n, line in enumerate(outlines, 1):
256  words = line.split()
257  if len(words) >= 2 and words[1] in messages:
258  messages[words[1]].append((n, line.rstrip()))
259 
260  messages_count = {key: len(value) for key, value in messages.items()}
261  try:
262  assert messages_count == expected_messages
263  except AssertionError:
264  reference["messages_count"] = messages_count
265  record_property("unexpected_messages_count", messages)
266  raise
267 
268  @pytest.mark.do_not_collect_source
270  self, stdout: bytes, record_property: Callable, reference: Dict
271  ) -> None:
272  """
273  Test the standard output against the reference.
274  """
275  out = self.preprocessor(stdout.decode("utf-8", errors="backslashreplace"))
276  self.validate_with_reference(out, "stdout", reference, record_property)
277 
278  @pytest.mark.do_not_collect_source
280  self, stdout: bytes, record_property: Callable, reference: Dict
281  ) -> None:
282  """
283  Test the TTree summaries against the reference.
284  """
285  if not self.reference or reference.get("ttrees") is None:
286  pytest.skip()
287 
288  ttrees = filter_dict(
289  find_ttree_summaries(stdout.decode()),
290  re.compile(r"Basket|.*size|Compression"),
291  )
292  try:
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")
298  raise
299 
300  @pytest.mark.do_not_collect_source
302  self, stdout: bytes, record_property: Callable, reference: Dict
303  ) -> None:
304  """
305  Test the histogram summaries against the reference.
306  """
307  if not self.reference or reference.get("histos") is None:
308  pytest.skip()
309 
310  histos = filter_dict(
311  find_histos_summaries(stdout.decode()),
312  re.compile(r"Basket|.*size|Compression"),
313  )
314  try:
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")
320  raise
321 
322  @pytest.mark.do_not_collect_source
324  self, stderr: bytes, record_property: Callable, reference: Dict
325  ) -> None:
326  """
327  Test the standard error output against the reference.
328  """
329  err = self.preprocessor(stderr.decode("utf-8", errors="backslashreplace"))
330  if self.reference and reference.get("stderr") is not None:
331  self.validate_with_reference(err, "stderr", reference, record_property)
332 
333  else:
334  assert not err.strip(), "Expected no standard error output, but got some."
335 
336  @pytest.mark.do_not_collect_source
337  def test_record_options(self, record_property: Callable):
338  if self.options_code:
339  record_property("options", self.options_code)
GaudiTesting.GaudiExeTest.GaudiExeTest._output_diff
str _output_diff(dict reference_data, str output, Callable[[str], str] preprocessor=lambda x:x)
Definition: GaudiExeTest.py:90
GaudiTesting.GaudiExeTest.GaudiExeTest
Definition: GaudiExeTest.py:34
GaudiTesting.utils.find_ttree_summaries
def find_ttree_summaries(stdout)
Definition: utils.py:374
GaudiTesting.GaudiExeTest.GaudiExeTest.options_code
options_code
Definition: GaudiExeTest.py:42
GaudiTesting.GaudiExeTest.GaudiExeTest.test_histos
None test_histos(self, bytes stdout, Callable record_property, Dict reference)
Definition: GaudiExeTest.py:301
GaudiTesting.GaudiExeTest.GaudiExeTest.test_stdout
None test_stdout(self, bytes stdout, Callable record_property, Dict reference)
Definition: GaudiExeTest.py:269
GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.str
str
Definition: SubprocessBaseTest.py:41
GaudiTesting.GaudiExeTest.GaudiExeTest.test_count_messages
def test_count_messages(self, reference, stdout, record_property)
Definition: GaudiExeTest.py:232
GaudiTesting.SubprocessBaseTest.SubprocessBaseTest.resolve_path
str resolve_path(cls, Union[Path, str] path)
Definition: SubprocessBaseTest.py:53
GaudiTesting.SubprocessBaseTest.SubprocessBaseTest
Definition: SubprocessBaseTest.py:33
GaudiTesting.utils
Definition: utils.py:1
GaudiTesting.GaudiExeTest.GaudiExeTest.find_reference_block
def find_reference_block(cls, str reference_block, Callable preprocessor=None, str signature=None, int signature_offset=0)
Definition: GaudiExeTest.py:177
GaudiTesting.SubprocessBaseTest
Definition: SubprocessBaseTest.py:1
GaudiTesting.GaudiExeTest.GaudiExeTest.test_ttrees
None test_ttrees(self, bytes stdout, Callable record_property, Dict reference)
Definition: GaudiExeTest.py:279
GaudiTesting.GaudiExeTest.GaudiExeTest.test_record_options
def test_record_options(self, Callable record_property)
Definition: GaudiExeTest.py:337
GaudiTesting.GaudiExeTest.GaudiExeTest.validate_with_reference
None validate_with_reference(cls, bytes data, str key, Dict reference, Callable[[str, str], None] record_property)
Definition: GaudiExeTest.py:115
GaudiTesting.GaudiExeTest.GaudiExeTest._prepare_command
List[str] _prepare_command(cls, tmp_path=Path())
Definition: GaudiExeTest.py:46
GaudiTesting.utils.find_histos_summaries
def find_histos_summaries(stdout)
Definition: utils.py:351
GaudiTesting.utils.CodeWrapper
Definition: utils.py:22
GaudiTesting.preprocessors
Definition: preprocessors.py:1
GaudiTesting.GaudiExeTest.GaudiExeTest.validate_json_with_reference
def validate_json_with_reference(cls, str output_file, str reference_file, detailed=True)
Definition: GaudiExeTest.py:145
GaudiTesting.GaudiExeTest.GaudiExeTest.test_stderr
None test_stderr(self, bytes stderr, Callable record_property, Dict reference)
Definition: GaudiExeTest.py:323
GaudiTesting.utils.filter_dict
Dict[str, Any] filter_dict(Dict[str, Any] d, re.Pattern ignore_re)
Definition: utils.py:166