blob: 73f16bdb25bc8ef424e18ffc2661f18603810488 [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
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020010import re
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020011import sys
12import traceback
13
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 +020054class TestDescriptions(check_test_cases.TestDescriptionExplorer):
55 """Collect the available test cases."""
56
57 def __init__(self):
58 super().__init__()
59 self.descriptions = set()
60
61 def process_test_case(self, _per_file_state,
62 file_name, _line_number, description):
63 """Record an available test case."""
64 base_name = re.sub(r'\.[^.]*$', '', re.sub(r'.*/', '', file_name))
65 key = ';'.join([base_name, description.decode('utf-8')])
66 self.descriptions.add(key)
67
68def collect_available_test_cases():
69 """Collect the available test cases."""
70 explorer = TestDescriptions()
71 explorer.walk_all()
72 return sorted(explorer.descriptions)
73
74def analyze_coverage(results, outcomes):
75 """Check that all available test cases are executed at least once."""
76 available = collect_available_test_cases()
77 for key in available:
78 hits = outcomes[key].hits() if key in outcomes else 0
79 if hits == 0:
80 # Make this a warning, not an error, as long as we haven't
81 # fixed this branch to have full coverage of test cases.
82 results.warning('Test case not executed: {}', key)
83
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020084def analyze_outcomes(outcomes):
85 """Run all analyses on the given outcome collection."""
86 results = Results()
Gilles Peskine8d3c70a2020-06-25 18:37:43 +020087 analyze_coverage(results, outcomes)
Gilles Peskine15c2cbf2020-06-25 18:36:28 +020088 return results
89
90def read_outcome_file(outcome_file):
91 """Parse an outcome file and return an outcome collection.
92
93An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
94The keys are the test suite name and the test case description, separated
95by a semicolon.
96"""
97 outcomes = {}
98 with open(outcome_file, 'r', encoding='utf-8') as input_file:
99 for line in input_file:
100 (platform, config, suite, case, result, _cause) = line.split(';')
101 key = ';'.join([suite, case])
102 setup = ';'.join([platform, config])
103 if key not in outcomes:
104 outcomes[key] = TestCaseOutcomes()
105 if result == 'PASS':
106 outcomes[key].successes.append(setup)
107 elif result == 'FAIL':
108 outcomes[key].failures.append(setup)
109 return outcomes
110
111def analyze_outcome_file(outcome_file):
112 """Analyze the given outcome file."""
113 outcomes = read_outcome_file(outcome_file)
114 return analyze_outcomes(outcomes)
115
116def main():
117 try:
118 parser = argparse.ArgumentParser(description=__doc__)
119 parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
120 help='Outcome file to analyze')
121 options = parser.parse_args()
122 results = analyze_outcome_file(options.outcomes)
123 if results.error_count > 0:
124 sys.exit(1)
125 except Exception: # pylint: disable=broad-except
126 # Print the backtrace and exit explicitly with our chosen status.
127 traceback.print_exc()
128 sys.exit(120)
129
130if __name__ == '__main__':
131 main()