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