Home | History | Annotate | Download | only in models
      1 # Copyright (C) 2010 Google Inc. All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions are
      5 # met:
      6 #
      7 #     * Redistributions of source code must retain the above copyright
      8 # notice, this list of conditions and the following disclaimer.
      9 #     * Redistributions in binary form must reproduce the above
     10 # copyright notice, this list of conditions and the following disclaimer
     11 # in the documentation and/or other materials provided with the
     12 # distribution.
     13 #     * Neither the name of Google Inc. nor the names of its
     14 # contributors may be used to endorse or promote products derived from
     15 # this software without specific prior written permission.
     16 #
     17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     28 
     29 """A helper class for reading in and dealing with tests expectations
     30 for layout tests.
     31 """
     32 
     33 import logging
     34 import re
     35 
     36 from webkitpy.layout_tests.models.test_configuration import TestConfigurationConverter
     37 
     38 _log = logging.getLogger(__name__)
     39 
     40 
     41 # Test expectation and specifier constants.
     42 #
     43 # FIXME: range() starts with 0 which makes if expectation checks harder
     44 # as PASS is 0.
     45 (PASS, FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO, TIMEOUT, CRASH, LEAK, SKIP, WONTFIX,
     46  SLOW, REBASELINE, NEEDS_REBASELINE, NEEDS_MANUAL_REBASELINE, MISSING, FLAKY, NOW, NONE) = range(19)
     47 
     48 # FIXME: Perhas these two routines should be part of the Port instead?
     49 BASELINE_SUFFIX_LIST = ('png', 'wav', 'txt')
     50 
     51 WEBKIT_BUG_PREFIX = 'webkit.org/b/'
     52 CHROMIUM_BUG_PREFIX = 'crbug.com/'
     53 V8_BUG_PREFIX = 'code.google.com/p/v8/issues/detail?id='
     54 NAMED_BUG_PREFIX = 'Bug('
     55 
     56 MISSING_KEYWORD = 'Missing'
     57 NEEDS_REBASELINE_KEYWORD = 'NeedsRebaseline'
     58 NEEDS_MANUAL_REBASELINE_KEYWORD = 'NeedsManualRebaseline'
     59 
     60 class ParseError(Exception):
     61     def __init__(self, warnings):
     62         super(ParseError, self).__init__()
     63         self.warnings = warnings
     64 
     65     def __str__(self):
     66         return '\n'.join(map(str, self.warnings))
     67 
     68     def __repr__(self):
     69         return 'ParseError(warnings=%s)' % self.warnings
     70 
     71 
     72 class TestExpectationParser(object):
     73     """Provides parsing facilities for lines in the test_expectation.txt file."""
     74 
     75     # FIXME: Rename these to *_KEYWORD as in MISSING_KEYWORD above, but make the case studdly-caps to match the actual file contents.
     76     REBASELINE_MODIFIER = 'rebaseline'
     77     NEEDS_REBASELINE_MODIFIER = 'needsrebaseline'
     78     NEEDS_MANUAL_REBASELINE_MODIFIER = 'needsmanualrebaseline'
     79     PASS_EXPECTATION = 'pass'
     80     SKIP_MODIFIER = 'skip'
     81     SLOW_MODIFIER = 'slow'
     82     WONTFIX_MODIFIER = 'wontfix'
     83 
     84     TIMEOUT_EXPECTATION = 'timeout'
     85 
     86     MISSING_BUG_WARNING = 'Test lacks BUG specifier.'
     87 
     88     def __init__(self, port, full_test_list, is_lint_mode):
     89         self._port = port
     90         self._test_configuration_converter = TestConfigurationConverter(set(port.all_test_configurations()), port.configuration_specifier_macros())
     91         self._full_test_list = full_test_list
     92         self._is_lint_mode = is_lint_mode
     93 
     94     def parse(self, filename, expectations_string):
     95         expectation_lines = []
     96         line_number = 0
     97         for line in expectations_string.split("\n"):
     98             line_number += 1
     99             test_expectation = self._tokenize_line(filename, line, line_number)
    100             self._parse_line(test_expectation)
    101             expectation_lines.append(test_expectation)
    102         return expectation_lines
    103 
    104     def _create_expectation_line(self, test_name, expectations, file_name):
    105         expectation_line = TestExpectationLine()
    106         expectation_line.original_string = test_name
    107         expectation_line.name = test_name
    108         expectation_line.filename = file_name
    109         expectation_line.expectations = expectations
    110         return expectation_line
    111 
    112     def expectation_line_for_test(self, test_name, expectations):
    113         expectation_line = self._create_expectation_line(test_name, expectations, '<Bot TestExpectations>')
    114         self._parse_line(expectation_line)
    115         return expectation_line
    116 
    117 
    118     def expectation_for_skipped_test(self, test_name):
    119         if not self._port.test_exists(test_name):
    120             _log.warning('The following test %s from the Skipped list doesn\'t exist' % test_name)
    121         expectation_line = self._create_expectation_line(test_name, [TestExpectationParser.PASS_EXPECTATION], '<Skipped file>')
    122         expectation_line.expectations = [TestExpectationParser.SKIP_MODIFIER, TestExpectationParser.WONTFIX_MODIFIER]
    123         expectation_line.is_skipped_outside_expectations_file = True
    124         self._parse_line(expectation_line)
    125         return expectation_line
    126 
    127     def _parse_line(self, expectation_line):
    128         if not expectation_line.name:
    129             return
    130 
    131         if not self._check_test_exists(expectation_line):
    132             return
    133 
    134         expectation_line.is_file = self._port.test_isfile(expectation_line.name)
    135         if expectation_line.is_file:
    136             expectation_line.path = expectation_line.name
    137         else:
    138             expectation_line.path = self._port.normalize_test_name(expectation_line.name)
    139 
    140         self._collect_matching_tests(expectation_line)
    141 
    142         self._parse_specifiers(expectation_line)
    143         self._parse_expectations(expectation_line)
    144 
    145     def _parse_specifiers(self, expectation_line):
    146         if self._is_lint_mode:
    147             self._lint_line(expectation_line)
    148 
    149         parsed_specifiers = set([specifier.lower() for specifier in expectation_line.specifiers])
    150         expectation_line.matching_configurations = self._test_configuration_converter.to_config_set(parsed_specifiers, expectation_line.warnings)
    151 
    152     def _lint_line(self, expectation_line):
    153         expectations = [expectation.lower() for expectation in expectation_line.expectations]
    154         if not expectation_line.bugs and self.WONTFIX_MODIFIER not in expectations:
    155             expectation_line.warnings.append(self.MISSING_BUG_WARNING)
    156         if self.REBASELINE_MODIFIER in expectations:
    157             expectation_line.warnings.append('REBASELINE should only be used for running rebaseline.py. Cannot be checked in.')
    158 
    159         if self.NEEDS_REBASELINE_MODIFIER in expectations or self.NEEDS_MANUAL_REBASELINE_MODIFIER in expectations:
    160             for test in expectation_line.matching_tests:
    161                 if self._port.reference_files(test):
    162                     expectation_line.warnings.append('A reftest cannot be marked as NeedsRebaseline/NeedsManualRebaseline')
    163 
    164     def _parse_expectations(self, expectation_line):
    165         result = set()
    166         for part in expectation_line.expectations:
    167             expectation = TestExpectations.expectation_from_string(part)
    168             if expectation is None:  # Careful, PASS is currently 0.
    169                 expectation_line.warnings.append('Unsupported expectation: %s' % part)
    170                 continue
    171             result.add(expectation)
    172         expectation_line.parsed_expectations = result
    173 
    174     def _check_test_exists(self, expectation_line):
    175         # WebKit's way of skipping tests is to add a -disabled suffix.
    176         # So we should consider the path existing if the path or the
    177         # -disabled version exists.
    178         if not self._port.test_exists(expectation_line.name) and not self._port.test_exists(expectation_line.name + '-disabled'):
    179             # Log a warning here since you hit this case any
    180             # time you update TestExpectations without syncing
    181             # the LayoutTests directory
    182             expectation_line.warnings.append('Path does not exist.')
    183             return False
    184         return True
    185 
    186     def _collect_matching_tests(self, expectation_line):
    187         """Convert the test specification to an absolute, normalized
    188         path and make sure directories end with the OS path separator."""
    189         # FIXME: full_test_list can quickly contain a big amount of
    190         # elements. We should consider at some point to use a more
    191         # efficient structure instead of a list. Maybe a dictionary of
    192         # lists to represent the tree of tests, leaves being test
    193         # files and nodes being categories.
    194 
    195         if not self._full_test_list:
    196             expectation_line.matching_tests = [expectation_line.path]
    197             return
    198 
    199         if not expectation_line.is_file:
    200             # this is a test category, return all the tests of the category.
    201             expectation_line.matching_tests = [test for test in self._full_test_list if test.startswith(expectation_line.path)]
    202             return
    203 
    204         # this is a test file, do a quick check if it's in the
    205         # full test suite.
    206         if expectation_line.path in self._full_test_list:
    207             expectation_line.matching_tests.append(expectation_line.path)
    208 
    209     # FIXME: Update the original specifiers and remove this once the old syntax is gone.
    210     _configuration_tokens_list = [
    211         'Mac', 'SnowLeopard', 'Lion', 'Retina', 'MountainLion', 'Mavericks',
    212         'Win', 'XP', 'Win7',
    213         'Linux',
    214         'Android',
    215         'Release',
    216         'Debug',
    217     ]
    218 
    219     _configuration_tokens = dict((token, token.upper()) for token in _configuration_tokens_list)
    220     _inverted_configuration_tokens = dict((value, name) for name, value in _configuration_tokens.iteritems())
    221 
    222     # FIXME: Update the original specifiers list and remove this once the old syntax is gone.
    223     _expectation_tokens = {
    224         'Crash': 'CRASH',
    225         'Leak': 'LEAK',
    226         'Failure': 'FAIL',
    227         'ImageOnlyFailure': 'IMAGE',
    228         MISSING_KEYWORD: 'MISSING',
    229         'Pass': 'PASS',
    230         'Rebaseline': 'REBASELINE',
    231         NEEDS_REBASELINE_KEYWORD: 'NEEDSREBASELINE',
    232         NEEDS_MANUAL_REBASELINE_KEYWORD: 'NEEDSMANUALREBASELINE',
    233         'Skip': 'SKIP',
    234         'Slow': 'SLOW',
    235         'Timeout': 'TIMEOUT',
    236         'WontFix': 'WONTFIX',
    237     }
    238 
    239     _inverted_expectation_tokens = dict([(value, name) for name, value in _expectation_tokens.iteritems()] +
    240                                         [('TEXT', 'Failure'), ('IMAGE+TEXT', 'Failure'), ('AUDIO', 'Failure')])
    241 
    242     # FIXME: Seems like these should be classmethods on TestExpectationLine instead of TestExpectationParser.
    243     @classmethod
    244     def _tokenize_line(cls, filename, expectation_string, line_number):
    245         """Tokenizes a line from TestExpectations and returns an unparsed TestExpectationLine instance using the old format.
    246 
    247         The new format for a test expectation line is:
    248 
    249         [[bugs] [ "[" <configuration specifiers> "]" <name> [ "[" <expectations> "]" ["#" <comment>]
    250 
    251         Any errant whitespace is not preserved.
    252 
    253         """
    254         expectation_line = TestExpectationLine()
    255         expectation_line.original_string = expectation_string
    256         expectation_line.filename = filename
    257         expectation_line.line_numbers = str(line_number)
    258 
    259         comment_index = expectation_string.find("#")
    260         if comment_index == -1:
    261             comment_index = len(expectation_string)
    262         else:
    263             expectation_line.comment = expectation_string[comment_index + 1:]
    264 
    265         remaining_string = re.sub(r"\s+", " ", expectation_string[:comment_index].strip())
    266         if len(remaining_string) == 0:
    267             return expectation_line
    268 
    269         # special-case parsing this so that we fail immediately instead of treating this as a test name
    270         if remaining_string.startswith('//'):
    271             expectation_line.warnings = ['use "#" instead of "//" for comments']
    272             return expectation_line
    273 
    274         bugs = []
    275         specifiers = []
    276         name = None
    277         expectations = []
    278         warnings = []
    279         has_unrecognized_expectation = False
    280 
    281         tokens = remaining_string.split()
    282         state = 'start'
    283         for token in tokens:
    284             if (token.startswith(WEBKIT_BUG_PREFIX) or
    285                 token.startswith(CHROMIUM_BUG_PREFIX) or
    286                 token.startswith(V8_BUG_PREFIX) or
    287                 token.startswith(NAMED_BUG_PREFIX)):
    288                 if state != 'start':
    289                     warnings.append('"%s" is not at the start of the line.' % token)
    290                     break
    291                 if token.startswith(WEBKIT_BUG_PREFIX):
    292                     bugs.append(token)
    293                 elif token.startswith(CHROMIUM_BUG_PREFIX):
    294                     bugs.append(token)
    295                 elif token.startswith(V8_BUG_PREFIX):
    296                     bugs.append(token)
    297                 else:
    298                     match = re.match('Bug\((\w+)\)$', token)
    299                     if not match:
    300                         warnings.append('unrecognized bug identifier "%s"' % token)
    301                         break
    302                     else:
    303                         bugs.append(token)
    304             elif token == '[':
    305                 if state == 'start':
    306                     state = 'configuration'
    307                 elif state == 'name_found':
    308                     state = 'expectations'
    309                 else:
    310                     warnings.append('unexpected "["')
    311                     break
    312             elif token == ']':
    313                 if state == 'configuration':
    314                     state = 'name'
    315                 elif state == 'expectations':
    316                     state = 'done'
    317                 else:
    318                     warnings.append('unexpected "]"')
    319                     break
    320             elif token in ('//', ':', '='):
    321                 warnings.append('"%s" is not legal in the new TestExpectations syntax.' % token)
    322                 break
    323             elif state == 'configuration':
    324                 specifiers.append(cls._configuration_tokens.get(token, token))
    325             elif state == 'expectations':
    326                 if token not in cls._expectation_tokens:
    327                     has_unrecognized_expectation = True
    328                     warnings.append('Unrecognized expectation "%s"' % token)
    329                 else:
    330                     expectations.append(cls._expectation_tokens.get(token, token))
    331             elif state == 'name_found':
    332                 warnings.append('expecting "[", "#", or end of line instead of "%s"' % token)
    333                 break
    334             else:
    335                 name = token
    336                 state = 'name_found'
    337 
    338         if not warnings:
    339             if not name:
    340                 warnings.append('Did not find a test name.')
    341             elif state not in ('name_found', 'done'):
    342                 warnings.append('Missing a "]"')
    343 
    344         if 'WONTFIX' in expectations and 'SKIP' not in expectations:
    345             expectations.append('SKIP')
    346 
    347         if ('SKIP' in expectations or 'WONTFIX' in expectations) and len(set(expectations) - set(['SKIP', 'WONTFIX'])):
    348             warnings.append('A test marked Skip or WontFix must not have other expectations.')
    349 
    350         if not expectations and not has_unrecognized_expectation:
    351             warnings.append('Missing expectations.')
    352 
    353         expectation_line.bugs = bugs
    354         expectation_line.specifiers = specifiers
    355         expectation_line.expectations = expectations
    356         expectation_line.name = name
    357         expectation_line.warnings = warnings
    358         return expectation_line
    359 
    360     @classmethod
    361     def _split_space_separated(cls, space_separated_string):
    362         """Splits a space-separated string into an array."""
    363         return [part.strip() for part in space_separated_string.strip().split(' ')]
    364 
    365 
    366 class TestExpectationLine(object):
    367     """Represents a line in test expectations file."""
    368 
    369     def __init__(self):
    370         """Initializes a blank-line equivalent of an expectation."""
    371         self.original_string = None
    372         self.filename = None  # this is the path to the expectations file for this line
    373         self.line_numbers = "0"
    374         self.name = None  # this is the path in the line itself
    375         self.path = None  # this is the normpath of self.name
    376         self.bugs = []
    377         self.specifiers = []
    378         self.parsed_specifiers = []
    379         self.matching_configurations = set()
    380         self.expectations = []
    381         self.parsed_expectations = set()
    382         self.comment = None
    383         self.matching_tests = []
    384         self.warnings = []
    385         self.is_skipped_outside_expectations_file = False
    386 
    387     def __eq__(self, other):
    388         return (self.original_string == other.original_string
    389             and self.filename == other.filename
    390             and self.line_numbers == other.line_numbers
    391             and self.name == other.name
    392             and self.path == other.path
    393             and self.bugs == other.bugs
    394             and self.specifiers == other.specifiers
    395             and self.parsed_specifiers == other.parsed_specifiers
    396             and self.matching_configurations == other.matching_configurations
    397             and self.expectations == other.expectations
    398             and self.parsed_expectations == other.parsed_expectations
    399             and self.comment == other.comment
    400             and self.matching_tests == other.matching_tests
    401             and self.warnings == other.warnings
    402             and self.is_skipped_outside_expectations_file == other.is_skipped_outside_expectations_file)
    403 
    404     def is_invalid(self):
    405         return bool(self.warnings and self.warnings != [TestExpectationParser.MISSING_BUG_WARNING])
    406 
    407     def is_flaky(self):
    408         return len(self.parsed_expectations) > 1
    409 
    410     def is_whitespace_or_comment(self):
    411         return bool(re.match("^\s*$", self.original_string.split('#')[0]))
    412 
    413     @staticmethod
    414     def create_passing_expectation(test):
    415         expectation_line = TestExpectationLine()
    416         expectation_line.name = test
    417         expectation_line.path = test
    418         expectation_line.parsed_expectations = set([PASS])
    419         expectation_line.expectations = set(['PASS'])
    420         expectation_line.matching_tests = [test]
    421         return expectation_line
    422 
    423     @staticmethod
    424     def merge_expectation_lines(line1, line2, model_all_expectations):
    425         """Merges the expectations of line2 into line1 and returns a fresh object."""
    426         if line1 is None:
    427             return line2
    428         if line2 is None:
    429             return line1
    430         if model_all_expectations and line1.filename != line2.filename:
    431             return line2
    432 
    433         # Don't merge original_string or comment.
    434         result = TestExpectationLine()
    435         # We only care about filenames when we're linting, in which case the filenames are the same.
    436         # Not clear that there's anything better to do when not linting and the filenames are different.
    437         if model_all_expectations:
    438             result.filename = line2.filename
    439         result.line_numbers = line1.line_numbers + "," + line2.line_numbers
    440         result.name = line1.name
    441         result.path = line1.path
    442         result.parsed_expectations = set(line1.parsed_expectations) | set(line2.parsed_expectations)
    443         result.expectations = list(set(line1.expectations) | set(line2.expectations))
    444         result.bugs = list(set(line1.bugs) | set(line2.bugs))
    445         result.specifiers = list(set(line1.specifiers) | set(line2.specifiers))
    446         result.parsed_specifiers = list(set(line1.parsed_specifiers) | set(line2.parsed_specifiers))
    447         result.matching_configurations = set(line1.matching_configurations) | set(line2.matching_configurations)
    448         result.matching_tests = list(list(set(line1.matching_tests) | set(line2.matching_tests)))
    449         result.warnings = list(set(line1.warnings) | set(line2.warnings))
    450         result.is_skipped_outside_expectations_file = line1.is_skipped_outside_expectations_file or line2.is_skipped_outside_expectations_file
    451         return result
    452 
    453     def to_string(self, test_configuration_converter, include_specifiers=True, include_expectations=True, include_comment=True):
    454         parsed_expectation_to_string = dict([[parsed_expectation, expectation_string] for expectation_string, parsed_expectation in TestExpectations.EXPECTATIONS.items()])
    455 
    456         if self.is_invalid():
    457             return self.original_string or ''
    458 
    459         if self.name is None:
    460             return '' if self.comment is None else "#%s" % self.comment
    461 
    462         if test_configuration_converter and self.bugs:
    463             specifiers_list = test_configuration_converter.to_specifiers_list(self.matching_configurations)
    464             result = []
    465             for specifiers in specifiers_list:
    466                 # FIXME: this is silly that we join the specifiers and then immediately split them.
    467                 specifiers = self._serialize_parsed_specifiers(test_configuration_converter, specifiers).split()
    468                 expectations = self._serialize_parsed_expectations(parsed_expectation_to_string).split()
    469                 result.append(self._format_line(self.bugs, specifiers, self.name, expectations, self.comment))
    470             return "\n".join(result) if result else None
    471 
    472         return self._format_line(self.bugs, self.specifiers, self.name, self.expectations, self.comment,
    473             include_specifiers, include_expectations, include_comment)
    474 
    475     def to_csv(self):
    476         # Note that this doesn't include the comments.
    477         return '%s,%s,%s,%s' % (self.name, ' '.join(self.bugs), ' '.join(self.specifiers), ' '.join(self.expectations))
    478 
    479     def _serialize_parsed_expectations(self, parsed_expectation_to_string):
    480         result = []
    481         for index in TestExpectations.EXPECTATIONS.values():
    482             if index in self.parsed_expectations:
    483                 result.append(parsed_expectation_to_string[index])
    484         return ' '.join(result)
    485 
    486     def _serialize_parsed_specifiers(self, test_configuration_converter, specifiers):
    487         result = []
    488         result.extend(sorted(self.parsed_specifiers))
    489         result.extend(test_configuration_converter.specifier_sorter().sort_specifiers(specifiers))
    490         return ' '.join(result)
    491 
    492     @staticmethod
    493     def _filter_redundant_expectations(expectations):
    494         if set(expectations) == set(['Pass', 'Skip']):
    495             return ['Skip']
    496         if set(expectations) == set(['Pass', 'Slow']):
    497             return ['Slow']
    498         return expectations
    499 
    500     @staticmethod
    501     def _format_line(bugs, specifiers, name, expectations, comment, include_specifiers=True, include_expectations=True, include_comment=True):
    502         new_specifiers = []
    503         new_expectations = []
    504         for specifier in specifiers:
    505             # FIXME: Make this all work with the mixed-cased specifiers (e.g. WontFix, Slow, etc).
    506             specifier = specifier.upper()
    507             new_specifiers.append(TestExpectationParser._inverted_configuration_tokens.get(specifier, specifier))
    508 
    509         for expectation in expectations:
    510             expectation = expectation.upper()
    511             new_expectations.append(TestExpectationParser._inverted_expectation_tokens.get(expectation, expectation))
    512 
    513         result = ''
    514         if include_specifiers and (bugs or new_specifiers):
    515             if bugs:
    516                 result += ' '.join(bugs) + ' '
    517             if new_specifiers:
    518                 result += '[ %s ] ' % ' '.join(new_specifiers)
    519         result += name
    520         if include_expectations and new_expectations:
    521             new_expectations = TestExpectationLine._filter_redundant_expectations(new_expectations)
    522             result += ' [ %s ]' % ' '.join(sorted(set(new_expectations)))
    523         if include_comment and comment is not None:
    524             result += " #%s" % comment
    525         return result
    526 
    527 
    528 # FIXME: Refactor API to be a proper CRUD.
    529 class TestExpectationsModel(object):
    530     """Represents relational store of all expectations and provides CRUD semantics to manage it."""
    531 
    532     def __init__(self, shorten_filename=None):
    533         # Maps a test to its list of expectations.
    534         self._test_to_expectations = {}
    535 
    536         # Maps a test to list of its specifiers (string values)
    537         self._test_to_specifiers = {}
    538 
    539         # Maps a test to a TestExpectationLine instance.
    540         self._test_to_expectation_line = {}
    541 
    542         self._expectation_to_tests = self._dict_of_sets(TestExpectations.EXPECTATIONS)
    543         self._timeline_to_tests = self._dict_of_sets(TestExpectations.TIMELINES)
    544         self._result_type_to_tests = self._dict_of_sets(TestExpectations.RESULT_TYPES)
    545 
    546         self._shorten_filename = shorten_filename or (lambda x: x)
    547 
    548     def _merge_test_map(self, self_map, other_map):
    549         for test in other_map:
    550             new_expectations = set(other_map[test])
    551             if test in self_map:
    552                 new_expectations |= set(self_map[test])
    553             self_map[test] = list(new_expectations) if isinstance(other_map[test], list) else new_expectations
    554 
    555     def _merge_dict_of_sets(self, self_dict, other_dict):
    556         for key in other_dict:
    557             self_dict[key] |= other_dict[key]
    558 
    559     def merge_model(self, other):
    560         self._merge_test_map(self._test_to_expectations, other._test_to_expectations)
    561 
    562         for test, line in other._test_to_expectation_line.items():
    563             if test in self._test_to_expectation_line:
    564                 line = TestExpectationLine.merge_expectation_lines(self._test_to_expectation_line[test], line, model_all_expectations=False)
    565             self._test_to_expectation_line[test] = line
    566 
    567         self._merge_dict_of_sets(self._expectation_to_tests, other._expectation_to_tests)
    568         self._merge_dict_of_sets(self._timeline_to_tests, other._timeline_to_tests)
    569         self._merge_dict_of_sets(self._result_type_to_tests, other._result_type_to_tests)
    570 
    571     def _dict_of_sets(self, strings_to_constants):
    572         """Takes a dict of strings->constants and returns a dict mapping
    573         each constant to an empty set."""
    574         d = {}
    575         for c in strings_to_constants.values():
    576             d[c] = set()
    577         return d
    578 
    579     def get_test_set(self, expectation, include_skips=True):
    580         tests = self._expectation_to_tests[expectation]
    581         if not include_skips:
    582             tests = tests - self.get_test_set(SKIP)
    583         return tests
    584 
    585     def get_test_set_for_keyword(self, keyword):
    586         expectation_enum = TestExpectations.EXPECTATIONS.get(keyword.lower(), None)
    587         if expectation_enum is not None:
    588             return self._expectation_to_tests[expectation_enum]
    589 
    590         matching_tests = set()
    591         for test, specifiers in self._test_to_specifiers.iteritems():
    592             if keyword.lower() in specifiers:
    593                 matching_tests.add(test)
    594         return matching_tests
    595 
    596     def get_tests_with_result_type(self, result_type):
    597         return self._result_type_to_tests[result_type]
    598 
    599     def get_tests_with_timeline(self, timeline):
    600         return self._timeline_to_tests[timeline]
    601 
    602     def has_test(self, test):
    603         return test in self._test_to_expectation_line
    604 
    605     def get_expectation_line(self, test):
    606         return self._test_to_expectation_line.get(test)
    607 
    608     def get_expectations(self, test):
    609         return self._test_to_expectations[test]
    610 
    611     def get_expectations_string(self, test):
    612         """Returns the expectatons for the given test as an uppercase string.
    613         If there are no expectations for the test, then "PASS" is returned."""
    614         if self.get_expectation_line(test).is_skipped_outside_expectations_file:
    615             return 'NOTRUN'
    616 
    617         expectations = self.get_expectations(test)
    618         retval = []
    619 
    620         # FIXME: WontFix should cause the test to get skipped without artificially adding SKIP to the expectations list.
    621         if WONTFIX in expectations and SKIP in expectations:
    622             expectations.remove(SKIP)
    623 
    624         for expectation in expectations:
    625             retval.append(self.expectation_to_string(expectation))
    626 
    627         return " ".join(retval)
    628 
    629     def expectation_to_string(self, expectation):
    630         """Return the uppercased string equivalent of a given expectation."""
    631         for item in TestExpectations.EXPECTATIONS.items():
    632             if item[1] == expectation:
    633                 return item[0].upper()
    634         raise ValueError(expectation)
    635 
    636     def remove_expectation_line(self, test):
    637         if not self.has_test(test):
    638             return
    639         self._clear_expectations_for_test(test)
    640         del self._test_to_expectation_line[test]
    641 
    642     def add_expectation_line(self, expectation_line,
    643                              model_all_expectations=False):
    644         """Returns a list of warnings encountered while matching specifiers."""
    645 
    646         if expectation_line.is_invalid():
    647             return
    648 
    649         for test in expectation_line.matching_tests:
    650             if self._already_seen_better_match(test, expectation_line):
    651                 continue
    652 
    653             if model_all_expectations:
    654                 expectation_line = TestExpectationLine.merge_expectation_lines(self.get_expectation_line(test), expectation_line, model_all_expectations)
    655 
    656             self._clear_expectations_for_test(test)
    657             self._test_to_expectation_line[test] = expectation_line
    658             self._add_test(test, expectation_line)
    659 
    660     def _add_test(self, test, expectation_line):
    661         """Sets the expected state for a given test.
    662 
    663         This routine assumes the test has not been added before. If it has,
    664         use _clear_expectations_for_test() to reset the state prior to
    665         calling this."""
    666         self._test_to_expectations[test] = expectation_line.parsed_expectations
    667         for expectation in expectation_line.parsed_expectations:
    668             self._expectation_to_tests[expectation].add(test)
    669 
    670         self._test_to_specifiers[test] = expectation_line.specifiers
    671 
    672         if WONTFIX in expectation_line.parsed_expectations:
    673             self._timeline_to_tests[WONTFIX].add(test)
    674         else:
    675             self._timeline_to_tests[NOW].add(test)
    676 
    677         if SKIP in expectation_line.parsed_expectations:
    678             self._result_type_to_tests[SKIP].add(test)
    679         elif expectation_line.parsed_expectations == set([PASS]):
    680             self._result_type_to_tests[PASS].add(test)
    681         elif expectation_line.is_flaky():
    682             self._result_type_to_tests[FLAKY].add(test)
    683         else:
    684             # FIXME: What is this?
    685             self._result_type_to_tests[FAIL].add(test)
    686 
    687     def _clear_expectations_for_test(self, test):
    688         """Remove prexisting expectations for this test.
    689         This happens if we are seeing a more precise path
    690         than a previous listing.
    691         """
    692         if self.has_test(test):
    693             self._test_to_expectations.pop(test, '')
    694             self._remove_from_sets(test, self._expectation_to_tests)
    695             self._remove_from_sets(test, self._timeline_to_tests)
    696             self._remove_from_sets(test, self._result_type_to_tests)
    697 
    698     def _remove_from_sets(self, test, dict_of_sets_of_tests):
    699         """Removes the given test from the sets in the dictionary.
    700 
    701         Args:
    702           test: test to look for
    703           dict: dict of sets of files"""
    704         for set_of_tests in dict_of_sets_of_tests.itervalues():
    705             if test in set_of_tests:
    706                 set_of_tests.remove(test)
    707 
    708     def _already_seen_better_match(self, test, expectation_line):
    709         """Returns whether we've seen a better match already in the file.
    710 
    711         Returns True if we've already seen a expectation_line.name that matches more of the test
    712             than this path does
    713         """
    714         # FIXME: See comment below about matching test configs and specificity.
    715         if not self.has_test(test):
    716             # We've never seen this test before.
    717             return False
    718 
    719         prev_expectation_line = self._test_to_expectation_line[test]
    720 
    721         if prev_expectation_line.filename != expectation_line.filename:
    722             # We've moved on to a new expectation file, which overrides older ones.
    723             return False
    724 
    725         if len(prev_expectation_line.path) > len(expectation_line.path):
    726             # The previous path matched more of the test.
    727             return True
    728 
    729         if len(prev_expectation_line.path) < len(expectation_line.path):
    730             # This path matches more of the test.
    731             return False
    732 
    733         # At this point we know we have seen a previous exact match on this
    734         # base path, so we need to check the two sets of specifiers.
    735 
    736         # FIXME: This code was originally designed to allow lines that matched
    737         # more specifiers to override lines that matched fewer specifiers.
    738         # However, we currently view these as errors.
    739         #
    740         # To use the "more specifiers wins" policy, change the errors for overrides
    741         # to be warnings and return False".
    742 
    743         if prev_expectation_line.matching_configurations == expectation_line.matching_configurations:
    744             expectation_line.warnings.append('Duplicate or ambiguous entry lines %s:%s and %s:%s.' % (
    745                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers,
    746                 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers))
    747             return True
    748 
    749         if prev_expectation_line.matching_configurations >= expectation_line.matching_configurations:
    750             expectation_line.warnings.append('More specific entry for %s on line %s:%s overrides line %s:%s.' % (expectation_line.name,
    751                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers,
    752                 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers))
    753             # FIXME: return False if we want more specific to win.
    754             return True
    755 
    756         if prev_expectation_line.matching_configurations <= expectation_line.matching_configurations:
    757             expectation_line.warnings.append('More specific entry for %s on line %s:%s overrides line %s:%s.' % (expectation_line.name,
    758                 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers,
    759                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers))
    760             return True
    761 
    762         if prev_expectation_line.matching_configurations & expectation_line.matching_configurations:
    763             expectation_line.warnings.append('Entries for %s on lines %s:%s and %s:%s match overlapping sets of configurations.' % (expectation_line.name,
    764                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers,
    765                 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers))
    766             return True
    767 
    768         # Configuration sets are disjoint, then.
    769         return False
    770 
    771 
    772 class TestExpectations(object):
    773     """Test expectations consist of lines with specifications of what
    774     to expect from layout test cases. The test cases can be directories
    775     in which case the expectations apply to all test cases in that
    776     directory and any subdirectory. The format is along the lines of:
    777 
    778       LayoutTests/fast/js/fixme.js [ Failure ]
    779       LayoutTests/fast/js/flaky.js [ Failure Pass ]
    780       LayoutTests/fast/js/crash.js [ Crash Failure Pass Timeout ]
    781       ...
    782 
    783     To add specifiers:
    784       LayoutTests/fast/js/no-good.js
    785       [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Timeout ]
    786       [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
    787       [ Linux Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
    788       [ Linux Win ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
    789 
    790     Skip: Doesn't run the test.
    791     Slow: The test takes a long time to run, but does not timeout indefinitely.
    792     WontFix: For tests that we never intend to pass on a given platform (treated like Skip).
    793 
    794     Notes:
    795       -A test cannot be both SLOW and TIMEOUT
    796       -A test can be included twice, but not via the same path.
    797       -If a test is included twice, then the more precise path wins.
    798       -CRASH tests cannot be WONTFIX
    799     """
    800 
    801     # FIXME: Update to new syntax once the old format is no longer supported.
    802     EXPECTATIONS = {'pass': PASS,
    803                     'audio': AUDIO,
    804                     'fail': FAIL,
    805                     'image': IMAGE,
    806                     'image+text': IMAGE_PLUS_TEXT,
    807                     'text': TEXT,
    808                     'timeout': TIMEOUT,
    809                     'crash': CRASH,
    810                     'leak': LEAK,
    811                     'missing': MISSING,
    812                     TestExpectationParser.SKIP_MODIFIER: SKIP,
    813                     TestExpectationParser.NEEDS_REBASELINE_MODIFIER: NEEDS_REBASELINE,
    814                     TestExpectationParser.NEEDS_MANUAL_REBASELINE_MODIFIER: NEEDS_MANUAL_REBASELINE,
    815                     TestExpectationParser.WONTFIX_MODIFIER: WONTFIX,
    816                     TestExpectationParser.SLOW_MODIFIER: SLOW,
    817                     TestExpectationParser.REBASELINE_MODIFIER: REBASELINE,
    818     }
    819 
    820     EXPECTATIONS_TO_STRING = dict((k, v) for (v, k) in EXPECTATIONS.iteritems())
    821 
    822     # (aggregated by category, pass/fail/skip, type)
    823     EXPECTATION_DESCRIPTIONS = {SKIP: 'skipped',
    824                                 PASS: 'passes',
    825                                 FAIL: 'failures',
    826                                 IMAGE: 'image-only failures',
    827                                 TEXT: 'text-only failures',
    828                                 IMAGE_PLUS_TEXT: 'image and text failures',
    829                                 AUDIO: 'audio failures',
    830                                 CRASH: 'crashes',
    831                                 LEAK: 'leaks',
    832                                 TIMEOUT: 'timeouts',
    833                                 MISSING: 'missing results'}
    834 
    835     NON_TEST_OUTCOME_EXPECTATIONS = (REBASELINE, SKIP, SLOW, WONTFIX)
    836 
    837     BUILD_TYPES = ('debug', 'release')
    838 
    839     TIMELINES = {TestExpectationParser.WONTFIX_MODIFIER: WONTFIX,
    840                  'now': NOW}
    841 
    842     RESULT_TYPES = {'skip': SKIP,
    843                     'pass': PASS,
    844                     'fail': FAIL,
    845                     'flaky': FLAKY}
    846 
    847     @classmethod
    848     def expectation_from_string(cls, string):
    849         assert(' ' not in string)  # This only handles one expectation at a time.
    850         return cls.EXPECTATIONS.get(string.lower())
    851 
    852     @staticmethod
    853     def result_was_expected(result, expected_results, test_needs_rebaselining):
    854         """Returns whether we got a result we were expecting.
    855         Args:
    856             result: actual result of a test execution
    857             expected_results: set of results listed in test_expectations
    858             test_needs_rebaselining: whether test was marked as REBASELINE"""
    859         if not (set(expected_results) - (set(TestExpectations.NON_TEST_OUTCOME_EXPECTATIONS))):
    860             expected_results = set([PASS])
    861 
    862         if result in expected_results:
    863             return True
    864         if result in (PASS, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO, MISSING) and (NEEDS_REBASELINE in expected_results or NEEDS_MANUAL_REBASELINE in expected_results):
    865             return True
    866         if result in (TEXT, IMAGE_PLUS_TEXT, AUDIO) and (FAIL in expected_results):
    867             return True
    868         if result == MISSING and test_needs_rebaselining:
    869             return True
    870         if result == SKIP:
    871             return True
    872         return False
    873 
    874     @staticmethod
    875     def remove_pixel_failures(expected_results):
    876         """Returns a copy of the expected results for a test, except that we
    877         drop any pixel failures and return the remaining expectations. For example,
    878         if we're not running pixel tests, then tests expected to fail as IMAGE
    879         will PASS."""
    880         expected_results = expected_results.copy()
    881         if IMAGE in expected_results:
    882             expected_results.remove(IMAGE)
    883             expected_results.add(PASS)
    884         return expected_results
    885 
    886     @staticmethod
    887     def remove_non_sanitizer_failures(expected_results):
    888         """Returns a copy of the expected results for a test, except that we
    889         drop any failures that the sanitizers don't care about."""
    890         expected_results = expected_results.copy()
    891         for result in (IMAGE, FAIL, IMAGE_PLUS_TEXT):
    892             if result in expected_results:
    893                 expected_results.remove(result)
    894                 expected_results.add(PASS)
    895         return expected_results
    896 
    897     @staticmethod
    898     def has_pixel_failures(actual_results):
    899         return IMAGE in actual_results or FAIL in actual_results
    900 
    901     @staticmethod
    902     def suffixes_for_expectations(expectations):
    903         suffixes = set()
    904         if IMAGE in expectations:
    905             suffixes.add('png')
    906         if FAIL in expectations:
    907             suffixes.add('txt')
    908             suffixes.add('png')
    909             suffixes.add('wav')
    910         return set(suffixes)
    911 
    912     @staticmethod
    913     def suffixes_for_actual_expectations_string(expectations):
    914         suffixes = set()
    915         if 'TEXT' in expectations:
    916             suffixes.add('txt')
    917         if 'IMAGE' in expectations:
    918             suffixes.add('png')
    919         if 'AUDIO' in expectations:
    920             suffixes.add('wav')
    921         if 'MISSING' in expectations:
    922             suffixes.add('txt')
    923             suffixes.add('png')
    924             suffixes.add('wav')
    925         return suffixes
    926 
    927     # FIXME: This constructor does too much work. We should move the actual parsing of
    928     # the expectations into separate routines so that linting and handling overrides
    929     # can be controlled separately, and the constructor can be more of a no-op.
    930     def __init__(self, port, tests=None, include_overrides=True, expectations_dict=None, model_all_expectations=False, is_lint_mode=False):
    931         self._full_test_list = tests
    932         self._test_config = port.test_configuration()
    933         self._is_lint_mode = is_lint_mode
    934         self._model_all_expectations = self._is_lint_mode or model_all_expectations
    935         self._model = TestExpectationsModel(self._shorten_filename)
    936         self._parser = TestExpectationParser(port, tests, self._is_lint_mode)
    937         self._port = port
    938         self._skipped_tests_warnings = []
    939         self._expectations = []
    940 
    941         if not expectations_dict:
    942             expectations_dict = port.expectations_dict()
    943 
    944         # Always parse the generic expectations (the generic file is required
    945         # to be the first one in the expectations_dict, which must be an OrderedDict).
    946         generic_path, generic_exps = expectations_dict.items()[0]
    947         expectations = self._parser.parse(generic_path, generic_exps)
    948         self._add_expectations(expectations, self._model)
    949         self._expectations += expectations
    950 
    951         # Now add the overrides if so requested.
    952         if include_overrides:
    953             for path, contents in expectations_dict.items()[1:]:
    954                 expectations = self._parser.parse(path, contents)
    955                 model = TestExpectationsModel(self._shorten_filename)
    956                 self._add_expectations(expectations, model)
    957                 self._expectations += expectations
    958                 self._model.merge_model(model)
    959 
    960         # FIXME: move ignore_tests into port.skipped_layout_tests()
    961         self.add_extra_skipped_tests(port.skipped_layout_tests(tests).union(set(port.get_option('ignore_tests', []))))
    962         self.add_expectations_from_bot()
    963 
    964         self._has_warnings = False
    965         self._report_warnings()
    966         self._process_tests_without_expectations()
    967 
    968     # TODO(ojan): Allow for removing skipped tests when getting the list of
    969     # tests to run, but not when getting metrics.
    970     def model(self):
    971         return self._model
    972 
    973     def get_needs_rebaseline_failures(self):
    974         return self._model.get_test_set(NEEDS_REBASELINE)
    975 
    976     def get_rebaselining_failures(self):
    977         return self._model.get_test_set(REBASELINE)
    978 
    979     # FIXME: Change the callsites to use TestExpectationsModel and remove.
    980     def get_expectations(self, test):
    981         return self._model.get_expectations(test)
    982 
    983     # FIXME: Change the callsites to use TestExpectationsModel and remove.
    984     def get_tests_with_result_type(self, result_type):
    985         return self._model.get_tests_with_result_type(result_type)
    986 
    987     # FIXME: Change the callsites to use TestExpectationsModel and remove.
    988     def get_test_set(self, expectation, include_skips=True):
    989         return self._model.get_test_set(expectation, include_skips)
    990 
    991     # FIXME: Change the callsites to use TestExpectationsModel and remove.
    992     def get_tests_with_timeline(self, timeline):
    993         return self._model.get_tests_with_timeline(timeline)
    994 
    995     def get_expectations_string(self, test):
    996         return self._model.get_expectations_string(test)
    997 
    998     def expectation_to_string(self, expectation):
    999         return self._model.expectation_to_string(expectation)
   1000 
   1001     def matches_an_expected_result(self, test, result, pixel_tests_are_enabled, sanitizer_is_enabled):
   1002         expected_results = self._model.get_expectations(test)
   1003         if sanitizer_is_enabled:
   1004             expected_results = self.remove_non_sanitizer_failures(expected_results)
   1005         elif not pixel_tests_are_enabled:
   1006             expected_results = self.remove_pixel_failures(expected_results)
   1007         return self.result_was_expected(result, expected_results, self.is_rebaselining(test))
   1008 
   1009     def is_rebaselining(self, test):
   1010         return REBASELINE in self._model.get_expectations(test)
   1011 
   1012     def _shorten_filename(self, filename):
   1013         if filename.startswith(self._port.path_from_webkit_base()):
   1014             return self._port.host.filesystem.relpath(filename, self._port.path_from_webkit_base())
   1015         return filename
   1016 
   1017     def _report_warnings(self):
   1018         warnings = []
   1019         for expectation in self._expectations:
   1020             for warning in expectation.warnings:
   1021                 warnings.append('%s:%s %s %s' % (self._shorten_filename(expectation.filename), expectation.line_numbers,
   1022                                 warning, expectation.name if expectation.expectations else expectation.original_string))
   1023 
   1024         if warnings:
   1025             self._has_warnings = True
   1026             if self._is_lint_mode:
   1027                 raise ParseError(warnings)
   1028             _log.warning('--lint-test-files warnings:')
   1029             for warning in warnings:
   1030                 _log.warning(warning)
   1031             _log.warning('')
   1032 
   1033     def _process_tests_without_expectations(self):
   1034         if self._full_test_list:
   1035             for test in self._full_test_list:
   1036                 if not self._model.has_test(test):
   1037                     self._model.add_expectation_line(TestExpectationLine.create_passing_expectation(test))
   1038 
   1039     def has_warnings(self):
   1040         return self._has_warnings
   1041 
   1042     def remove_configurations(self, removals):
   1043         expectations_to_remove = []
   1044         modified_expectations = []
   1045 
   1046         for test, test_configuration in removals:
   1047             for expectation in self._expectations:
   1048                 if expectation.name != test or not expectation.parsed_expectations:
   1049                     continue
   1050                 if test_configuration not in expectation.matching_configurations:
   1051                     continue
   1052 
   1053                 expectation.matching_configurations.remove(test_configuration)
   1054                 if expectation.matching_configurations:
   1055                     modified_expectations.append(expectation)
   1056                 else:
   1057                     expectations_to_remove.append(expectation)
   1058 
   1059         for expectation in expectations_to_remove:
   1060             index = self._expectations.index(expectation)
   1061             self._expectations.remove(expectation)
   1062 
   1063             if index == len(self._expectations) or self._expectations[index].is_whitespace_or_comment():
   1064                 while index and self._expectations[index - 1].is_whitespace_or_comment():
   1065                     index = index - 1
   1066                     self._expectations.pop(index)
   1067 
   1068         return self.list_to_string(self._expectations, self._parser._test_configuration_converter, modified_expectations)
   1069 
   1070     def _add_expectations(self, expectation_list, model):
   1071         for expectation_line in expectation_list:
   1072             if not expectation_line.expectations:
   1073                 continue
   1074 
   1075             if self._model_all_expectations or self._test_config in expectation_line.matching_configurations:
   1076                 model.add_expectation_line(expectation_line, model_all_expectations=self._model_all_expectations)
   1077 
   1078     def add_extra_skipped_tests(self, tests_to_skip):
   1079         if not tests_to_skip:
   1080             return
   1081         for test in self._expectations:
   1082             if test.name and test.name in tests_to_skip:
   1083                 test.warnings.append('%s:%s %s is also in a Skipped file.' % (test.filename, test.line_numbers, test.name))
   1084 
   1085         model = TestExpectationsModel(self._shorten_filename)
   1086         for test_name in tests_to_skip:
   1087             expectation_line = self._parser.expectation_for_skipped_test(test_name)
   1088             model.add_expectation_line(expectation_line)
   1089         self._model.merge_model(model)
   1090 
   1091     def add_expectations_from_bot(self):
   1092         # FIXME: With mode 'very-flaky' and 'maybe-flaky', this will show the expectations entry in the flakiness
   1093         # dashboard rows for each test to be whatever the bot thinks they should be. Is this a good thing?
   1094         bot_expectations = self._port.bot_expectations()
   1095         model = TestExpectationsModel(self._shorten_filename)
   1096         for test_name in bot_expectations:
   1097             expectation_line = self._parser.expectation_line_for_test(test_name, bot_expectations[test_name])
   1098 
   1099             # Unexpected results are merged into existing expectations.
   1100             merge = self._port.get_option('ignore_flaky_tests') == 'unexpected'
   1101             model.add_expectation_line(expectation_line)
   1102         self._model.merge_model(model)
   1103 
   1104     def add_expectation_line(self, expectation_line):
   1105         self._model.add_expectation_line(expectation_line)
   1106         self._expectations += [expectation_line]
   1107 
   1108     def remove_expectation_line(self, test):
   1109         if not self._model.has_test(test):
   1110             return
   1111         self._expectations.remove(self._model.get_expectation_line(test))
   1112         self._model.remove_expectation_line(test)
   1113 
   1114     @staticmethod
   1115     def list_to_string(expectation_lines, test_configuration_converter=None, reconstitute_only_these=None):
   1116         def serialize(expectation_line):
   1117             # If reconstitute_only_these is an empty list, we want to return original_string.
   1118             # So we need to compare reconstitute_only_these to None, not just check if it's falsey.
   1119             if reconstitute_only_these is None or expectation_line in reconstitute_only_these:
   1120                 return expectation_line.to_string(test_configuration_converter)
   1121             return expectation_line.original_string
   1122 
   1123         def nones_out(expectation_line):
   1124             return expectation_line is not None
   1125 
   1126         return "\n".join(filter(nones_out, map(serialize, expectation_lines)))
   1127