Explorar el Código

vobject: add upstream tests

Unrud hace 7 años
padre
commit
dc7ce824da

+ 101 - 0
radicale_vobject/change_tz.py

@@ -0,0 +1,101 @@
+"""Translate an ics file's events to a different timezone."""
+
+from optparse import OptionParser
+from radicale_vobject import icalendar, base
+
+try:
+    import PyICU
+except:
+    PyICU = None
+
+from datetime import datetime
+
+
+def change_tz(cal, new_timezone, default, utc_only=False, utc_tz=icalendar.utc):
+    """
+    Change the timezone of the specified component.
+
+    Args:
+        cal (Component): the component to change
+        new_timezone (tzinfo): the timezone to change to
+        default (tzinfo): a timezone to assume if the dtstart or dtend in cal
+            doesn't have an existing timezone
+        utc_only (bool): only convert dates that are in utc
+        utc_tz (tzinfo): the tzinfo to compare to for UTC when processing
+            utc_only=True
+    """
+
+    for vevent in getattr(cal, 'vevent_list', []):
+        start = getattr(vevent, 'dtstart', None)
+        end = getattr(vevent, 'dtend', None)
+        for node in (start, end):
+            if node:
+                dt = node.value
+                if (isinstance(dt, datetime) and
+                        (not utc_only or dt.tzinfo == utc_tz)):
+                    if dt.tzinfo is None:
+                        dt = dt.replace(tzinfo=default)
+                    node.value = dt.astimezone(new_timezone)
+
+
+def main():
+    options, args = get_options()
+    if PyICU is None:
+        print("Failure. change_tz requires PyICU, exiting")
+    elif options.list:
+        for tz_string in PyICU.TimeZone.createEnumeration():
+            print(tz_string)
+    elif args:
+        utc_only = options.utc
+        if utc_only:
+            which = "only UTC"
+        else:
+            which = "all"
+        print("Converting {0!s} events".format(which))
+        ics_file = args[0]
+        if len(args) > 1:
+            timezone = PyICU.ICUtzinfo.getInstance(args[1])
+        else:
+            timezone = PyICU.ICUtzinfo.default
+        print("... Reading {0!s}".format(ics_file))
+        cal = base.readOne(open(ics_file))
+        change_tz(cal, timezone, PyICU.ICUtzinfo.default, utc_only)
+
+        out_name = ics_file + '.converted'
+        print("... Writing {0!s}".format(out_name))
+
+        with open(out_name, 'wb') as out:
+            cal.serialize(out)
+
+        print("Done")
+
+
+version = "0.1"
+
+
+def get_options():
+    # Configuration options
+
+    usage = """usage: %prog [options] ics_file [timezone]"""
+    parser = OptionParser(usage=usage, version=version)
+    parser.set_description("change_tz will convert the timezones in an ics file. ")
+
+    parser.add_option("-u", "--only-utc", dest="utc", action="store_true",
+                      default=False, help="Only change UTC events.")
+    parser.add_option("-l", "--list", dest="list", action="store_true",
+                      default=False, help="List available timezones")
+
+    (cmdline_options, args) = parser.parse_args()
+    if not args and not cmdline_options.list:
+        print("error: too few arguments given")
+        print
+        print(parser.format_help())
+        return False, False
+
+    return cmdline_options, args
+
+if __name__ == "__main__":
+    try:
+        main()
+    except KeyboardInterrupt:
+        print("Aborted")

+ 951 - 0
radicale_vobject/tests/test_base.py

