1# Copyright 2011 Google Inc. All Rights Reserved. 2# Author: kbaclawski@google.com (Krystian Baclawski) 3# 4 5from collections import defaultdict 6from collections import namedtuple 7from datetime import datetime 8from fnmatch import fnmatch 9from itertools import groupby 10import logging 11import os.path 12import re 13 14 15class DejaGnuTestResult(namedtuple('Result', 'name variant result flaky')): 16 """Stores the result of a single test case.""" 17 18 # avoid adding __dict__ to the class 19 __slots__ = () 20 21 LINE_RE = re.compile(r'([A-Z]+):\s+([\w/+.-]+)(.*)') 22 23 @classmethod 24 def FromLine(cls, line): 25 """Alternate constructor which takes a string and parses it.""" 26 try: 27 attrs, line = line.split('|', 1) 28 29 if attrs.strip() != 'flaky': 30 return None 31 32 line = line.strip() 33 flaky = True 34 except ValueError: 35 flaky = False 36 37 fields = cls.LINE_RE.match(line.strip()) 38 39 if fields: 40 result, path, variant = fields.groups() 41 42 # some of the tests are generated in build dir and are issued from there, 43 # because every test run is performed in randomly named tmp directory we 44 # need to remove random part 45 try: 46 # assume that 2nd field is a test path 47 path_parts = path.split('/') 48 49 index = path_parts.index('testsuite') 50 path = '/'.join(path_parts[index + 1:]) 51 except ValueError: 52 path = '/'.join(path_parts) 53 54 # Remove junk from test description. 55 variant = variant.strip(', ') 56 57 substitutions = [ 58 # remove include paths - they contain name of tmp directory 59 ('-I\S+', ''), 60 # compress white spaces 61 ('\s+', ' ') 62 ] 63 64 for pattern, replacement in substitutions: 65 variant = re.sub(pattern, replacement, variant) 66 67 # Some tests separate last component of path by space, so actual filename 68 # ends up in description instead of path part. Correct that. 69 try: 70 first, rest = variant.split(' ', 1) 71 except ValueError: 72 pass 73 else: 74 if first.endswith('.o'): 75 path = os.path.join(path, first) 76 variant = rest 77 78 # DejaGNU framework errors don't contain path part at all, so description 79 # part has to be reconstructed. 80 if not any(os.path.basename(path).endswith('.%s' % suffix) 81 for suffix in ['h', 'c', 'C', 'S', 'H', 'cc', 'i', 'o']): 82 variant = '%s %s' % (path, variant) 83 path = '' 84 85 # Some tests are picked up from current directory (presumably DejaGNU 86 # generates some test files). Remove the prefix for these files. 87 if path.startswith('./'): 88 path = path[2:] 89 90 return cls(path, variant or '', result, flaky=flaky) 91 92 def __str__(self): 93 """Returns string representation of a test result.""" 94 if self.flaky: 95 fmt = 'flaky | ' 96 else: 97 fmt = '' 98 fmt += '{2}: {0}' 99 if self.variant: 100 fmt += ' {1}' 101 return fmt.format(*self) 102 103 104class DejaGnuTestRun(object): 105 """Container for test results that were a part of single test run. 106 107 The class stores also metadata related to the test run. 108 109 Attributes: 110 board: Name of DejaGNU board, which was used to run the tests. 111 date: The date when the test run was started. 112 target: Target triple. 113 host: Host triple. 114 tool: The tool that was tested (e.g. gcc, binutils, g++, etc.) 115 results: a list of DejaGnuTestResult objects. 116 """ 117 118 __slots__ = ('board', 'date', 'target', 'host', 'tool', 'results') 119 120 def __init__(self, **kwargs): 121 assert all(name in self.__slots__ for name in kwargs) 122 123 self.results = set() 124 self.date = kwargs.get('date', datetime.now()) 125 126 for name in ('board', 'target', 'tool', 'host'): 127 setattr(self, name, kwargs.get(name, 'unknown')) 128 129 @classmethod 130 def FromFile(cls, filename): 131 """Alternate constructor - reads a DejaGNU output file.""" 132 test_run = cls() 133 test_run.FromDejaGnuOutput(filename) 134 test_run.CleanUpTestResults() 135 return test_run 136 137 @property 138 def summary(self): 139 """Returns a summary as {ResultType -> Count} dictionary.""" 140 summary = defaultdict(int) 141 142 for r in self.results: 143 summary[r.result] += 1 144 145 return summary 146 147 def _ParseBoard(self, fields): 148 self.board = fields.group(1).strip() 149 150 def _ParseDate(self, fields): 151 self.date = datetime.strptime(fields.group(2).strip(), '%a %b %d %X %Y') 152 153 def _ParseTarget(self, fields): 154 self.target = fields.group(2).strip() 155 156 def _ParseHost(self, fields): 157 self.host = fields.group(2).strip() 158 159 def _ParseTool(self, fields): 160 self.tool = fields.group(1).strip() 161 162 def FromDejaGnuOutput(self, filename): 163 """Read in and parse DejaGNU output file.""" 164 165 logging.info('Reading "%s" DejaGNU output file.', filename) 166 167 with open(filename, 'r') as report: 168 lines = [line.strip() for line in report.readlines() if line.strip()] 169 170 parsers = ((re.compile(r'Running target (.*)'), self._ParseBoard), 171 (re.compile(r'Test Run By (.*) on (.*)'), self._ParseDate), 172 (re.compile(r'=== (.*) tests ==='), self._ParseTool), 173 (re.compile(r'Target(\s+)is (.*)'), self._ParseTarget), 174 (re.compile(r'Host(\s+)is (.*)'), self._ParseHost)) 175 176 for line in lines: 177 result = DejaGnuTestResult.FromLine(line) 178 179 if result: 180 self.results.add(result) 181 else: 182 for regexp, parser in parsers: 183 fields = regexp.match(line) 184 if fields: 185 parser(fields) 186 break 187 188 logging.debug('DejaGNU output file parsed successfully.') 189 logging.debug(self) 190 191 def CleanUpTestResults(self): 192 """Remove certain test results considered to be spurious. 193 194 1) Large number of test reported as UNSUPPORTED are also marked as 195 UNRESOLVED. If that's the case remove latter result. 196 2) If a test is performed on compiler output and for some reason compiler 197 fails, we don't want to report all failures that depend on the former. 198 """ 199 name_key = lambda v: v.name 200 results_by_name = sorted(self.results, key=name_key) 201 202 for name, res_iter in groupby(results_by_name, key=name_key): 203 results = set(res_iter) 204 205 # If DejaGnu was unable to compile a test it will create following result: 206 failed = DejaGnuTestResult(name, '(test for excess errors)', 'FAIL', 207 False) 208 209 # If a test compilation failed, remove all results that are dependent. 210 if failed in results: 211 dependants = set(filter(lambda r: r.result != 'FAIL', results)) 212 213 self.results -= dependants 214 215 for res in dependants: 216 logging.info('Removed {%s} dependance.', res) 217 218 # Remove all UNRESOLVED results that were also marked as UNSUPPORTED. 219 unresolved = [res._replace(result='UNRESOLVED') 220 for res in results if res.result == 'UNSUPPORTED'] 221 222 for res in unresolved: 223 if res in self.results: 224 self.results.remove(res) 225 logging.info('Removed {%s} duplicate.', res) 226 227 def _IsApplicable(self, manifest): 228 """Checks if test results need to be reconsidered based on the manifest.""" 229 check_list = [(self.tool, manifest.tool), (self.board, manifest.board)] 230 231 return all(fnmatch(text, pattern) for text, pattern in check_list) 232 233 def SuppressTestResults(self, manifests): 234 """Suppresses all test results listed in manifests.""" 235 236 # Get a set of tests results that are going to be suppressed if they fail. 237 manifest_results = set() 238 239 for manifest in filter(self._IsApplicable, manifests): 240 manifest_results |= set(manifest.results) 241 242 suppressed_results = self.results & manifest_results 243 244 for result in sorted(suppressed_results): 245 logging.debug('Result suppressed for {%s}.', result) 246 247 new_result = '!' + result.result 248 249 # Mark result suppression as applied. 250 manifest_results.remove(result) 251 252 # Rewrite test result. 253 self.results.remove(result) 254 self.results.add(result._replace(result=new_result)) 255 256 for result in sorted(manifest_results): 257 logging.warning('Result {%s} listed in manifest but not suppressed.', 258 result) 259 260 def __str__(self): 261 return '{0}, {1} @{2} on {3}'.format(self.target, self.tool, self.board, 262 self.date) 263