Modified: trunk/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py (90919 => 90920)
--- trunk/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py 2011-07-13 15:43:43 UTC (rev 90919)
+++ trunk/Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py 2011-07-13 15:49:08 UTC (rev 90920)
@@ -88,6 +88,7 @@
return IMAGE in actual_results or IMAGE_PLUS_TEXT in actual_results
+# FIXME: This method is no longer used here in this module. Remove remaining callsite in manager.py and this method.
def strip_comments(line):
"""Strips comments from a line and return None if the line is empty
or else the contents of line with leading and trailing spaces removed
@@ -137,6 +138,131 @@
"expectations": obj.expectations}
+class TestExpectationSerializer:
+ """Provides means of serializing TestExpectationLine instances."""
+ @classmethod
+ def to_string(cls, expectation):
+ result = []
+ if expectation.malformed:
+ result.append(expectation.comment)
+ else:
+ if expectation.name is None:
+ if expectation.comment is not None:
+ result.append('//')
+ result.append(expectation.comment)
+ else:
+ result.append("%s : %s = %s" % (" ".join(expectation.modifiers).upper(), expectation.name, " ".join(expectation.expectations).upper()))
+ if expectation.comment is not None:
+ result.append(" //%s" % (expectation.comment))
+
+ return ''.join(result)
+
+
+class TestExpectationParser:
+ """Provides parsing facilities for lines in the test_expectation.txt file."""
+
+ @classmethod
+ def parse(cls, expectation_string):
+ """Parses a line from test_expectations.txt and returns a tuple, containing a TestExpectationLine instance
+ and a list syntax erros, if any.
+
+ The format of test expectation is:
+
+ [[<modifiers>] : <name> = <expectations>][ //<comment>]
+
+ Any errant whitespace is not preserved.
+
+ """
+ result = TestExpectationLine()
+ errors = []
+ (modifiers, name, expectations, comment) = cls._split_expectation_string(expectation_string, errors)
+ if len(errors) > 0:
+ result.malformed = True
+ result.comment = expectation_string
+ result.valid = False
+ else:
+ result.malformed = False
+ result.comment = comment
+ result.valid = True
+ result.name = name
+ # FIXME: Modifiers should be its own class eventually.
+ if modifiers is not None:
+ result.modifiers = cls._split_space_separated(modifiers)
+ # FIXME: Expectations should be its own class eventually.
+ if expectations is not None:
+ result.expectations = cls._split_space_separated(expectations)
+
+ return (result, errors)
+
+ @classmethod
+ def _split_expectation_string(cls, line, errors):
+ """Splits line into a string of modifiers, a test name, a string of expectations, and a comment,
+ returning them as a tuple. In case parsing error, returns empty tuple.
+ """
+ comment_index = line.find("//")
+ comment = ''
+ if comment_index == -1:
+ comment_index = len(line)
+ comment = None
+ else:
+ comment = line[comment_index + 2:]
+
+ line = re.sub(r"\s+", " ", line[:comment_index].strip())
+ if len(line) == 0:
+ return (None, None, None, comment)
+
+ parts = line.split(':')
+ if len(parts) != 2:
+ errors.append(("Missing a ':' in" if len(parts) < 2 else "Extraneous ':' in", "'" + line + "'"))
+ return (None, None, None, None)
+
+ test_and_expectation = parts[1].split('=')
+ if len(test_and_expectation) != 2:
+ errors.append(("Missing expectations in" if len(test_and_expectation) < 2 else "Extraneous '=' in", "'" + line + "'"))
+ return (None, None, None, None)
+
+ return (parts[0].strip(), test_and_expectation[0].strip(), test_and_expectation[1].strip(), comment)
+
+ @classmethod
+ def _split_space_separated(cls, space_separated_string):
+ """Splits a space-separated string into an array."""
+ # FIXME: Lower-casing is necessary to support legacy code. Need to eliminate.
+ return [part.strip().lower() for part in space_separated_string.strip().split(' ')]
+
+
+class TestExpectationLine:
+ """Represents a line in test expectations file."""
+
+ def __init__(self):
+ """Initializes a blank-line equivalent of an expectation."""
+ self.name = None
+ self.modifiers = []
+ self.expectations = []
+ self.comment = None
+ # FIXME: Should valid and malformed be a single state flag? Probably not, since "malformed" is also "not valid".
+ self.valid = False
+ self.malformed = False
+
+
+class TestExpectationsFile:
+ """Represents a test expectation file, which is a mutable collection of comments and test expectations."""
+
+ def __init__(self):
+ self._expectations = []
+
+ def __iter__(self):
+ return self._expectations.__iter__()
+
+ def append(self, expectations_string, validator):
+ """Add a TestExpectationLine for each item in expectations_string."""
+ line_number = 0
+ for line in expectations_string.split("\n"):
+ expectation, errors = TestExpectationParser.parse(line)
+ line_number += 1
+ expectation.valid = validator.validate(line_number, expectation, errors)
+ self._expectations.append(expectation)
+
+
class TestExpectations:
"""Test expectations consist of lines with specifications of what
to expect from layout test cases. The test cases can be directories
@@ -240,7 +366,6 @@
"""
self._port = port
self._fs = port._filesystem
- self._expectations = expectations
self._full_test_list = tests
self._test_config = test_config
self._is_lint_mode = is_lint_mode
@@ -274,8 +399,10 @@
self._timeline_to_tests = self._dict_of_sets(self.TIMELINES)
self._result_type_to_tests = self._dict_of_sets(self.RESULT_TYPES)
- self._read(self._get_iterable_expectations(self._expectations),
- overrides_allowed=False)
+ self._matcher = ModifierMatcher(self._test_config)
+ self._expectations = TestExpectationsFile()
+ self._overrides_allowed = False
+ self._expectations.append(expectations, self)
# List of tests that are in the overrides file (used for checking for
# duplicates inside the overrides file itself). Note that just because
@@ -285,8 +412,9 @@
self._overridding_tests = set()
if overrides:
- self._read(self._get_iterable_expectations(self._overrides),
- overrides_allowed=True)
+ self._overrides_allowed = True
+ self._expectations.append(self._overrides, self)
+ self._overrides_allowed = False
self._handle_any_read_errors()
self._process_tests_without_expectations()
@@ -362,17 +490,6 @@
d[c] = set()
return d
- def _get_iterable_expectations(self, expectations_str):
- """Returns an object that can be iterated over. Allows for not caring
- about whether we're iterating over a file or a new-line separated
- string."""
- iterable = [x + "\n" for x in expectations_str.split("\n")]
- # Strip final entry if it's empty to avoid added in an extra
- # newline.
- if iterable[-1] == "\n":
- return iterable[:-1]
- return iterable
-
def get_test_set(self, modifier, expectation=None, include_skips=True):
if expectation is None:
tests = self._modifier_to_tests[modifier]
@@ -414,60 +531,29 @@
def remove_rebaselined_tests(self, tests):
"""Returns a copy of the expectations with the tests removed."""
lines = []
- for (lineno, line) in enumerate(self._get_iterable_expectations(self._expectations)):
- test, options, _ = self.parse_expectations_line(line, lineno)
- if not (test and test in tests and 'rebaseline' in options):
- lines.append(line)
- return ''.join(lines)
+ for expectation in self._expectations:
+ if not (expectation.valid and expectation.name in tests and "rebaseline" in expectation.modifiers):
+ lines.append(TestExpectationSerializer.to_string(expectation))
+ return "\n".join(lines)
- def parse_expectations_line(self, line, lineno):
- """Parses a line from test_expectations.txt and returns a tuple
- with the test path, options as a list, expectations as a list."""
- line = strip_comments(line)
- if not line:
- return (None, None, None)
-
- options = []
- if line.find(":") is -1:
- self._add_error(lineno, "Missing a ':'", line)
- return (None, None, None)
-
- parts = line.split(':')
-
- # FIXME: verify that there is exactly one colon in the line.
-
- options = self._get_options_list(parts[0])
- test_and_expectation = parts[1].split('=')
- test = test_and_expectation[0].strip()
- if (len(test_and_expectation) is not 2):
- self._add_error(lineno, "Missing expectations.",
- test_and_expectation)
- expectations = None
- else:
- expectations = self._get_options_list(test_and_expectation[1])
-
- return (test, options, expectations)
-
def _add_to_all_expectations(self, test, options, expectations):
if not test in self._all_expectations:
self._all_expectations[test] = []
self._all_expectations[test].append(
ModifiersAndExpectations(options, expectations))
- def _read(self, expectations, overrides_allowed):
- """For each test in an expectations iterable, generate the
- expectations for it."""
- lineno = 0
- matcher = ModifierMatcher(self._test_config)
- for line in expectations:
- lineno += 1
- self._process_line(line, lineno, matcher, overrides_allowed)
+ def validate(self, lineno, expectation, syntax_errors):
+ test_list_path = expectation.name
+ options = expectation.modifiers
+ expectations = expectation.expectations
+ matcher = self._matcher
+ overrides_allowed = self._overrides_allowed
- def _process_line(self, line, lineno, matcher, overrides_allowed):
- test_list_path, options, expectations = \
- self.parse_expectations_line(line, lineno)
+ for (message, source) in syntax_errors:
+ self._add_error(lineno, message, source)
+
if not expectations:
- return
+ return False
self._add_to_all_expectations(test_list_path,
" ".join(options).upper(),
@@ -476,7 +562,7 @@
num_matches = self._check_options(matcher, options, lineno,
test_list_path)
if num_matches == ModifierMatcher.NO_MATCH:
- return
+ return False
expectations = self._parse_expectations(expectations, lineno,
test_list_path)
@@ -485,7 +571,7 @@
lineno, test_list_path)
if self._check_path_does_not_exist(lineno, test_list_path):
- return
+ return False
if not self._full_test_list:
tests = [test_list_path]
@@ -495,6 +581,8 @@
modifiers = [o for o in options if o in self.MODIFIERS]
self._add_tests(tests, expectations, test_list_path, lineno, modifiers, num_matches, options, overrides_allowed)
+ return True
+
def _get_options_list(self, listString):
return [part.strip().lower() for part in listString.strip().split(' ')]
Modified: trunk/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py (90919 => 90920)
--- trunk/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py 2011-07-13 15:43:43 UTC (rev 90919)
+++ trunk/Tools/Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py 2011-07-13 15:49:08 UTC (rev 90920)
@@ -195,7 +195,7 @@
except ParseError, e:
self.assertTrue(e.fatal)
exp_errors = [u"Line:1 Unrecognized option 'foo' failures/expected/text.html",
- u"Line:2 Missing expectations. [' failures/expected/image.html']"]
+ u"Line:2 Missing expectations in 'SKIP : failures/expected/image.html'"]
self.assertEqual(str(e), '\n'.join(map(str, exp_errors)))
self.assertEqual(e.errors, exp_errors)
@@ -461,5 +461,177 @@
self.match(['none'], num_errors=1)
+class TestExpectationParserTests(unittest.TestCase):
+ def test_blank(self):
+ (expectation, errors) = TestExpectationParser.parse('')
+ self.assertEqual(expectation.malformed, False)
+ self.assertEqual(expectation.valid, True)
+ self.assertEqual(expectation.comment, None)
+ self.assertEqual(len(errors), 0)
+
+ def test_missing_colon(self):
+ (expectation, errors) = TestExpectationParser.parse('Qux.')
+ self.assertEqual(expectation.malformed, True)
+ self.assertEqual(expectation.valid, False)
+ self.assertEqual(expectation.comment, 'Qux.')
+ self.assertEqual(str(errors), '[("Missing a \':\' in", "\'Qux.\'")]')
+
+ def test_extra_colon(self):
+ (expectation, errors) = TestExpectationParser.parse('FOO : : bar')
+ self.assertEqual(expectation.malformed, True)
+ self.assertEqual(expectation.valid, False)
+ self.assertEqual(expectation.comment, 'FOO : : bar')
+ self.assertEqual(str(errors), '[("Extraneous \':\' in", "\'FOO : : bar\'")]')
+
+ def test_empty_comment(self):
+ (expectation, errors) = TestExpectationParser.parse('//')
+ self.assertEqual(expectation.malformed, False)
+ self.assertEqual(expectation.valid, True)
+ self.assertEqual(expectation.comment, '')
+ self.assertEqual(len(errors), 0)
+
+ def test_comment(self):
+ (expectation, errors) = TestExpectationParser.parse('//Qux.')
+ self.assertEqual(expectation.malformed, False)
+ self.assertEqual(expectation.valid, True)
+ self.assertEqual(expectation.comment, 'Qux.')
+ self.assertEqual(len(errors), 0)
+
+ def test_missing_equal(self):
+ (expectation, errors) = TestExpectationParser.parse('FOO : bar')
+ self.assertEqual(expectation.malformed, True)
+ self.assertEqual(expectation.valid, False)
+ self.assertEqual(expectation.comment, 'FOO : bar')
+ self.assertEqual(str(errors), '[(\'Missing expectations in\', "\'FOO : bar\'")]')
+
+ def test_extra_equal(self):
+ (expectation, errors) = TestExpectationParser.parse('FOO : bar = BAZ = Qux.')
+ self.assertEqual(expectation.malformed, True)
+ self.assertEqual(expectation.valid, False)
+ self.assertEqual(expectation.comment, 'FOO : bar = BAZ = Qux.')
+ self.assertEqual(str(errors), '[("Extraneous \'=\' in", "\'FOO : bar = BAZ = Qux.\'")]')
+
+ def test_valid(self):
+ (expectation, errors) = TestExpectationParser.parse('FOO : bar = BAZ')
+ self.assertEqual(expectation.malformed, False)
+ self.assertEqual(expectation.valid, True)
+ self.assertEqual(expectation.comment, None)
+ self.assertEqual(len(errors), 0)
+
+ def test_valid_with_comment(self):
+ (expectation, errors) = TestExpectationParser.parse('FOO : bar = BAZ //Qux.')
+ self.assertEqual(expectation.malformed, False)
+ self.assertEqual(expectation.valid, True)
+ self.assertEqual(expectation.comment, 'Qux.')
+ self.assertEqual(str(expectation.modifiers), '[\'foo\']')
+ self.assertEqual(str(expectation.expectations), '[\'baz\']')
+ self.assertEqual(len(errors), 0)
+
+ def test_valid_with_multiple_modifiers(self):
+ (expectation, errors) = TestExpectationParser.parse('FOO1 FOO2 : bar = BAZ //Qux.')
+ self.assertEqual(expectation.malformed, False)
+ self.assertEqual(expectation.valid, True)
+ self.assertEqual(expectation.comment, 'Qux.')
+ self.assertEqual(str(expectation.modifiers), '[\'foo1\', \'foo2\']')
+ self.assertEqual(str(expectation.expectations), '[\'baz\']')
+ self.assertEqual(len(errors), 0)
+
+
+class TestExpectationSerializerTests(unittest.TestCase):
+ def assert_round_trip(self, in_string, expected_string=None):
+ (expectation, _) = TestExpectationParser.parse(in_string)
+ if expected_string is None:
+ expected_string = in_string
+ self.assertEqual(expected_string, TestExpectationSerializer.to_string(expectation))
+
+ def assert_to_string(self, expectation, expected_string):
+ self.assertEqual(TestExpectationSerializer.to_string(expectation), expected_string)
+
+ def test_string_serializer(self):
+ expectation = TestExpectationLine()
+ self.assert_to_string(expectation, '')
+ expectation.comment = 'Qux.'
+ self.assert_to_string(expectation, '//Qux.')
+ expectation.name = 'bar'
+ self.assert_to_string(expectation, ' : bar = //Qux.')
+ expectation.modifiers = ['foo']
+ self.assert_to_string(expectation, 'FOO : bar = //Qux.')
+ expectation.expectations = ['bAz']
+ self.assert_to_string(expectation, 'FOO : bar = BAZ //Qux.')
+ expectation.expectations = ['bAz1', 'baZ2']
+ self.assert_to_string(expectation, 'FOO : bar = BAZ1 BAZ2 //Qux.')
+ expectation.modifiers = ['foo1', 'foO2']
+ self.assert_to_string(expectation, 'FOO1 FOO2 : bar = BAZ1 BAZ2 //Qux.')
+ expectation.malformed = True
+ self.assert_to_string(expectation, 'Qux.')
+
+ def test_string_roundtrip(self):
+ self.assert_round_trip('')
+ self.assert_round_trip('FOO')
+ self.assert_round_trip(':')
+ self.assert_round_trip('FOO :')
+ self.assert_round_trip('FOO : bar')
+ self.assert_round_trip(' FOO :')
+ self.assert_round_trip(' FOO : bar')
+ self.assert_round_trip('FOO : bar = BAZ')
+ self.assert_round_trip('FOO : bar = BAZ //Qux.')
+ self.assert_round_trip('FOO : bar = BAZ // Qux.')
+ self.assert_round_trip('FOO : bar = BAZ // Qux. ')
+ self.assert_round_trip('FOO : bar = BAZ // Qux. ')
+ self.assert_round_trip('FOO : : bar = BAZ')
+ self.assert_round_trip('FOO : : bar = BAZ')
+ self.assert_round_trip('FOO : : bar ==== BAZ')
+ self.assert_round_trip('=')
+ self.assert_round_trip('//')
+ self.assert_round_trip('// ')
+ self.assert_round_trip('// Foo')
+ self.assert_round_trip('// Foo')
+ self.assert_round_trip('// Foo :')
+ self.assert_round_trip('// Foo : =')
+
+ def test_string_whitespace_stripping(self):
+ self.assert_round_trip('\n', '')
+ self.assert_round_trip(' FOO : bar = BAZ', 'FOO : bar = BAZ')
+ self.assert_round_trip('FOO : bar = BAZ', 'FOO : bar = BAZ')
+ self.assert_round_trip('FOO : bar = BAZ // Qux.', 'FOO : bar = BAZ // Qux.')
+ self.assert_round_trip('FOO : bar = BAZ // Qux.', 'FOO : bar = BAZ // Qux.')
+ self.assert_round_trip('FOO : bar = BAZ // Qux.', 'FOO : bar = BAZ // Qux.')
+ self.assert_round_trip('FOO : bar = BAZ // Qux.', 'FOO : bar = BAZ // Qux.')
+
+
+class TestValidator:
+ def __init__(self):
+ self.line_numbers = []
+
+ def validate(self, line_number, expectation, errors):
+ self.line_numbers.append(line_number)
+ return line_number % 2 == 0
+
+
+class TestExpectationsFileTests(unittest.TestCase):
+ def test_line_number_increment(self):
+ validator = TestValidator()
+ expectations = TestExpectationsFile()
+ expectations.append('// Bar\nFOO : bar = BAZ\n\n', validator)
+ self.assertEqual(str(validator.line_numbers), '[1, 2, 3, 4]')
+
+ def test_iterator(self):
+ validator = TestValidator()
+ expectations = TestExpectationsFile()
+ expectations.append('\n\n\n\n', validator)
+ line_number = 0
+ for expectation in expectations:
+ line_number += 1
+ self.assertEqual(line_number, 5)
+
+ def test_validator_feedback(self):
+ validator = TestValidator()
+ expectations = TestExpectationsFile()
+ expectations.append('FOO : bar1 = BAZ\nFOO : bar2 = BAZ\nFOO : bar3 = BAZ', validator)
+ line_number = 0
+ for expectation in expectations:
+ line_number += 1
+ self.assertEqual(line_number % 2 == 0, expectation.valid)
+
if __name__ == '__main__':
unittest.main()