blob: 1100086c1ba5ab5be86ea7bd766aee61b81e582f [file] [log] [blame]
Gilles Peskine15c2cbf2020-06-25 18:36:28 +02001#!/usr/bin/env python3
2
3"""Analyze the test outcomes from a full CI run.
4
5This script can also run on outcomes from a partial run, but the results are
6less likely to be useful.
7"""
8
9import argparse
10import sys
11import traceback
12
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020013import check_test_cases
14
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020015class Results:
16 """Process analysis results."""
17
18 def __init__(self):
19 self.error_count = 0
20 self.warning_count = 0
21
22 @staticmethod
23 def log(fmt, *args, **kwargs):
24 sys.stderr.write((fmt + '\n').format(*args, **kwargs))
25
26 def error(self, fmt, *args, **kwargs):
27 self.log('Error: ' + fmt, *args, **kwargs)
28 self.error_count += 1
29
30 def warning(self, fmt, *args, **kwargs):
31 self.log('Warning: ' + fmt, *args, **kwargs)
32 self.warning_count += 1
33
34class TestCaseOutcomes:
35 """The outcomes of one test case across many configurations."""
36 # pylint: disable=too-few-public-methods
37
38 def __init__(self):
Gilles Peskine3d863f22020-06-26 13:02:30 +020039 # Collect a list of witnesses of the test case succeeding or failing.
40 # Currently we don't do anything with witnesses except count them.
41 # The format of a witness is determined by the read_outcome_file
42 # function; it's the platform and configuration joined by ';'.
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020043 self.successes = []
44 self.failures = []
45
46 def hits(self):
47 """Return the number of times a test case has been run.
48
49 This includes passes and failures, but not skips.
50 """
51 return len(self.successes) + len(self.failures)
52
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020053def analyze_coverage(results, outcomes):
54 """Check that all available test cases are executed at least once."""
Gilles Peskine686c2922022-01-07 15:58:38 +010055 available = check_test_cases.collect_available_test_cases()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020056 for key in available:
57 hits = outcomes[key].hits() if key in outcomes else 0
58 if hits == 0:
59 # Make this a warning, not an error, as long as we haven't
60 # fixed this branch to have full coverage of test cases.
61 results.warning('Test case not executed: {}', key)
62
Przemek Stekiel4e955902022-10-21 13:42:08 +020063def analyze_driver_vs_reference(outcomes, components, ignored_tests):
64 """Check that all tests executed in the reference component are also
65 executed in the corresponding driver component.
66 Skip test suits provided in ignored_tests list.
67 """
68 driver_component = components[0]
69 reference_component = components[1]
70 available = check_test_cases.collect_available_test_cases()
71 result = True
72
73 for key in available:
74 # Skip ignored test suites
75 test_suit = key.split(';')[0] # retrieve test suit name
76 test_suit = test_suit.split('.')[0] # retrieve main part of test suit name
77 if(test_suit in ignored_tests):
78 continue
79 # Continue if test was not executed by any component
80 hits = outcomes[key].hits() if key in outcomes else 0
81 if(hits == 0):
82 continue
83 # Search for tests that run in reference component and not in driver component
84 driver_test_passed = False
85 reference_test_passed = False
86 for entry in outcomes[key].successes:
87 if(driver_component in entry):
88 driver_test_passed = True
89 if(reference_component in entry):
90 reference_test_passed = True
91 #if(driver_test_passed == True and reference_test_passed == False):
92 # print('{}: driver: passed; reference: skipped'.format(key))
93 if(driver_test_passed == False and reference_test_passed == True):
94 print('{}: driver: skipped/failed; reference: passed'.format(key))
95 result = False
96 return result
97
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020098def analyze_outcomes(outcomes):
99 """Run all analyses on the given outcome collection."""
100 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +0200101 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200102 return results
103
104def read_outcome_file(outcome_file):
105 """Parse an outcome file and return an outcome collection.
106
107An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
108The keys are the test suite name and the test case description, separated
109by a semicolon.
110"""
111 outcomes = {}
112 with open(outcome_file, 'r', encoding='utf-8') as input_file:
113 for line in input_file:
114 (platform, config, suite, case, result, _cause) = line.split(';')
115 key = ';'.join([suite, case])
116 setup = ';'.join([platform, config])
117 if key not in outcomes:
118 outcomes[key] = TestCaseOutcomes()
119 if result == 'PASS':
120 outcomes[key].successes.append(setup)
121 elif result == 'FAIL':
122 outcomes[key].failures.append(setup)
123 return outcomes
124
Przemek Stekiel4e955902022-10-21 13:42:08 +0200125def do_analyze_coverage(outcome_file):
126 """Perform coverage analyze."""
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200127 outcomes = read_outcome_file(outcome_file)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200128 results = analyze_outcomes(outcomes)
129 return (True if results.error_count == 0 else False)
130
131def do_analyze_driver_vs_reference(outcome_file, components, ignored_tests):
132 """Perform driver vs reference analyze."""
133 # We need exactly 2 components to analyze (first driver and second reference)
134 if(len(components) != 2 or "accel" not in components[0] or "reference" not in components[1]):
135 print('Error: Wrong component list. Exactly 2 components are required (driver,reference). ')
136 return False
137 outcomes = read_outcome_file(outcome_file)
138 return analyze_driver_vs_reference(outcomes, components, ignored_tests)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200139
140def main():
141 try:
142 parser = argparse.ArgumentParser(description=__doc__)
Przemek Stekiel58bbc232022-10-24 08:10:10 +0200143 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200144 help='Outcome file to analyze')
Przemek Stekiel58bbc232022-10-24 08:10:10 +0200145 parser.add_argument('--task', default='analyze_coverage',
Przemek Stekiel4e955902022-10-21 13:42:08 +0200146 help='Analyze to be done: analyze_coverage or analyze_driver_vs_reference')
147 parser.add_argument('--components',
148 help='List of test components to compare. Must be exactly 2 in valid order: driver,reference. '
149 'Apply only for analyze_driver_vs_reference task.')
150 parser.add_argument('--ignore',
151 help='List of test suits to ignore. Apply only for analyze_driver_vs_reference task.')
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200152 options = parser.parse_args()
Przemek Stekiel4e955902022-10-21 13:42:08 +0200153
154 result = False
155
156 if(options.task == 'analyze_coverage'):
157 result = do_analyze_coverage(options.outcomes)
158 elif(options.task == 'analyze_driver_vs_reference'):
159 components_list = options.components.split(',')
160 ignored_tests_list = options.ignore.split(',')
161 ignored_tests_list = ['test_suite_' + x for x in ignored_tests_list]
162 result = do_analyze_driver_vs_reference(options.outcomes, components_list, ignored_tests_list)
163 else:
164 print('Error: Unknown task: {}'.format(options.task))
165
166 if(result == False):
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200167 sys.exit(1)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200168 print("SUCCESS :-)")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200169 except Exception: # pylint: disable=broad-except
170 # Print the backtrace and exit explicitly with our chosen status.
171 traceback.print_exc()
172 sys.exit(120)
173
174if __name__ == '__main__':
175 main()