analyze_outcomes.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. #!/usr/bin/env python3
  2. """Analyze the test outcomes from a full CI run.
  3. This script can also run on outcomes from a partial run, but the results are
  4. less likely to be useful.
  5. """
  6. import argparse
  7. import re
  8. import sys
  9. import traceback
  10. import check_test_cases
  11. class Results:
  12. """Process analysis results."""
  13. def __init__(self):
  14. self.error_count = 0
  15. self.warning_count = 0
  16. @staticmethod
  17. def log(fmt, *args, **kwargs):
  18. sys.stderr.write((fmt + '\n').format(*args, **kwargs))
  19. def error(self, fmt, *args, **kwargs):
  20. self.log('Error: ' + fmt, *args, **kwargs)
  21. self.error_count += 1
  22. def warning(self, fmt, *args, **kwargs):
  23. self.log('Warning: ' + fmt, *args, **kwargs)
  24. self.warning_count += 1
  25. class TestCaseOutcomes:
  26. """The outcomes of one test case across many configurations."""
  27. # pylint: disable=too-few-public-methods
  28. def __init__(self):
  29. # Collect a list of witnesses of the test case succeeding or failing.
  30. # Currently we don't do anything with witnesses except count them.
  31. # The format of a witness is determined by the read_outcome_file
  32. # function; it's the platform and configuration joined by ';'.
  33. self.successes = []
  34. self.failures = []
  35. def hits(self):
  36. """Return the number of times a test case has been run.
  37. This includes passes and failures, but not skips.
  38. """
  39. return len(self.successes) + len(self.failures)
  40. class TestDescriptions(check_test_cases.TestDescriptionExplorer):
  41. """Collect the available test cases."""
  42. def __init__(self):
  43. super().__init__()
  44. self.descriptions = set()
  45. def process_test_case(self, _per_file_state,
  46. file_name, _line_number, description):
  47. """Record an available test case."""
  48. base_name = re.sub(r'\.[^.]*$', '', re.sub(r'.*/', '', file_name))
  49. key = ';'.join([base_name, description.decode('utf-8')])
  50. self.descriptions.add(key)
  51. def collect_available_test_cases():
  52. """Collect the available test cases."""
  53. explorer = TestDescriptions()
  54. explorer.walk_all()
  55. return sorted(explorer.descriptions)
  56. def analyze_coverage(results, outcomes):
  57. """Check that all available test cases are executed at least once."""
  58. available = collect_available_test_cases()
  59. for key in available:
  60. hits = outcomes[key].hits() if key in outcomes else 0
  61. if hits == 0:
  62. # Make this a warning, not an error, as long as we haven't
  63. # fixed this branch to have full coverage of test cases.
  64. results.warning('Test case not executed: {}', key)
  65. def analyze_outcomes(outcomes):
  66. """Run all analyses on the given outcome collection."""
  67. results = Results()
  68. analyze_coverage(results, outcomes)
  69. return results
  70. def read_outcome_file(outcome_file):
  71. """Parse an outcome file and return an outcome collection.
  72. An outcome collection is a dictionary mapping keys to TestCaseOutcomes objects.
  73. The keys are the test suite name and the test case description, separated
  74. by 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. def analyze_outcome_file(outcome_file):
  90. """Analyze the given outcome file."""
  91. outcomes = read_outcome_file(outcome_file)
  92. return analyze_outcomes(outcomes)
  93. def main():
  94. try:
  95. parser = argparse.ArgumentParser(description=__doc__)
  96. parser.add_argument('outcomes', metavar='OUTCOMES.CSV',
  97. help='Outcome file to analyze')
  98. options = parser.parse_args()
  99. results = analyze_outcome_file(options.outcomes)
  100. if results.error_count > 0:
  101. sys.exit(1)
  102. except Exception: # pylint: disable=broad-except
  103. # Print the backtrace and exit explicitly with our chosen status.
  104. traceback.print_exc()
  105. sys.exit(120)
  106. if __name__ == '__main__':
  107. main()