blob: 482d64e7c930571d0f8c1f8a85bcbc1254c15162 [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
Przemek Stekiel85c54ea2022-11-17 11:50:23 +010012import re
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020013
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020014import check_test_cases
15
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020016class Results:
17 """Process analysis results."""
18
19 def __init__(self):
20 self.error_count = 0
21 self.warning_count = 0
22
23 @staticmethod
24 def log(fmt, *args, **kwargs):
25 sys.stderr.write((fmt + '\n').format(*args, **kwargs))
26
27 def error(self, fmt, *args, **kwargs):
28 self.log('Error: ' + fmt, *args, **kwargs)
29 self.error_count += 1
30
31 def warning(self, fmt, *args, **kwargs):
32 self.log('Warning: ' + fmt, *args, **kwargs)
33 self.warning_count += 1
34
35class TestCaseOutcomes:
36 """The outcomes of one test case across many configurations."""
37 # pylint: disable=too-few-public-methods
38
39 def __init__(self):
Gilles Peskine3d863f22020-06-26 13:02:30 +020040 # Collect a list of witnesses of the test case succeeding or failing.
41 # Currently we don't do anything with witnesses except count them.
42 # The format of a witness is determined by the read_outcome_file
43 # function; it's the platform and configuration joined by ';'.
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020044 self.successes = []
45 self.failures = []
46
47 def hits(self):
48 """Return the number of times a test case has been run.
49
50 This includes passes and failures, but not skips.
51 """
52 return len(self.successes) + len(self.failures)
53
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020054def analyze_coverage(results, outcomes):
55 """Check that all available test cases are executed at least once."""
Gilles Peskine686c2922022-01-07 15:58:38 +010056 available = check_test_cases.collect_available_test_cases()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020057 for key in available:
58 hits = outcomes[key].hits() if key in outcomes else 0
59 if hits == 0:
60 # Make this a warning, not an error, as long as we haven't
61 # fixed this branch to have full coverage of test cases.
62 results.warning('Test case not executed: {}', key)
63
Przemek Stekiel733c76e2022-11-14 08:33:21 +010064def analyze_driver_vs_reference(outcomes, component_ref, component_driver, ignored_tests):
Przemek Stekiel4e955902022-10-21 13:42:08 +020065 """Check that all tests executed in the reference component are also
66 executed in the corresponding driver component.
Przemek Stekiel6856f4c2022-11-09 10:50:29 +010067 Skip test suites provided in ignored_tests list.
Przemek Stekiel4e955902022-10-21 13:42:08 +020068 """
Przemek Stekiel4e955902022-10-21 13:42:08 +020069 available = check_test_cases.collect_available_test_cases()
70 result = True
71
72 for key in available:
73 # Skip ignored test suites
Przemek Stekiel6856f4c2022-11-09 10:50:29 +010074 test_suite = key.split(';')[0] # retrieve test suit name
75 test_suite = test_suite.split('.')[0] # retrieve main part of test suit name
76 if test_suite in ignored_tests:
Przemek Stekiel4e955902022-10-21 13:42:08 +020077 continue
78 # Continue if test was not executed by any component
79 hits = outcomes[key].hits() if key in outcomes else 0
Przemek Stekielc86dedf2022-10-24 09:16:04 +020080 if hits == 0:
Przemek Stekiel4e955902022-10-21 13:42:08 +020081 continue
82 # Search for tests that run in reference component and not in driver component
83 driver_test_passed = False
84 reference_test_passed = False
85 for entry in outcomes[key].successes:
Przemek Stekiel51f30ff2022-11-09 12:07:29 +010086 if component_driver in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +020087 driver_test_passed = True
Przemek Stekiel51f30ff2022-11-09 12:07:29 +010088 if component_ref in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +020089 reference_test_passed = True
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +010090 if(reference_test_passed and not driver_test_passed):
91 print(key)
Przemek Stekiel4e955902022-10-21 13:42:08 +020092 result = False
93 return result
94
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020095def analyze_outcomes(outcomes):
96 """Run all analyses on the given outcome collection."""
97 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020098 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020099 return results
100
101def read_outcome_file(outcome_file):
102 """Parse an outcome file and return an outcome collection.
103
104An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
105The keys are the test suite name and the test case description, separated
106by a semicolon.
107"""
108 outcomes = {}
109 with open(outcome_file, 'r', encoding='utf-8') as input_file:
110 for line in input_file:
111 (platform, config, suite, case, result, _cause) = line.split(';')
112 key = ';'.join([suite, case])
113 setup = ';'.join([platform, config])
114 if key not in outcomes:
115 outcomes[key] = TestCaseOutcomes()
116 if result == 'PASS':
117 outcomes[key].successes.append(setup)
118 elif result == 'FAIL':
119 outcomes[key].failures.append(setup)
120 return outcomes
121
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200122def do_analyze_coverage(outcome_file, args):
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100123 """Perform coverage analysis."""
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200124 del args # unused
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200125 outcomes = read_outcome_file(outcome_file)
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +0100126 print("\n*** Analyze coverage ***\n")
Przemek Stekiel4e955902022-10-21 13:42:08 +0200127 results = analyze_outcomes(outcomes)
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200128 return results.error_count == 0
Przemek Stekiel4e955902022-10-21 13:42:08 +0200129
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200130def do_analyze_driver_vs_reference(outcome_file, args):
Przemek Stekiel4e955902022-10-21 13:42:08 +0200131 """Perform driver vs reference analyze."""
Przemek Stekielbe279c72022-11-09 12:17:08 +0100132 ignored_tests = ['test_suite_' + x for x in args['ignored_suites']]
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100133
Przemek Stekiel4e955902022-10-21 13:42:08 +0200134 outcomes = read_outcome_file(outcome_file)
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +0100135 print("\n*** Analyze driver {} vs reference {} ***\n".format(
136 args['component_driver'], args['component_ref']))
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100137 return analyze_driver_vs_reference(outcomes, args['component_ref'],
138 args['component_driver'], ignored_tests)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200139
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100140# List of tasks with a function that can handle this task and additional arguments if required
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200141TASKS = {
142 'analyze_coverage': {
143 'test_function': do_analyze_coverage,
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100144 'args': {}
145 },
146 # How to use analyze_driver_vs_reference_xxx locally:
147 # 1. tests/scripts/all.sh --outcome-file "$PWD/out.csv" <component_ref> <component_driver>
148 # 2. tests/scripts/analyze_outcomes.py out.csv analyze_driver_vs_reference_xxx
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200149 'analyze_driver_vs_reference_hash': {
150 'test_function': do_analyze_driver_vs_reference,
151 'args': {
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100152 'component_ref': 'test_psa_crypto_config_reference_hash_use_psa',
153 'component_driver': 'test_psa_crypto_config_accel_hash_use_psa',
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100154 'ignored_suites': [
155 'shax', 'mdx', # the software implementations that are being excluded
156 'md', # the legacy abstraction layer that's being excluded
157 ]}},
158 'analyze_driver_vs_reference_ecdsa': {
159 'test_function': do_analyze_driver_vs_reference,
160 'args': {
161 'component_ref': 'test_psa_crypto_config_reference_ecdsa_use_psa',
162 'component_driver': 'test_psa_crypto_config_accel_ecdsa_use_psa',
163 'ignored_suites': [
164 'ecdsa', # the software implementation that's excluded
165 # the following lines should not be needed,
166 # they will be removed by upcoming work
167 'psa_crypto_se_driver_hal',
168 'random',
169 'ecp',
170 'pk',
171 'x509parse',
172 'x509write',
173 'debug',
174 'ssl',
175 ]}},
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200176}
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200177
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200178def main():
179 try:
180 parser = argparse.ArgumentParser(description=__doc__)
Przemek Stekiel58bbc232022-10-24 08:10:10 +0200181 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200182 help='Outcome file to analyze')
Przemek Stekiel542d9322022-11-17 09:43:34 +0100183 parser.add_argument('task', default='all', nargs='?',
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100184 help='Analysis to be done. By default, run all tasks. '
185 'With one or more TASK, run only those. '
186 'TASK can be the name of a single task or '
Przemek Stekiel85c54ea2022-11-17 11:50:23 +0100187 'comma/space-separated list of tasks. ')
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100188 parser.add_argument('--list', action='store_true',
189 help='List all available tasks and exit.')
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200190 options = parser.parse_args()
Przemek Stekiel4e955902022-10-21 13:42:08 +0200191
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100192 if options.list:
193 for task in TASKS:
194 print(task)
195 sys.exit(0)
196
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200197 result = True
Przemek Stekiel4e955902022-10-21 13:42:08 +0200198
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200199 if options.task == 'all':
Przemek Stekield3068af2022-11-14 16:15:19 +0100200 tasks = TASKS.keys()
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100201 else:
Przemek Stekiel85c54ea2022-11-17 11:50:23 +0100202 tasks = re.split(r'[, ]+', options.task)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100203
Przemek Stekield3068af2022-11-14 16:15:19 +0100204 for task in tasks:
205 if task not in TASKS:
206 print('Error: invalid task: {}'.format(task))
207 sys.exit(1)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100208
209 for task in TASKS:
210 if task in tasks:
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200211 if not TASKS[task]['test_function'](options.outcomes, TASKS[task]['args']):
212 result = False
Przemek Stekiel4e955902022-10-21 13:42:08 +0200213
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200214 if result is False:
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200215 sys.exit(1)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200216 print("SUCCESS :-)")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200217 except Exception: # pylint: disable=broad-except
218 # Print the backtrace and exit explicitly with our chosen status.
219 traceback.print_exc()
220 sys.exit(120)
221
222if __name__ == '__main__':
223 main()