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