181875d9209f4592003e8dac4dc009874c87c783
[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',
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         CHROMIUM_BUG_PREFIX = 'crbug.com/'
280         V8_BUG_PREFIX = 'code.google.com/p/v8/issues/detail?id='
281
282         tokens = remaining_string.split()
283         state = 'start'
284         for token in tokens:
285             if (token.startswith(WEBKIT_BUG_PREFIX) or
286                 token.startswith(CHROMIUM_BUG_PREFIX) or
287                 token.startswith(V8_BUG_PREFIX) or
288                 token.startswith('Bug(')):
289                 if state != 'start':
290                     warnings.append('"%s" is not at the start of the line.' % token)
291                     break
292                 if token.startswith(WEBKIT_BUG_PREFIX):
293                     bugs.append(token.replace(WEBKIT_BUG_PREFIX, 'BUGWK'))
294                 elif token.startswith(CHROMIUM_BUG_PREFIX):
295                     bugs.append(token.replace(CHROMIUM_BUG_PREFIX, 'BUGCR'))
296                 elif token.startswith(V8_BUG_PREFIX):
297                     bugs.append(token.replace(V8_BUG_PREFIX, 'BUGV8_'))
298                 else:
299                     match = re.match('Bug\((\w+)\)$', token)
300                     if not match:
301                         warnings.append('unrecognized bug identifier "%s"' % token)
302                         break
303                     else:
304                         bugs.append('BUG' + match.group(1).upper())
305             elif token.startswith('BUG'):
306                 warnings.append('unrecognized old-style bug identifier "%s"' % token)
307                 break
308             elif token == '[':
309                 if state == 'start':
310                     state = 'configuration'
311                 elif state == 'name_found':
312                     state = 'expectations'
313                 else:
314                     warnings.append('unexpected "["')
315                     break
316             elif token == ']':
317                 if state == 'configuration':
318                     state = 'name'
319                 elif state == 'expectations':
320                     state = 'done'
321                 else:
322                     warnings.append('unexpected "]"')
323                     break
324             elif token in ('//', ':', '='):
325                 warnings.append('"%s" is not legal in the new TestExpectations syntax.' % token)
326                 break
327             elif state == 'configuration':
328                 modifiers.append(cls._configuration_tokens.get(token, token))
329             elif state == 'expectations':
330                 if token in ('Rebaseline', 'Skip', 'Slow', 'WontFix'):
331                     modifiers.append(token.upper())
332                 else:
333                     expectations.append(cls._expectation_tokens.get(token, token))
334             elif state == 'name_found':
335                 warnings.append('expecting "[", "#", or end of line instead of "%s"' % token)
336                 break
337             else:
338                 name = token
339                 state = 'name_found'
340
341         if not warnings:
342             if not name:
343                 warnings.append('Did not find a test name.')
344             elif state not in ('name_found', 'done'):
345                 warnings.append('Missing a "]"')
346
347         if 'WONTFIX' in modifiers and 'SKIP' not in modifiers:
348             modifiers.append('SKIP')
349
350         if 'SKIP' in modifiers and expectations:
351             # FIXME: This is really a semantic warning and shouldn't be here. Remove when we drop the old syntax.
352             warnings.append('A test marked Skip or WontFix must not have other expectations.')
353         elif not expectations:
354             if 'SKIP' not in modifiers and 'REBASELINE' not in modifiers and 'SLOW' not in modifiers:
355                 modifiers.append('SKIP')
356             expectations = ['PASS']
357
358         # FIXME: expectation line should just store bugs and modifiers separately.
359         expectation_line.modifiers = bugs + modifiers
360         expectation_line.expectations = expectations
361         expectation_line.name = name
362         expectation_line.warnings = warnings
363         return expectation_line
364
365     @classmethod
366     def _split_space_separated(cls, space_separated_string):
367         """Splits a space-separated string into an array."""
368         return [part.strip() for part in space_separated_string.strip().split(' ')]
369
370
371 class TestExpectationLine(object):
372     """Represents a line in test expectations file."""
373
374     def __init__(self):
375         """Initializes a blank-line equivalent of an expectation."""
376         self.original_string = None
377         self.filename = None  # this is the path to the expectations file for this line
378         self.line_number = None
379         self.name = None  # this is the path in the line itself
380         self.path = None  # this is the normpath of self.name
381         self.modifiers = []
382         self.parsed_modifiers = []
383         self.parsed_bug_modifiers = []
384         self.matching_configurations = set()
385         self.expectations = []
386         self.parsed_expectations = set()
387         self.comment = None
388         self.matching_tests = []
389         self.warnings = []
390
391     def is_invalid(self):
392         return self.warnings and self.warnings != [TestExpectationParser.MISSING_BUG_WARNING]
393
394     def is_flaky(self):
395         return len(self.parsed_expectations) > 1
396
397     @staticmethod
398     def create_passing_expectation(test):
399         expectation_line = TestExpectationLine()
400         expectation_line.name = test
401         expectation_line.path = test
402         expectation_line.parsed_expectations = set([PASS])
403         expectation_line.expectations = set(['PASS'])
404         expectation_line.matching_tests = [test]
405         return expectation_line
406
407     def to_string(self, test_configuration_converter, include_modifiers=True, include_expectations=True, include_comment=True):
408         parsed_expectation_to_string = dict([[parsed_expectation, expectation_string] for expectation_string, parsed_expectation in TestExpectations.EXPECTATIONS.items()])
409
410         if self.is_invalid():
411             return self.original_string or ''
412
413         if self.name is None:
414             return '' if self.comment is None else "#%s" % self.comment
415
416         if test_configuration_converter and self.parsed_bug_modifiers:
417             specifiers_list = test_configuration_converter.to_specifiers_list(self.matching_configurations)
418             result = []
419             for specifiers in specifiers_list:
420                 # FIXME: this is silly that we join the modifiers and then immediately split them.
421                 modifiers = self._serialize_parsed_modifiers(test_configuration_converter, specifiers).split()
422                 expectations = self._serialize_parsed_expectations(parsed_expectation_to_string).split()
423                 result.append(self._format_line(modifiers, self.name, expectations, self.comment))
424             return "\n".join(result) if result else None
425
426         return self._format_line(self.modifiers, self.name, self.expectations, self.comment,
427             include_modifiers, include_expectations, include_comment)
428
429     def to_csv(self):
430         # Note that this doesn't include the comments.
431         return '%s,%s,%s' % (self.name, ' '.join(self.modifiers), ' '.join(self.expectations))
432
433     def _serialize_parsed_expectations(self, parsed_expectation_to_string):
434         result = []
435         for index in TestExpectations.EXPECTATION_ORDER:
436             if index in self.parsed_expectations:
437                 result.append(parsed_expectation_to_string[index])
438         return ' '.join(result)
439
440     def _serialize_parsed_modifiers(self, test_configuration_converter, specifiers):
441         result = []
442         if self.parsed_bug_modifiers:
443             result.extend(sorted(self.parsed_bug_modifiers))
444         result.extend(sorted(self.parsed_modifiers))
445         result.extend(test_configuration_converter.specifier_sorter().sort_specifiers(specifiers))
446         return ' '.join(result)
447
448     @staticmethod
449     def _format_line(modifiers, name, expectations, comment, include_modifiers=True, include_expectations=True, include_comment=True):
450         bugs = []
451         new_modifiers = []
452         new_expectations = []
453         for modifier in modifiers:
454             modifier = modifier.upper()
455             if modifier.startswith('BUGWK'):
456                 bugs.append('webkit.org/b/' + modifier.replace('BUGWK', ''))
457             elif modifier.startswith('BUGCR'):
458                 bugs.append('crbug.com/' + modifier.replace('BUGCR', ''))
459             elif modifier.startswith('BUG'):
460                 # FIXME: we should preserve case once we can drop the old syntax.
461                 bugs.append('Bug(' + modifier[3:].lower() + ')')
462             elif modifier in ('SLOW', 'SKIP', 'REBASELINE', 'WONTFIX'):
463                 new_expectations.append(TestExpectationParser._inverted_expectation_tokens.get(modifier))
464             else:
465                 new_modifiers.append(TestExpectationParser._inverted_configuration_tokens.get(modifier, modifier))
466
467         for expectation in expectations:
468             expectation = expectation.upper()
469             new_expectations.append(TestExpectationParser._inverted_expectation_tokens.get(expectation, expectation))
470
471         result = ''
472         if include_modifiers and (bugs or new_modifiers):
473             if bugs:
474                 result += ' '.join(bugs) + ' '
475             if new_modifiers:
476                 result += '[ %s ] ' % ' '.join(new_modifiers)
477         result += name
478         if include_expectations and new_expectations and set(new_expectations) != set(['Skip', 'Pass']):
479             result += ' [ %s ]' % ' '.join(sorted(set(new_expectations)))
480         if include_comment and comment is not None:
481             result += " #%s" % comment
482         return result
483
484
485 # FIXME: Refactor API to be a proper CRUD.
486 class TestExpectationsModel(object):
487     """Represents relational store of all expectations and provides CRUD semantics to manage it."""
488
489     def __init__(self, shorten_filename=None):
490         # Maps a test to its list of expectations.
491         self._test_to_expectations = {}
492
493         # Maps a test to list of its modifiers (string values)
494         self._test_to_modifiers = {}
495
496         # Maps a test to a TestExpectationLine instance.
497         self._test_to_expectation_line = {}
498
499         self._modifier_to_tests = self._dict_of_sets(TestExpectations.MODIFIERS)
500         self._expectation_to_tests = self._dict_of_sets(TestExpectations.EXPECTATIONS)
501         self._timeline_to_tests = self._dict_of_sets(TestExpectations.TIMELINES)
502         self._result_type_to_tests = self._dict_of_sets(TestExpectations.RESULT_TYPES)
503
504         self._shorten_filename = shorten_filename or (lambda x: x)
505
506     def _dict_of_sets(self, strings_to_constants):
507         """Takes a dict of strings->constants and returns a dict mapping
508         each constant to an empty set."""
509         d = {}
510         for c in strings_to_constants.values():
511             d[c] = set()
512         return d
513
514     def get_test_set(self, modifier, expectation=None, include_skips=True):
515         if expectation is None:
516             tests = self._modifier_to_tests[modifier]
517         else:
518             tests = (self._expectation_to_tests[expectation] &
519                 self._modifier_to_tests[modifier])
520
521         if not include_skips:
522             tests = tests - self.get_test_set(SKIP, expectation)
523
524         return tests
525
526     def get_test_set_for_keyword(self, keyword):
527         # FIXME: get_test_set() is an awkward public interface because it requires
528         # callers to know the difference between modifiers and expectations. We
529         # should replace that with this where possible.
530         expectation_enum = TestExpectations.EXPECTATIONS.get(keyword.lower(), None)
531         if expectation_enum is not None:
532             return self._expectation_to_tests[expectation_enum]
533         modifier_enum = TestExpectations.MODIFIERS.get(keyword.lower(), None)
534         if modifier_enum is not None:
535             return self._modifier_to_tests[modifier_enum]
536
537         # We must not have an index on this modifier.
538         matching_tests = set()
539         for test, modifiers in self._test_to_modifiers.iteritems():
540             if keyword.lower() in modifiers:
541                 matching_tests.add(test)
542         return matching_tests
543
544     def get_tests_with_result_type(self, result_type):
545         return self._result_type_to_tests[result_type]
546
547     def get_tests_with_timeline(self, timeline):
548         return self._timeline_to_tests[timeline]
549
550     def get_modifiers(self, test):
551         """This returns modifiers for the given test (the modifiers plus the BUGXXXX identifier). This is used by the LTTF dashboard."""
552         return self._test_to_modifiers[test]
553
554     def has_modifier(self, test, modifier):
555         return test in self._modifier_to_tests[modifier]
556
557     def has_keyword(self, test, keyword):
558         return (keyword.upper() in self.get_expectations_string(test) or
559                 keyword.lower() in self.get_modifiers(test))
560
561     def has_test(self, test):
562         return test in self._test_to_expectation_line
563
564     def get_expectation_line(self, test):
565         return self._test_to_expectation_line.get(test)
566
567     def get_expectations(self, test):
568         return self._test_to_expectations[test]
569
570     def get_expectations_string(self, test):
571         """Returns the expectatons for the given test as an uppercase string.
572         If there are no expectations for the test, then "PASS" is returned."""
573         expectations = self.get_expectations(test)
574         retval = []
575
576         for expectation in expectations:
577             retval.append(self.expectation_to_string(expectation))
578
579         return " ".join(retval)
580
581     def expectation_to_string(self, expectation):
582         """Return the uppercased string equivalent of a given expectation."""
583         for item in TestExpectations.EXPECTATIONS.items():
584             if item[1] == expectation:
585                 return item[0].upper()
586         raise ValueError(expectation)
587
588
589     def add_expectation_line(self, expectation_line, in_skipped=False):
590         """Returns a list of warnings encountered while matching modifiers."""
591
592         if expectation_line.is_invalid():
593             return
594
595         for test in expectation_line.matching_tests:
596             if not in_skipped and self._already_seen_better_match(test, expectation_line):
597                 continue
598
599             self._clear_expectations_for_test(test)
600             self._test_to_expectation_line[test] = expectation_line
601             self._add_test(test, expectation_line)
602
603     def _add_test(self, test, expectation_line):
604         """Sets the expected state for a given test.
605
606         This routine assumes the test has not been added before. If it has,
607         use _clear_expectations_for_test() to reset the state prior to
608         calling this."""
609         self._test_to_expectations[test] = expectation_line.parsed_expectations
610         for expectation in expectation_line.parsed_expectations:
611             self._expectation_to_tests[expectation].add(test)
612
613         self._test_to_modifiers[test] = expectation_line.modifiers
614         for modifier in expectation_line.parsed_modifiers:
615             mod_value = TestExpectations.MODIFIERS[modifier]
616             self._modifier_to_tests[mod_value].add(test)
617
618         if TestExpectationParser.WONTFIX_MODIFIER in expectation_line.parsed_modifiers:
619             self._timeline_to_tests[WONTFIX].add(test)
620         else:
621             self._timeline_to_tests[NOW].add(test)
622
623         if TestExpectationParser.SKIP_MODIFIER in expectation_line.parsed_modifiers:
624             self._result_type_to_tests[SKIP].add(test)
625         elif expectation_line.parsed_expectations == set([PASS]):
626             self._result_type_to_tests[PASS].add(test)
627         elif expectation_line.is_flaky():
628             self._result_type_to_tests[FLAKY].add(test)
629         else:
630             # FIXME: What is this?
631             self._result_type_to_tests[FAIL].add(test)
632
633     def _clear_expectations_for_test(self, test):
634         """Remove prexisting expectations for this test.
635         This happens if we are seeing a more precise path
636         than a previous listing.
637         """
638         if self.has_test(test):
639             self._test_to_expectations.pop(test, '')
640             self._remove_from_sets(test, self._expectation_to_tests)
641             self._remove_from_sets(test, self._modifier_to_tests)
642             self._remove_from_sets(test, self._timeline_to_tests)
643             self._remove_from_sets(test, self._result_type_to_tests)
644
645     def _remove_from_sets(self, test, dict_of_sets_of_tests):
646         """Removes the given test from the sets in the dictionary.
647
648         Args:
649           test: test to look for
650           dict: dict of sets of files"""
651         for set_of_tests in dict_of_sets_of_tests.itervalues():
652             if test in set_of_tests:
653                 set_of_tests.remove(test)
654
655     def _already_seen_better_match(self, test, expectation_line):
656         """Returns whether we've seen a better match already in the file.
657
658         Returns True if we've already seen a expectation_line.name that matches more of the test
659             than this path does
660         """
661         # FIXME: See comment below about matching test configs and specificity.
662         if not self.has_test(test):
663             # We've never seen this test before.
664             return False
665
666         prev_expectation_line = self._test_to_expectation_line[test]
667
668         if prev_expectation_line.filename != expectation_line.filename:
669             # We've moved on to a new expectation file, which overrides older ones.
670             return False
671
672         if len(prev_expectation_line.path) > len(expectation_line.path):
673             # The previous path matched more of the test.
674             return True
675
676         if len(prev_expectation_line.path) < len(expectation_line.path):
677             # This path matches more of the test.
678             return False
679
680         # At this point we know we have seen a previous exact match on this
681         # base path, so we need to check the two sets of modifiers.
682
683         # FIXME: This code was originally designed to allow lines that matched
684         # more modifiers to override lines that matched fewer modifiers.
685         # However, we currently view these as errors.
686         #
687         # To use the "more modifiers wins" policy, change the errors for overrides
688         # to be warnings and return False".
689
690         if prev_expectation_line.matching_configurations == expectation_line.matching_configurations:
691             expectation_line.warnings.append('Duplicate or ambiguous entry lines %s:%d and %s:%d.' % (
692                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number,
693                 self._shorten_filename(expectation_line.filename), expectation_line.line_number))
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(prev_expectation_line.filename), prev_expectation_line.line_number,
699                 self._shorten_filename(expectation_line.filename), expectation_line.line_number))
700             # FIXME: return False if we want more specific to win.
701             return True
702
703         if prev_expectation_line.matching_configurations <= expectation_line.matching_configurations:
704             expectation_line.warnings.append('More specific entry for %s on line %s:%d overrides line %s:%d.' % (expectation_line.name,
705                 self._shorten_filename(expectation_line.filename), expectation_line.line_number,
706                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number))
707             return True
708
709         if prev_expectation_line.matching_configurations & expectation_line.matching_configurations:
710             expectation_line.warnings.append('Entries for %s on lines %s:%d and %s:%d match overlapping sets of configurations.' % (expectation_line.name,
711                 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_number,
712                 self._shorten_filename(expectation_line.filename), expectation_line.line_number))
713             return True
714
715         # Configuration sets are disjoint, then.
716         return False
717
718
719 class TestExpectations(object):
720     """Test expectations consist of lines with specifications of what
721     to expect from layout test cases. The test cases can be directories
722     in which case the expectations apply to all test cases in that
723     directory and any subdirectory. The format is along the lines of:
724
725       LayoutTests/fast/js/fixme.js [ Failure ]
726       LayoutTests/fast/js/flaky.js [ Failure Pass ]
727       LayoutTests/fast/js/crash.js [ Crash Failure Pass Timeout ]
728       ...
729
730     To add modifiers:
731       LayoutTests/fast/js/no-good.js
732       [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Timeout ]
733       [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
734       [ Linux Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
735       [ Linux Win ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
736
737     Skip: Doesn't run the test.
738     Slow: The test takes a long time to run, but does not timeout indefinitely.
739     WontFix: For tests that we never intend to pass on a given platform (treated like Skip).
740
741     Notes:
742       -A test cannot be both SLOW and TIMEOUT
743       -A test can be included twice, but not via the same path.
744       -If a test is included twice, then the more precise path wins.
745       -CRASH tests cannot be WONTFIX
746     """
747
748     # FIXME: Update to new syntax once the old format is no longer supported.
749     EXPECTATIONS = {'pass': PASS,
750                     'audio': AUDIO,
751                     'fail': FAIL,
752                     'image': IMAGE,
753                     'image+text': IMAGE_PLUS_TEXT,
754                     'text': TEXT,
755                     'timeout': TIMEOUT,
756                     'crash': CRASH,
757                     'missing': MISSING,
758                     'skip': SKIP}
759
760     # (aggregated by category, pass/fail/skip, type)
761     EXPECTATION_DESCRIPTIONS = {SKIP: 'skipped',
762                                 PASS: 'passes',
763                                 FAIL: 'failures',
764                                 IMAGE: 'image-only failures',
765                                 TEXT: 'text-only failures',
766                                 IMAGE_PLUS_TEXT: 'image and text failures',
767                                 AUDIO: 'audio failures',
768                                 CRASH: 'crashes',
769                                 TIMEOUT: 'timeouts',
770                                 MISSING: 'missing results'}
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     # FIXME: This constructor does too much work. We should move the actual parsing of
841     # the expectations into separate routines so that linting and handling overrides
842     # can be controlled separately, and the constructor can be more of a no-op.
843     def __init__(self, port, tests=None, include_overrides=True, expectations_to_lint=None):
844         self._full_test_list = tests
845         self._test_config = port.test_configuration()
846         self._is_lint_mode = expectations_to_lint is not None
847         self._model = TestExpectationsModel(self._shorten_filename)
848         self._parser = TestExpectationParser(port, tests, self._is_lint_mode)
849         self._port = port
850         self._skipped_tests_warnings = []
851
852         expectations_dict = expectations_to_lint or port.expectations_dict()
853         self._expectations = self._parser.parse(expectations_dict.keys()[0], expectations_dict.values()[0])
854         self._add_expectations(self._expectations)
855
856         if len(expectations_dict) > 1 and include_overrides:
857             for name in expectations_dict.keys()[1:]:
858                 expectations = self._parser.parse(name, expectations_dict[name])
859                 self._add_expectations(expectations)
860                 self._expectations += expectations
861
862         # FIXME: move ignore_tests into port.skipped_layout_tests()
863         self.add_skipped_tests(port.skipped_layout_tests(tests).union(set(port.get_option('ignore_tests', []))))
864
865         self._has_warnings = False
866         self._report_warnings()
867         self._process_tests_without_expectations()
868
869     # TODO(ojan): Allow for removing skipped tests when getting the list of
870     # tests to run, but not when getting metrics.
871     def model(self):
872         return self._model
873
874     def get_rebaselining_failures(self):
875         return self._model.get_test_set(REBASELINE)
876
877     # FIXME: Change the callsites to use TestExpectationsModel and remove.
878     def get_expectations(self, test):
879         return self._model.get_expectations(test)
880
881     # FIXME: Change the callsites to use TestExpectationsModel and remove.
882     def has_modifier(self, test, modifier):
883         return self._model.has_modifier(test, modifier)
884
885     # FIXME: Change the callsites to use TestExpectationsModel and remove.
886     def get_tests_with_result_type(self, result_type):
887         return self._model.get_tests_with_result_type(result_type)
888
889     # FIXME: Change the callsites to use TestExpectationsModel and remove.
890     def get_test_set(self, modifier, expectation=None, include_skips=True):
891         return self._model.get_test_set(modifier, expectation, include_skips)
892
893     # FIXME: Change the callsites to use TestExpectationsModel and remove.
894     def get_modifiers(self, test):
895         return self._model.get_modifiers(test)
896
897     # FIXME: Change the callsites to use TestExpectationsModel and remove.
898     def get_tests_with_timeline(self, timeline):
899         return self._model.get_tests_with_timeline(timeline)
900
901     def get_expectations_string(self, test):
902         return self._model.get_expectations_string(test)
903
904     def expectation_to_string(self, expectation):
905         return self._model.expectation_to_string(expectation)
906
907     def matches_an_expected_result(self, test, result, pixel_tests_are_enabled):
908         expected_results = self._model.get_expectations(test)
909         if not pixel_tests_are_enabled:
910             expected_results = self.remove_pixel_failures(expected_results)
911         return self.result_was_expected(result,
912                                    expected_results,
913                                    self.is_rebaselining(test),
914                                    self._model.has_modifier(test, SKIP))
915
916     def is_rebaselining(self, test):
917         return self._model.has_modifier(test, REBASELINE)
918
919     def _shorten_filename(self, filename):
920         if filename.startswith(self._port.path_from_webkit_base()):
921             return self._port.host.filesystem.relpath(filename, self._port.path_from_webkit_base())
922         return filename
923
924     def _report_warnings(self):
925         warnings = []
926         for expectation in self._expectations:
927             for warning in expectation.warnings:
928                 warnings.append('%s:%d %s %s' % (self._shorten_filename(expectation.filename), expectation.line_number,
929                                 warning, expectation.name if expectation.expectations else expectation.original_string))
930
931         if warnings:
932             self._has_warnings = True
933             if self._is_lint_mode:
934                 raise ParseError(warnings)
935             _log.warning('--lint-test-files warnings:')
936             for warning in warnings:
937                 _log.warning(warning)
938             _log.warning('')
939
940     def _process_tests_without_expectations(self):
941         if self._full_test_list:
942             for test in self._full_test_list:
943                 if not self._model.has_test(test):
944                     self._model.add_expectation_line(TestExpectationLine.create_passing_expectation(test))
945
946     def has_warnings(self):
947         return self._has_warnings
948
949     def remove_configuration_from_test(self, test, test_configuration):
950         expectations_to_remove = []
951         modified_expectations = []
952
953         for expectation in self._expectations:
954             if expectation.name != test or expectation.is_flaky() or not expectation.parsed_expectations:
955                 continue
956             if iter(expectation.parsed_expectations).next() not in (FAIL, IMAGE):
957                 continue
958             if test_configuration not in expectation.matching_configurations:
959                 continue
960
961             expectation.matching_configurations.remove(test_configuration)
962             if expectation.matching_configurations:
963                 modified_expectations.append(expectation)
964             else:
965                 expectations_to_remove.append(expectation)
966
967         for expectation in expectations_to_remove:
968             self._expectations.remove(expectation)
969
970         return self.list_to_string(self._expectations, self._parser._test_configuration_converter, modified_expectations)
971
972     def remove_rebaselined_tests(self, except_these_tests, filename):
973         """Returns a copy of the expectations in the file with the tests removed."""
974         def without_rebaseline_modifier(expectation):
975             return (expectation.filename == filename and
976                     not (not expectation.is_invalid() and
977                          expectation.name in except_these_tests and
978                          'rebaseline' in expectation.parsed_modifiers))
979
980         return self.list_to_string(filter(without_rebaseline_modifier, self._expectations), reconstitute_only_these=[])
981
982     def _add_expectations(self, expectation_list):
983         for expectation_line in expectation_list:
984             if not expectation_line.expectations:
985                 continue
986
987             if self._is_lint_mode or self._test_config in expectation_line.matching_configurations:
988                 self._model.add_expectation_line(expectation_line)
989
990     def add_skipped_tests(self, tests_to_skip):
991         if not tests_to_skip:
992             return
993         for test in self._expectations:
994             if test.name and test.name in tests_to_skip:
995                 test.warnings.append('%s:%d %s is also in a Skipped file.' % (test.filename, test.line_number, test.name))
996
997         for test_name in tests_to_skip:
998             expectation_line = self._parser.expectation_for_skipped_test(test_name)
999             self._model.add_expectation_line(expectation_line, in_skipped=True)
1000
1001     @staticmethod
1002     def list_to_string(expectation_lines, test_configuration_converter=None, reconstitute_only_these=None):
1003         def serialize(expectation_line):
1004             # If reconstitute_only_these is an empty list, we want to return original_string.
1005             # So we need to compare reconstitute_only_these to None, not just check if it's falsey.
1006             if reconstitute_only_these is None or expectation_line in reconstitute_only_these:
1007                 return expectation_line.to_string(test_configuration_converter)
1008             return expectation_line.original_string
1009
1010         def nones_out(expectation_line):
1011             return expectation_line is not None
1012
1013         return "\n".join(filter(nones_out, map(serialize, expectation_lines)))