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