@@ -0,0 +1,951 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import print_function
+
+import datetime
+import dateutil
+import os
+import re
+import sys
+import unittest
+import json
+
+from dateutil.tz import tzutc
+from dateutil.rrule import rrule, rruleset, WEEKLY, MONTHLY
+
+from radicale_vobject import base, iCalendar
+from radicale_vobject import icalendar
+
+from radicale_vobject.base import __behaviorRegistry as behavior_registry
+from radicale_vobject.base import ContentLine, parseLine, ParseError
+from radicale_vobject.base import readComponents, textLineToContentLine
+
+from radicale_vobject.change_tz import change_tz
+
+from radicale_vobject.icalendar import MultiDateBehavior, PeriodBehavior, \
+    RecurringComponent, utc
+from radicale_vobject.icalendar import parseDtstart, stringToTextValues, \
+    stringToPeriod, timedeltaToString
+
+two_hours = datetime.timedelta(hours=2)
+
+
+def get_test_filepath(path):
+   """
+   Helper function to get the filepath of test files.
+   """
+   return os.path.join(os.path.dirname(__file__), "test_files", path)
+
+
+def get_test_file(path):
+    """
+    Helper function to open and read test files.
+    """
+    filepath = get_test_filepath(path)
+    if sys.version_info[0] < 3:
+        # On python 2, this library operates on bytes.
+        f = open(filepath, 'r')
+    else:
+        # On python 3, it operates on unicode. We need to specify an encoding
+        # for systems for which the preferred encoding isn't utf-8 (e.g windows)
+        f = open(filepath, 'r', encoding='utf-8')
+    text = f.read()
+    f.close()
+    return text
+
+
+class TestCalendarSerializing(unittest.TestCase):
+    """
+    Test creating an iCalendar file
+    """
+    max_diff = None
+
+    def test_scratchbuild(self):
+        """
+        CreateCalendar 2.0 format from scratch
+        """
+        test_cal = get_test_file("simple_2_0_test.ics")
+        cal = base.newFromBehavior('vcalendar', '2.0')
+        cal.add('vevent')
+        cal.vevent.add('dtstart').value = datetime.datetime(2006, 5, 9)
+        cal.vevent.add('description').value = "Test event"
+        cal.vevent.add('created').value = \
+            datetime.datetime(2006, 1, 1, 10,
+                              tzinfo=dateutil.tz.tzical(
+                                  get_test_filepath("timezones.ics")).get('US/Pacific'))
+        cal.vevent.add('uid').value = "Not very random UID"
+        cal.vevent.add('dtstamp').value = datetime.datetime(2017, 6, 26, 0, tzinfo=tzutc())
+
+        # Note we're normalizing line endings, because no one got time for that.
+        self.assertEqual(
+            cal.serialize().replace('\r\n', '\n'),
+            test_cal.replace('\r\n', '\n')
+        )
+
+    def test_unicode(self):
+        """
+        Test unicode characters
+        """
+        test_cal = get_test_file("utf8_test.ics")
+        vevent = base.readOne(test_cal).vevent
+        vevent2 = base.readOne(vevent.serialize())
+        self.assertEqual(str(vevent), str(vevent2))
+
+        self.assertEqual(
+            vevent.summary.value,
+            'The title こんにちはキティ'
+        )
+
+        if sys.version_info[0] < 3:
+            test_cal = test_cal.decode('utf-8')
+            vevent = base.readOne(test_cal).vevent
+            vevent2 = base.readOne(vevent.serialize())
+            self.assertEqual(str(vevent), str(vevent2))
+            self.assertEqual(
+                vevent.summary.value,
+                u'The title こんにちはキティ'
+            )
+
+    def test_wrapping(self):
+        """
+        Should support input file with a long text field covering multiple lines
+        """
+        test_journal = get_test_file("journal.ics")
+        vobj = base.readOne(test_journal)
+        vjournal = base.readOne(vobj.serialize())
+        self.assertTrue('Joe, Lisa, and Bob' in vjournal.description.value)
+        self.assertTrue('Tuesday.\n2.' in vjournal.description.value)
+
+    def test_multiline(self):
+        """
+        Multi-text serialization test
+        """
+        category = base.newFromBehavior('categories')
+        category.value = ['Random category']
+        self.assertEqual(
+            category.serialize().strip(),
+            "CATEGORIES:Random category"
+        )
+
+        category.value.append('Other category')
+        self.assertEqual(
+            category.serialize().strip(),
+            "CATEGORIES:Random category,Other category"
+        )
+
+    def test_semicolon_separated(self):
+        """
+        Semi-colon separated multi-text serialization test
+        """
+        request_status = base.newFromBehavior('request-status')
+        request_status.value = ['5.1', 'Service unavailable']
+        self.assertEqual(
+            request_status.serialize().strip(),
+            "REQUEST-STATUS:5.1;Service unavailable"
+        )
+
+    @staticmethod
+    def test_unicode_multiline():
+        """
+        Test multiline unicode characters
+        """
+        cal = iCalendar()
+        cal.add('method').value = 'REQUEST'
+        cal.add('vevent')
+        cal.vevent.add('created').value = datetime.datetime.now()
+        cal.vevent.add('summary').value = 'Классное событие'
+        cal.vevent.add('description').value = ('Классное событие Классное событие Классное событие Классное событие '
+                                               'Классное событие Классsdssdное событие')
+
+        # json tries to encode as utf-8 and it would break if some chars could not be encoded
+        json.dumps(cal.serialize())
+
+    @staticmethod
+    def test_ical_to_hcal():
+        """
+        Serializing iCalendar to hCalendar.
+
+        Since Hcalendar is experimental and the behavior doesn't seem to want to load,
+        This test will have to wait.
+
+
+        tzs = dateutil.tz.tzical(get_test_filepath("timezones.ics"))
+        cal = base.newFromBehavior('hcalendar')
+        self.assertEqual(
+            str(cal.behavior),
+            "<class 'radicale_vobject.hcalendar.HCalendar'>"
+        )
+        cal.add('vevent')
+        cal.vevent.add('summary').value = "this is a note"
+        cal.vevent.add('url').value = "http://microformats.org/code/hcalendar/creator"
+        cal.vevent.add('dtstart').value = datetime.date(2006,2,27)
+        cal.vevent.add('location').value = "a place"
+        cal.vevent.add('dtend').value = datetime.date(2006,2,27) + datetime.timedelta(days = 2)
+
+        event2 = cal.add('vevent')
+        event2.add('summary').value = "Another one"
+        event2.add('description').value = "The greatest thing ever!"
+        event2.add('dtstart').value = datetime.datetime(1998, 12, 17, 16, 42, tzinfo = tzs.get('US/Pacific'))
+        event2.add('location').value = "somewhere else"
+        event2.add('dtend').value = event2.dtstart.value + datetime.timedelta(days = 6)
+        hcal = cal.serialize()
+        """
+        #self.assertEqual(
+        #    str(hcal),
+        #    """<span class="vevent">
+        #           <a class="url" href="http://microformats.org/code/hcalendar/creator">
+        #             <span class="summary">this is a note</span>:
+        #              <abbr class="dtstart", title="20060227">Monday, February 27</abbr>
+        #              - <abbr class="dtend", title="20060301">Tuesday, February 28</abbr>
+        #              at <span class="location">a place</span>
+        #           </a>
+        #        </span>
+        #        <span class="vevent">
+        #           <span class="summary">Another one</span>:
+        #           <abbr class="dtstart", title="19981217T164200-0800">Thursday, December 17, 16:42</abbr>
+        #           - <abbr class="dtend", title="19981223T164200-0800">Wednesday, December 23, 16:42</abbr>
+        #           at <span class="location">somewhere else</span>
+        #           <div class="description">The greatest thing ever!</div>
+        #        </span>
+        #    """
+        #)
+
+
+class TestBehaviors(unittest.TestCase):
+    """
+    Test Behaviors
+    """
+    def test_general_behavior(self):
+        """
+        Tests for behavior registry, getting and creating a behavior.
+        """
+        # Check expected behavior registry.
+        self.assertEqual(
+            sorted(behavior_registry.keys()),
+            ['', 'ACTION', 'ADR', 'AVAILABLE', 'BUSYTYPE', 'CALSCALE',
+             'CATEGORIES', 'CLASS', 'COMMENT', 'COMPLETED', 'CONTACT',
+             'CREATED', 'DAYLIGHT', 'DESCRIPTION', 'DTEND', 'DTSTAMP',
+             'DTSTART', 'DUE', 'DURATION', 'EXDATE', 'EXRULE', 'FN', 'FREEBUSY',
+             'LABEL', 'LAST-MODIFIED', 'LOCATION', 'METHOD', 'N', 'ORG',
+             'PHOTO', 'PRODID', 'RDATE', 'RECURRENCE-ID', 'RELATED-TO',
+             'REQUEST-STATUS', 'RESOURCES', 'REV', 'RRULE', 'STANDARD', 'STATUS',
+             'SUMMARY', 'TRANSP', 'TRIGGER', 'UID', 'VALARM', 'VAVAILABILITY',
+             'VCALENDAR', 'VCARD', 'VEVENT', 'VFREEBUSY', 'VJOURNAL',
+             'VTIMEZONE', 'VTODO']
+        )
+
+        # test get_behavior
+        behavior = base.getBehavior('VCALENDAR')
+        self.assertEqual(
+            str(behavior),
+            "<class 'radicale_vobject.icalendar.VCalendar2_0'>"
+        )
+        self.assertTrue(behavior.isComponent)
+
+        self.assertEqual(
+            base.getBehavior("invalid_name"),
+            None
+        )
+        # test for ContentLine (not a component)
+        non_component_behavior = base.getBehavior('RDATE')
+        self.assertFalse(non_component_behavior.isComponent)
+
+    def test_MultiDateBehavior(self):
+        """
+        Test MultiDateBehavior
+        """
+        parseRDate = MultiDateBehavior.transformToNative
+        self.assertEqual(
+            str(parseRDate(textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904"))),
+            "<RDATE{'VALUE': ['DATE']}[datetime.date(1997, 3, 4), datetime.date(1997, 5, 4), datetime.date(1997, 7, 4), datetime.date(1997, 9, 4)]>"
+        )
+        self.assertEqual(
+            str(parseRDate(textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H"))),
+            "<RDATE{'VALUE': ['PERIOD']}[(datetime.datetime(1996, 4, 3, 2, 0, tzinfo=tzutc()), datetime.datetime(1996, 4, 3, 4, 0, tzinfo=tzutc())), (datetime.datetime(1996, 4, 4, 1, 0, tzinfo=tzutc()), datetime.timedelta(0, 10800))]>"
+        )
+
+    def test_periodBehavior(self):
+        """
+        Test PeriodBehavior
+        """
+        line = ContentLine('test', [], '', isNative=True)
+        line.behavior = PeriodBehavior
+        line.value = [(datetime.datetime(2006, 2, 16, 10), two_hours)]
+
+        self.assertEqual(
+            line.transformFromNative().value,
+            '20060216T100000/PT2H'
+        )
+        self.assertEqual(
+            line.transformToNative().value,
+            [(datetime.datetime(2006, 2, 16, 10, 0),
+              datetime.timedelta(0, 7200))]
+        )
+
+        line.value.append((datetime.datetime(2006, 5, 16, 10), two_hours))
+
+        self.assertEqual(
+            line.serialize().strip(),
+            'TEST:20060216T100000/PT2H,20060516T100000/PT2H'
+        )
+
+
+class TestVTodo(unittest.TestCase):
+    """
+    VTodo Tests
+    """
+    def test_vtodo(self):
+        """
+        Test VTodo
+        """
+        vtodo = get_test_file("vtodo.ics")
+        obj = base.readOne(vtodo)
+        obj.vtodo.add('completed')
+        obj.vtodo.completed.value = datetime.datetime(2015,5,5,13,30)
+        self.assertEqual(obj.vtodo.completed.serialize()[0:23],
+                         'COMPLETED:20150505T1330')
+        obj = base.readOne(obj.serialize())
+        self.assertEqual(obj.vtodo.completed.value,
+                         datetime.datetime(2015,5,5,13,30))
+
+
+class TestVobject(unittest.TestCase):
+    """
+    VObject Tests
+    """
+    max_diff = None
+
+    @classmethod
+    def setUpClass(cls):
+        """
+        Method for setting up class fixture before running tests in the class.
+        Fetches test file.
+        """
+        cls.simple_test_cal = get_test_file("simple_test.ics")
+
+    def test_readComponents(self):
+        """
+        Test if reading components correctly
+        """
+        cal = next(readComponents(self.simple_test_cal))
+
+        self.assertEqual(str(cal), "<VCALENDAR| [<VEVENT| [<SUMMARY{'BLAH': ['hi!']}Bastille Day Party>]>]>")
+        self.assertEqual(str(cal.vevent.summary), "<SUMMARY{'BLAH': ['hi!']}Bastille Day Party>")
+
+    def test_parseLine(self):
+        """
+        Test line parsing
+        """
+        self.assertEqual(parseLine("BLAH:"), ('BLAH', [], '', None))
+        self.assertEqual(
+            parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904"),
+            ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None)
+        )
+        self.assertEqual(
+            parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA'),
+            ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None)
+        )
+        self.assertEqual(
+            parseLine("EMAIL;PREF;INTERNET:john@nowhere.com"),
+            ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None)
+        )
+        self.assertEqual(
+            parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com'),
+            ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None)
+        )
+        self.assertEqual(
+            parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;'),
+            ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1')
+        )
+        self.assertRaises(ParseError, parseLine, ":")
+
+
+class TestGeneralFileParsing(unittest.TestCase):
+    """
+    General tests for parsing ics files.
+    """
+    def test_readOne(self):
+        """
+        Test reading first component of ics
+        """
+        cal = get_test_file("silly_test.ics")
+        silly = base.readOne(cal)
+        self.assertEqual(
+            str(silly),
+            "<SILLYPROFILE| [<MORESTUFF{}this line is not folded, but in practice probably ought to be, as it is exceptionally long, and moreover demonstratively stupid>, <SILLYNAME{}name>, <STUFF{}foldedline>]>"
+        )
+        self.assertEqual(
+            str(silly.stuff),
+            "<STUFF{}foldedline>"
+        )
+
+    def test_importing(self):
+        """
+        Test importing ics
+        """
+        cal = get_test_file("standard_test.ics")
+        c = base.readOne(cal, validate=True)
+        self.assertEqual(
+            str(c.vevent.valarm.trigger),
+            "<TRIGGER{}-1 day, 0:00:00>"
+        )
+
+        self.assertEqual(
+            str(c.vevent.dtstart.value),
+            "2002-10-28 14:00:00-08:00"
+        )
+        self.assertTrue(
+            isinstance(c.vevent.dtstart.value, datetime.datetime)
+        )
+        self.assertEqual(
+            str(c.vevent.dtend.value),
+            "2002-10-28 15:00:00-08:00"
+        )
+        self.assertTrue(
+            isinstance(c.vevent.dtend.value, datetime.datetime)
+        )
+        self.assertEqual(
+            c.vevent.dtstamp.value,
+            datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc())
+        )
+
+        vevent = c.vevent.transformFromNative()
+        self.assertEqual(
+            str(vevent.rrule),
+            "<RRULE{}FREQ=Weekly;COUNT=10>"
+        )
+
+    def test_bad_stream(self):
+        """
+        Test bad ics stream
+        """
+        cal = get_test_file("badstream.ics")
+        self.assertRaises(ParseError, base.readOne, cal)
+
+    def test_bad_line(self):
+        """
+        Test bad line in ics file
+        """
+        cal = get_test_file("badline.ics")
+        self.assertRaises(ParseError, base.readOne, cal)
+
+        newcal = base.readOne(cal, ignoreUnreadable=True)
+        self.assertEqual(
+            str(newcal.vevent.x_bad_underscore),
+            '<X-BAD-UNDERSCORE{}TRUE>'
+        )
+
+    def test_parseParams(self):
+        """
+        Test parsing parameters
+        """
+        self.assertEqual(
+            base.parseParams(';ALTREP="http://www.wiz.org"'),
+            [['ALTREP', 'http://www.wiz.org']]
+        )
+        self.assertEqual(
+            base.parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR'),
+            [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'],
+             ['NEXT', 'Nope'], ['BAR']]
+        )
+
+
+class TestVcards(unittest.TestCase):
+    """
+    Test VCards
+    """
+    @classmethod
+    def setUpClass(cls):
+        """
+        Method for setting up class fixture before running tests in the class.
+        Fetches test file.
+        """
+        cls.test_file = get_test_file("vcard_with_groups.ics")
+        cls.card = base.readOne(cls.test_file)
+
+    def test_vcard_creation(self):
+        """
+        Test creating a vCard
+        """
+        vcard = base.newFromBehavior('vcard', '3.0')
+        self.assertEqual(
+            str(vcard),
+            "<VCARD| []>"
+        )
+
+    def test_default_behavior(self):
+        """
+        Default behavior test.
+        """
+        card = self.card
+        self.assertEqual(
+            base.getBehavior('note'),
+            None
+        )
+        self.assertEqual(
+            str(card.note.value),
+            "The Mayor of the great city of Goerlitz in the great country of Germany.\nNext line."
+        )
+
+    def test_with_groups(self):
+        """
+        vCard groups test
+        """
+        card = self.card
+        self.assertEqual(
+            str(card.group),
+            'home'
+        )
+        self.assertEqual(
+            str(card.tel.group),
+            'home'
+        )
+
+        card.group = card.tel.group = 'new'
+        self.assertEqual(
+            str(card.tel.serialize().strip()),
+            'new.TEL;TYPE=fax,voice,msg:+49 3581 123456'
+        )
+        self.assertEqual(
+            str(card.serialize().splitlines()[0]),
+            'new.BEGIN:VCARD'
+        )
+
+
+    def test_vcard_3_parsing(self):
+        """
+        VCARD 3.0 parse test
+        """
+        test_file = get_test_file("simple_3_0_test.ics")
+        card = base.readOne(test_file)
+        # value not rendering correctly?
+        #self.assertEqual(
+        #    card.adr.value,
+        #    "<Address: Haight Street 512;\nEscape, Test\nNovosibirsk,  80214\nGnuland>"
+        #)
+        self.assertEqual(
+            card.org.value,
+            ["University of Novosibirsk", "Department of Octopus Parthenogenesis"]
+        )
+
+        for _ in range(3):
+            new_card = base.readOne(card.serialize())
+            self.assertEqual(new_card.org.value, card.org.value)
+            card = new_card
+
+
+class TestIcalendar(unittest.TestCase):
+    """
+    Tests for icalendar.py
+    """
+    max_diff = None
+    def test_parseDTStart(self):
+        """
+        Should take a content line and return a datetime object.
+        """
+        self.assertEqual(
+            parseDtstart(textLineToContentLine("DTSTART:20060509T000000")),
+            datetime.datetime(2006, 5, 9, 0, 0)
+        )
+
+    def test_regexes(self):
+        """
+        Test regex patterns
+        """
+        self.assertEqual(
+            re.findall(base.patterns['name'], '12foo-bar:yay'),
+            ['12foo-bar', 'yay']
+        )
+        self.assertEqual(
+            re.findall(base.patterns['safe_char'], 'a;b"*,cd'),
+            ['a', 'b', '*', 'c', 'd']
+        )
+        self.assertEqual(
+            re.findall(base.patterns['qsafe_char'], 'a;b"*,cd'),
+            ['a', ';', 'b', '*', ',', 'c', 'd']
+        )
+        self.assertEqual(
+            re.findall(base.patterns['param_value'],
+                       '"quoted";not-quoted;start"after-illegal-quote',
+                       re.VERBOSE),
+            ['"quoted"', '', 'not-quoted', '', 'start', '',
+             'after-illegal-quote', '']
+        )
+        match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"')
+        self.assertEqual(
+            match.group('value'),
+            'value:;"'
+        )
+        self.assertEqual(
+            match.group('name'),
+            'TEST'
+        )
+        self.assertEqual(
+            match.group('params'),
+            ';ALTREP="http://www.wiz.org"'
+        )
+
+    def test_stringToTextValues(self):
+        """
+        Test string lists
+        """
+        self.assertEqual(
+            stringToTextValues(''),
+            ['']
+        )
+        self.assertEqual(
+            stringToTextValues('abcd,efgh'),
+            ['abcd', 'efgh']
+        )
+
+    def test_stringToPeriod(self):
+        """
+        Test datetime strings
+        """
+        self.assertEqual(
+            stringToPeriod("19970101T180000Z/19970102T070000Z"),
+            (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()),
+             datetime.datetime(1997, 1, 2, 7, 0, tzinfo=tzutc()))
+        )
+        self.assertEqual(
+            stringToPeriod("19970101T180000Z/PT1H"),
+            (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()),
+             datetime.timedelta(0, 3600))
+        )
+
+    def test_timedeltaToString(self):
+        """
+        Test timedelta strings
+        """
+        self.assertEqual(
+            timedeltaToString(two_hours),
+            'PT2H'
+        )
+        self.assertEqual(
+            timedeltaToString(datetime.timedelta(minutes=20)),
+            'PT20M'
+        )
+
+    def test_vtimezone_creation(self):
+        """
+        Test timezones
+        """
+        tzs = dateutil.tz.tzical(get_test_filepath("timezones.ics"))
+        pacific = icalendar.TimezoneComponent(tzs.get('US/Pacific'))
+        self.assertEqual(
+            str(pacific),
+            "<VTIMEZONE | <TZID{}US/Pacific>>"
+        )
+        santiago = icalendar.TimezoneComponent(tzs.get('Santiago'))
+        self.assertEqual(
+            str(santiago),
+            "<VTIMEZONE | <TZID{}Santiago>>"
+        )
+        for year in range(2001, 2010):
+            for month in (2, 9):
+                dt = datetime.datetime(year, month, 15,
+                                       tzinfo=tzs.get('Santiago'))
+                self.assertTrue(dt.replace(tzinfo=tzs.get('Santiago')), dt)
+
+    @staticmethod
+    def test_timezone_serializing():
+        """
+        Serializing with timezones test
+        """
+        tzs = dateutil.tz.tzical(get_test_filepath("timezones.ics"))
+        pacific = tzs.get('US/Pacific')
+        cal = base.Component('VCALENDAR')
+        cal.setBehavior(icalendar.VCalendar2_0)
+        ev = cal.add('vevent')
+        ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9,
+                                                    tzinfo=pacific)
+        evruleset = rruleset()
+        evruleset.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4],
+                              until=datetime.datetime(2005, 12, 15, 9)))
+        evruleset.rrule(rrule(MONTHLY, bymonthday=[-1,-5]))
+        evruleset.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo=pacific))
+        ev.rruleset = evruleset
+        ev.add('duration').value = datetime.timedelta(hours=1)
+
+        apple = tzs.get('America/Montreal')
+        ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo=apple)
+
+    def test_pytz_timezone_serializing(self):
+        """
+        Serializing with timezones from pytz test
+        """
+        try:
+            import pytz
+        except ImportError:
+            return self.skipTest("pytz not installed")  # NOQA
+
+        # Avoid conflicting cached tzinfo from other tests
+        def unregister_tzid(tzid):
+            """Clear tzid from icalendar TZID registry"""
+            if icalendar.getTzid(tzid, False):
+                icalendar.registerTzid(tzid, None)
+
+        unregister_tzid('US/Eastern')
+        eastern = pytz.timezone('US/Eastern')
+        cal = base.Component('VCALENDAR')
+        cal.setBehavior(icalendar.VCalendar2_0)
+        ev = cal.add('vevent')
+        ev.add('dtstart').value = eastern.localize(
+            datetime.datetime(2008, 10, 12, 9))
+        serialized = cal.serialize()
+
+        expected_vtimezone = get_test_file("tz_us_eastern.ics")
+        self.assertIn(
+            expected_vtimezone.replace('\r\n', '\n'),
+            serialized.replace('\r\n', '\n')
+        )
+
+        # Exhaustively test all zones (just looking for no errors)
+        for tzname in pytz.all_timezones:
+            unregister_tzid(tzname)
+            tz = icalendar.TimezoneComponent(tzinfo=pytz.timezone(tzname))
+            tz.serialize()
+
+    def test_freeBusy(self):
+        """
+        Test freebusy components
+        """
+        test_cal = get_test_file("freebusy.ics")
+
+        vfb = base.newFromBehavior('VFREEBUSY')
+        vfb.add('uid').value = 'test'
+        vfb.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+        vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc)
+        vfb.add('dtend').value   = vfb.dtstart.value + two_hours
+        vfb.add('freebusy').value = [(vfb.dtstart.value, two_hours / 2)]
+        vfb.add('freebusy').value = [(vfb.dtstart.value, vfb.dtend.value)]
+
+        self.assertEqual(
+            vfb.serialize().replace('\r\n', '\n'),
+            test_cal.replace('\r\n', '\n')
+        )
+
+    def test_availablity(self):
+        """
+        Test availability components
+        """
+        test_cal = get_test_file("availablity.ics")
+
+        vcal = base.newFromBehavior('VAVAILABILITY')
+        vcal.add('uid').value = 'test'
+        vcal.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+        vcal.add('dtstart').value = datetime.datetime(2006, 2, 16, 0, tzinfo=utc)
+        vcal.add('dtend').value   = datetime.datetime(2006, 2, 17, 0, tzinfo=utc)
+        vcal.add('busytype').value = "BUSY"
+
+        av = base.newFromBehavior('AVAILABLE')
+        av.add('uid').value = 'test1'
+        av.add('dtstamp').value = datetime.datetime(2006, 2, 15, 0, tzinfo=utc)
+        av.add('dtstart').value = datetime.datetime(2006, 2, 16, 9, tzinfo=utc)
+        av.add('dtend').value   = datetime.datetime(2006, 2, 16, 12, tzinfo=utc)
+        av.add('summary').value = "Available in the morning"
+
+        vcal.add(av)
+
+        self.assertEqual(
+            vcal.serialize().replace('\r\n', '\n'),
+            test_cal.replace('\r\n', '\n')
+        )
+
+    def test_recurrence(self):
+        """
+        Ensure date valued UNTILs in rrules are in a reasonable timezone,
+        and include that day (12/28 in this test)
+        """
+        test_file = get_test_file("recurrence.ics")
+        cal = base.readOne(test_file)
+        dates = list(cal.vevent.getrruleset())
+        self.assertEqual(
+            dates[0],
+            datetime.datetime(2006, 1, 26, 23, 0, tzinfo=tzutc())
+        )
+        self.assertEqual(
+            dates[1],
+            datetime.datetime(2006, 2, 23, 23, 0, tzinfo=tzutc())
+        )
+        self.assertEqual(
+            dates[-1],
+            datetime.datetime(2006, 12, 28, 23, 0, tzinfo=tzutc())
+        )
+
+    def test_recurring_component(self):
+        """
+        Test recurring events
+        """
+        vevent = RecurringComponent(name='VEVENT')
+
+        # init
+        self.assertTrue(vevent.isNative)
+
+        # rruleset should be None at this point.
+        # No rules have been passed or created.
+        self.assertEqual(vevent.rruleset, None)
+
+        # Now add start and rule for recurring event
+        vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9)
+        vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH"
+        self.assertEqual(
+            list(vevent.rruleset),
+            [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)]
+        )
+        self.assertEqual(
+            list(vevent.getrruleset(addRDate=True)),
+            [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)]
+        )
+
+        # Also note that dateutil will expand all-day events (datetime.date values)
+        # to datetime.datetime value with time 0 and no timezone.
+        vevent.dtstart.value = datetime.date(2005,3,18)
+        self.assertEqual(
+            list(vevent.rruleset),
+            [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)]
+        )
+        self.assertEqual(
+            list(vevent.getrruleset(True)),
+            [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)]
+        )
+
+    def test_recurrence_without_tz(self):
+        """
+        Test recurring vevent missing any time zone definitions.
+        """
+        test_file = get_test_file("recurrence-without-tz.ics")
+        cal = base.readOne(test_file)
+        dates = list(cal.vevent.getrruleset())
+        self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0))
+        self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0))
+        self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0))
+
+    def test_recurrence_offset_naive(self):
+        """
+        Ensure recurring vevent missing some time zone definitions is
+        parsing. See isseu #75.
+        """
+        test_file = get_test_file("recurrence-offset-naive.ics")
+        cal = base.readOne(test_file)
+        dates = list(cal.vevent.getrruleset())
+        self.assertEqual(dates[0], datetime.datetime(2013, 1, 17, 0, 0))
+        self.assertEqual(dates[1], datetime.datetime(2013, 1, 24, 0, 0))
+        self.assertEqual(dates[-1], datetime.datetime(2013, 3, 28, 0, 0))
+
+
+class TestChangeTZ(unittest.TestCase):
+    """
+    Tests for change_tz.change_tz
+    """
+    class StubCal(object):
+        class StubEvent(object):
+            class Node(object):
+                def __init__(self, value):
+                    self.value = value
+
+            def __init__(self, dtstart, dtend):
+                self.dtstart = self.Node(dtstart)
+                self.dtend = self.Node(dtend)
+
+        def __init__(self, dates):
+            """
+            dates is a list of tuples (dtstart, dtend)
+            """
+            self.vevent_list = [self.StubEvent(*d) for d in dates]
+
+    def test_change_tz(self):
+        """
+        Change the timezones of events in a component to a different
+        timezone
+        """
+
+        # Setup - create a stub vevent list
+        old_tz = dateutil.tz.gettz('UTC')  # 0:00
+        new_tz = dateutil.tz.gettz('America/Chicago')  # -5:00
+
+        dates = [
+            (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=old_tz),
+             datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=old_tz)),
+            (datetime.datetime(2010, 12, 31, 23, 59, 59, 0, tzinfo=old_tz),
+             datetime.datetime(2011, 1, 2, 3, 0, 0, 0, tzinfo=old_tz))]
+
+        cal = self.StubCal(dates)
+
+        # Exercise - change the timezone
+        change_tz(cal, new_tz, dateutil.tz.gettz('UTC'))
+
+        # Test - that the tzs were converted correctly
+        expected_new_dates = [
+            (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz)),
+            (datetime.datetime(2010, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             datetime.datetime(2011, 1, 1, 21, 0, 0, 0, tzinfo=new_tz))]
+
+        for vevent, expected_datepair in zip(cal.vevent_list,
+                                             expected_new_dates):
+            self.assertEqual(vevent.dtstart.value, expected_datepair[0])
+            self.assertEqual(vevent.dtend.value, expected_datepair[1])
+
+    def test_change_tz_utc_only(self):
+        """
+        Change any UTC timezones of events in a component to a different
+        timezone
+        """
+
+        # Setup - create a stub vevent list
+        utc_tz = dateutil.tz.gettz('UTC')  # 0:00
+        non_utc_tz = dateutil.tz.gettz('America/Santiago')  # -4:00
+        new_tz = dateutil.tz.gettz('America/Chicago')  # -5:00
+
+        dates = [
+            (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=utc_tz),
+             datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=non_utc_tz))]
+
+        cal = self.StubCal(dates)
+
+        # Exercise - change the timezone passing utc_only=True
+        change_tz(cal, new_tz, dateutil.tz.gettz('UTC'), utc_only=True)
+
+        # Test - that only the utc item has changed
+        expected_new_dates = [
+            (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             dates[0][1])]
+
+        for vevent, expected_datepair in zip(cal.vevent_list,
+                                             expected_new_dates):
+            self.assertEqual(vevent.dtstart.value, expected_datepair[0])
+            self.assertEqual(vevent.dtend.value, expected_datepair[1])
+
+    def test_change_tz_default(self):
+        """
+        Change the timezones of events in a component to a different
+        timezone, passing a default timezone that is assumed when the events
+        don't have one
+        """
+
+        # Setup - create a stub vevent list
+        new_tz = dateutil.tz.gettz('America/Chicago')  # -5:00
+
+        dates = [
+            (datetime.datetime(1999, 12, 31, 23, 59, 59, 0, tzinfo=None),
+             datetime.datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=None))]
+
+        cal = self.StubCal(dates)
+
+        # Exercise - change the timezone
+        change_tz(cal, new_tz, dateutil.tz.gettz('UTC'))
+
+        # Test - that the tzs were converted correctly
+        expected_new_dates = [
+            (datetime.datetime(1999, 12, 31, 17, 59, 59, 0, tzinfo=new_tz),
+             datetime.datetime(1999, 12, 31, 18, 0, 0, 0, tzinfo=new_tz))]
+
+        for vevent, expected_datepair in zip(cal.vevent_list,
+                                             expected_new_dates):
+            self.assertEqual(vevent.dtstart.value, expected_datepair[0])
+            self.assertEqual(vevent.dtend.value, expected_datepair[1])
+
+
+if __name__ == '__main__':
+    unittest.main()

