Title: [90920] trunk/Tools
Revision
90920
Author
[email protected]
Date
2011-07-13 08:49:08 -0700 (Wed, 13 Jul 2011)

Log Message

Extract model-like TestExpectationLine and TestExpectationFile from TestExpectations.
https://bugs.webkit.org/show_bug.cgi?id=64386

This is the first step in converting TestExpectations to a real model.
* TestExpectationsLine represents a line in the test_expectations.txt file, and
* TestExpectationsFile represents the file, which is a collection of lines.

Reviewed by Adam Barth.

* Scripts/webkitpy/layout_tests/models/test_expectations.py:
* Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py:
* Scripts/webkitpy/style/checkers/test_expectations_unittest.py:

Modified Paths

Diff

Modified: trunk/Tools/ChangeLog (90919 => 90920)


--- trunk/Tools/ChangeLog	2011-07-13 15:43:43 UTC (rev 90919)
+++ trunk/Tools/ChangeLog	2011-07-13 15:49:08 UTC (rev 90920)
@@ -1,3 +1,18 @@
+2011-07-12  Dimitri Glazkov  <[email protected]>
+
+        Extract model-like TestExpectationLine and TestExpectationFile from TestExpectations.
+        https://bugs.webkit.org/show_bug.cgi?id=64386
+
+        This is the first step in converting TestExpectations to a real model.
+        * TestExpectationsLine represents a line in the test_expectations.txt file, and
+        * TestExpectationsFile represents the file, which is a collection of lines.
+
+        Reviewed by Adam Barth.
+
+        * Scripts/webkitpy/layout_tests/models/test_expectations.py:
+        * Scripts/webkitpy/layout_tests/models/test_expectations_unittest.py:
+        * Scripts/webkitpy/style/checkers/test_expectations_unittest.py:
+
 2011-07-13  Xan Lopez  <[email protected]>
 
         [GTK] Do not grab focus too early in DRT.

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()

Modified: trunk/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py (90919 => 90920)


--- trunk/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py	2011-07-13 15:43:43 UTC (rev 90919)
+++ trunk/Tools/Scripts/webkitpy/style/checkers/test_expectations_unittest.py	2011-07-13 15:49:08 UTC (rev 90920)
@@ -122,7 +122,7 @@
     def test_expectation_errors(self):
         self.assert_lines_lint(
             ["missing expectations"],
-            "Missing a ':' missing expectations  [test/expectations] [5]")
+            "Missing a ':' in 'missing expectations'  [test/expectations] [5]")
         self.assert_lines_lint(
             ["SLOW : passes/text.html = TIMEOUT"],
             "A test can not be both SLOW and TIMEOUT. "
_______________________________________________
webkit-changes mailing list
[email protected]
http://lists.webkit.org/mailman/listinfo.cgi/webkit-changes

Reply via email to