File: test_issue_75_range_parameter.py

package info (click to toggle)
python-recurring-ical-events 3.3.3-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,128 kB
  • sloc: python: 2,896; sh: 15; makefile: 3
file content (249 lines) | stat: -rw-r--r-- 9,936 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
"""This tests the range parameter for ics file.
see https://github.com/niccokunzmann/python-recurring-ical-events/issues/75
Description:  This parameter can be specified on a property that
    specifies a recurrence identifier.  The parameter specifies the
    effective range of recurrence instances that is specified by the
    property.  The effective range is from the recurrence identifier
    specified by the property.  If this parameter is not specified on
    an allowed property, then the default range is the single instance
    specified by the recurrence identifier value of the property.  The
    parameter value can only be "THISANDFUTURE" to indicate a range
    defined by the recurrence identifier and all subsequent instances.
    The value "THISANDPRIOR" is deprecated by this revision of
    iCalendar and MUST NOT be generated by applications.

    - https://www.rfc-editor.org/rfc/rfc5545.html#section-3.2.13
"""

from datetime import time
from datetime import timedelta as td
from typing import TYPE_CHECKING

import pytest

from recurring_ical_events import EventAdapter

if TYPE_CHECKING:
    from calendar import Calendar


@pytest.mark.parametrize(
    ("date", "summary"),
    [
        ("20240901", "ORIGINAL EVENT"),
        ("20240911", "ORIGINAL EVENT"),
        ("20240913", "MODIFIED EVENT"),
        ("20240914", "MODIFIED EVENT"),  # RDATE
        ("20240915", "MODIFIED EVENT"),  # Normal recurrence-id
        ("20240917", "MODIFIED EVENT"),
        ("20240919", "MODIFIED EVENT"),
        ("20240922", "EDITED EVENT"),
        ("20240924", "EDITED EVENT"),
        ("20240926", "EDITED EVENT"),
    ],
)
def test_issue_75_RANGE_AT_parameter(calendars, date, summary):
    events = calendars.issue_75_range_parameter.at(date)
    assert len(events) == 1, f"Expecting one event at {date}"
    event = events[0]
    assert str(event["SUMMARY"]) == summary


@pytest.mark.parametrize(
    ("start", "end", "summary", "total"),
    [
        ("20240901T000000Z", "20240911T235959Z", "ORIGINAL EVENT", 6),
        ("20240901T000000Z", "20240913T000000Z", "ORIGINAL EVENT", 6),
        ("20240901T000000Z", "20240913T235959Z", "MODIFIED EVENT", 7),
        ("20240901T000000Z", "20240914T235959Z", "MODIFIED EVENT", 8),  # RDATE
        (
            "20240901T000000Z",
            "20240915T235959Z",
            "MODIFIED EVENT",
            9,
        ),  # Normal recurrence-id
        ("20240901T000000Z", "20240917T235959Z", "MODIFIED EVENT", 10),
        ("20240901T000000Z", "20240919T235959Z", "MODIFIED EVENT", 11),
        ("20240901T000000Z", "20240921T235959Z", "MODIFIED EVENT", 11),
        ("20240901T000000Z", "20240922T000000Z", "MODIFIED EVENT", 11),
        ("20240901T000000Z", "20240922T235959Z", "EDITED EVENT", 12),
        ("20240901T000000Z", "20240923T000000Z", "EDITED EVENT", 12),
        ("20240901T000000Z", "20240923T235959Z", "EDITED EVENT", 12),
        ("20240901T000000Z", "20240924T235959Z", "EDITED EVENT", 13),
        ("20240901T000000Z", "20240925T235959Z", "EDITED EVENT", 13),
        (
            "20240913T000000Z",
            "20240922T000000Z",
            "MODIFIED EVENT",
            5,
        ),  # out of query bounds
        (
            "20240913T000000Z",
            "20240922T235959Z",
            "EDITED EVENT",
            6,
        ),  # out of query bounds
        (
            "20240924T000000Z",
            "20240925T235959Z",
            "EDITED EVENT",
            1,
        ),  # out of query bounds
    ],
)
def test_issue_75_RANGE_BETWEEN_parameter(calendars, start, end, summary, total):
    events = calendars.issue_75_range_parameter.between(start, end)
    assert (
        len(events) == total
    ), f"Expecting {total} events at range {start}, {end}, get {len(events)}"
    event = events[-1]
    assert str(event["SUMMARY"]) == summary


@pytest.mark.parametrize(
    ("date", "start", "end"),
    [
        # moved by 3 hours forward
        ((2024, 9, 13, 9), (9, 0), (16, 0)),  # The modification itself
        ((2024, 9, 17, 9), (9, 0), (16, 0)),  # The recurrence after this moved
        # moved by 2h22m backward
        ((2024, 9, 22, 14, 22), (14, 22), (16, 13)),  # The modification itself
        ((2024, 9, 24, 14, 22), (14, 22), (16, 13)),  # The recurrence after this moved
    ],
)
def test_the_length_of_modified_events(calendars, date, start, end):
    """There should be one event exactly starting and ending at these times."""
    events = calendars.issue_75_range_parameter.at(date)
    assert len(events) != 0, "The calculation could not find an event!"
    assert len(events) == 1, "Modify the test to yield one event only!"
    event = events[0]
    assert event["DTSTART"].dt.time() == time(*start)
    assert event["DTEND"].dt.time() == time(*end)