+ 14 - 0
radicale_vobject/tests/test_files/availablity.ics

@@ -0,0 +1,14 @@
+BEGIN:VAVAILABILITY
+UID:test
+DTSTART:20060216T000000Z
+DTEND:20060217T000000Z
+BEGIN:AVAILABLE
+UID:test1
+DTSTART:20060216T090000Z
+DTEND:20060216T120000Z
+DTSTAMP:20060215T000000Z
+SUMMARY:Available in the morning
+END:AVAILABLE
+BUSYTYPE:BUSY
+DTSTAMP:20060215T000000Z
+END:VAVAILABILITY

+ 10 - 0
radicale_vobject/tests/test_files/badline.ics

@@ -0,0 +1,10 @@
+BEGIN:VCALENDAR
+METHOD:PUBLISH
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:19870405T020000
+X-BAD/SLASH:TRUE
+X-BAD_UNDERSCORE:TRUE
+UID:EC9439B1-FF65-11D6-9973-003065F99D04
+END:VEVENT
+END:VCALENDAR

+ 16 - 0
radicale_vobject/tests/test_files/badstream.ics

@@ -0,0 +1,16 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
+METHOD:PUBLISH
+PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
+X-WR-CALNAME;VALUE=TEXT:Example
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART:20021028T140000Z
+BEGIN:VALARM
+TRIGGER:a20021028120000
+ACTION:DISPLAY
+DESCRIPTION:This trigger has a nonsensical value
+END:VALARM
+END:VEVENT
+END:VCALENDAR

