Belle II Software  release-08-01-10
test_variables_meta.py
1 #!/usr/bin/env python3
2 
3 
10 import re
11 import logging
12 import subprocess
13 import unittest
14 
15 import basf2
16 
17 
18 def findMatchedParenthesis(string: str, openchar: str, closechar: str) -> int:
19  """Find matching control token in string.
20 
21  Args:
22  string (str): input
23  openchar (str): opening char e.g '{'
24  closechar (str): closing char e.g '}'
25 
26  Returns:
27  int: position of matching closing char in string.
28  """
29  end = 1
30  if string[0] == openchar:
31  count = 1
32  while end < len(string) and count > 0:
33  if string[end] == openchar:
34  count += 1
35  elif string[end] == closechar:
36  count -= 1
37  end += 1
38  return end - 1
39 
40 
41 class MetavariableDataTypeTest(unittest.TestCase):
42  """
43  Determine metavariable types from source code and assert correctness
44  """
45 
46 
48  hardcoded = {
49  "softwareTriggerResult": "double",
50  "formula": "double",
51  "softwareTriggerResultNonPrescaled": "double",
52  "isDaughterOfList": "bool",
53  "isGrandDaughterOfList": "bool",
54  "daughterDiffOf": "double",
55  "daughterDiffOfPhi": "double",
56  "daughterDiffOfClusterPhi": "double",
57  "mcDaughterDiffOfPhi": "double",
58  "grandDaughterDiffOfPhi": "double",
59  "grandDaughterDiffOfClusterPhi": "double",
60  "daughterDiffOfPhiCMS": "double",
61  "daughterDiffOfClusterPhiCMS": "double",
62  }
63 
64  # regular expressions
65 
66  registering_regex = re.compile(
67  r"(?s)REGISTER_METAVARIABLE.*?Manager::VariableDataType::(?:c_double|c_int|c_bool)" # noqa
68  )
69 
70  extract_regex = re.compile(
71  r'REGISTER_METAVARIABLE\‍(".*?",(?P<function_name>[^,]*),.*?Manager::VariableDataType::c_(?P<enumtype>double|bool|int)' # noqa
72  )
73 
74  lambda_type_regex = re.compile(r"-> (?P<lambdatype>double|bool|int)")
75 
76  def process_file(self, filepath: str) -> int:
77  """Check all metavariable types for specified file.
78 
79  Args:
80  filepath (str): path to file containing REGISTER_METAVARIABLE
81 
82  Raises:
83  AssertionError: Raised if no expected function definition is found.
84  AssertionError: Rased if lambda function has no associated
85  type information, or no lambda function is defined.
86 
87  Returns:
88  int: number of metavariables in file.
89  Used for sanity checks of the coverage.
90  """
91 
92  # Read file contents
93  with open(filepath) as fp:
94  filecontent = fp.read()
95 
96  # List for all found registering statements
97  registering_statements = self.registering_regexregistering_regex.findall(filecontent)
98  # Preprocess all statements by removing spaces and newlines
99  # This makes extraction of function name and enum type easier
100  registering_statements = list(
101  map(
102  lambda line: line.replace(" ", "").replace("\n", ""),
103  registering_statements,
104  ),
105  )
106 
107  for statement in registering_statements:
108  # Extract enum type and name
109  match_content = self.extract_regexextract_regex.match(statement)
110  function_name = match_content.groupdict()["function_name"]
111  enumtype = match_content.groupdict()["enumtype"]
112 
113  if function_name in self.hardcodedhardcoded:
114  self.assertEqual(
115  enumtype,
116  self.hardcodedhardcoded[function_name],
117  f"Metavariable '{function_name}' in file {filepath}:\n"
118  f"Metavariable function return type and Manager::VariableDataType have to match.\n" # noqa
119  f"Expected: Manager::VariableDataType::{self.hardcoded[function_name]}, actual: Manager::VariableDataType::{enumtype}", # noqa
120  )
121  continue
122 
123  # compile regex for finding metavariable function definition
124  function_definition_regex = re.compile(
125  r"Manager::FunctionPtr %s\‍(.*\‍)[^\{]*" % function_name
126  )
127  # compile regex for regular typed function definition
128  regular_definition_regex = re.compile(
129  r"(?P<return_type>double|int|bool) %s\‍(.*?\‍)" % function_name
130  )
131 
132  # Find function body start with regex
133  definition = function_definition_regex.search(filecontent)
134  if definition is not None:
135  func_body_start = definition.end()
136  else: # Manager::FunctionPtr definition not found
137  # search for a regular typed function
138  return_type = (
139  regular_definition_regex.search(filecontent)
140  .groupdict()
141  .get("return_type")
142  ) # noqa
143  if return_type is not None:
144  self.assertEqual(
145  return_type,
146  enumtype,
147  f"Metavariable '{function_name}' in file {filepath}:\n"
148  "Metavariable function return type and Manager::VariableDataType have to match." # noqa
149  f"Return type is {return_type} but it is registered as Manager::VariableDataType::c_{enumtype}.\n", # noqa
150  )
151  continue
152  else:
153  raise AssertionError(
154  f"Metavariable '{function_name}' in file {filepath}:\n"
155  "Metavariable function return type and Manager::VariableDataType have to match." # noqa
156  "Return type of function could not be automatically determined from the source code." # noqa
157  "You can add an exception by adding the expected return type information to " # noqa
158  "the 'hardcoded' dictionary of this testcase."
159  )
160 
161  # grab function body end
162  func_body_end = func_body_start + findMatchedParenthesis(
163  filecontent[func_body_start:], "{", "}"
164  )
165  # Get function body slice from filecontent
166  func_body = filecontent[func_body_start:func_body_end]
167 
168  # grab lambda type definition
169  lambdatype_match = self.lambda_type_regexlambda_type_regex.search(func_body)
170  if lambdatype_match is not None:
171  lambdatype = lambdatype_match.groupdict()["lambdatype"]
172  self.assertEqual(
173  lambdatype,
174  enumtype,
175  f"Metavariable '{function_name}' in file {filepath}:\n"
176  f"Lambda function has return type {lambdatype} "
177  f"but is registered with Manager::VariableDataType::c_{enumtype}.\n" # noqa
178  "VariableDataType and lambda return type have to match.",
179  )
180  else: # lambda type or definition not found
181  raise AssertionError(
182  f"Metavariable '{function_name}' in {filepath}:\n"
183  "VariableDataType and lambda definition have to match.\n"
184  "Either lambda function is missing return type information"
185  ", or lambda definition could not be found.\n" # noqa
186  "Please add return type annotation '(const Particle * particle) -> double/int/bool' to lambda.\n" # noqa
187  "Or add this metavariable as exception, by adding the expected return type information in the 'hardcoded' dictionary of this testcase\n" # noqa
188  f"{func_body}"
189  )
190 
191  # Return the number of registering statements in this file
192  return len(registering_statements)
193 
195  """Metavariables have to be registered with the correct Manager::Variable::VariableDataType enum value. This test makes sure Metavariable definition and variable registration are correct.""" # noqa
196  # check if grep is available
197  try:
198  subprocess.run(["grep", "-V"], check=True, capture_output=True)
199  except subprocess.CalledProcessError:
200  logging.basicConfig(format="%(message)s")
201  logging.error(
202  "TEST SKIPPED: MetavariableDataTypeTest skipped because grep is not available." # noqa
203  )
204  self.fail()
205 
206  # Use grep to find files with REGISTER_METAVARIABLE statements
207  analysis_module = basf2.find_file("analysis")
208  files = subprocess.run(
209  [
210  "grep",
211  "REGISTER_METAVARIABLE",
212  "-r",
213  analysis_module,
214  "-I",
215  "-l",
216  ],
217  capture_output=True,
218  )
219  # Decode stdout and extract filenames
220  files = files.stdout.decode().split("\n")
221  files = list(filter(lambda file: file.endswith(".cc"), files))
222 
223  num_files = len(files)
224  print(f"Number of files including meta-variables is {num_files}")
225 
226  # There should be at least 13 files
227  self.assertGreaterEqual(num_files, 13)
228  # We track the number of metavariables to make sure we don't miss some
229  num_metavariables = 0
230  for filepath in files:
231  num_metavariables += self.process_fileprocess_file(filepath)
232 
233  # We should get at least 229 registering statements
234  print(f"Number of meta-variables is {num_metavariables}")
235  self.assertGreaterEqual(num_metavariables, 229)
236 
237 
238 if __name__ == "__main__":
239  unittest.main()
dictionary hardcoded
we have to hardcode some special cases as data types can't be obtained from source code directly
extract_regex
regex for extracting the function name and the enum type from REGISTER_METAVARIABLE statements # noqa
lambda_type_regex
regex for extracting the type of the lambda function in metavariable function definition # noqa
registering_regex
regex for finding the REGISTER_METAVARIABLE statements.
unsigned long int findMatchedParenthesis(std::string str, char open='[', char close=']')
Returns position of the matched closing parenthesis if the first character in the given string contai...
Definition: CutHelpers.cc:33
std::map< ExpRun, std::pair< double, double > > filter(const std::map< ExpRun, std::pair< double, double >> &runs, double cut, std::map< ExpRun, std::pair< double, double >> &runsRemoved)
filter events to remove runs shorter than cut, it stores removed runs in runsRemoved
Definition: Splitter.cc:38