@pytest.mark.parametrize(
    ("calendar", "event_index", "expected_start_delta", "expected_end_delta"),
    [
        # no recurrence id means 0
        ("issue_62_moved_event", 1, td(0), td(0)),
        # we moved 31 -> 17; 31-17
        ("issue_62_moved_event", 0, td(0), td(14)),
        # we have a duration added on top
        ("one_event", 0, td(minutes=30), td(0)),
        # we move to a later date, +1 day
        ("same_event_recurring_at_same_time", 1, td(days=1, hours=1), td(0)),
        # we move to the front, so we should still add the duration
        ("same_event_recurring_at_same_time", 2, td(0), td(hours=1)),
        # we moved with the THISANDFUTURE
        (
            "issue_75_range_parameter",
            3,
            td(days=1, hours=2, minutes=22) + td(hours=1, minutes=51),
            td(0),
        ),
    ],
)
def test_span_extension(
    calendars, calendar, event_index, expected_start_delta, expected_end_delta
):
    """If we have an event that is moved with THISANDFUTURE,
    other events move, too.

    This requires us to extend the range which we query:
    - If an event moves forward, we need to extend the span to the back ...
    - If an event moves backward, we need to extend the span to the front ...
    ... in order to capture the recurrences from the rrule that would yield
    the occurrence.

    If the length is extended, we can shorten the span
    If the length is reduced, we have to extend the span

    This tests the adapter to yield the correct values for the given types
    of moves.

    We only have to extend the range for THISANDFUTURE events because
    we iterate over all modifications either way.
    TODO: However, for optimization, one could approach to create ranges that
    specify how to extend and contract the spans.

    This test has to test of types of recurrence id, start and end.
    - date
    - datetime without tzinfo
    - datetime with UTC
    - datetime with tzinfo other than UTC

    >.The default value type is DATE-TIME.  The value type can
      be set to a DATE value type.  This property MUST have the same
      value type as the "DTSTART" property contained within the
      recurring component.  Furthermore, this property MUST be specified
      as a date with local time if and only if the "DTSTART" property
      contained within the recurring component is specified as a date
      with local time.
      - https://www.rfc-editor.org/rfc/rfc5545.html#section-3.8.4.4

    moves must include:
    - time forward
    - time backward
    - several days forward
    - several days backward

    Assumptions
    -----------

    This test is for a rought estimate. We can extend the range by +1 day into each direction.
    This will allow us to capture everything.
    Future examples and tests may help us improve the situation by narrowing it further down.

    Safe:
    - move 1 h forward -> END: add 1 day for timezone + 1 day for timedelta without timezone involvement (round up)
    """
    cal = calendars.raw[calendar]
    event = list(cal.walk("VEVENT"))[event_index]
    adapter = EventAdapter(event)
    assert adapter.duration >= td(0)
    start_delta, end_delta = adapter.extend_query_span_by
    assert start_delta >= expected_start_delta
    assert end_delta >= expected_end_delta


def test_can_calculate_query_span_extension_on_all_events(calendars, calendar_name):
    """Check that the calclulation succeeds."""
    for i, event in enumerate(calendars.raw[calendar_name].walk("VEVENT")):
        adapter = EventAdapter(event)
        start_delta, end_delta = adapter.extend_query_span_by
        message = f"{calendar_name}.VEVENT[{i}]"
        assert isinstance(start_delta, td), message
        assert isinstance(end_delta, td), message
        assert start_delta >= td(0), message
        assert end_delta >= td(0), message


def test_deletion_of_THISANDFUTURE_by_SEQUENCE():
    """We need to make sure that the components we have only work on what is actual."""
    pytest.skip("TODO")


def test_RDATE_with_PERIOD():
    """When an RDATE has a PERIOD, we can assume that that defines the new length."""
    pytest.skip("TODO")


@pytest.mark.parametrize(
    ("calendar_name", "event_index", "delta"),
    [
        ("one_event", 0, td(0)),
        ("same_event_recurring_at_same_time", 0, td(0)),
        ("issue_75_range_parameter", 1, td(hours=-3)),
        ("issue_75_range_parameter", 3, td(days=1, hours=2, minutes=22)),
    ],
)
def test_move_by_time(calendars, calendar_name, event_index, delta):
    """Check the moving of events."""
    cal: Calendar = calendars.raw[calendar_name]
    event = list(cal.walk("VEVENT"))[event_index]
    adapter = EventAdapter(event)
    assert adapter.move_recurrences_by == delta


# TODO: Test event with DTSTART = DATE - does it occur properly as it is
#       one day long, I believe. Loot at the RFC 5545.