+ 8 - 0
radicale_vobject/tests/test_files/freebusy.ics

@@ -0,0 +1,8 @@
+BEGIN:VFREEBUSY
+UID:test
+DTSTART:20060216T010000Z
+DTEND:20060216T030000Z
+DTSTAMP:20060215T000000Z
+FREEBUSY:20060216T010000Z/PT1H
+FREEBUSY:20060216T010000Z/20060216T030000Z
+END:VFREEBUSY

+ 15 - 0
radicale_vobject/tests/test_files/journal.ics

@@ -0,0 +1,15 @@
+BEGIN:VJOURNAL
+UID:19970901T130000Z-123405@example.com
+DTSTAMP:19970901T130000Z
+DTSTART;VALUE=DATE:19970317
+SUMMARY:Staff meeting minutes
+DESCRIPTION:1. Staff meeting: Participants include Joe\,
+  Lisa\, and Bob. Aurora project plans were reviewed.
+  There is currently no budget reserves for this project.
+  Lisa will escalate to management. Next meeting on Tuesday.\n
+ 2. Telephone Conference: ABC Corp. sales representative
+  called to discuss new printer. Promised to get us a demo by
+  Friday.\n3. Henry Miller (Handsoff Insurance): Car was
+  totaled by tree. Is looking into a loaner car. 555-2323
+  (tel).
+END:VJOURNAL

