1 # Copyright (C) 2010 Google Inc. All rights reserved.
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
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
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.
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.
29 """A helper class for reading in and dealing with tests expectations
36 from webkitpy.layout_tests.models.test_configuration import TestConfigurationConverter
38 _log = logging.getLogger(__name__)
41 # Test expectation and specifier constants.
43 # FIXME: range() starts with 0 which makes if expectation checks harder
45 (PASS, FAIL, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO, TIMEOUT, CRASH, LEAK, SKIP, WONTFIX,
46 SLOW, REBASELINE, NEEDS_REBASELINE, NEEDS_MANUAL_REBASELINE, MISSING, FLAKY, NOW, NONE) = range(19)
48 # FIXME: Perhas these two routines should be part of the Port instead?
49 BASELINE_SUFFIX_LIST = ('png', 'wav', 'txt')
51 WEBKIT_BUG_PREFIX = 'webkit.org/b/'
52 CHROMIUM_BUG_PREFIX = 'crbug.com/'
53 V8_BUG_PREFIX = 'code.google.com/p/v8/issues/detail?id='
54 NAMED_BUG_PREFIX = 'Bug('
56 MISSING_KEYWORD = 'Missing'
57 NEEDS_REBASELINE_KEYWORD = 'NeedsRebaseline'
58 NEEDS_MANUAL_REBASELINE_KEYWORD = 'NeedsManualRebaseline'
60 class ParseError(Exception):
61 def __init__(self, warnings):
62 super(ParseError, self).__init__()
63 self.warnings = warnings
66 return '\n'.join(map(str, self.warnings))
69 return 'ParseError(warnings=%s)' % self.warnings
72 class TestExpectationParser(object):
73 """Provides parsing facilities for lines in the test_expectation.txt file."""
75 # FIXME: Rename these to *_KEYWORD as in MISSING_KEYWORD above, but make the case studdly-caps to match the actual file contents.
76 REBASELINE_MODIFIER = 'rebaseline'
77 NEEDS_REBASELINE_MODIFIER = 'needsrebaseline'
78 NEEDS_MANUAL_REBASELINE_MODIFIER = 'needsmanualrebaseline'
79 PASS_EXPECTATION = 'pass'
80 SKIP_MODIFIER = 'skip'
81 SLOW_MODIFIER = 'slow'
82 WONTFIX_MODIFIER = 'wontfix'
84 TIMEOUT_EXPECTATION = 'timeout'
86 MISSING_BUG_WARNING = 'Test lacks BUG specifier.'
88 def __init__(self, port, full_test_list, is_lint_mode):
90 self._test_configuration_converter = TestConfigurationConverter(set(port.all_test_configurations()), port.configuration_specifier_macros())
91 self._full_test_list = full_test_list
92 self._is_lint_mode = is_lint_mode
94 def parse(self, filename, expectations_string):
95 expectation_lines = []
97 for line in expectations_string.split("\n"):
99 test_expectation = self._tokenize_line(filename, line, line_number)
100 self._parse_line(test_expectation)
101 expectation_lines.append(test_expectation)
102 return expectation_lines
104 def _create_expectation_line(self, test_name, expectations, file_name):
105 expectation_line = TestExpectationLine()
106 expectation_line.original_string = test_name
107 expectation_line.name = test_name
108 expectation_line.filename = file_name
109 expectation_line.expectations = expectations
110 return expectation_line
112 def expectation_line_for_test(self, test_name, expectations):
113 expectation_line = self._create_expectation_line(test_name, expectations, '<Bot TestExpectations>')
114 self._parse_line(expectation_line)
115 return expectation_line
118 def expectation_for_skipped_test(self, test_name):
119 if not self._port.test_exists(test_name):
120 _log.warning('The following test %s from the Skipped list doesn\'t exist' % test_name)
121 expectation_line = self._create_expectation_line(test_name, [TestExpectationParser.PASS_EXPECTATION], '<Skipped file>')
122 expectation_line.expectations = [TestExpectationParser.SKIP_MODIFIER, TestExpectationParser.WONTFIX_MODIFIER]
123 expectation_line.is_skipped_outside_expectations_file = True
124 self._parse_line(expectation_line)
125 return expectation_line
127 def _parse_line(self, expectation_line):
128 if not expectation_line.name:
131 if not self._check_test_exists(expectation_line):
134 expectation_line.is_file = self._port.test_isfile(expectation_line.name)
135 if expectation_line.is_file:
136 expectation_line.path = expectation_line.name
138 expectation_line.path = self._port.normalize_test_name(expectation_line.name)
140 self._collect_matching_tests(expectation_line)
142 self._parse_specifiers(expectation_line)
143 self._parse_expectations(expectation_line)
145 def _parse_specifiers(self, expectation_line):
146 if self._is_lint_mode:
147 self._lint_line(expectation_line)
149 parsed_specifiers = set([specifier.lower() for specifier in expectation_line.specifiers])
150 expectation_line.matching_configurations = self._test_configuration_converter.to_config_set(parsed_specifiers, expectation_line.warnings)
152 def _lint_line(self, expectation_line):
153 expectations = [expectation.lower() for expectation in expectation_line.expectations]
154 if not expectation_line.bugs and self.WONTFIX_MODIFIER not in expectations:
155 expectation_line.warnings.append(self.MISSING_BUG_WARNING)
156 if self.REBASELINE_MODIFIER in expectations:
157 expectation_line.warnings.append('REBASELINE should only be used for running rebaseline.py. Cannot be checked in.')
159 if self.NEEDS_REBASELINE_MODIFIER in expectations or self.NEEDS_MANUAL_REBASELINE_MODIFIER in expectations:
160 for test in expectation_line.matching_tests:
161 if self._port.reference_files(test):
162 expectation_line.warnings.append('A reftest cannot be marked as NeedsRebaseline/NeedsManualRebaseline')
164 def _parse_expectations(self, expectation_line):
166 for part in expectation_line.expectations:
167 expectation = TestExpectations.expectation_from_string(part)
168 if expectation is None: # Careful, PASS is currently 0.
169 expectation_line.warnings.append('Unsupported expectation: %s' % part)
171 result.add(expectation)
172 expectation_line.parsed_expectations = result
174 def _check_test_exists(self, expectation_line):
175 # WebKit's way of skipping tests is to add a -disabled suffix.
176 # So we should consider the path existing if the path or the
177 # -disabled version exists.
178 if not self._port.test_exists(expectation_line.name) and not self._port.test_exists(expectation_line.name + '-disabled'):
179 # Log a warning here since you hit this case any
180 # time you update TestExpectations without syncing
181 # the LayoutTests directory
182 expectation_line.warnings.append('Path does not exist.')
186 def _collect_matching_tests(self, expectation_line):
187 """Convert the test specification to an absolute, normalized
188 path and make sure directories end with the OS path separator."""
189 # FIXME: full_test_list can quickly contain a big amount of
190 # elements. We should consider at some point to use a more
191 # efficient structure instead of a list. Maybe a dictionary of
192 # lists to represent the tree of tests, leaves being test
193 # files and nodes being categories.
195 if not self._full_test_list:
196 expectation_line.matching_tests = [expectation_line.path]
199 if not expectation_line.is_file:
200 # this is a test category, return all the tests of the category.
201 expectation_line.matching_tests = [test for test in self._full_test_list if test.startswith(expectation_line.path)]
204 # this is a test file, do a quick check if it's in the
206 if expectation_line.path in self._full_test_list:
207 expectation_line.matching_tests.append(expectation_line.path)
209 # FIXME: Update the original specifiers and remove this once the old syntax is gone.
210 _configuration_tokens_list = [
211 'Mac', 'SnowLeopard', 'Lion', 'Retina', 'MountainLion', 'Mavericks',
219 _configuration_tokens = dict((token, token.upper()) for token in _configuration_tokens_list)
220 _inverted_configuration_tokens = dict((value, name) for name, value in _configuration_tokens.iteritems())
222 # FIXME: Update the original specifiers list and remove this once the old syntax is gone.
223 _expectation_tokens = {
227 'ImageOnlyFailure': 'IMAGE',
228 MISSING_KEYWORD: 'MISSING',
230 'Rebaseline': 'REBASELINE',
231 NEEDS_REBASELINE_KEYWORD: 'NEEDSREBASELINE',
232 NEEDS_MANUAL_REBASELINE_KEYWORD: 'NEEDSMANUALREBASELINE',
235 'Timeout': 'TIMEOUT',
236 'WontFix': 'WONTFIX',
239 _inverted_expectation_tokens = dict([(value, name) for name, value in _expectation_tokens.iteritems()] +
240 [('TEXT', 'Failure'), ('IMAGE+TEXT', 'Failure'), ('AUDIO', 'Failure')])
242 # FIXME: Seems like these should be classmethods on TestExpectationLine instead of TestExpectationParser.
244 def _tokenize_line(cls, filename, expectation_string, line_number):
245 """Tokenizes a line from TestExpectations and returns an unparsed TestExpectationLine instance using the old format.
247 The new format for a test expectation line is:
249 [[bugs] [ "[" <configuration specifiers> "]" <name> [ "[" <expectations> "]" ["#" <comment>]
251 Any errant whitespace is not preserved.
254 expectation_line = TestExpectationLine()
255 expectation_line.original_string = expectation_string
256 expectation_line.filename = filename
257 expectation_line.line_numbers = str(line_number)
259 comment_index = expectation_string.find("#")
260 if comment_index == -1:
261 comment_index = len(expectation_string)
263 expectation_line.comment = expectation_string[comment_index + 1:]
265 remaining_string = re.sub(r"\s+", " ", expectation_string[:comment_index].strip())
266 if len(remaining_string) == 0:
267 return expectation_line
269 # special-case parsing this so that we fail immediately instead of treating this as a test name
270 if remaining_string.startswith('//'):
271 expectation_line.warnings = ['use "#" instead of "//" for comments']
272 return expectation_line
279 has_unrecognized_expectation = False
281 tokens = remaining_string.split()
284 if (token.startswith(WEBKIT_BUG_PREFIX) or
285 token.startswith(CHROMIUM_BUG_PREFIX) or
286 token.startswith(V8_BUG_PREFIX) or
287 token.startswith(NAMED_BUG_PREFIX)):
289 warnings.append('"%s" is not at the start of the line.' % token)
291 if token.startswith(WEBKIT_BUG_PREFIX):
293 elif token.startswith(CHROMIUM_BUG_PREFIX):
295 elif token.startswith(V8_BUG_PREFIX):
298 match = re.match('Bug\((\w+)\)$', token)
300 warnings.append('unrecognized bug identifier "%s"' % token)
306 state = 'configuration'
307 elif state == 'name_found':
308 state = 'expectations'
310 warnings.append('unexpected "["')
313 if state == 'configuration':
315 elif state == 'expectations':
318 warnings.append('unexpected "]"')
320 elif token in ('//', ':', '='):
321 warnings.append('"%s" is not legal in the new TestExpectations syntax.' % token)
323 elif state == 'configuration':
324 specifiers.append(cls._configuration_tokens.get(token, token))
325 elif state == 'expectations':
326 if token not in cls._expectation_tokens:
327 has_unrecognized_expectation = True
328 warnings.append('Unrecognized expectation "%s"' % token)
330 expectations.append(cls._expectation_tokens.get(token, token))
331 elif state == 'name_found':
332 warnings.append('expecting "[", "#", or end of line instead of "%s"' % token)
340 warnings.append('Did not find a test name.')
341 elif state not in ('name_found', 'done'):
342 warnings.append('Missing a "]"')
344 if 'WONTFIX' in expectations and 'SKIP' not in expectations:
345 expectations.append('SKIP')
347 if ('SKIP' in expectations or 'WONTFIX' in expectations) and len(set(expectations) - set(['SKIP', 'WONTFIX'])):
348 warnings.append('A test marked Skip or WontFix must not have other expectations.')
350 if not expectations and not has_unrecognized_expectation:
351 warnings.append('Missing expectations.')
353 expectation_line.bugs = bugs
354 expectation_line.specifiers = specifiers
355 expectation_line.expectations = expectations
356 expectation_line.name = name
357 expectation_line.warnings = warnings
358 return expectation_line
361 def _split_space_separated(cls, space_separated_string):
362 """Splits a space-separated string into an array."""
363 return [part.strip() for part in space_separated_string.strip().split(' ')]
366 class TestExpectationLine(object):
367 """Represents a line in test expectations file."""
370 """Initializes a blank-line equivalent of an expectation."""
371 self.original_string = None
372 self.filename = None # this is the path to the expectations file for this line
373 self.line_numbers = "0"
374 self.name = None # this is the path in the line itself
375 self.path = None # this is the normpath of self.name
378 self.parsed_specifiers = []
379 self.matching_configurations = set()
380 self.expectations = []
381 self.parsed_expectations = set()
383 self.matching_tests = []
385 self.is_skipped_outside_expectations_file = False
387 def __eq__(self, other):
388 return (self.original_string == other.original_string
389 and self.filename == other.filename
390 and self.line_numbers == other.line_numbers
391 and self.name == other.name
392 and self.path == other.path
393 and self.bugs == other.bugs
394 and self.specifiers == other.specifiers
395 and self.parsed_specifiers == other.parsed_specifiers
396 and self.matching_configurations == other.matching_configurations
397 and self.expectations == other.expectations
398 and self.parsed_expectations == other.parsed_expectations
399 and self.comment == other.comment
400 and self.matching_tests == other.matching_tests
401 and self.warnings == other.warnings
402 and self.is_skipped_outside_expectations_file == other.is_skipped_outside_expectations_file)
404 def is_invalid(self):
405 return bool(self.warnings and self.warnings != [TestExpectationParser.MISSING_BUG_WARNING])
408 return len(self.parsed_expectations) > 1
410 def is_whitespace_or_comment(self):
411 return bool(re.match("^\s*$", self.original_string.split('#')[0]))
414 def create_passing_expectation(test):
415 expectation_line = TestExpectationLine()
416 expectation_line.name = test
417 expectation_line.path = test
418 expectation_line.parsed_expectations = set([PASS])
419 expectation_line.expectations = set(['PASS'])
420 expectation_line.matching_tests = [test]
421 return expectation_line
424 def merge_expectation_lines(line1, line2, model_all_expectations):
425 """Merges the expectations of line2 into line1 and returns a fresh object."""
430 if model_all_expectations and line1.filename != line2.filename:
433 # Don't merge original_string or comment.
434 result = TestExpectationLine()
435 # We only care about filenames when we're linting, in which case the filenames are the same.
436 # Not clear that there's anything better to do when not linting and the filenames are different.
437 if model_all_expectations:
438 result.filename = line2.filename
439 result.line_numbers = line1.line_numbers + "," + line2.line_numbers
440 result.name = line1.name
441 result.path = line1.path
442 result.parsed_expectations = set(line1.parsed_expectations) | set(line2.parsed_expectations)
443 result.expectations = list(set(line1.expectations) | set(line2.expectations))
444 result.bugs = list(set(line1.bugs) | set(line2.bugs))
445 result.specifiers = list(set(line1.specifiers) | set(line2.specifiers))
446 result.parsed_specifiers = list(set(line1.parsed_specifiers) | set(line2.parsed_specifiers))
447 result.matching_configurations = set(line1.matching_configurations) | set(line2.matching_configurations)
448 result.matching_tests = list(list(set(line1.matching_tests) | set(line2.matching_tests)))
449 result.warnings = list(set(line1.warnings) | set(line2.warnings))
450 result.is_skipped_outside_expectations_file = line1.is_skipped_outside_expectations_file or line2.is_skipped_outside_expectations_file
453 def to_string(self, test_configuration_converter, include_specifiers=True, include_expectations=True, include_comment=True):
454 parsed_expectation_to_string = dict([[parsed_expectation, expectation_string] for expectation_string, parsed_expectation in TestExpectations.EXPECTATIONS.items()])
456 if self.is_invalid():
457 return self.original_string or ''
459 if self.name is None:
460 return '' if self.comment is None else "#%s" % self.comment
462 if test_configuration_converter and self.bugs:
463 specifiers_list = test_configuration_converter.to_specifiers_list(self.matching_configurations)
465 for specifiers in specifiers_list:
466 # FIXME: this is silly that we join the specifiers and then immediately split them.
467 specifiers = self._serialize_parsed_specifiers(test_configuration_converter, specifiers).split()
468 expectations = self._serialize_parsed_expectations(parsed_expectation_to_string).split()
469 result.append(self._format_line(self.bugs, specifiers, self.name, expectations, self.comment))
470 return "\n".join(result) if result else None
472 return self._format_line(self.bugs, self.specifiers, self.name, self.expectations, self.comment,
473 include_specifiers, include_expectations, include_comment)
476 # Note that this doesn't include the comments.
477 return '%s,%s,%s,%s' % (self.name, ' '.join(self.bugs), ' '.join(self.specifiers), ' '.join(self.expectations))
479 def _serialize_parsed_expectations(self, parsed_expectation_to_string):
481 for index in TestExpectations.EXPECTATIONS.values():
482 if index in self.parsed_expectations:
483 result.append(parsed_expectation_to_string[index])
484 return ' '.join(result)
486 def _serialize_parsed_specifiers(self, test_configuration_converter, specifiers):
488 result.extend(sorted(self.parsed_specifiers))
489 result.extend(test_configuration_converter.specifier_sorter().sort_specifiers(specifiers))
490 return ' '.join(result)
493 def _filter_redundant_expectations(expectations):
494 if set(expectations) == set(['Pass', 'Skip']):
496 if set(expectations) == set(['Pass', 'Slow']):
501 def _format_line(bugs, specifiers, name, expectations, comment, include_specifiers=True, include_expectations=True, include_comment=True):
503 new_expectations = []
504 for specifier in specifiers:
505 # FIXME: Make this all work with the mixed-cased specifiers (e.g. WontFix, Slow, etc).
506 specifier = specifier.upper()
507 new_specifiers.append(TestExpectationParser._inverted_configuration_tokens.get(specifier, specifier))
509 for expectation in expectations:
510 expectation = expectation.upper()
511 new_expectations.append(TestExpectationParser._inverted_expectation_tokens.get(expectation, expectation))
514 if include_specifiers and (bugs or new_specifiers):
516 result += ' '.join(bugs) + ' '
518 result += '[ %s ] ' % ' '.join(new_specifiers)
520 if include_expectations and new_expectations:
521 new_expectations = TestExpectationLine._filter_redundant_expectations(new_expectations)
522 result += ' [ %s ]' % ' '.join(sorted(set(new_expectations)))
523 if include_comment and comment is not None:
524 result += " #%s" % comment
528 # FIXME: Refactor API to be a proper CRUD.
529 class TestExpectationsModel(object):
530 """Represents relational store of all expectations and provides CRUD semantics to manage it."""
532 def __init__(self, shorten_filename=None):
533 # Maps a test to its list of expectations.
534 self._test_to_expectations = {}
536 # Maps a test to list of its specifiers (string values)
537 self._test_to_specifiers = {}
539 # Maps a test to a TestExpectationLine instance.
540 self._test_to_expectation_line = {}
542 self._expectation_to_tests = self._dict_of_sets(TestExpectations.EXPECTATIONS)
543 self._timeline_to_tests = self._dict_of_sets(TestExpectations.TIMELINES)
544 self._result_type_to_tests = self._dict_of_sets(TestExpectations.RESULT_TYPES)
546 self._shorten_filename = shorten_filename or (lambda x: x)
548 def _merge_test_map(self, self_map, other_map):
549 for test in other_map:
550 new_expectations = set(other_map[test])
552 new_expectations |= set(self_map[test])
553 self_map[test] = list(new_expectations) if isinstance(other_map[test], list) else new_expectations
555 def _merge_dict_of_sets(self, self_dict, other_dict):
556 for key in other_dict:
557 self_dict[key] |= other_dict[key]
559 def merge_model(self, other):
560 self._merge_test_map(self._test_to_expectations, other._test_to_expectations)
562 for test, line in other._test_to_expectation_line.items():
563 if test in self._test_to_expectation_line:
564 line = TestExpectationLine.merge_expectation_lines(self._test_to_expectation_line[test], line, model_all_expectations=False)
565 self._test_to_expectation_line[test] = line
567 self._merge_dict_of_sets(self._expectation_to_tests, other._expectation_to_tests)
568 self._merge_dict_of_sets(self._timeline_to_tests, other._timeline_to_tests)
569 self._merge_dict_of_sets(self._result_type_to_tests, other._result_type_to_tests)
571 def _dict_of_sets(self, strings_to_constants):
572 """Takes a dict of strings->constants and returns a dict mapping
573 each constant to an empty set."""
575 for c in strings_to_constants.values():
579 def get_test_set(self, expectation, include_skips=True):
580 tests = self._expectation_to_tests[expectation]
581 if not include_skips:
582 tests = tests - self.get_test_set(SKIP)
585 def get_test_set_for_keyword(self, keyword):
586 expectation_enum = TestExpectations.EXPECTATIONS.get(keyword.lower(), None)
587 if expectation_enum is not None:
588 return self._expectation_to_tests[expectation_enum]
590 matching_tests = set()
591 for test, specifiers in self._test_to_specifiers.iteritems():
592 if keyword.lower() in specifiers:
593 matching_tests.add(test)
594 return matching_tests
596 def get_tests_with_result_type(self, result_type):
597 return self._result_type_to_tests[result_type]
599 def get_tests_with_timeline(self, timeline):
600 return self._timeline_to_tests[timeline]
602 def has_test(self, test):
603 return test in self._test_to_expectation_line
605 def get_expectation_line(self, test):
606 return self._test_to_expectation_line.get(test)
608 def get_expectations(self, test):
609 return self._test_to_expectations[test]
611 def get_expectations_string(self, test):
612 """Returns the expectatons for the given test as an uppercase string.
613 If there are no expectations for the test, then "PASS" is returned."""
614 if self.get_expectation_line(test).is_skipped_outside_expectations_file:
617 expectations = self.get_expectations(test)
620 # FIXME: WontFix should cause the test to get skipped without artificially adding SKIP to the expectations list.
621 if WONTFIX in expectations and SKIP in expectations:
622 expectations.remove(SKIP)
624 for expectation in expectations:
625 retval.append(self.expectation_to_string(expectation))
627 return " ".join(retval)
629 def expectation_to_string(self, expectation):
630 """Return the uppercased string equivalent of a given expectation."""
631 for item in TestExpectations.EXPECTATIONS.items():
632 if item[1] == expectation:
633 return item[0].upper()
634 raise ValueError(expectation)
636 def remove_expectation_line(self, test):
637 if not self.has_test(test):
639 self._clear_expectations_for_test(test)
640 del self._test_to_expectation_line[test]
642 def add_expectation_line(self, expectation_line,
643 model_all_expectations=False):
644 """Returns a list of warnings encountered while matching specifiers."""
646 if expectation_line.is_invalid():
649 for test in expectation_line.matching_tests:
650 if self._already_seen_better_match(test, expectation_line):
653 if model_all_expectations:
654 expectation_line = TestExpectationLine.merge_expectation_lines(self.get_expectation_line(test), expectation_line, model_all_expectations)
656 self._clear_expectations_for_test(test)
657 self._test_to_expectation_line[test] = expectation_line
658 self._add_test(test, expectation_line)
660 def _add_test(self, test, expectation_line):
661 """Sets the expected state for a given test.
663 This routine assumes the test has not been added before. If it has,
664 use _clear_expectations_for_test() to reset the state prior to
666 self._test_to_expectations[test] = expectation_line.parsed_expectations
667 for expectation in expectation_line.parsed_expectations:
668 self._expectation_to_tests[expectation].add(test)
670 self._test_to_specifiers[test] = expectation_line.specifiers
672 if WONTFIX in expectation_line.parsed_expectations:
673 self._timeline_to_tests[WONTFIX].add(test)
675 self._timeline_to_tests[NOW].add(test)
677 if SKIP in expectation_line.parsed_expectations:
678 self._result_type_to_tests[SKIP].add(test)
679 elif expectation_line.parsed_expectations == set([PASS]):
680 self._result_type_to_tests[PASS].add(test)
681 elif expectation_line.is_flaky():
682 self._result_type_to_tests[FLAKY].add(test)
684 # FIXME: What is this?
685 self._result_type_to_tests[FAIL].add(test)
687 def _clear_expectations_for_test(self, test):
688 """Remove prexisting expectations for this test.
689 This happens if we are seeing a more precise path
690 than a previous listing.
692 if self.has_test(test):
693 self._test_to_expectations.pop(test, '')
694 self._remove_from_sets(test, self._expectation_to_tests)
695 self._remove_from_sets(test, self._timeline_to_tests)
696 self._remove_from_sets(test, self._result_type_to_tests)
698 def _remove_from_sets(self, test, dict_of_sets_of_tests):
699 """Removes the given test from the sets in the dictionary.
702 test: test to look for
703 dict: dict of sets of files"""
704 for set_of_tests in dict_of_sets_of_tests.itervalues():
705 if test in set_of_tests:
706 set_of_tests.remove(test)
708 def _already_seen_better_match(self, test, expectation_line):
709 """Returns whether we've seen a better match already in the file.
711 Returns True if we've already seen a expectation_line.name that matches more of the test
714 # FIXME: See comment below about matching test configs and specificity.
715 if not self.has_test(test):
716 # We've never seen this test before.
719 prev_expectation_line = self._test_to_expectation_line[test]
721 if prev_expectation_line.filename != expectation_line.filename:
722 # We've moved on to a new expectation file, which overrides older ones.
725 if len(prev_expectation_line.path) > len(expectation_line.path):
726 # The previous path matched more of the test.
729 if len(prev_expectation_line.path) < len(expectation_line.path):
730 # This path matches more of the test.
733 # At this point we know we have seen a previous exact match on this
734 # base path, so we need to check the two sets of specifiers.
736 # FIXME: This code was originally designed to allow lines that matched
737 # more specifiers to override lines that matched fewer specifiers.
738 # However, we currently view these as errors.
740 # To use the "more specifiers wins" policy, change the errors for overrides
741 # to be warnings and return False".
743 if prev_expectation_line.matching_configurations == expectation_line.matching_configurations:
744 expectation_line.warnings.append('Duplicate or ambiguous entry lines %s:%s and %s:%s.' % (
745 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers,
746 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers))
749 if prev_expectation_line.matching_configurations >= expectation_line.matching_configurations:
750 expectation_line.warnings.append('More specific entry for %s on line %s:%s overrides line %s:%s.' % (expectation_line.name,
751 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers,
752 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers))
753 # FIXME: return False if we want more specific to win.
756 if prev_expectation_line.matching_configurations <= expectation_line.matching_configurations:
757 expectation_line.warnings.append('More specific entry for %s on line %s:%s overrides line %s:%s.' % (expectation_line.name,
758 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers,
759 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers))
762 if prev_expectation_line.matching_configurations & expectation_line.matching_configurations:
763 expectation_line.warnings.append('Entries for %s on lines %s:%s and %s:%s match overlapping sets of configurations.' % (expectation_line.name,
764 self._shorten_filename(prev_expectation_line.filename), prev_expectation_line.line_numbers,
765 self._shorten_filename(expectation_line.filename), expectation_line.line_numbers))
768 # Configuration sets are disjoint, then.
772 class TestExpectations(object):
773 """Test expectations consist of lines with specifications of what
774 to expect from layout test cases. The test cases can be directories
775 in which case the expectations apply to all test cases in that
776 directory and any subdirectory. The format is along the lines of:
778 LayoutTests/fast/js/fixme.js [ Failure ]
779 LayoutTests/fast/js/flaky.js [ Failure Pass ]
780 LayoutTests/fast/js/crash.js [ Crash Failure Pass Timeout ]
784 LayoutTests/fast/js/no-good.js
785 [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Timeout ]
786 [ Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
787 [ Linux Debug ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
788 [ Linux Win ] LayoutTests/fast/js/no-good.js [ Pass Skip Timeout ]
790 Skip: Doesn't run the test.
791 Slow: The test takes a long time to run, but does not timeout indefinitely.
792 WontFix: For tests that we never intend to pass on a given platform (treated like Skip).
795 -A test cannot be both SLOW and TIMEOUT
796 -A test can be included twice, but not via the same path.
797 -If a test is included twice, then the more precise path wins.
798 -CRASH tests cannot be WONTFIX
801 # FIXME: Update to new syntax once the old format is no longer supported.
802 EXPECTATIONS = {'pass': PASS,
806 'image+text': IMAGE_PLUS_TEXT,
812 TestExpectationParser.SKIP_MODIFIER: SKIP,
813 TestExpectationParser.NEEDS_REBASELINE_MODIFIER: NEEDS_REBASELINE,
814 TestExpectationParser.NEEDS_MANUAL_REBASELINE_MODIFIER: NEEDS_MANUAL_REBASELINE,
815 TestExpectationParser.WONTFIX_MODIFIER: WONTFIX,
816 TestExpectationParser.SLOW_MODIFIER: SLOW,
817 TestExpectationParser.REBASELINE_MODIFIER: REBASELINE,
820 EXPECTATIONS_TO_STRING = dict((k, v) for (v, k) in EXPECTATIONS.iteritems())
822 # (aggregated by category, pass/fail/skip, type)
823 EXPECTATION_DESCRIPTIONS = {SKIP: 'skipped',
826 IMAGE: 'image-only failures',
827 TEXT: 'text-only failures',
828 IMAGE_PLUS_TEXT: 'image and text failures',
829 AUDIO: 'audio failures',
833 MISSING: 'missing results'}
835 NON_TEST_OUTCOME_EXPECTATIONS = (REBASELINE, SKIP, SLOW, WONTFIX)
837 BUILD_TYPES = ('debug', 'release')
839 TIMELINES = {TestExpectationParser.WONTFIX_MODIFIER: WONTFIX,
842 RESULT_TYPES = {'skip': SKIP,
848 def expectation_from_string(cls, string):
849 assert(' ' not in string) # This only handles one expectation at a time.
850 return cls.EXPECTATIONS.get(string.lower())
853 def result_was_expected(result, expected_results, test_needs_rebaselining):
854 """Returns whether we got a result we were expecting.
856 result: actual result of a test execution
857 expected_results: set of results listed in test_expectations
858 test_needs_rebaselining: whether test was marked as REBASELINE"""
859 if not (set(expected_results) - (set(TestExpectations.NON_TEST_OUTCOME_EXPECTATIONS))):
860 expected_results = set([PASS])
862 if result in expected_results:
864 if result in (PASS, TEXT, IMAGE, IMAGE_PLUS_TEXT, AUDIO, MISSING) and (NEEDS_REBASELINE in expected_results or NEEDS_MANUAL_REBASELINE in expected_results):
866 if result in (TEXT, IMAGE_PLUS_TEXT, AUDIO) and (FAIL in expected_results):
868 if result == MISSING and test_needs_rebaselining:
875 def remove_pixel_failures(expected_results):
876 """Returns a copy of the expected results for a test, except that we
877 drop any pixel failures and return the remaining expectations. For example,
878 if we're not running pixel tests, then tests expected to fail as IMAGE
880 expected_results = expected_results.copy()
881 if IMAGE in expected_results:
882 expected_results.remove(IMAGE)
883 expected_results.add(PASS)
884 return expected_results
887 def remove_non_sanitizer_failures(expected_results):
888 """Returns a copy of the expected results for a test, except that we
889 drop any failures that the sanitizers don't care about."""
890 expected_results = expected_results.copy()
891 for result in (IMAGE, FAIL, IMAGE_PLUS_TEXT):
892 if result in expected_results:
893 expected_results.remove(result)
894 expected_results.add(PASS)
895 return expected_results
898 def has_pixel_failures(actual_results):
899 return IMAGE in actual_results or FAIL in actual_results
902 def suffixes_for_expectations(expectations):
904 if IMAGE in expectations:
906 if FAIL in expectations:
913 def suffixes_for_actual_expectations_string(expectations):
915 if 'TEXT' in expectations:
917 if 'IMAGE' in expectations:
919 if 'AUDIO' in expectations:
921 if 'MISSING' in expectations:
927 # FIXME: This constructor does too much work. We should move the actual parsing of
928 # the expectations into separate routines so that linting and handling overrides
929 # can be controlled separately, and the constructor can be more of a no-op.
930 def __init__(self, port, tests=None, include_overrides=True, expectations_dict=None, model_all_expectations=False, is_lint_mode=False):
931 self._full_test_list = tests
932 self._test_config = port.test_configuration()
933 self._is_lint_mode = is_lint_mode
934 self._model_all_expectations = self._is_lint_mode or model_all_expectations
935 self._model = TestExpectationsModel(self._shorten_filename)
936 self._parser = TestExpectationParser(port, tests, self._is_lint_mode)
938 self._skipped_tests_warnings = []
939 self._expectations = []
941 if not expectations_dict:
942 expectations_dict = port.expectations_dict()
944 # Always parse the generic expectations (the generic file is required
945 # to be the first one in the expectations_dict, which must be an OrderedDict).
946 generic_path, generic_exps = expectations_dict.items()[0]
947 expectations = self._parser.parse(generic_path, generic_exps)
948 self._add_expectations(expectations, self._model)
949 self._expectations += expectations
951 # Now add the overrides if so requested.
952 if include_overrides:
953 for path, contents in expectations_dict.items()[1:]:
954 expectations = self._parser.parse(path, contents)
955 model = TestExpectationsModel(self._shorten_filename)
956 self._add_expectations(expectations, model)
957 self._expectations += expectations
958 self._model.merge_model(model)
960 # FIXME: move ignore_tests into port.skipped_layout_tests()
961 self.add_extra_skipped_tests(port.skipped_layout_tests(tests).union(set(port.get_option('ignore_tests', []))))
962 self.add_expectations_from_bot()
964 self._has_warnings = False
965 self._report_warnings()
966 self._process_tests_without_expectations()
968 # TODO(ojan): Allow for removing skipped tests when getting the list of
969 # tests to run, but not when getting metrics.
973 def get_needs_rebaseline_failures(self):
974 return self._model.get_test_set(NEEDS_REBASELINE)
976 def get_rebaselining_failures(self):
977 return self._model.get_test_set(REBASELINE)
979 # FIXME: Change the callsites to use TestExpectationsModel and remove.
980 def get_expectations(self, test):
981 return self._model.get_expectations(test)
983 # FIXME: Change the callsites to use TestExpectationsModel and remove.
984 def get_tests_with_result_type(self, result_type):
985 return self._model.get_tests_with_result_type(result_type)
987 # FIXME: Change the callsites to use TestExpectationsModel and remove.
988 def get_test_set(self, expectation, include_skips=True):
989 return self._model.get_test_set(expectation, include_skips)
991 # FIXME: Change the callsites to use TestExpectationsModel and remove.
992 def get_tests_with_timeline(self, timeline):
993 return self._model.get_tests_with_timeline(timeline)
995 def get_expectations_string(self, test):
996 return self._model.get_expectations_string(test)
998 def expectation_to_string(self, expectation):
999 return self._model.expectation_to_string(expectation)
1001 def matches_an_expected_result(self, test, result, pixel_tests_are_enabled, sanitizer_is_enabled):
1002 expected_results = self._model.get_expectations(test)
1003 if sanitizer_is_enabled:
1004 expected_results = self.remove_non_sanitizer_failures(expected_results)
1005 elif not pixel_tests_are_enabled:
1006 expected_results = self.remove_pixel_failures(expected_results)
1007 return self.result_was_expected(result, expected_results, self.is_rebaselining(test))
1009 def is_rebaselining(self, test):
1010 return REBASELINE in self._model.get_expectations(test)
1012 def _shorten_filename(self, filename):
1013 if filename.startswith(self._port.path_from_webkit_base()):
1014 return self._port.host.filesystem.relpath(filename, self._port.path_from_webkit_base())
1017 def _report_warnings(self):
1019 for expectation in self._expectations:
1020 for warning in expectation.warnings:
1021 warnings.append('%s:%s %s %s' % (self._shorten_filename(expectation.filename), expectation.line_numbers,
1022 warning, expectation.name if expectation.expectations else expectation.original_string))
1025 self._has_warnings = True
1026 if self._is_lint_mode:
1027 raise ParseError(warnings)
1028 _log.warning('--lint-test-files warnings:')
1029 for warning in warnings:
1030 _log.warning(warning)
1033 def _process_tests_without_expectations(self):
1034 if self._full_test_list:
1035 for test in self._full_test_list:
1036 if not self._model.has_test(test):
1037 self._model.add_expectation_line(TestExpectationLine.create_passing_expectation(test))
1039 def has_warnings(self):
1040 return self._has_warnings
1042 def remove_configurations(self, removals):
1043 expectations_to_remove = []
1044 modified_expectations = []
1046 for test, test_configuration in removals:
1047 for expectation in self._expectations:
1048 if expectation.name != test or not expectation.parsed_expectations:
1050 if test_configuration not in expectation.matching_configurations:
1053 expectation.matching_configurations.remove(test_configuration)
1054 if expectation.matching_configurations:
1055 modified_expectations.append(expectation)
1057 expectations_to_remove.append(expectation)
1059 for expectation in expectations_to_remove:
1060 index = self._expectations.index(expectation)
1061 self._expectations.remove(expectation)
1063 if index == len(self._expectations) or self._expectations[index].is_whitespace_or_comment():
1064 while index and self._expectations[index - 1].is_whitespace_or_comment():
1066 self._expectations.pop(index)
1068 return self.list_to_string(self._expectations, self._parser._test_configuration_converter, modified_expectations)
1070 def _add_expectations(self, expectation_list, model):
1071 for expectation_line in expectation_list:
1072 if not expectation_line.expectations:
1075 if self._model_all_expectations or self._test_config in expectation_line.matching_configurations:
1076 model.add_expectation_line(expectation_line, model_all_expectations=self._model_all_expectations)
1078 def add_extra_skipped_tests(self, tests_to_skip):
1079 if not tests_to_skip:
1081 for test in self._expectations:
1082 if test.name and test.name in tests_to_skip:
1083 test.warnings.append('%s:%s %s is also in a Skipped file.' % (test.filename, test.line_numbers, test.name))
1085 model = TestExpectationsModel(self._shorten_filename)
1086 for test_name in tests_to_skip:
1087 expectation_line = self._parser.expectation_for_skipped_test(test_name)
1088 model.add_expectation_line(expectation_line)
1089 self._model.merge_model(model)
1091 def add_expectations_from_bot(self):
1092 # FIXME: With mode 'very-flaky' and 'maybe-flaky', this will show the expectations entry in the flakiness
1093 # dashboard rows for each test to be whatever the bot thinks they should be. Is this a good thing?
1094 bot_expectations = self._port.bot_expectations()
1095 model = TestExpectationsModel(self._shorten_filename)
1096 for test_name in bot_expectations:
1097 expectation_line = self._parser.expectation_line_for_test(test_name, bot_expectations[test_name])
1099 # Unexpected results are merged into existing expectations.
1100 merge = self._port.get_option('ignore_flaky_tests') == 'unexpected'
1101 model.add_expectation_line(expectation_line)
1102 self._model.merge_model(model)
1104 def add_expectation_line(self, expectation_line):
1105 self._model.add_expectation_line(expectation_line)
1106 self._expectations += [expectation_line]
1108 def remove_expectation_line(self, test):
1109 if not self._model.has_test(test):
1111 self._expectations.remove(self._model.get_expectation_line(test))
1112 self._model.remove_expectation_line(test)
1115 def list_to_string(expectation_lines, test_configuration_converter=None, reconstitute_only_these=None):
1116 def serialize(expectation_line):
1117 # If reconstitute_only_these is an empty list, we want to return original_string.
1118 # So we need to compare reconstitute_only_these to None, not just check if it's falsey.
1119 if reconstitute_only_these is None or expectation_line in reconstitute_only_these:
1120 return expectation_line.to_string(test_configuration_converter)
1121 return expectation_line.original_string
1123 def nones_out(expectation_line):
1124 return expectation_line is not None
1126 return "\n".join(filter(nones_out, map(serialize, expectation_lines)))