| # Copyright 2017 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import absolute_import |
| from __future__ import division |
| from __future__ import print_function |
| import re |
| import six |
| |
| |
| class ParseError(Exception): |
| pass |
| |
| |
| class Expectation(object): |
| def __init__(self, reason, test, conditions, results): |
| """Constructor for expectations. |
| |
| Args: |
| reason: String that indicates the reason for disabling. |
| test: String indicating which test is being disabled. |
| conditions: List of tags indicating which conditions to disable for. |
| Conditions are combined using logical and. Example: ['Mac', 'Debug'] |
| results: List of outcomes for test. Example: ['Skip', 'Pass'] |
| """ |
| assert isinstance(reason, six.string_types) or reason is None |
| self._reason = reason |
| assert isinstance(test, six.string_types) |
| self._test = test |
| assert isinstance(conditions, list) |
| self._conditions = conditions |
| assert isinstance(results, list) |
| self._results = results |
| |
| def __eq__(self, other): |
| return (self.reason == other.reason and |
| self.test == other.test and |
| self.conditions == other.conditions and |
| self.results == other.results) |
| |
| @property |
| def reason(self): |
| return self._reason |
| |
| @property |
| def test(self): |
| return self._test |
| |
| @property |
| def conditions(self): |
| return self._conditions |
| |
| @property |
| def results(self): |
| return self._results |
| |
| |
| class TestExpectationParser(object): |
| """Parse expectations data in TA/DA format. |
| |
| This parser covers the 'tagged' test lists format in: |
| bit.ly/chromium-test-list-format |
| |
| Takes raw expectations data as a string read from the TA/DA expectation file |
| in the format: |
| |
| # This is an example expectation file. |
| # |
| # tags: Mac Mac10.10 Mac10.11 |
| # tags: Win Win8 |
| |
| crbug.com/123 [ Win ] benchmark/story [ Skip ] |
| ... |
| """ |
| |
| TAG_TOKEN = '# tags:' |
| _MATCH_STRING = r'^(?:(crbug.com/\d+) )?' # The bug field (optional). |
| _MATCH_STRING += r'(?:\[ (.+) \] )?' # The label field (optional). |
| _MATCH_STRING += r'(\S+) ' # The test path field. |
| _MATCH_STRING += r'\[ ([^\[.]+) \]' # The expectation field. |
| _MATCH_STRING += r'(\s+#.*)?$' # End comment (optional). |
| MATCHER = re.compile(_MATCH_STRING) |
| |
| def __init__(self, raw_data): |
| self._tags = [] |
| self._expectations = [] |
| self._ParseRawExpectationData(raw_data) |
| |
| def _ParseRawExpectationData(self, raw_data): |
| for count, line in list(enumerate(raw_data.splitlines(), start=1)): |
| # Handle metadata and comments. |
| if line.startswith(self.TAG_TOKEN): |
| for word in line[len(self.TAG_TOKEN):].split(): |
| # Expectations must be after all tags are declared. |
| if self._expectations: |
| raise ParseError('Tag found after first expectation.') |
| self._tags.append(word) |
| elif line.startswith('#') or not line: |
| continue # Ignore, it is just a comment or empty. |
| else: |
| self._expectations.append( |
| self._ParseExpectationLine(count, line, self._tags)) |
| |
| def _ParseExpectationLine(self, line_number, line, tags): |
| match = self.MATCHER.match(line) |
| if not match: |
| raise ParseError( |
| 'Expectation has invalid syntax on line %d: %s' |
| % (line_number, line)) |
| # Unused group is optional trailing comment. |
| reason, raw_conditions, test, results, _ = match.groups() |
| conditions = [c for c in raw_conditions.split()] if raw_conditions else [] |
| |
| for c in conditions: |
| if c not in tags: |
| raise ParseError( |
| 'Condition %s not found in expectations tag data. Line %d' |
| % (c, line_number)) |
| return Expectation(reason, test, conditions, [r for r in results.split()]) |
| |
| @property |
| def expectations(self): |
| return self._expectations |
| |
| @property |
| def tags(self): |
| return self._tags |