+ 85 - 0
radicale_vobject/tests/test_files/more_tests.txt

@@ -0,0 +1,85 @@
+
+Unicode in vCards
+.................
+
+>>> import vobject
+>>> card = vobject.vCard()
+>>> card.add('fn').value = u'Hello\u1234 World!'
+>>> card.add('n').value = vobject.vcard.Name('World', u'Hello\u1234')
+>>> card.add('adr').value = vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA')
+>>> card
+<VCARD| [<ADR{}5? Nowhere, Apt 1\nBerkeley, CA 94704\nUSA>, <FN{}Hello? World!>, <N{} Hello?  World >]>
+>>> card.serialize()
+u'BEGIN:VCARD\r\nVERSION:3.0\r\nADR:;;5\u1234 Nowhere\\, Apt 1;Berkeley;CA;94704;USA\r\nFN:Hello\u1234 World!\r\nN:World;Hello\u1234;;;\r\nEND:VCARD\r\n'
+>>> print(card.serialize())
+BEGIN:VCARD
+VERSION:3.0
+ADR:;;5ሴ Nowhere\, Apt 1;Berkeley;CA;94704;USA
+FN:Helloሴ World!
+N:World;Helloሴ;;;
+END:VCARD
+
+Helper function
+...............
+>>> from pkg_resources import resource_stream
+>>> def get_stream(path):
+...     try:
+...         return resource_stream(__name__, 'test_files/' + path)
+...     except: # different paths, depending on whether doctest is run directly
+...         return resource_stream(__name__, path)
+
+Unicode in TZID
+...............
+>>> f = get_stream("tzid_8bit.ics")
+>>> cal = vobject.readOne(f)
+>>> print(cal.vevent.dtstart.value)
+2008-05-30 15:00:00+06:00
+>>> print(cal.vevent.dtstart.serialize())
+DTSTART;TZID=Екатеринбург:20080530T150000
+
+Commas in TZID
+..............
+>>> f = get_stream("ms_tzid.ics")
+>>> cal = vobject.readOne(f)
+>>> print(cal.vevent.dtstart.value)
+2008-05-30 15:00:00+10:00
+
+Equality in vCards
+..................
+
+>>> card.adr.value == vobject.vcard.Address('Just a street')
+False
+>>> card.adr.value == vobject.vcard.Address(u'5\u1234 Nowhere, Apt 1', 'Berkeley', 'CA', '94704', 'USA')
+True
+
+Organization (org)
+..................
+
+>>> card.add('org').value = ["Company, Inc.", "main unit", "sub-unit"]
+>>> print(card.org.serialize())
+ORG:Company\, Inc.;main unit;sub-unit
+
+Ruby escapes semi-colons in rrules
+..................................
+
+>>> f = get_stream("ruby_rrule.ics")
+>>> cal = vobject.readOne(f)
+>>> iter(cal.vevent.rruleset).next()
+datetime.datetime(2003, 1, 1, 7, 0)
+
+quoted-printable
+................
+
+>>> vcf = 'BEGIN:VCARD\nVERSION:2.1\nN;ENCODING=QUOTED-PRINTABLE:;=E9\nFN;ENCODING=QUOTED-PRINTABLE:=E9\nTEL;HOME:0111111111\nEND:VCARD\n\n'
+>>> vcf = vobject.readOne(vcf)
+>>> vcf.n.value
+<Name:  ?   >
+>>> vcf.n.value.given
+u'\xe9'
+>>> vcf.serialize()
+'BEGIN:VCARD\r\nVERSION:2.1\r\nFN:\xc3\xa9\r\nN:;\xc3\xa9;;;\r\nTEL:0111111111\r\nEND:VCARD\r\n'
+
+>>> vcs = 'BEGIN:VCALENDAR\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nVERSION:1.0\r\nBEGIN:VEVENT\r\nDESCRIPTION;CHARSET=UTF-8;ENCODING=QUOTED-PRINTABLE:foo =C3=A5=0Abar =C3=A4=\r\n=0Abaz =C3=B6\r\nUID:20080406T152030Z-7822\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n'
+>>> vcs = vobject.readOne(vcs, allowQP = True)
+>>> vcs.serialize()
+'BEGIN:VCALENDAR\r\nVERSION:1.0\r\nPRODID:-//OpenSync//NONSGML OpenSync vformat 0.3//EN\r\nBEGIN:VEVENT\r\nUID:20080406T152030Z-7822\r\nDESCRIPTION:foo \xc3\xa5\\nbar \xc3\xa4\\nbaz \xc3\xb6\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n'

