blob: 85ec97c1647f157ed990fdf22be619f5e1da113d [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 Stekiel51f30ff2022-11-09 12:07:29 +010063def analyze_driver_vs_reference(outcomes, component_ref,component_driver, ignored_tests):
Przemek Stekiel4e955902022-10-21 13:42:08 +020064 """Check that all tests executed in the reference component are also
65 executed in the corresponding driver component.
Przemek Stekiel6856f4c2022-11-09 10:50:29 +010066 Skip test suites provided in ignored_tests list.
Przemek Stekiel4e955902022-10-21 13:42:08 +020067 """
Przemek Stekiel4e955902022-10-21 13:42:08 +020068 available = check_test_cases.collect_available_test_cases()
69 result = True
70
71 for key in available:
72 # Skip ignored test suites
Przemek Stekiel6856f4c2022-11-09 10:50:29 +010073 test_suite = key.split(';')[0] # retrieve test suit name
74 test_suite = test_suite.split('.')[0] # retrieve main part of test suit name
75 if test_suite in ignored_tests:
Przemek Stekiel4e955902022-10-21 13:42:08 +020076 continue
77 # Continue if test was not executed by any component
78 hits = outcomes[key].hits() if key in outcomes else 0
Przemek Stekielc86dedf2022-10-24 09:16:04 +020079 if hits == 0:
Przemek Stekiel4e955902022-10-21 13:42:08 +020080 continue
81 # Search for tests that run in reference component and not in driver component
82 driver_test_passed = False
83 reference_test_passed = False
84 for entry in outcomes[key].successes:
Przemek Stekiel51f30ff2022-11-09 12:07:29 +010085 if component_driver in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +020086 driver_test_passed = True
Przemek Stekiel51f30ff2022-11-09 12:07:29 +010087 if component_ref in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +020088 reference_test_passed = True
Przemek Stekielc86dedf2022-10-24 09:16:04 +020089 if(driver_test_passed is False and reference_test_passed is True):
Przemek Stekiel4e955902022-10-21 13:42:08 +020090 print('{}: driver: skipped/failed; reference: passed'.format(key))
91 result = False
92 return result
93
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020094def analyze_outcomes(outcomes):
95 """Run all analyses on the given outcome collection."""
96 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020097 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020098 return results
99
100def read_outcome_file(outcome_file):
101 """Parse an outcome file and return an outcome collection.
102
103An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
104The keys are the test suite name and the test case description, separated
105by a semicolon.
106"""
107 outcomes = {}
108 with open(outcome_file, 'r', encoding='utf-8') as input_file:
109 for line in input_file:
110 (platform, config, suite, case, result, _cause) = line.split(';')
111 key = ';'.join([suite, case])
112 setup = ';'.join([platform, config])
113 if key not in outcomes:
114 outcomes[key] = TestCaseOutcomes()
115 if result == 'PASS':
116 outcomes[key].successes.append(setup)
117 elif result == 'FAIL':
118 outcomes[key].failures.append(setup)
119 return outcomes
120
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200121def do_analyze_coverage(outcome_file, args):
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100122 """Perform coverage analysis."""
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200123 del args # unused
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200124 outcomes = read_outcome_file(outcome_file)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200125 results = analyze_outcomes(outcomes)
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200126 return results.error_count == 0
Przemek Stekiel4e955902022-10-21 13:42:08 +0200127
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200128def do_analyze_driver_vs_reference(outcome_file, args):
Przemek Stekiel4e955902022-10-21 13:42:08 +0200129 """Perform driver vs reference analyze."""
Przemek Stekielbe279c72022-11-09 12:17:08 +0100130 ignored_tests = ['test_suite_' + x for x in args['ignored_suites']]
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100131
Przemek Stekiel4e955902022-10-21 13:42:08 +0200132 outcomes = read_outcome_file(outcome_file)
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100133 return analyze_driver_vs_reference(outcomes, args['component_ref'],
134 args['component_driver'], ignored_tests)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200135
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100136# List of tasks with a function that can handle this task and additional arguments if required
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200137TASKS = {
138 'analyze_coverage': {
139 'test_function': do_analyze_coverage,
140 'args': {}},
141 'analyze_driver_vs_reference_hash': {
142 'test_function': do_analyze_driver_vs_reference,
143 'args': {
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100144 'component_ref': 'test_psa_crypto_config_reference_hash_use_psa',
145 'component_driver': 'test_psa_crypto_config_accel_hash_use_psa',
Przemek Stekielbe279c72022-11-09 12:17:08 +0100146 'ignored_suites': ['shax','mdx', # the software implementations that are being excluded
147 'md' # the legacy abstraction layer that's being excluded
148 'entropy','hmac_drbg','random', # temporary limitation (see RNG EPIC)
149 'psa_crypto_init', # doesn't work with external RNG
150 'hkdf', # legacy still depends on MD, but there's a PSA interface that doesn't
151 'pkcs7 ' # recent addition, will be addressed later
152 ]}}
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200153}
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200154
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200155def main():
156 try:
157 parser = argparse.ArgumentParser(description=__doc__)
Przemek Stekiel58bbc232022-10-24 08:10:10 +0200158 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200159 help='Outcome file to analyze')
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200160 parser.add_argument('--task', default='all',
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100161 help='Analysis to be done: all or analyze_coverage or '
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200162 'analyze_driver_vs_reference_hash')
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200163 options = parser.parse_args()
Przemek Stekiel4e955902022-10-21 13:42:08 +0200164
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200165 result = True
Przemek Stekiel4e955902022-10-21 13:42:08 +0200166
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200167 if options.task == 'all':
168 for task in TASKS:
169 if not TASKS[task]['test_function'](options.outcomes, TASKS[task]['args']):
170 result = False
171 elif options.task in TASKS:
172 if not TASKS[options.task]['test_function'](options.outcomes,
173 TASKS[options.task]['args']):
174 result = False
Przemek Stekiel4e955902022-10-21 13:42:08 +0200175 else:
176 print('Error: Unknown task: {}'.format(options.task))
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200177 result = False
Przemek Stekiel4e955902022-10-21 13:42:08 +0200178
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200179 if result is False:
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200180 sys.exit(1)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200181 print("SUCCESS :-)")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200182 except Exception: # pylint: disable=broad-except
183 # Print the backtrace and exit explicitly with our chosen status.
184 traceback.print_exc()
185 sys.exit(120)
186
187if __name__ == '__main__':
188 main()