c8ea322fa803279f6964ad2e408283861f3a8c27
[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):
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
846         expectations_dict = expectations_to_lint or port.expectations_dict()
847
848         expectations_dict_index = 0
849         # Populate generic expectations (if enabled by include_generic).
850         if port.path_to_generic_test_expectations_file() in expectations_dict:
851             if include_generic:
852                 expectations = self._parser.parse(expectations_dict.keys()[expectations_dict_index], expectations_dict.values()[expectations_dict_index])
853                 self._add_expectations(expectations)
854                 self._expectations += expectations
855             expectations_dict_index += 1
856
857         # Populate default port expectations (always enabled).
858         if len(expectations_dict) > expectations_dict_index:
859             expectations = self._parser.parse(expectations_dict.keys()[expectations_dict_index], expectations_dict.values()[expectations_dict_index])
860             self._add_expectations(expectations)
861             self._expectations += expectations
862             expectations_dict_index += 1
863
864         # Populate override expectations (if enabled by include_overrides).
865         while len(expectations_dict) > expectations_dict_index and include_overrides:
866             expectations = self._parser.parse(expectations_dict.keys()[expectations_dict_index], expectations_dict.values()[expectations_dict_index])
867             self._add_expectations(expectations)
868             self._expectations += expectations
869             expectations_dict_index += 1
870
871         # FIXME: move ignore_tests into port.skipped_layout_tests()
872         self.add_skipped_tests(port.skipped_layout_tests(tests).union(set(port.get_option('ignore_tests', []))))
873
874         self._has_warnings = False
875         self._report_warnings()
876         self._process_tests_without_expectations()
877
878     # TODO(ojan): Allow for removing skipped tests when getting the list of
879     # tests to run, but not when getting metrics.
880     def model(self):
881         return self._model
882
883     def get_rebaselining_failures(self):
884         return self._model.get_test_set(REBASELINE)
885
886     # FIXME: Change the callsites to use TestExpectationsModel and remove.
887     def get_expectations(self, test):
888         return self._model.get_expectations(test)
889
890     # FIXME: Change the callsites to use TestExpectationsModel and remove.
891     def has_modifier(self, test, modifier):
892         return self._model.has_modifier(test, modifier)
893
894     # FIXME: Change the callsites to use TestExpectationsModel and remove.
895     def get_tests_with_result_type(self, result_type):
896         return self._model.get_tests_with_result_type(result_type)
897
898     # FIXME: Change the callsites to use TestExpectationsModel and remove.
899     def get_test_set(self, modifier, expectation=None, include_skips=True):
900         return self._model.get_test_set(modifier, expectation, include_skips)
901
902     # FIXME: Change the callsites to use TestExpectationsModel and remove.
903     def get_modifiers(self, test):
904         return self._model.get_modifiers(test)
905
906     # FIXME: Change the callsites to use TestExpectationsModel and remove.
907     def get_tests_with_timeline(self, timeline):
908         return self._model.get_tests_with_timeline(timeline)
909
910     def get_expectations_string(self, test):
911         return self._model.get_expectations_string(test)
912
913     def expectation_to_string(self, expectation):
914         return self._model.expectation_to_string(expectation)
915
916     def matches_an_expected_result(self, test, result, pixel_tests_are_enabled):
917         expected_results = self._model.get_expectations(test)
918         if not pixel_tests_are_enabled:
919             expected_results = self.remove_pixel_failures(expected_results)
920         return self.result_was_expected(result,
921                                    expected_results,
922                                    self.is_rebaselining(test),
923                                    self._model.has_modifier(test, SKIP))
924
925     def is_rebaselining(self, test):
926         return self._model.has_modifier(test, REBASELINE)
927
928     def _shorten_filename(self, filename):
929         if filename.startswith(self._port.path_from_webkit_base()):
930             return self._port.host.filesystem.relpath(filename, self._port.path_from_webkit_base())
931         return filename
932
933     def _report_warnings(self):
934         warnings = []
935         for expectation in self._expectations:
936             for warning in expectation.warnings:
937                 warnings.append('%s:%d %s %s' % (self._shorten_filename(expectation.filename), expectation.line_number,
938                                 warning, expectation.name if expectation.expectations else expectation.original_string))
939
940         if warnings:
941             self._has_warnings = True
942             if self._is_lint_mode:
943                 raise ParseError(warnings)
944             _log.warning('--lint-test-files warnings:')
945             for warning in warnings:
946                 _log.warning(warning)
947             _log.warning('')
948
949     def _process_tests_without_expectations(self):
950         if self._full_test_list:
951             for test in self._full_test_list:
952                 if not self._model.has_test(test):
953                     self._model.add_expectation_line(TestExpectationLine.create_passing_expectation(test))
954
955     def has_warnings(self):
956         return self._has_warnings
957
958     def remove_configuration_from_test(self, test, test_configuration):
959         expectations_to_remove = []
960         modified_expectations = []
961
962         for expectation in self._expectations:
963             if expectation.name != test or expectation.is_flaky() or not expectation.parsed_expectations:
964                 continue
965             if iter(expectation.parsed_expectations).next() not in (FAIL, IMAGE):
966                 continue
967             if test_configuration not in expectation.matching_configurations:
968                 continue
969
970             expectation.matching_configurations.remove(test_configuration)
971             if expectation.matching_configurations:
972                 modified_expectations.append(expectation)
973             else:
974                 expectations_to_remove.append(expectation)
975
976         for expectation in expectations_to_remove:
977             self._expectations.remove(expectation)
978
979         return self.list_to_string(self._expectations, self._parser._test_configuration_converter, modified_expectations)
980
981     def remove_rebaselined_tests(self, except_these_tests, filename):
982         """Returns a copy of the expectations in the file with the tests removed."""
983         def without_rebaseline_modifier(expectation):
984             return (expectation.filename == filename and
985                     not (not expectation.is_invalid() and
986                          expectation.name in except_these_tests and
987                          'rebaseline' in expectation.parsed_modifiers))
988
989         return self.list_to_string(filter(without_rebaseline_modifier, self._expectations), reconstitute_only_these=[])
990
991     def _add_expectations(self, expectation_list):
992         for expectation_line in expectation_list:
993             if not expectation_line.expectations:
994                 continue
995
996             if self._is_lint_mode or self._test_config in expectation_line.matching_configurations:
997                 self._model.add_expectation_line(expectation_line)
998
999     def add_skipped_tests(self, tests_to_skip):
1000         if not tests_to_skip:
1001             return
1002         for test in self._expectations:
1003             if test.name and test.name in tests_to_skip:
1004                 test.warnings.append('%s:%d %s is also in a Skipped file.' % (test.filename, test.line_number, test.name))
1005
1006         for test_name in tests_to_skip:
1007             expectation_line = self._parser.expectation_for_skipped_test(test_name)
1008             self._model.add_expectation_line(expectation_line, in_skipped=True)
1009
1010     @staticmethod
1011     def list_to_string(expectation_lines, test_configuration_converter=None, reconstitute_only_these=None):
1012         def serialize(expectation_line):
1013             # If reconstitute_only_these is an empty list, we want to return original_string.
1014             # So we need to compare reconstitute_only_these to None, not just check if it's falsey.
1015             if reconstitute_only_these is None or expectation_line in reconstitute_only_these:
1016                 return expectation_line.to_string(test_configuration_converter)
1017             return expectation_line.original_string
1018
1019         def nones_out(expectation_line):
1020             return expectation_line is not None
1021
1022         return "\n".join(filter(nones_out, map(serialize, expectation_lines)))