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