blob: f88d7992e1674b7c28307a7fe70b1a6520834dc6 [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
Tomás González2fdd5032023-08-23 16:43:26 +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
Tomás González45d49592023-08-11 15:22:04 +010054def analyze_coverage(results, outcomes, allow_list, full_coverage):
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020055 """Check that all available test cases are executed at least once."""
Gilles Peskine0c2f8ee2022-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
Tomás González2fdd5032023-08-23 16:43:26 +010059 if hits == 0 and key not in allow_list:
Tomás González45d49592023-08-11 15:22:04 +010060 if full_coverage:
61 results.error('Test case not executed: {}', key)
62 else:
63 results.warning('Test case not executed: {}', key)
Tomás González2fdd5032023-08-23 16:43:26 +010064 elif hits != 0 and key in allow_list:
65 # Test Case should be removed from the allow list.
Tomás González14b36ef2023-08-22 09:40:23 +010066 if full_coverage:
67 results.error('Allow listed test case was executed: {}', key)
68 else:
69 results.warning('Allow listed test case was executed: {}', key)
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020070
Tomás González45d49592023-08-11 15:22:04 +010071def analyze_outcomes(outcomes, args):
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020072 """Run all analyses on the given outcome collection."""
73 results = Results()
Tomás González45d49592023-08-11 15:22:04 +010074 analyze_coverage(results, outcomes, args['allow_list'],
75 args['full_coverage'])
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020076 return results
77
78def read_outcome_file(outcome_file):
79 """Parse an outcome file and return an outcome collection.
80
81An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
82The keys are the test suite name and the test case description, separated
83by a semicolon.
84"""
85 outcomes = {}
86 with open(outcome_file, 'r', encoding='utf-8') as input_file:
87 for line in input_file:
88 (platform, config, suite, case, result, _cause) = line.split(';')
89 key = ';'.join([suite, case])
90 setup = ';'.join([platform, config])
91 if key not in outcomes:
92 outcomes[key] = TestCaseOutcomes()
93 if result == 'PASS':
94 outcomes[key].successes.append(setup)
95 elif result == 'FAIL':
96 outcomes[key].failures.append(setup)
97 return outcomes
98
Tomás González2fdd5032023-08-23 16:43:26 +010099def do_analyze_coverage(outcome_file, args):
100 """Perform coverage analysis."""
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200101 outcomes = read_outcome_file(outcome_file)
Tomás González2fdd5032023-08-23 16:43:26 +0100102 Results.log("\n*** Analyze coverage ***\n")
Tomás González45d49592023-08-11 15:22:04 +0100103 results = analyze_outcomes(outcomes, args)
Tomás González2fdd5032023-08-23 16:43:26 +0100104 return results.error_count == 0
105
106# List of tasks with a function that can handle this task and additional arguments if required
107TASKS = {
108 'analyze_coverage': {
109 'test_function': do_analyze_coverage,
110 'args': {
Tomás Gonzálezc8957332023-08-14 15:43:46 +0100111 'allow_list': [
112 # Algorithm not supported yet
113 'test_suite_psa_crypto_metadata;Asymmetric signature: pure EdDSA',
114 # Algorithm not supported yet
115 'test_suite_psa_crypto_metadata;Cipher: XTS',
Manuel Pégourié-Gonnard2e1d2fe2024-04-09 23:13:49 +0200116 # compat.sh tests with OpenSSL, DTLS 1.2 and singled-DES:
117 # we have no version of OpenSSL on the CI that supports both
118 # DTLS 1.2 and single-DES (1.0.2g is too recent for single-DES
119 # and 1.0.1j is too old for DTLS 1.2).
120 'compat;O->m dtls12,no DES-CBC-SHA',
121 'compat;O->m dtls12,no EDH-RSA-DES-CBC-SHA',
122 'compat;O->m dtls12,yes DES-CBC-SHA',
123 'compat;O->m dtls12,yes EDH-RSA-DES-CBC-SHA',
124 'compat;m->O dtls12,no TLS-DHE-RSA-WITH-DES-CBC-SHA',
125 'compat;m->O dtls12,no TLS-RSA-WITH-DES-CBC-SHA',
126 'compat;m->O dtls12,yes TLS-DHE-RSA-WITH-DES-CBC-SHA',
127 'compat;m->O dtls12,yes TLS-RSA-WITH-DES-CBC-SHA',
Tomás González1cf437b2023-08-24 09:27:28 +0100128 ],
Tomás González45d49592023-08-11 15:22:04 +0100129 'full_coverage': False,
Tomás González2fdd5032023-08-23 16:43:26 +0100130 }
131 },
132}
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200133
134def main():
135 try:
136 parser = argparse.ArgumentParser(description=__doc__)
137 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
138 help='Outcome file to analyze')
Tomás González2fdd5032023-08-23 16:43:26 +0100139 parser.add_argument('task', default='all', nargs='?',
140 help='Analysis to be done. By default, run all tasks. '
141 'With one or more TASK, run only those. '
142 'TASK can be the name of a single task or '
143 'comma/space-separated list of tasks. ')
144 parser.add_argument('--list', action='store_true',
145 help='List all available tasks and exit.')
Tomás González45d49592023-08-11 15:22:04 +0100146 parser.add_argument('--require-full-coverage', action='store_true',
147 dest='full_coverage', help="Require all available "
148 "test cases to be executed and issue an error "
149 "otherwise. This flag is ignored if 'task' is "
150 "neither 'all' nor 'analyze_coverage'")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200151 options = parser.parse_args()
Tomás González2fdd5032023-08-23 16:43:26 +0100152
153 if options.list:
154 for task in TASKS:
155 Results.log(task)
156 sys.exit(0)
157
158 result = True
159
160 if options.task == 'all':
161 tasks = TASKS.keys()
162 else:
163 tasks = re.split(r'[, ]+', options.task)
164
165 for task in tasks:
166 if task not in TASKS:
167 Results.log('Error: invalid task: {}'.format(task))
168 sys.exit(1)
169
Tomás González45d49592023-08-11 15:22:04 +0100170 TASKS['analyze_coverage']['args']['full_coverage'] = \
171 options.full_coverage
172
Tomás González2fdd5032023-08-23 16:43:26 +0100173 for task in TASKS:
174 if task in tasks:
175 if not TASKS[task]['test_function'](options.outcomes, TASKS[task]['args']):
176 result = False
177
178 if result is False:
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200179 sys.exit(1)
Tomás González2fdd5032023-08-23 16:43:26 +0100180 Results.log("SUCCESS :-)")
Gilles Peskine15c2cbf2020-06-25 18:36:28 +0200181 except Exception: # pylint: disable=broad-except
182 # Print the backtrace and exit explicitly with our chosen status.
183 traceback.print_exc()
184 sys.exit(120)
185
186if __name__ == '__main__':
187 main()