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