|  | #!/usr/bin/env python3 | 
|  |  | 
|  | """Analyze the test outcomes from a full CI run. | 
|  |  | 
|  | This script can also run on outcomes from a partial run, but the results are | 
|  | less likely to be useful. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import re | 
|  | import sys | 
|  | import traceback | 
|  |  | 
|  | import check_test_cases | 
|  |  | 
|  | class Results: | 
|  | """Process analysis results.""" | 
|  |  | 
|  | def __init__(self): | 
|  | self.error_count = 0 | 
|  | self.warning_count = 0 | 
|  |  | 
|  | @staticmethod | 
|  | def log(fmt, *args, **kwargs): | 
|  | sys.stderr.write((fmt + '\n').format(*args, **kwargs)) | 
|  |  | 
|  | def error(self, fmt, *args, **kwargs): | 
|  | self.log('Error: ' + fmt, *args, **kwargs) | 
|  | self.error_count += 1 | 
|  |  | 
|  | def warning(self, fmt, *args, **kwargs): | 
|  | self.log('Warning: ' + fmt, *args, **kwargs) | 
|  | self.warning_count += 1 | 
|  |  | 
|  | class TestCaseOutcomes: | 
|  | """The outcomes of one test case across many configurations.""" | 
|  | # pylint: disable=too-few-public-methods | 
|  |  | 
|  | def __init__(self): | 
|  | # Collect a list of witnesses of the test case succeeding or failing. | 
|  | # Currently we don't do anything with witnesses except count them. | 
|  | # The format of a witness is determined by the read_outcome_file | 
|  | # function; it's the platform and configuration joined by ';'. | 
|  | self.successes = [] | 
|  | self.failures = [] | 
|  |  | 
|  | def hits(self): | 
|  | """Return the number of times a test case has been run. | 
|  |  | 
|  | This includes passes and failures, but not skips. | 
|  | """ | 
|  | return len(self.successes) + len(self.failures) | 
|  |  | 
|  | class TestDescriptions(check_test_cases.TestDescriptionExplorer): | 
|  | """Collect the available test cases.""" | 
|  |  | 
|  | def __init__(self): | 
|  | super().__init__() | 
|  | self.descriptions = set() | 
|  |  | 
|  | def process_test_case(self, _per_file_state, | 
|  | file_name, _line_number, description): | 
|  | """Record an available test case.""" | 
|  | base_name = re.sub(r'\.[^.]*$', '', re.sub(r'.*/', '', file_name)) | 
|  | key = ';'.join([base_name, description.decode('utf-8')]) | 
|  | self.descriptions.add(key) | 
|  |  | 
|  | def collect_available_test_cases(): | 
|  | """Collect the available test cases.""" | 
|  | explorer = TestDescriptions() | 
|  | explorer.walk_all() | 
|  | return sorted(explorer.descriptions) | 
|  |  | 
|  | def analyze_coverage(results, outcomes): | 
|  | """Check that all available test cases are executed at least once.""" | 
|  | available = collect_available_test_cases() | 
|  | for key in available: | 
|  | hits = outcomes[key].hits() if key in outcomes else 0 | 
|  | if hits == 0: | 
|  | # Make this a warning, not an error, as long as we haven't | 
|  | # fixed this branch to have full coverage of test cases. | 
|  | results.warning('Test case not executed: {}', key) | 
|  |  | 
|  | def analyze_outcomes(outcomes): | 
|  | """Run all analyses on the given outcome collection.""" | 
|  | results = Results() | 
|  | analyze_coverage(results, outcomes) | 
|  | return results | 
|  |  | 
|  | def read_outcome_file(outcome_file): | 
|  | """Parse an outcome file and return an outcome collection. | 
|  |  | 
|  | An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects. | 
|  | The keys are the test suite name and the test case description, separated | 
|  | by a semicolon. | 
|  | """ | 
|  | outcomes = {} | 
|  | with open(outcome_file, 'r', encoding='utf-8') as input_file: | 
|  | for line in input_file: | 
|  | (platform, config, suite, case, result, _cause) = line.split(';') | 
|  | key = ';'.join([suite, case]) | 
|  | setup = ';'.join([platform, config]) | 
|  | if key not in outcomes: | 
|  | outcomes[key] = TestCaseOutcomes() | 
|  | if result == 'PASS': | 
|  | outcomes[key].successes.append(setup) | 
|  | elif result == 'FAIL': | 
|  | outcomes[key].failures.append(setup) | 
|  | return outcomes | 
|  |  | 
|  | def analyze_outcome_file(outcome_file): | 
|  | """Analyze the given outcome file.""" | 
|  | outcomes = read_outcome_file(outcome_file) | 
|  | return analyze_outcomes(outcomes) | 
|  |  | 
|  | def main(): | 
|  | try: | 
|  | parser = argparse.ArgumentParser(description=__doc__) | 
|  | parser.add_argument('outcomes', metavar='OUTCOMES.CSV', | 
|  | help='Outcome file to analyze') | 
|  | options = parser.parse_args() | 
|  | results = analyze_outcome_file(options.outcomes) | 
|  | if results.error_count > 0: | 
|  | sys.exit(1) | 
|  | except Exception: # pylint: disable=broad-except | 
|  | # Print the backtrace and exit explicitly with our chosen status. | 
|  | traceback.print_exc() | 
|  | sys.exit(120) | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | main() |