+ 39 - 0
radicale_vobject/tests/test_files/ms_tzid.ics

@@ -0,0 +1,39 @@
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Canberra, Melbourne, Sydney
+BEGIN:STANDARD
+DTSTART:20010325T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3;UNTIL=20050327T070000Z
+TZOFFSETFROM:+1100
+TZOFFSETTO:+1000
+TZNAME:Standard Time
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:20060402T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z
+TZOFFSETFROM:+1100
+TZOFFSETTO:+1000
+TZNAME:Standard Time
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:20070325T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3
+TZOFFSETFROM:+1100
+TZOFFSETTO:+1000
+TZNAME:Standard Time
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20001029T020000
+RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:+1000
+TZOFFSETTO:+1100
+TZNAME:Daylight Savings Time
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:CommaTest
+DTSTART;TZID="Canberra, Melbourne, Sydney":20080530T150000
+END:VEVENT
+END:VCALENDAR

+ 9 - 0
radicale_vobject/tests/test_files/recurrence-offset-naive.ics

@@ -0,0 +1,9 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20130117
+DTEND;VALUE=DATE:20130118
+RRULE:FREQ=WEEKLY;UNTIL=20130330T230000Z;BYDAY=TH
+SUMMARY:Meeting
+END:VEVENT
+END:VCALENDAR

