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
5import re
6
7
8class ParseError(Exception):
9  pass
10
11
12class 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
55class 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