blob: d06a0596f3f9d43409d46008eab4441924bef4f9 [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 Peskine0c2f8ee2022-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
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020063def analyze_outcomes(outcomes):
64 """Run all analyses on the given outcome collection."""
65 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020066 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020067 return results
68
69def read_outcome_file(outcome_file):
70 """Parse an outcome file and return an outcome collection.
71
72An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
73The keys are the test suite name and the test case description, separated
74by a semicolon.
75"""
76 outcomes = {}
77 with open(outcome_file, 'r', encoding='utf-8') as input_file:
78 for line in input_file:
79 (platform, config, suite, case, result, _cause) = line.split(';')
80 key = ';'.join([suite, case])
81 setup = ';'.join([platform, config])
82 if key not in outcomes:
83 outcomes[key] = TestCaseOutcomes()
84 if result == 'PASS':
85 outcomes[key].successes.append(setup)
86 elif result == 'FAIL':
87 outcomes[key].failures.append(setup)
88 return outcomes
89
90def analyze_outcome_file(outcome_file):
91 """Analyze the given outcome file."""
92 outcomes = read_outcome_file(outcome_file)
93 return analyze_outcomes(outcomes)
94
95def main():
96 try:
97 parser = argparse.ArgumentParser(description=__doc__)
98 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
99 help='Outcome file to analyze')
100 options = parser.parse_args()
101 results = analyze_outcome_file(options.outcomes)
102 if results.error_count > 0:
103 sys.exit(1)
104 except Exception: # pylint: disable=broad-except
105 # Print the backtrace and exit explicitly with our chosen status.
106 traceback.print_exc()
107 sys.exit(120)
108
109if __name__ == '__main__':
110 main()