+ 9 - 0
radicale_vobject/tests/test_files/recurrence-without-tz.ics

@@ -0,0 +1,9 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+DTSTART;VALUE=DATE:20130117
+DTEND;VALUE=DATE:20130118
+RRULE:FREQ=WEEKLY;UNTIL=20130330;BYDAY=TH
+SUMMARY:Meeting
+END:VEVENT
+END:VCALENDAR

+ 30 - 0
radicale_vobject/tests/test_files/recurrence.ics

@@ -0,0 +1,30 @@
+BEGIN:VCALENDAR
+VERSION
+ :2.0
+PRODID
+ :-//Mozilla.org/NONSGML Mozilla Calendar V1.0//EN
+BEGIN:VEVENT
+CREATED
+ :20060327T214227Z
+LAST-MODIFIED
+ :20060313T080829Z
+DTSTAMP
+ :20060116T231602Z
+UID
+ :70922B3051D34A9E852570EC00022388
+SUMMARY
+ :Monthly - All Hands Meeting with Joe Smith
+STATUS
+ :CONFIRMED
+CLASS
+ :PUBLIC
+RRULE
+ :FREQ=MONTHLY;UNTIL=20061228;INTERVAL=1;BYDAY=4TH
+DTSTART
+ :20060126T230000Z
+DTEND
+ :20060127T000000Z
+DESCRIPTION
+ :Repeat Meeting: - Occurs every 4th Thursday of each month
+END:VEVENT
+END:VCALENDAR

+ 16 - 0
radicale_vobject/tests/test_files/ruby_rrule.ics

@@ -0,0 +1,16 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+METHOD:PUBLISH
+PRODID:-//LinkeSOFT GmbH//NONSGML DIMEX//EN
+BEGIN:VEVENT
+SEQUENCE:0
+RRULE:FREQ=DAILY\;COUNT=10
+DTEND:20030101T080000
+UID:2008-05-29T17:31:42+02:00_865561242
+CATEGORIES:Unfiled
+SUMMARY:Something
+DTSTART:20030101T070000
+DTSTAMP:20080529T152100
+END:VEVENT
+END:VCALENDAR

+ 5 - 0
radicale_vobject/tests/test_files/silly_test.ics

@@ -0,0 +1,5 @@
+sillyname:name
+profile:sillyprofile
+stuff:folded
+ line
+morestuff;asinine:this line is not folded, but in practice probably ought to be, as it is exceptionally long, and moreover demonstratively stupid

+ 11 - 0
radicale_vobject/tests/test_files/simple_2_0_test.ics

@@ -0,0 +1,11 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:Not very random UID
+DTSTART:20060509T000000
+CREATED:20060101T180000Z
+DESCRIPTION:Test event
+DTSTAMP:20170626T000000Z
+END:VEVENT
+END:VCALENDAR

+ 13 - 0
radicale_vobject/tests/test_files/simple_3_0_test.ics

@@ -0,0 +1,13 @@
+BEGIN:VCARD
+VERSION:3.0
+FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto)
+N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto)
+NICKNAME:gnat and gnu and pluto
+BDAY;value=date:02-10
+TEL;type=HOME:+01-(0)2-765.43.21
+TEL;type=CELL:+01-(0)5-555.55.55
+ACCOUNT;type=HOME:010-1234567-05
+ADR;type=HOME:;;Haight Street 512\;\nEscape\, Test;Novosibirsk;;80214;Gnuland
+TEL;type=HOME:+01-(0)2-876.54.32
+ORG:University of Novosibirsk;Department of Octopus Parthenogenesis
+END:VCARD

+ 5 - 0
radicale_vobject/tests/test_files/simple_test.ics

@@ -0,0 +1,5 @@
+BEGIN:VCALENDAR
+BEGIN:VEVENT
+SUMMARY;blah=hi!:Bastille Day Party
+END:VEVENT
+END:VCALENDAR

+ 41 - 0
radicale_vobject/tests/test_files/standard_test.ics

@@ -0,0 +1,41 @@
+BEGIN:VCALENDAR
+CALSCALE:GREGORIAN
+X-WR-TIMEZONE;VALUE=TEXT:US/Pacific
+METHOD:PUBLISH
+PRODID:-//Apple Computer\, Inc//iCal 1.0//EN
+X-WR-CALNAME;VALUE=TEXT:Example
+VERSION:2.0
+BEGIN:VEVENT
+SEQUENCE:5
+DTSTART;TZID=US/Pacific:20021028T140000
+RRULE:FREQ=Weekly;COUNT=10
+DTSTAMP:20021028T011706Z
+SUMMARY:Coffee with Jason
+UID:EC9439B1-FF65-11D6-9973-003065F99D04
+DTEND;TZID=US/Pacific:20021028T150000
+BEGIN:VALARM
+TRIGGER;VALUE=DURATION:-P1D
+ACTION:DISPLAY
+DESCRIPTION:Event reminder\, with comma\nand line feed
+END:VALARM
+END:VEVENT
+BEGIN:VTIMEZONE
+X-LIC-LOCATION:Random location
+TZID:US/Pacific
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+TZNAME:PST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+TZNAME:PDT
+END:DAYLIGHT
+END:VTIMEZONE
+END:VCALENDAR

+ 107 - 0
radicale_vobject/tests/test_files/timezones.ics

