blob: 7b09bb52419fec7275b9806c2fc92315bb0b042e [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
Valerio Setti5d8d1a72023-03-06 11:08:17 +010023 # Private method
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020024 @staticmethod
Valerio Setti5d8d1a72023-03-06 11:08:17 +010025 def __log(fmt, *args, **kwargs):
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020026 sys.stderr.write((fmt + '\n').format(*args, **kwargs))
27
28 def error(self, fmt, *args, **kwargs):
Valerio Setti5d8d1a72023-03-06 11:08:17 +010029 self.__log('Error: ' + fmt, *args, **kwargs)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020030 self.error_count += 1
31
32 def warning(self, fmt, *args, **kwargs):
Valerio Setti5d8d1a72023-03-06 11:08:17 +010033 self.__log('Warning: ' + fmt, *args, **kwargs)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020034 self.warning_count += 1
35
Valerio Setti5d8d1a72023-03-06 11:08:17 +010036 # This is a static method because we don't need to track any data about
37 # the number of times it is called
38 @staticmethod
39 def info(fmt, *args, **kwargs):
40 Results.__log(fmt, *args, **kwargs)
41
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020042class TestCaseOutcomes:
43 """The outcomes of one test case across many configurations."""
44 # pylint: disable=too-few-public-methods
45
46 def __init__(self):
Gilles Peskine3d863f22020-06-26 13:02:30 +020047 # Collect a list of witnesses of the test case succeeding or failing.
48 # Currently we don't do anything with witnesses except count them.
49 # The format of a witness is determined by the read_outcome_file
50 # function; it's the platform and configuration joined by ';'.
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020051 self.successes = []
52 self.failures = []
53
54 def hits(self):
55 """Return the number of times a test case has been run.
56
57 This includes passes and failures, but not skips.
58 """
59 return len(self.successes) + len(self.failures)
60
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020061def analyze_coverage(results, outcomes):
62 """Check that all available test cases are executed at least once."""
Gilles Peskine686c2922022-01-07 15:58:38 +010063 available = check_test_cases.collect_available_test_cases()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020064 for key in available:
65 hits = outcomes[key].hits() if key in outcomes else 0
66 if hits == 0:
67 # Make this a warning, not an error, as long as we haven't
68 # fixed this branch to have full coverage of test cases.
69 results.warning('Test case not executed: {}', key)
70
Valerio Setti3002c992023-01-18 17:28:36 +010071def analyze_driver_vs_reference(outcomes, component_ref, component_driver,
72 ignored_suites, ignored_test=None):
Przemek Stekiel4e955902022-10-21 13:42:08 +020073 """Check that all tests executed in the reference component are also
74 executed in the corresponding driver component.
Valerio Setti3002c992023-01-18 17:28:36 +010075 Skip:
76 - full test suites provided in ignored_suites list
77 - only some specific test inside a test suite, for which the corresponding
78 output string is provided
Przemek Stekiel4e955902022-10-21 13:42:08 +020079 """
Przemek Stekiel4e955902022-10-21 13:42:08 +020080 available = check_test_cases.collect_available_test_cases()
81 result = True
82
83 for key in available:
Przemek Stekiel4e955902022-10-21 13:42:08 +020084 # Continue if test was not executed by any component
85 hits = outcomes[key].hits() if key in outcomes else 0
Przemek Stekielc86dedf2022-10-24 09:16:04 +020086 if hits == 0:
Przemek Stekiel4e955902022-10-21 13:42:08 +020087 continue
Valerio Setti00c1ccb2023-02-02 11:33:31 +010088 # Skip ignored test suites
89 full_test_suite = key.split(';')[0] # retrieve full test suite name
90 test_string = key.split(';')[1] # retrieve the text string of this test
91 test_suite = full_test_suite.split('.')[0] # retrieve main part of test suite name
92 if test_suite in ignored_suites:
93 continue
Valerio Setti3002c992023-01-18 17:28:36 +010094 if ((full_test_suite in ignored_test) and
95 (test_string in ignored_test[full_test_suite])):
96 continue
Przemek Stekiel4e955902022-10-21 13:42:08 +020097 # Search for tests that run in reference component and not in driver component
98 driver_test_passed = False
99 reference_test_passed = False
100 for entry in outcomes[key].successes:
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100101 if component_driver in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +0200102 driver_test_passed = True
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100103 if component_ref in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +0200104 reference_test_passed = True
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +0100105 if(reference_test_passed and not driver_test_passed):
Valerio Setti5d8d1a72023-03-06 11:08:17 +0100106 Results.info(key)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200107 result = False
108 return result
109
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200110def analyze_outcomes(outcomes):
111 """Run all analyses on the given outcome collection."""
112 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +0200113 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200114 return results
115
116def read_outcome_file(outcome_file):
117 """Parse an outcome file and return an outcome collection.
118
119An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
120The keys are the test suite name and the test case description, separated
121by a semicolon.
122"""
123 outcomes = {}
124 with open(outcome_file, 'r', encoding='utf-8') as input_file:
125 for line in input_file:
126 (platform, config, suite, case, result, _cause) = line.split(';')
127 key = ';'.join([suite, case])
128 setup = ';'.join([platform, config])
129 if key not in outcomes:
130 outcomes[key] = TestCaseOutcomes()
131 if result == 'PASS':
132 outcomes[key].successes.append(setup)
133 elif result == 'FAIL':
134 outcomes[key].failures.append(setup)
135 return outcomes
136
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200137def do_analyze_coverage(outcome_file, args):
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100138 """Perform coverage analysis."""
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200139 del args # unused
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200140 outcomes = read_outcome_file(outcome_file)
Valerio Setti5d8d1a72023-03-06 11:08:17 +0100141 Results.info("\n*** Analyze coverage ***\n")
Przemek Stekiel4e955902022-10-21 13:42:08 +0200142 results = analyze_outcomes(outcomes)
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200143 return results.error_count == 0
Przemek Stekiel4e955902022-10-21 13:42:08 +0200144
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200145def do_analyze_driver_vs_reference(outcome_file, args):
Przemek Stekiel4e955902022-10-21 13:42:08 +0200146 """Perform driver vs reference analyze."""
Valerio Setti3002c992023-01-18 17:28:36 +0100147 ignored_suites = ['test_suite_' + x for x in args['ignored_suites']]
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100148
Przemek Stekiel4e955902022-10-21 13:42:08 +0200149 outcomes = read_outcome_file(outcome_file)
Valerio Setti5d8d1a72023-03-06 11:08:17 +0100150 Results.info("\n*** Analyze driver {} vs reference {} ***\n".format(
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +0100151 args['component_driver'], args['component_ref']))
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100152 return analyze_driver_vs_reference(outcomes, args['component_ref'],
Valerio Setti3002c992023-01-18 17:28:36 +0100153 args['component_driver'], ignored_suites,
154 args['ignored_tests'])
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200155
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100156# List of tasks with a function that can handle this task and additional arguments if required
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200157TASKS = {
158 'analyze_coverage': {
159 'test_function': do_analyze_coverage,
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100160 'args': {}
161 },
162 # How to use analyze_driver_vs_reference_xxx locally:
163 # 1. tests/scripts/all.sh --outcome-file "$PWD/out.csv" <component_ref> <component_driver>
164 # 2. tests/scripts/analyze_outcomes.py out.csv analyze_driver_vs_reference_xxx
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200165 'analyze_driver_vs_reference_hash': {
166 'test_function': do_analyze_driver_vs_reference,
167 'args': {
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100168 'component_ref': 'test_psa_crypto_config_reference_hash_use_psa',
169 'component_driver': 'test_psa_crypto_config_accel_hash_use_psa',
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100170 'ignored_suites': [
171 'shax', 'mdx', # the software implementations that are being excluded
172 'md', # the legacy abstraction layer that's being excluded
Valerio Setti3002c992023-01-18 17:28:36 +0100173 ],
174 'ignored_tests': {
175 }
176 }
177 },
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100178 'analyze_driver_vs_reference_ecdsa': {
179 'test_function': do_analyze_driver_vs_reference,
180 'args': {
181 'component_ref': 'test_psa_crypto_config_reference_ecdsa_use_psa',
182 'component_driver': 'test_psa_crypto_config_accel_ecdsa_use_psa',
183 'ignored_suites': [
184 'ecdsa', # the software implementation that's excluded
Valerio Setti3002c992023-01-18 17:28:36 +0100185 ],
186 'ignored_tests': {
Valerio Setti9cb0f7a2023-01-18 17:29:29 +0100187 'test_suite_random': [
188 'PSA classic wrapper: ECDSA signature (SECP256R1)',
189 ],
Valerio Setti3002c992023-01-18 17:28:36 +0100190 }
191 }
192 },
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200193}
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200194
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200195def main():
196 try:
197 parser = argparse.ArgumentParser(description=__doc__)
Przemek Stekiel58bbc232022-10-24 08:10:10 +0200198 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200199 help='Outcome file to analyze')
Przemek Stekiel542d9322022-11-17 09:43:34 +0100200 parser.add_argument('task', default='all', nargs='?',
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100201 help='Analysis to be done. By default, run all tasks. '
202 'With one or more TASK, run only those. '
203 'TASK can be the name of a single task or '
Przemek Stekiel85c54ea2022-11-17 11:50:23 +0100204 'comma/space-separated list of tasks. ')
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100205 parser.add_argument('--list', action='store_true',
206 help='List all available tasks and exit.')
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200207 options = parser.parse_args()
Przemek Stekiel4e955902022-10-21 13:42:08 +0200208
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100209 if options.list:
210 for task in TASKS:
Valerio Setti5d8d1a72023-03-06 11:08:17 +0100211 Results.info(task)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100212 sys.exit(0)
213
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200214 result = True
Przemek Stekiel4e955902022-10-21 13:42:08 +0200215
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200216 if options.task == 'all':
Przemek Stekield3068af2022-11-14 16:15:19 +0100217 tasks = TASKS.keys()
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100218 else:
Przemek Stekiel85c54ea2022-11-17 11:50:23 +0100219 tasks = re.split(r'[, ]+', options.task)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100220
Przemek Stekield3068af2022-11-14 16:15:19 +0100221 for task in tasks:
222 if task not in TASKS:
Valerio Setti5d8d1a72023-03-06 11:08:17 +0100223 Results.info('Error: invalid task: {}'.format(task))
Przemek Stekield3068af2022-11-14 16:15:19 +0100224 sys.exit(1)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100225
226 for task in TASKS:
227 if task in tasks:
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200228 if not TASKS[task]['test_function'](options.outcomes, TASKS[task]['args']):
229 result = False
Przemek Stekiel4e955902022-10-21 13:42:08 +0200230
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200231 if result is False:
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200232 sys.exit(1)
Valerio Setti5d8d1a72023-03-06 11:08:17 +0100233 Results.info("SUCCESS :-)")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200234 except Exception: # pylint: disable=broad-except
235 # Print the backtrace and exit explicitly with our chosen status.
236 traceback.print_exc()
237 sys.exit(120)
238
239if __name__ == '__main__':
240 main()