1 # Copyright 2017 The Chromium Authors. All rights reserved. 2 # Use of this source code is governed by a BSD-style license that can be 3 # found in the LICENSE file. 4 5 import re 6 7 8 class ParseError(Exception): 9 pass 10 11 12 class Expectation(object): 13 def __init__(self, reason, test, conditions, results): 14 """Constructor for expectations. 15 16 Args: 17 reason: String that indicates the reason for disabling. 18 test: String indicating which test is being disabled. 19 conditions: List of tags indicating which conditions to disable for. 20 Conditions are combined using logical and. Example: ['Mac', 'Debug'] 21 results: List of outcomes for test. Example: ['Skip', 'Pass'] 22 """ 23 assert isinstance(reason, basestring) or reason is None 24 self._reason = reason 25 assert isinstance(test, basestring) 26 self._test = test 27 assert isinstance(conditions, list) 28 self._conditions = conditions 29 assert isinstance(results, list) 30 self._results = results 31 32 def __eq__(self, other): 33 return (self.reason == other.reason and 34 self.test == other.test and 35 self.conditions == other.conditions and 36 self.results == other.results) 37 38 @property 39 def reason(self): 40 return self._reason 41 42 @property 43 def test(self): 44 return self._test 45 46 @property 47 def conditions(self): 48 return self._conditions 49 50 @property 51 def results(self): 52 return self._results 53 54 55 class TestExpectationParser(object): 56 """Parse expectations data in TA/DA format. 57 58 This parser covers the 'tagged' test lists format in: 59 bit.ly/chromium-test-list-format 60 61 Takes raw expectations data as a string read from the TA/DA expectation file 62 in the format: 63 64 # This is an example expectation file. 65 # 66 # tags: Mac Mac10.10 Mac10.11 67 # tags: Win Win8 68 69 crbug.com/123 [ Win ] benchmark/story [ Skip ] 70 ... 71 """ 72 73 TAG_TOKEN = '# tags:' 74 _MATCH_STRING = r'^(?:(crbug.com/\d+) )?' # The bug field (optional). 75 _MATCH_STRING += r'(?:\[ (.+) \] )?' # The label field (optional). 76 _MATCH_STRING += r'(\S+) ' # The test path field. 77 _MATCH_STRING += r'\[ ([^\[.]+) \]' # The expectation field. 78 _MATCH_STRING += r'(\s+#.*)?$' # End comment (optional). 79 MATCHER = re.compile(_MATCH_STRING) 80 81 def __init__(self, raw_data): 82 self._tags = [] 83 self._expectations = [] 84 self._ParseRawExpectationData(raw_data) 85 86 def _ParseRawExpectationData(self, raw_data): 87 for count, line in list(enumerate(raw_data.splitlines(), start=1)): 88 # Handle metadata and comments. 89 if line.startswith(self.TAG_TOKEN): 90 for word in line[len(self.TAG_TOKEN):].split(): 91 # Expectations must be after all tags are declared. 92 if self._expectations: 93 raise ParseError('Tag found after first expectation.') 94 self._tags.append(word) 95 elif line.startswith('#') or not line: 96 continue # Ignore, it is just a comment or empty. 97 else: 98 self._expectations.append( 99 self._ParseExpectationLine(count, line, self._tags)) 100 101 def _ParseExpectationLine(self, line_number, line, tags): 102 match = self.MATCHER.match(line) 103 if not match: 104 raise ParseError( 105 'Expectation has invalid syntax on line %d: %s' 106 % (line_number, line)) 107 # Unused group is optional trailing comment. 108 reason, raw_conditions, test, results, _ = match.groups() 109 conditions = [c for c in raw_conditions.split()] if raw_conditions else [] 110 111 for c in conditions: 112 if c not in tags: 113 raise ParseError( 114 'Condition %s not found in expectations tag data. Line %d' 115 % (c, line_number)) 116 return Expectation(reason, test, conditions, [r for r in results.split()]) 117 118 @property 119 def expectations(self): 120 return self._expectations 121 122 @property 123 def tags(self): 124 return self._tags 125