Prechádzať zdrojové kódy

Added unit tests for parser and fixed a lot of bugs on the way

Taddeus Kroes 13 rokov pred
rodič
commit
cc85a34e3a
2 zmenil súbory, kde vykonal 268 pridanie a 26 odobranie
  1. 54 25
      parse.py
  2. 214 1
      tests/test_parse.py

+ 54 - 25
parse.py

@@ -3,7 +3,7 @@ def split_selectors(raw_selector):
     Split a selector with commas and arbitrary whitespaces into a list of
     selector swith single-space whitespaces.
     """
-    return [' '.join(s.split()) for s in raw_selector.split(',')]
+    return filter(None, (' '.join(s.split()) for s in raw_selector.split(',')))
 
 
 def parse_groups(css):
@@ -16,20 +16,12 @@ def parse_groups(css):
     prev_char = None
     lineno = 1
     properties = []
-    current_group = root_group = []
-    groups = [(None, root_group)]
+    current_group = (None, [])
+    groups = []
     selectors = None
     comment = False
-
-    def parse_property():
-        assert selectors is not None
-
-        if stack.strip():
-            parts = stack.split(':', 1)
-            assert len(parts) == 2
-            name, value = map(str.strip, parts)
-            assert '\n' not in name
-            properties.append((name, value))
+    nesting_level = 0
+    property_name = None
 
     try:
         for c in css:
@@ -43,32 +35,59 @@ def parse_groups(css):
                 if char == '/' and prev_char == '*':
                     comment = False
             elif char == '{':
+                nesting_level += 1
+
                 # Block start
                 if selectors is not None:
-                    # Block is nested, save group selector
-                    current_group = []
-                    groups.append((selectors, current_group))
+                    # Block is nested, push current root group and continue
+                    # with this group
+                    if len(current_group[1]):
+                        groups.append(current_group)
+
+                    current_group = (selectors, [])
 
                 selectors = split_selectors(stack)
-                #print stack.strip(), '->', selectors
                 stack = ''
                 assert len(selectors)
             elif char == '}':
-                # Last property may not have been closed with a semicolon
-                parse_property()
+                assert nesting_level > 0
+                nesting_level -= 1
 
                 if selectors is None:
                     # Closing group
-                    current_group = root_group
+                    groups.append(current_group)
+                    current_group = (None, [])
                 else:
                     # Closing block
-                    current_group.append((selectors, properties))
+                    # Last property may not have been closed with a semicolon
+                    property_value = stack.strip()
+
+                    if len(property_value):
+                        assert property_name is not None
+                        properties.append((property_name, property_value))
+                        property_name = None
+                        stack = ''
+
+                    current_group[1].append((selectors, properties))
                     selectors = None
                     properties = []
-            elif char == ';':
-                # Property definition
-                parse_property()
+            elif char == ':' and nesting_level > 0:
+                assert selectors is not None
+                # Property name
+                property_name = stack.strip()
+                assert '\n' not in property_name
                 stack = ''
+            elif char == ';':
+                # Property value
+                property_value = stack.strip()
+
+                if len(property_value):
+                    assert property_name is not None
+                    properties.append((property_name, property_value))
+                    property_name = None
+                    stack = ''
+                else:
+                    assert property_name is None
             elif char == '*' and prev_char == '/':
                 # Comment start
                 comment = True
@@ -77,7 +96,17 @@ def parse_groups(css):
                 stack += char
 
             prev_char = char
+
+        if len(current_group[1]):
+            groups.append(current_group)
+
+        if stack.split() or nesting_level > 0:
+            char = '<EOF>'
+            raise AssertionError()
     except AssertionError:
-        raise Exception('unexpected \'%c\' on line %d' % (char, lineno))
+        if len(char) < 2:
+            char = "'" + char + "'"
+
+        raise Exception('unexpected %s on line %d' % (char, lineno))
 
     return groups

+ 214 - 1
tests/test_parse.py

@@ -3,10 +3,223 @@ from unittest import TestCase
 from parse import split_selectors, parse_groups
 
 
-class TestParse(TestCase):
+class TestHelpers(TestCase):
     def test_split_selectors(self):
+        self.assertEqual(split_selectors(''), [])
         self.assertEqual(split_selectors('a, b'), ['a', 'b'])
         self.assertEqual(split_selectors('a ,b'), ['a', 'b'])
         self.assertEqual(split_selectors('\na ,b '), ['a', 'b'])
         self.assertEqual(split_selectors('a, b\nc'), ['a', 'b c'])
         self.assertEqual(split_selectors('a,\nb c ,d\n'), ['a', 'b c', 'd'])
+
+
+class TestParseGroups(TestCase):
+    def test_empty_stylesheet(self):
+        self.assertParse('', [])
+        self.assertParse(' \n ', [])
+
+    def test_empty_block(self):
+        self.assertParse('div {}', [(None, [(['div'], [])])])
+
+    def test_single_property(self):
+        self.assertParse('div {color:black;}',
+                         [(None, [(['div'], [('color', 'black')])])])
+        self.assertParse('div {color:black}',
+                         [(None, [(['div'], [('color', 'black')])])])
+
+        self.assertParse('''
+        div {
+            color: black;
+        }
+        ''', [(None, [(['div'], [('color', 'black')])])])
+
+        self.assertParse('''
+        div {
+            color: black
+        }
+        ''', [(None, [(['div'], [('color', 'black')])])])
+
+    def test_multiple_properties(self):
+        self.assertParse('''
+        div {
+            color: black;
+            border: none;
+        }
+        ''', [(None, [(['div'], [('color', 'black'), ('border', 'none')])])])
+
+        self.assertParse('''
+        div {
+            color: black;
+            border: none
+        }
+        ''', [(None, [(['div'], [('color', 'black'), ('border', 'none')])])])
+
+    def test_multiple_selectors(self):
+        self.assertParse('div,p {color: black}',
+                         [(None, [(['div', 'p'], [('color', 'black')])])])
+        self.assertParse('div, p {color: black}',
+                         [(None, [(['div', 'p'], [('color', 'black')])])])
+        self.assertParse('p,\n\tdiv {color: black}',
+                         [(None, [(['p', 'div'], [('color', 'black')])])])
+
+    def test_single_group(self):
+        self.assertParse('''
+        @media (min-width: 970px) {
+            div {color: black}
+        }
+        ''', [(['@media (min-width: 970px)'],
+               [(['div'], [('color', 'black')])])])
+
+    def test_single_group_multiple_blocks(self):
+        self.assertParse('''
+        @media (min-width: 970px) {
+            div {color: black}
+            p {margin: 0}
+        }
+        ''', [(['@media (min-width: 970px)'],
+               [(['div'], [('color', 'black')]), (['p'], [('margin', '0')])])])
+
+    def test_multiple_groups(self):
+        self.assertParse('''
+        @media (min-width: 970px) {
+            div {color: black}
+        }
+        @media (max-width: 969px) {
+            div {color: red}
+        }
+        ''', [
+            (['@media (min-width: 970px)'], [(['div'], [('color', 'black')])]),
+            (['@media (max-width: 969px)'], [(['div'], [('color', 'red')])])
+        ])
+
+    def test_group_with_root(self):
+        self.assertParse('''
+        div {
+            color: black
+        }
+        @media (max-width: 969px) {
+            div {color: red}
+        }
+        ''', [
+            (None, [(['div'], [('color', 'black')])]),
+            (['@media (max-width: 969px)'], [(['div'], [('color', 'red')])])
+        ])
+
+        self.assertParse('''
+        @media (max-width: 969px) {
+            div {color: red}
+        }
+        div {
+            color: black
+        }
+        ''', [
+            (['@media (max-width: 969px)'], [(['div'], [('color', 'red')])]),
+            (None, [(['div'], [('color', 'black')])])
+        ])
+
+    def test_group_with_multiple_roots(self):
+        self.assertParse('''
+        div {
+            color: black
+        }
+        @media (max-width: 969px) {
+            div {color: red}
+        }
+        p {
+            margin: 0
+        }
+        ''', [
+            (None, [(['div'], [('color', 'black')])]),
+            (['@media (max-width: 969px)'], [(['div'], [('color', 'red')])]),
+            (None, [(['p'], [('margin', '0')])])
+        ])
+
+    def test_multiple_groups_multiple_roots(self):
+        self.assertParse('''
+        @media (min-width: 970px) {
+            div {color: black}
+        }
+        div {
+            color: black
+        }
+        @media (max-width: 969px) {
+            div {color: red}
+        }
+        p {
+            margin: 0
+        }
+        ''', [
+            (['@media (min-width: 970px)'], [(['div'], [('color', 'black')])]),
+            (None, [(['div'], [('color', 'black')])]),
+            (['@media (max-width: 969px)'], [(['div'], [('color', 'red')])]),
+            (None, [(['p'], [('margin', '0')])])
+        ])
+
+        self.assertParse('''
+        p {
+            margin: 0
+        }
+        @media (min-width: 970px) {
+            div {color: black}
+        }
+        div {
+            color: black
+        }
+        @media (max-width: 969px) {
+            div {color: red}
+        }
+        ''', [
+            (None, [(['p'], [('margin', '0')])]),
+            (['@media (min-width: 970px)'], [(['div'], [('color', 'black')])]),
+            (None, [(['div'], [('color', 'black')])]),
+            (['@media (max-width: 969px)'], [(['div'], [('color', 'red')])]),
+        ])
+
+    def test_comment(self):
+        self.assertParse('''
+        /* this is a comment */
+        div {color: black}
+        ''', [(None, [(['div'], [('color', 'black')])])])
+
+        self.assertParse('''
+        /*
+         * this is a multiline comment
+         */
+
+        div {color: black}
+        ''', [(None, [(['div'], [('color', 'black')])])])
+
+        self.assertParse('''
+        div {color: /*inline comment*/ black}
+        ''', [(None, [(['div'], [('color', 'black')])])])
+
+    def test_error_brackets(self):
+        self.assertRaisesRegexp(Exception, 'unexpected \'{\' on line 1',
+                parse_groups, '{')
+        self.assertRaisesRegexp(Exception, 'unexpected \'{\' on line 2',
+                parse_groups, 'div {\n{}')
+
+        self.assertRaisesRegexp(Exception, 'unexpected \'}\' on line 1',
+                parse_groups, '}')
+        self.assertRaisesRegexp(Exception, 'unexpected \'}\' on line 2',
+                parse_groups, 'div\n}')
+
+    def test_error_EOF(self):
+        self.assertRaisesRegexp(Exception, 'unexpected <EOF> on line 3',
+                parse_groups, '\ndiv\n')
+        self.assertRaisesRegexp(Exception, 'unexpected <EOF> on line 2',
+                parse_groups, '\ndiv {')
+
+    def test_error_property_name(self):
+        self.assertRaisesRegexp(Exception, 'unexpected \':\' on line 2',
+                parse_groups, 'div{margin\nleft: 0}')
+
+    def test_error_property_value(self):
+        self.assertRaisesRegexp(Exception, 'unexpected \';\' on line 1',
+                parse_groups, 'div{margin:;}')
+        self.assertRaisesRegexp(Exception, 'unexpected \';\' on line 1',
+                parse_groups, 'div{foo;}')
+        self.assertParse('div {;}', [(None, [(['div'], [])])])
+
+    def assertParse(self, css, result):
+        self.assertEqual(parse_groups(css), result)