ATLAS Offline Software
chainComp.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 #
3 # Copyright (C) 2002-2020 CERN for the benefit of the ATLAS collaboration
4 #
5 
6 '''
7 Script to compare trigger chain counts in YAML format produced by
8 chainDump.py --yaml and providing information and instructions for
9 updating counts reference in the athena repository.
10 '''
11 
12 import sys
13 import os
14 import argparse
15 import logging
16 import yaml
17 import glob
18 import subprocess
19 from AthenaCommon.Utils import unixtools
20 from TrigValTools.TrigValSteering.Common import package_prefix_dict as packages
21 from TrigValTools.TrigValSteering.Common import running_in_CI
22 
23 
24 def get_parser():
25  parser = argparse.ArgumentParser(usage='%(prog)s [options] inputFile',
26  description=__doc__)
27  parser.add_argument('inputFile',
28  metavar='PATH',
29  help='Name of input counts file')
30  parser.add_argument('-r', '--referenceFile',
31  metavar='PATH',
32  help='Name of reference counts file. If empty, try to guess from inputFile')
33  parser.add_argument('-s', '--sourceRepository',
34  metavar='PATH',
35  help='Path to the git checkout of the athena repository.'
36  'If empty, try to guess from the environment')
37  parser.add_argument('-p', '--patch',
38  action='store_true',
39  help='Create a git patch file with the count differences')
40  parser.add_argument('-m', '--matchingOnly',
41  action='store_true',
42  help='Ignore added and removed chains, and only compare counts for chains ' + \
43  'present in both reference and input file')
44  parser.add_argument('-v', '--verbose',
45  action='store_true',
46  help='Increase output verbosity')
47  return parser
48 
49 
50 def find_reference(inputFile):
51  ref_path = None
52  if inputFile.endswith('.new'):
53  inp_name = os.path.split(inputFile)[-1]
54  ref_name = inp_name[0:-4] + '.ref'
55  logging.debug('Searching for file %s in DATAPATH', ref_name)
56  for pkg_name in packages:
57  ref_path = unixtools.find_datafile(pkg_name+'/'+ref_name)
58  if ref_path:
59  break
60  if ref_path:
61  logging.debug('Found %s', ref_path)
62  else:
63  logging.debug('Not found')
64  return ref_path
65 
66 
67 def find_repository(ref_path):
68  '''
69  Try to find the athena git repository based on paths in env.
70  Needs to work both in local WorkDir build and in full/incremental CI build.
71  '''
72  logging.debug('Searching for the source repository path')
73  search_str = '/Trigger/TrigValidation'
74  def try_path(p):
75  logging.debug('Trying %s', p)
76  if os.path.islink(p) and os.path.isfile(p) and search_str in os.path.realpath(p):
77  candidate = os.path.realpath(p).split(search_str)[0]
78  logging.debug('Candidate %s', candidate)
79  if os.path.isdir(candidate + '/.git'):
80  return candidate
81  return None
82 
83  result = try_path(ref_path)
84  if result:
85  return result
86 
87  env_vars = ['DATAPATH', 'JOBOPTSEARCHPATH', 'CALIBPATH', 'XMLPATH']
88  path_lists = [os.getenv(var).split(os.pathsep) for var in env_vars]
89  max_len = max([len(pl) for pl in path_lists])
90  max_len = min(max_len, 2) # Limit to first two paths to avoid very deep and slow search
91  for i in range(max_len):
92  for pl in path_lists:
93  if i >= len(pl):
94  continue
95  paths = glob.glob(pl[i]+'/**', recursive=True)
96  for p in paths:
97  result = try_path(p)
98  if result:
99  return result
100  return None
101 
102 
103 def load_yaml(path):
104  result = None
105  if not os.path.isfile(path):
106  logging.error('Not a file: %s', path)
107  return result
108  with open(path) as f:
109  result = yaml.safe_load(f)
110  return result
111 
112 
113 def print_event_diff(inp_data, ref_data, key):
114  if inp_data[key] != ref_data[key]:
115  logging.info(' %s: %d -> %d', key, ref_data[key], inp_data[key])
116 
117 
118 def print_step_diff(inp_data, ref_data, key):
119  inp_steps = inp_data.get(key, dict())
120  ref_steps = ref_data.get(key, dict())
121  if inp_steps != ref_steps:
122  logging.info(' %s:', key)
123  for step in range(max(len(inp_steps), len(ref_steps))):
124  inp_count = inp_steps.get(step, 0)
125  ref_count = ref_steps.get(step, 0)
126  if inp_count != ref_count:
127  logging.info(' %d: %d -> %d', step, ref_count, inp_count)
128 
129 
130 def print_diff(inp_dict, ref_dict, matching_only=False):
131  retcode = 0
132  chains_not_in_ref = [c[0] for c in inp_dict.items() if c[0] not in ref_dict]
133  chains_not_in_inp = [c[0] for c in ref_dict.items() if c[0] not in inp_dict]
134  chains_count_diff = [c[0] for c in inp_dict.items() if c[0] in ref_dict and c[1] != ref_dict[c[0]]]
135 
136  # New chains (missing in reference)
137  n_new_chains = len(chains_not_in_ref)
138  if n_new_chains > 0 and not matching_only:
139  retcode = 1
140  logging.info('Found %d new chain%s added:', n_new_chains, 's' if n_new_chains > 1 else '')
141  for chain_name in chains_not_in_ref:
142  logging.info(' %s', chain_name)
143 
144  # Removed chains (missing in test file)
145  n_removed_chains = len(chains_not_in_inp)
146  if n_removed_chains > 0 and not matching_only:
147  retcode = 1
148  logging.info('Found %d chain%s removed:', n_removed_chains, 's' if n_removed_chains > 1 else '')
149  for chain_name in chains_not_in_inp:
150  logging.info(' %s', chain_name)
151 
152  # Count differences
153  n_diff_chains = len(chains_count_diff)
154  if n_diff_chains > 0:
155  retcode = 1
156  logging.info('Found %d chain%s with count differences:', n_diff_chains, 's' if n_diff_chains > 1 else '')
157  for chain_name in chains_count_diff:
158  logging.info(' %s:', chain_name)
159  inp_data = inp_dict[chain_name]
160  ref_data = ref_dict[chain_name]
161  print_event_diff(inp_data, ref_data, 'eventCount')
162  print_step_diff(inp_data, ref_data, 'stepCounts')
163  print_step_diff(inp_data, ref_data, 'stepFeatures')
164 
165  return retcode
166 
167 
168 def create_patch(inp_path, ref_path):
169  def find_ref_package():
170  for path in [ref_path, find_reference(inp_path), find_reference(ref_path)]:
171  for pkg in packages:
172  if '/data/'+pkg in ref_path:
173  return pkg, os.path.split(path)[1]
174  return None, None
175  package_name, ref_file_name = find_ref_package()
176  if not package_name:
177  logging.warning('Failed to find reference package name, cannot create git patch file')
178  return
179 
180  ref_rel_path = '/Trigger/TrigValidation/' + package_name + '/share/' + ref_file_name
181 
182  diff_cmd = 'diff -U 1 -b'
183  diff_cmd += ' --label a' + ref_rel_path + ' ' + ref_path
184  diff_cmd += ' --label b' + ref_rel_path + ' ' + inp_path
185 
186  diff_txt = subprocess.getoutput(diff_cmd)
187  patch_name = os.path.split(ref_path)[1]
188  if patch_name.endswith('.ref') or patch_name.endswith('.new'):
189  patch_name = patch_name[0:-4]
190  patch_name += '.patch'
191  with open(patch_name, 'w') as patch_file:
192  patch_file.write('diff --git a{p:s} b{p:s}\n'.format(p=ref_rel_path))
193  patch_file.write(diff_txt + '\n')
194  logging.info('Patch file created. To apply, run in the athena source directory:')
195  logging.info('git apply %s/%s', os.getcwd(), patch_name)
196  logging.info('Then check with git diff and, if everything is correct, add and commit')
197  if running_in_CI():
198  logging.info('or see the patch below:\n')
199  subprocess.call('cat ' + patch_name, shell=True)
200  print('\n') # noqa: ATL901
201 
202 
203 def main():
204  args = get_parser().parse_args()
205  logging.basicConfig(stream=sys.stdout,
206  format='chainComp %(levelname)-8s %(message)s',
207  level=logging.DEBUG if args.verbose else logging.INFO)
208 
209  inp_path = args.inputFile
210  ref_path = args.referenceFile or find_reference(inp_path)
211  if not ref_path:
212  logging.error('No reference file specified and couldn\'t guess from '
213  'input name. Please use the -r argument (see --help).')
214  return 1
215 
216  logging.info('Test file: %s', inp_path)
217  logging.info('Reference file: %s', ref_path)
218 
219  # Currently not used, but ready for implementing checks against upstream
220  # src_path = args.sourceRepository or find_repository(ref_path)
221  # logging.info('Repository path: %s', src_path)
222 
223  inp_dict = load_yaml(inp_path)
224  ref_dict = load_yaml(ref_path)
225  if (not inp_dict) or (not ref_dict):
226  return 1
227 
228  retcode = print_diff(inp_dict, ref_dict, args.matchingOnly)
229  if retcode:
230  logging.error('Trigger counts differ from the reference. If the above differences are intended, update the reference')
231  if args.patch:
232  create_patch(inp_path, ref_path)
233  else:
234  logging.info('Trigger counts match the reference')
235 
236  return retcode
237 
238 
239 if '__main__' in __name__:
240  sys.exit(main())
max
#define max(a, b)
Definition: cfImp.cxx:41
vtune_athena.format
format
Definition: vtune_athena.py:14
chainComp.print_diff
def print_diff(inp_dict, ref_dict, matching_only=False)
Definition: chainComp.py:130
chainComp.create_patch
def create_patch(inp_path, ref_path)
Definition: chainComp.py:168
chainComp.find_reference
def find_reference(inputFile)
Definition: chainComp.py:50
chainComp.print_event_diff
def print_event_diff(inp_data, ref_data, key)
Definition: chainComp.py:113
python.TrigValSteering.Common.running_in_CI
def running_in_CI()
Definition: Common.py:78
chainComp.main
def main()
Definition: chainComp.py:203
chainComp.get_parser
def get_parser()
Definition: chainComp.py:24
chainComp.find_repository
def find_repository(ref_path)
Definition: chainComp.py:67
plotBeamSpotVxVal.range
range
Definition: plotBeamSpotVxVal.py:195
min
#define min(a, b)
Definition: cfImp.cxx:40
chainComp.print_step_diff
def print_step_diff(inp_data, ref_data, key)
Definition: chainComp.py:118
Trk::open
@ open
Definition: BinningType.h:40
Muon::print
std::string print(const MuPatSegment &)
Definition: MuonTrackSteering.cxx:28
confTool.parse_args
def parse_args()
Definition: confTool.py:35
chainComp.load_yaml
def load_yaml(path)
Definition: chainComp.py:103
Trk::split
@ split
Definition: LayerMaterialProperties.h:38