@@ -0,0 +1,107 @@
+BEGIN:VTIMEZONE
+TZID:US/Pacific
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0700
+TZOFFSETTO:-0800
+TZNAME:PST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0800
+TZOFFSETTO:-0700
+TZNAME:PDT
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:US/Eastern
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:Santiago
+BEGIN:STANDARD
+DTSTART:19700314T000000
+TZOFFSETFROM:-0300
+TZOFFSETTO:-0400
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA
+TZNAME:Pacific SA Standard Time
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19701010T000000
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0300
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA
+TZNAME:Pacific SA Daylight Time
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:W. Europe
+BEGIN:STANDARD
+DTSTART:19701025T030000
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
+TZNAME:W. Europe Standard Time
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19700329T020000
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
+TZNAME:W. Europe Daylight Time
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:US/Fictitious-Eastern
+LAST-MODIFIED:19870101T000000Z
+BEGIN:STANDARD
+DTSTART:19671029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+TZNAME:EST
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19870405T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+TZNAME:EDT
+END:DAYLIGHT
+END:VTIMEZONE
+
+BEGIN:VTIMEZONE
+TZID:America/Montreal
+LAST-MODIFIED:20051013T233643Z
+BEGIN:DAYLIGHT
+DTSTART:20050403T070000
+TZOFFSETTO:-0400
+TZOFFSETFROM:+0000
+TZNAME:EDT
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20051030T020000
+TZOFFSETTO:-0500
+TZOFFSETFROM:-0400
+TZNAME:EST
+END:STANDARD
+END:VTIMEZONE

+ 31 - 0
radicale_vobject/tests/test_files/tz_us_eastern.ics

@@ -0,0 +1,31 @@
+BEGIN:VTIMEZONE
+TZID:US/Eastern
+BEGIN:STANDARD
+DTSTART:20001029T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10;UNTIL=20061029T060000Z
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:STANDARD
+DTSTART:20071104T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:20000402T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20060402T070000Z
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:DAYLIGHT
+DTSTART:20070311T020000
+RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE

+ 23 - 0
radicale_vobject/tests/test_files/tzid_8bit.ics

@@ -0,0 +1,23 @@
+BEGIN:VCALENDAR
+PRODID:-//Microsoft Corporation//Outlook 12.0 MIMEDIR//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Екатеринбург
+BEGIN:STANDARD
+DTSTART:16011028T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZOFFSETFROM:+0600
+TZOFFSETTO:+0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:16010325T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+TZOFFSETFROM:+0500
+TZOFFSETTO:+0600
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:CyrillicTest
+DTSTART;TZID=Екатеринбург:20080530T150000
+END:VEVENT
+END:VCALENDAR

+ 39 - 0
radicale_vobject/tests/test_files/utf8_test.ics

@@ -0,0 +1,39 @@
+BEGIN:VCALENDAR
+METHOD:PUBLISH
+CALSCALE:GREGORIAN
+PRODID:-//EVDB//www.evdb.com//EN
+VERSION:2.0
+X-WR-CALNAME:EVDB Event Feed
+BEGIN:VEVENT
+DTSTART:20060922T000100Z
+DTEND:20060922T050100Z
+DTSTAMP:20050914T163414Z
+SUMMARY:The title こんにちはキティ
+DESCRIPTION:hello\nHere is a description\n\n\nこんにちはキティ
+	\n\n\n\nZwei Java-schwere Entwicklerpositionen und irgendeine Art sond
+	erbar-klingende Netzsichtbarmachungöffnung\, an einer interessanten F
+	irma im Gebäude\, in dem ich angerufenen Semantic Research bearbeite.
+	 1. Zauberer Des Semantica Software Engineer 2. Älterer Semantica Sof
+	tware-Englisch-3. Graph/Semantica Netz-Visualization/Navigation Sie ei
+	ngestufte Software-Entwicklung für die Regierung. Die Firma ist stark
+	 und die Projekte sind sehr kühl und schließen irgendeinen Spielraum
+	 ein. Wenn ich Ihnen irgendwie mehr erkläre\, muß ich Sie töten. Ps
+	. Tat schnell -- jemand ist\, wenn es hier interviewt\, wie ich dieses
+	 schreibe. Er schaut intelligent (er trägt Kleidhosen) Semantica Soft
+	ware Engineer FIRMA: Semantische Forschung\, Inc. REPORTS ZU: Vizeprä
+	sident\, Produkt-Entwicklung POSITION: San Diego (Pint Loma) WEB SITE:
+	 www.semanticresearch.com email: dorie@semanticresearch.com FIRMA-HINT
+	ERGRUND Semantische Forschung ist der führende Versorger der semantis
+	cher Netzwerkanschluß gegründeten nicht linearen Wissen Darstellung 
+	Werkzeuge. Die Firma stellt diese Werkzeuge zum Intel\, zur reg.\, zum
+	 EDU und zu den kommerziellen Märkten zur Verfügung. BRINGEN SIE ZUS
+	AMMENFASSUNG IN POSITION Semantische Forschung\, Inc. basiert in San D
+	iego\, Ca im alten realen Weltsan Diego Haus...\, das wir den Weltbest
+	en Platz haben zum zu arbeiten. Wir suchen nach Superstarentwicklern\,
+	 um uns in der fortfahrenden Entwicklung unserer Semantica Produktseri
+	e zu unterstützen.
+LOCATION:こんにちはキティ
+SEQUENCE:0
+UID:E0-001-000276068-2
+END:VEVENT
+END:VCALENDAR

+ 18 - 0
radicale_vobject/tests/test_files/vcard_with_groups.ics

@@ -0,0 +1,18 @@
+home.begin:vcard
+version:3.0
+source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE
+name:Meister Berger
+fn:Meister Berger
+n:Berger;Meister
+bday;value=date:1963-09-21
+o:Universit=E6t G=F6rlitz
+title:Mayor
+title;language=de;value=text:Burgermeister
+note:The Mayor of the great city of
+  Goerlitz in the great country of Germany.\nNext line.
+email;internet:mb@goerlitz.de
+home.tel;type=fax,voice;type=msg:+49 3581 123456
+home.label:Hufenshlagel 1234\n
+ 02828 Goerlitz\n
+ Deutschland
+END:VCARD

+ 13 - 0
radicale_vobject/tests/test_files/vtodo.ics

@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Example Corp.//CalDAV Client//EN
+BEGIN:VTODO
+UID:20070313T123432Z-456553@example.com
+DTSTAMP:20070313T123432Z
+DUE;VALUE=DATE:20070501
+SUMMARY:Submit Quebec Income Tax Return for 2006
+CLASS:CONFIDENTIAL
+CATEGORIES:FAMILY,FINANCE
+STATUS:NEEDS-ACTION
+END:VTODO
+END:VCALENDAR

+ 8 - 2
setup.cfg

@@ -5,5 +5,11 @@ test = pytest
 python-tag = py3
 
 [tool:pytest]
-addopts = --flake8 --isort --cov radicale -r s
-norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv radicale_vobject
+addopts = --flake8 --isort --cov radicale --cov radicale_vobject -r s
+norecursedirs = dist .cache .git build Radicale.egg-info .eggs venv
+
+[isort]
+skip = radicale_vobject
+
+[flake8]
+exclude = radicale_vobject/*