From 3514d7348cb294416f401b8addba54a832d3ffc8 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 22 Jan 2017 23:46:14 +0100 Subject: [PATCH] More tests for vobject (#541) * More tests for vobject * wip * wip * stylefix --- tests/unit/utils/test_vobject.py | 128 ++++++++++++++++++++++++++++++- vdirsyncer/utils/vobject.py | 30 ++++++-- 2 files changed, 152 insertions(+), 6 deletions(-) diff --git a/tests/unit/utils/test_vobject.py b/tests/unit/utils/test_vobject.py index 93c56b4..236cb55 100644 --- a/tests/unit/utils/test_vobject.py +++ b/tests/unit/utils/test_vobject.py @@ -3,7 +3,8 @@ from textwrap import dedent import hypothesis.strategies as st -from hypothesis import given +from hypothesis import assume, given +from hypothesis.stateful import Bundle, RuleBasedStateMachine, rule import pytest @@ -214,3 +215,128 @@ def test_replace_uid(template, uid): assert item.raw.count('\nUID:{}'.format(uid)) == 1 else: assert '\nUID:' not in item.raw + + +def test_broken_item(): + with pytest.raises(ValueError) as excinfo: + vobject._Component.parse('END:FOO') + + assert 'Parsing error at line 1' in str(excinfo.value) + + item = vobject.Item('END:FOO') + assert item.parsed is None + + +def test_multiple_items(): + with pytest.raises(ValueError) as excinfo: + vobject._Component.parse([ + 'BEGIN:FOO', + 'END:FOO', + 'BEGIN:FOO', + 'END:FOO', + ]) + + assert 'Found 2 components, expected one' in str(excinfo.value) + + c1, c2 = vobject._Component.parse([ + 'BEGIN:FOO', + 'END:FOO', + 'BEGIN:FOO', + 'END:FOO', + ], multiple=True) + assert c1.name == c2.name == 'FOO' + + +def test_input_types(): + lines = ['BEGIN:FOO', 'FOO:BAR', 'END:FOO'] + + for x in (lines, '\r\n'.join(lines), '\r\n'.join(lines).encode('ascii')): + c = vobject._Component.parse(x) + assert c.name == 'FOO' + assert c.props == ['FOO:BAR'] + assert not c.subcomponents + + +value_strategy = st.text( + st.characters(blacklist_categories=( + 'Zs', 'Zl', 'Zp', + 'Cc', 'Cs' + ), blacklist_characters=':='), + min_size=1 +).filter(lambda x: x.strip() == x) + + +class VobjectMachine(RuleBasedStateMachine): + Unparsed = Bundle('unparsed') + Parsed = Bundle('parsed') + + @rule(target=Unparsed, + joined=st.booleans(), + encoded=st.booleans()) + def get_unparsed_lines(self, joined, encoded): + rv = ['BEGIN:FOO', 'FOO:YES', 'END:FOO'] + if joined: + rv = '\r\n'.join(rv) + if encoded: + rv = rv.encode('utf-8') + elif encoded: + assume(False) + return rv + + @rule(unparsed=Unparsed, target=Parsed) + def parse(self, unparsed): + return vobject._Component.parse(unparsed) + + @rule(parsed=Parsed, target=Unparsed) + def serialize(self, parsed): + return list(parsed.dump_lines()) + + @rule(c=Parsed, + key=uid_strategy, + value=uid_strategy) + def add_prop(self, c, key, value): + c[key] = value + assert c[key] == value + assert key in c + assert c.get(key) == value + dump = '\r\n'.join(c.dump_lines()) + assert key in dump and value in dump + + @rule(c=Parsed, + key=uid_strategy, + value=uid_strategy, + params=st.lists(st.tuples(value_strategy, value_strategy))) + def add_prop_raw(self, c, key, value, params): + params_str = ','.join(k + '=' + v for k, v in params) + c.props.append('{};{}:{}'.format(key, params_str, value)) + assert c[key] == value + assert key in c + assert c.get(key) == value + + @rule(c=Parsed, sub_c=Parsed) + def add_component(self, c, sub_c): + assume(sub_c is not c and sub_c not in c) + c.subcomponents.append(sub_c) + assert '\r\n'.join(sub_c.dump_lines()) in '\r\n'.join(c.dump_lines()) + + @rule(c=Parsed) + def sanity_check(self, c): + c1 = vobject._Component.parse(c.dump_lines()) + assert c1 == c + + +TestVobjectMachine = VobjectMachine.TestCase + + +def test_component_contains(): + item = vobject._Component.parse([ + 'BEGIN:FOO', + 'FOO:YES', + 'END:FOO' + ]) + + assert 'FOO' in item + assert 'BAZ' not in item + + with pytest.raises(ValueError): + 42 in item diff --git a/vdirsyncer/utils/vobject.py b/vdirsyncer/utils/vobject.py index a3fa30b..d0b5007 100644 --- a/vdirsyncer/utils/vobject.py +++ b/vdirsyncer/utils/vobject.py @@ -272,8 +272,7 @@ class _Component(object): if line.strip(): stack[-1].props.append(line) except IndexError: - raise ValueError('Parsing error at line {}. Check the debug log ' - 'for more information.'.format(_i + 1)) + raise ValueError('Parsing error at line {}'.format(_i + 1)) if multiple: return rv @@ -319,12 +318,25 @@ class _Component(object): line = u'{}:{}'.format(key, val) self.props.append(line) + def __contains__(self, obj): + if isinstance(obj, type(self)): + return obj not in self.subcomponents and \ + not any(obj in x for x in self.subcomponents) + elif isinstance(obj, str): + return self.get(obj, None) is not None + else: + raise ValueError(obj) + def __getitem__(self, key): - prefix = (u'{}:'.format(key), u'{};'.format(key)) + prefix_without_params = '{}:'.format(key) + prefix_with_params = '{};'.format(key) iterlines = iter(self.props) for line in iterlines: - if line.startswith(prefix): - rv = line.split(u':', 1)[-1] + if line.startswith(prefix_without_params): + rv = line[len(prefix_without_params):] + break + elif line.startswith(prefix_with_params): + rv = line[len(prefix_with_params):].split(':', 1)[-1] break else: raise KeyError() @@ -342,3 +354,11 @@ class _Component(object): return self[key] except KeyError: return default + + def __eq__(self, other): + return ( + isinstance(other, type(self)) and + self.name == other.name and + self.props == other.props and + self.subcomponents == other.subcomponents + )