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
30for layout tests.
31"""
32
33import logging
34import re
35
36from 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?
49BASELINE_SUFFIX_LIST = ('png', 'wav', 'txt')
50
51WEBKIT_BUG_PREFIX = 'webkit.org/b/'
52CHROMIUM_BUG_PREFIX = 'crbug.com/'
53V8_BUG_PREFIX = 'code.google.com/p/v8/issues/detail?id='
54NAMED_BUG_PREFIX = 'Bug('
55
56MISSING_KEYWORD = 'Missing'
57NEEDS_REBASELINE_KEYWORD = 'NeedsRebaseline'
58NEEDS_MANUAL_REBASELINE_KEYWORD = 'NeedsManualRebaseline'
59
60class 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
72class 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
366class 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.
529class 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
772class 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