blob: 7af910f16aab373e4d667a79e5b141e7f2ec87e0 [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
Valerio Settia2663322023-03-24 08:20:18 +010013import subprocess
14import os
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020015
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020016import check_test_cases
17
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020018class Results:
19 """Process analysis results."""
20
21 def __init__(self):
22 self.error_count = 0
23 self.warning_count = 0
24
25 @staticmethod
26 def log(fmt, *args, **kwargs):
27 sys.stderr.write((fmt + '\n').format(*args, **kwargs))
28
29 def error(self, fmt, *args, **kwargs):
30 self.log('Error: ' + fmt, *args, **kwargs)
31 self.error_count += 1
32
33 def warning(self, fmt, *args, **kwargs):
34 self.log('Warning: ' + fmt, *args, **kwargs)
35 self.warning_count += 1
36
37class TestCaseOutcomes:
38 """The outcomes of one test case across many configurations."""
39 # pylint: disable=too-few-public-methods
40
41 def __init__(self):
Gilles Peskine3d863f22020-06-26 13:02:30 +020042 # Collect a list of witnesses of the test case succeeding or failing.
43 # Currently we don't do anything with witnesses except count them.
44 # The format of a witness is determined by the read_outcome_file
45 # function; it's the platform and configuration joined by ';'.
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020046 self.successes = []
47 self.failures = []
48
49 def hits(self):
50 """Return the number of times a test case has been run.
51
52 This includes passes and failures, but not skips.
53 """
54 return len(self.successes) + len(self.failures)
55
Valerio Settia2663322023-03-24 08:20:18 +010056def execute_reference_driver_tests(ref_component, driver_component, outcome_file):
Valerio Setti22992a02023-03-29 11:15:28 +020057 """Run the tests specified in ref_component and driver_component. Results
58 are stored in the output_file and they will be used for the following
Valerio Settia2663322023-03-24 08:20:18 +010059 coverage analysis"""
60 # If the outcome file already exists, we assume that the user wants to
61 # perform the comparison analysis again without repeating the tests.
62 if os.path.exists(outcome_file):
63 Results.log("Outcome file (" + outcome_file + ") already exists. " + \
64 "Tests will be skipped.")
65 return
66
67 shell_command = "tests/scripts/all.sh --outcome-file " + outcome_file + \
68 " " + ref_component + " " + driver_component
69 print("Running: " + shell_command)
70 ret_val = subprocess.run(shell_command.split(), check=False).returncode
71
72 if ret_val != 0:
73 Results.log("Error: failed to run reference/driver components")
74 sys.exit(ret_val)
75
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020076def analyze_coverage(results, outcomes):
77 """Check that all available test cases are executed at least once."""
Gilles Peskine686c2922022-01-07 15:58:38 +010078 available = check_test_cases.collect_available_test_cases()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020079 for key in available:
80 hits = outcomes[key].hits() if key in outcomes else 0
81 if hits == 0:
82 # Make this a warning, not an error, as long as we haven't
83 # fixed this branch to have full coverage of test cases.
84 results.warning('Test case not executed: {}', key)
85
Valerio Setti3002c992023-01-18 17:28:36 +010086def analyze_driver_vs_reference(outcomes, component_ref, component_driver,
87 ignored_suites, ignored_test=None):
Przemek Stekiel4e955902022-10-21 13:42:08 +020088 """Check that all tests executed in the reference component are also
89 executed in the corresponding driver component.
Valerio Setti3002c992023-01-18 17:28:36 +010090 Skip:
91 - full test suites provided in ignored_suites list
92 - only some specific test inside a test suite, for which the corresponding
93 output string is provided
Przemek Stekiel4e955902022-10-21 13:42:08 +020094 """
Przemek Stekiel4e955902022-10-21 13:42:08 +020095 available = check_test_cases.collect_available_test_cases()
96 result = True
97
98 for key in available:
Przemek Stekiel4e955902022-10-21 13:42:08 +020099 # Continue if test was not executed by any component
100 hits = outcomes[key].hits() if key in outcomes else 0
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200101 if hits == 0:
Przemek Stekiel4e955902022-10-21 13:42:08 +0200102 continue
Valerio Setti00c1ccb2023-02-02 11:33:31 +0100103 # Skip ignored test suites
104 full_test_suite = key.split(';')[0] # retrieve full test suite name
105 test_string = key.split(';')[1] # retrieve the text string of this test
106 test_suite = full_test_suite.split('.')[0] # retrieve main part of test suite name
Manuel Pégourié-Gonnard7d381f52023-03-17 15:13:08 +0100107 if test_suite in ignored_suites or full_test_suite in ignored_suites:
Valerio Setti00c1ccb2023-02-02 11:33:31 +0100108 continue
Valerio Setti3002c992023-01-18 17:28:36 +0100109 if ((full_test_suite in ignored_test) and
110 (test_string in ignored_test[full_test_suite])):
111 continue
Przemek Stekiel4e955902022-10-21 13:42:08 +0200112 # Search for tests that run in reference component and not in driver component
113 driver_test_passed = False
114 reference_test_passed = False
115 for entry in outcomes[key].successes:
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100116 if component_driver in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +0200117 driver_test_passed = True
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100118 if component_ref in entry:
Przemek Stekiel4e955902022-10-21 13:42:08 +0200119 reference_test_passed = True
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +0100120 if(reference_test_passed and not driver_test_passed):
Valerio Setti3951d1b2023-03-13 18:37:34 +0100121 Results.log(key)
Przemek Stekiel4e955902022-10-21 13:42:08 +0200122 result = False
123 return result
124
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200125def analyze_outcomes(outcomes):
126 """Run all analyses on the given outcome collection."""
127 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +0200128 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200129 return results
130
131def read_outcome_file(outcome_file):
132 """Parse an outcome file and return an outcome collection.
133
134An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
135The keys are the test suite name and the test case description, separated
136by a semicolon.
137"""
138 outcomes = {}
139 with open(outcome_file, 'r', encoding='utf-8') as input_file:
140 for line in input_file:
141 (platform, config, suite, case, result, _cause) = line.split(';')
142 key = ';'.join([suite, case])
143 setup = ';'.join([platform, config])
144 if key not in outcomes:
145 outcomes[key] = TestCaseOutcomes()
146 if result == 'PASS':
147 outcomes[key].successes.append(setup)
148 elif result == 'FAIL':
149 outcomes[key].failures.append(setup)
150 return outcomes
151
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200152def do_analyze_coverage(outcome_file, args):
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100153 """Perform coverage analysis."""
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200154 del args # unused
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200155 outcomes = read_outcome_file(outcome_file)
Valerio Setti3951d1b2023-03-13 18:37:34 +0100156 Results.log("\n*** Analyze coverage ***\n")
Przemek Stekiel4e955902022-10-21 13:42:08 +0200157 results = analyze_outcomes(outcomes)
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200158 return results.error_count == 0
Przemek Stekiel4e955902022-10-21 13:42:08 +0200159
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200160def do_analyze_driver_vs_reference(outcome_file, args):
Przemek Stekiel4e955902022-10-21 13:42:08 +0200161 """Perform driver vs reference analyze."""
Valerio Settia2663322023-03-24 08:20:18 +0100162 execute_reference_driver_tests(args['component_ref'], \
163 args['component_driver'], outcome_file)
164
Valerio Setti3002c992023-01-18 17:28:36 +0100165 ignored_suites = ['test_suite_' + x for x in args['ignored_suites']]
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100166
Przemek Stekiel4e955902022-10-21 13:42:08 +0200167 outcomes = read_outcome_file(outcome_file)
Valerio Setti3951d1b2023-03-13 18:37:34 +0100168 Results.log("\n*** Analyze driver {} vs reference {} ***\n".format(
Manuel Pégourié-Gonnardc6967d22022-12-30 13:40:34 +0100169 args['component_driver'], args['component_ref']))
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100170 return analyze_driver_vs_reference(outcomes, args['component_ref'],
Valerio Setti3002c992023-01-18 17:28:36 +0100171 args['component_driver'], ignored_suites,
172 args['ignored_tests'])
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200173
Przemek Stekiel6856f4c2022-11-09 10:50:29 +0100174# List of tasks with a function that can handle this task and additional arguments if required
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200175TASKS = {
176 'analyze_coverage': {
177 'test_function': do_analyze_coverage,
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100178 'args': {}
179 },
Valerio Settia2663322023-03-24 08:20:18 +0100180 # There are 2 options to use analyze_driver_vs_reference_xxx locally:
181 # 1. Run tests and then analysis:
182 # - tests/scripts/all.sh --outcome-file "$PWD/out.csv" <component_ref> <component_driver>
183 # - tests/scripts/analyze_outcomes.py out.csv analyze_driver_vs_reference_xxx
184 # 2. Let this script run both automatically:
185 # - tests/scripts/analyze_outcomes.py out.csv analyze_driver_vs_reference_xxx
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200186 'analyze_driver_vs_reference_hash': {
187 'test_function': do_analyze_driver_vs_reference,
188 'args': {
Przemek Stekiel51f30ff2022-11-09 12:07:29 +0100189 'component_ref': 'test_psa_crypto_config_reference_hash_use_psa',
190 'component_driver': 'test_psa_crypto_config_accel_hash_use_psa',
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100191 'ignored_suites': [
192 'shax', 'mdx', # the software implementations that are being excluded
Manuel Pégourié-Gonnard7d381f52023-03-17 15:13:08 +0100193 'md.psa', # purposefully depends on whether drivers are present
Valerio Setti3002c992023-01-18 17:28:36 +0100194 ],
195 'ignored_tests': {
196 }
197 }
198 },
Manuel Pégourié-Gonnard10e39632022-12-29 12:29:09 +0100199 'analyze_driver_vs_reference_ecdsa': {
200 'test_function': do_analyze_driver_vs_reference,
201 'args': {
202 'component_ref': 'test_psa_crypto_config_reference_ecdsa_use_psa',
203 'component_driver': 'test_psa_crypto_config_accel_ecdsa_use_psa',
204 'ignored_suites': [
205 'ecdsa', # the software implementation that's excluded
Valerio Setti3002c992023-01-18 17:28:36 +0100206 ],
207 'ignored_tests': {
Valerio Setti9cb0f7a2023-01-18 17:29:29 +0100208 'test_suite_random': [
209 'PSA classic wrapper: ECDSA signature (SECP256R1)',
210 ],
Valerio Setti3002c992023-01-18 17:28:36 +0100211 }
212 }
213 },
Manuel Pégourié-Gonnarde91bcf32023-02-21 13:07:19 +0100214 'analyze_driver_vs_reference_ecdh': {
215 'test_function': do_analyze_driver_vs_reference,
216 'args': {
217 'component_ref': 'test_psa_crypto_config_reference_ecdh_use_psa',
218 'component_driver': 'test_psa_crypto_config_accel_ecdh_use_psa',
219 'ignored_suites': [
220 'ecdh', # the software implementation that's excluded
221 ],
222 'ignored_tests': {
Manuel Pégourié-Gonnarde91bcf32023-02-21 13:07:19 +0100223 }
224 }
225 },
Valerio Settid0fffc52023-03-13 16:08:03 +0100226 'analyze_driver_vs_reference_ecjpake': {
227 'test_function': do_analyze_driver_vs_reference,
228 'args': {
229 'component_ref': 'test_psa_crypto_config_reference_ecjpake_use_psa',
230 'component_driver': 'test_psa_crypto_config_accel_ecjpake_use_psa',
231 'ignored_suites': [
232 'ecjpake', # the software implementation that's excluded
233 ],
234 'ignored_tests': {
235 }
236 }
237 },
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200238}
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200239
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200240def main():
241 try:
242 parser = argparse.ArgumentParser(description=__doc__)
Przemek Stekiel58bbc232022-10-24 08:10:10 +0200243 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200244 help='Outcome file to analyze')
Przemek Stekiel542d9322022-11-17 09:43:34 +0100245 parser.add_argument('task', default='all', nargs='?',
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100246 help='Analysis to be done. By default, run all tasks. '
247 'With one or more TASK, run only those. '
248 'TASK can be the name of a single task or '
Przemek Stekiel85c54ea2022-11-17 11:50:23 +0100249 'comma/space-separated list of tasks. ')
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100250 parser.add_argument('--list', action='store_true',
251 help='List all available tasks and exit.')
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200252 options = parser.parse_args()
Przemek Stekiel4e955902022-10-21 13:42:08 +0200253
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100254 if options.list:
255 for task in TASKS:
Valerio Setti3951d1b2023-03-13 18:37:34 +0100256 Results.log(task)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100257 sys.exit(0)
258
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200259 result = True
Przemek Stekiel4e955902022-10-21 13:42:08 +0200260
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200261 if options.task == 'all':
Przemek Stekield3068af2022-11-14 16:15:19 +0100262 tasks = TASKS.keys()
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100263 else:
Przemek Stekiel85c54ea2022-11-17 11:50:23 +0100264 tasks = re.split(r'[, ]+', options.task)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100265
Przemek Stekield3068af2022-11-14 16:15:19 +0100266 for task in tasks:
267 if task not in TASKS:
Valerio Setti3951d1b2023-03-13 18:37:34 +0100268 Results.log('Error: invalid task: {}'.format(task))
Przemek Stekield3068af2022-11-14 16:15:19 +0100269 sys.exit(1)
Przemek Stekiel992de3c2022-11-09 13:54:49 +0100270
271 for task in TASKS:
272 if task in tasks:
Przemek Stekiel4d13c832022-10-26 16:11:26 +0200273 if not TASKS[task]['test_function'](options.outcomes, TASKS[task]['args']):
274 result = False
Przemek Stekiel4e955902022-10-21 13:42:08 +0200275
Przemek Stekielc86dedf2022-10-24 09:16:04 +0200276 if result is False:
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200277 sys.exit(1)
Valerio Setti3951d1b2023-03-13 18:37:34 +0100278 Results.log("SUCCESS :-)")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200279 except Exception: # pylint: disable=broad-except
280 # Print the backtrace and exit explicitly with our chosen status.
281 traceback.print_exc()
282 sys.exit(120)
283
284if __name__ == '__main__':
285 main()