File: test_issue_186_alarms.py

package info (click to toggle)
python-recurring-ical-events 3.8.0%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,584 kB
  • sloc: python: 4,476; makefile: 84
file content (331 lines) | stat: -rw-r--r-- 11,528 bytes parent folder | download
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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
"""Test VALARM recurrence.

VALARM is specified in RFC 5545 and RFC 9074.
See also https://github.com/niccokunzmann/python-recurring-ical-events/issues/186
"""

from datetime import datetime, timedelta, timezone

import icalendar
import pytest


@pytest.mark.parametrize(
    ("when", "count"),
    [
        ("20241003", 1),
        ((2024, 10, 3, 13), 1),
        ((2024, 10, 3, 13, 0), 1),
        ((2024, 10, 3, 12), 0),
        ((2024, 10, 3, 14), 0),
    ],
)
def test_can_find_absolute_alarm(alarms, when, count):
    """Find the absolute alarm."""
    a = alarms.alarm_absolute.at(when)
    assert len(a) == count
    if count == 1:
        e: icalendar.Event = a[0]
        assert len(e.alarms.times) == 1
        t = e.alarms.times[0]
        assert t.trigger == datetime(2024, 10, 3, 13, 0, 0, tzinfo=timezone.utc)


def test_edited_alarm_is_moved(alarms):
    """When an absolute alarm is edited, the old one does not occur."""
    assert len(alarms.alarm_absolute_edited.at("20241004")) == 1, "New alarm is found"
    assert len(alarms.alarm_absolute_edited.at("20241003")) == 0, "Old alarm is removed"


@pytest.mark.parametrize(
    ("when", "deltas"),
    [
        ("20241003", {0, 45, 90}),
        ((2024, 10, 3, 13), {0, 45}),
        ((2024, 10, 3, 13, 0), {0}),
        ((2024, 10, 3, 12), set()),
        ((2024, 10, 3, 14), {90}),
    ],
)
def test_can_find_absolute_alarm_with_repeat(alarms, when, deltas):
    """This absolute alarm has 2 repetitions in 45 min later."""
    a = alarms.alarm_absolute_repeat.at(when)
    deltas = {timedelta(minutes=m) for m in deltas}
    e_deltas = set()
    for e in a:
        assert len(e.alarms.times) == 1
        t = e.alarms.times[0]
        e_deltas.add(t.trigger - datetime(2024, 10, 3, 13, 0, 0, tzinfo=timezone.utc))
    assert e_deltas == deltas


@pytest.mark.parametrize("day", [17, 18, 19, 20])
def test_collect_alarms_from_todos_relative_to_start(alarms, day):
    """We also collect alarms from todos."""
    todos = alarms.alarm_removed_and_moved.at((2023, 12, day))
    assert len(todos) == 1
    todo = todos[0]
    assert len(todo.alarms.times) == 1
    alarm = todo.alarms.times[0]
    assert alarm.trigger.replace(tzinfo=None) == datetime(2023, 12, day, 8, 0)


@pytest.mark.parametrize("day", [17, 18, 19, 20])
def test_collect_todos_with_alarms(calendars, day):
    """We also collect alarms from todos."""
    calendars.components = ["VTODO"]
    todos = calendars.alarm_removed_and_moved.at((2023, 12, day))
    assert len(todos) == 1
    todo = todos[0]
    assert todo.start.replace(tzinfo=None) == datetime(2023, 12, day, 9, 0)


def test_collect_alarms_from_todos_relative_to_end(alarms):
    """We also collect alarms from todos."""
    todos = alarms.alarm_removed_and_moved.at((2023, 12, 16, 10))
    assert len(todos) == 1
    todo = todos[0]
    assert todo.start.replace(tzinfo=None) == datetime(2023, 12, 16, 9, 0)
    assert len(todo.alarms.times) == 1
    alarm = todo.alarms.times[0]
    assert alarm.trigger.replace(tzinfo=None) == datetime(2023, 12, 16, 10, 0)


def test_todo_occurs(calendars):
    """The todo should occur so we can find the alarm."""
    calendars.components = ["VTODO"]
    todos = calendars.alarm_removed_and_moved.at((2023, 12, 16, 9))
    for x in todos:
        print(x.to_ical().decode())
        print()
    assert len(todos) == 1
    todo = todos[0]
    assert todo.start.replace(tzinfo=None) == datetime(2023, 12, 16, 9, 0)


def test_collect_alarms_from_todos_absolute(alarms):
    """We also collect alarms from todos."""
    todos = alarms.alarm_removed_and_moved.at((2023, 12, 13, 18, 0))
    assert len(todos) == 1
    todo = todos[0]
    assert len(todo.alarms.times) == 1
    alarm = todo.alarms.times[0]
    assert alarm.trigger.replace(tzinfo=None) == datetime(2023, 12, 13, 18, 0)


def test_series_of_events_with_alarms_but_alarm_removed(alarms):
    """We test that an alarm is removed and does not turn up."""
    assert alarms.alarm_removed_and_moved.at("20241221") == []


def test_alarm_is_moved(alarms):
    """The alarm is moved to 30 min before."""
    a = alarms.alarm_removed_and_moved.at("20241222")
    assert len(a) == 1
    e = a[0]
    assert len(e.alarms.times) == 1
    alarm = e.alarms.times[0]
    assert alarm.trigger.hour == 8
    assert alarm.trigger.minute == 30


def test_event_is_moved(alarms):
    """The event has been moved but the alarm is still 1h before."""
    a = alarms.alarm_removed_and_moved.at("20241219")
    for x in a:
        print(x.to_ical().decode())
        print()
    assert len(a) == 1
    e = a[0]
    assert len(e.alarms.times) == 1
    alarm = e.alarms.times[0]
    assert alarm.trigger.hour == 11
    assert alarm.trigger.minute == 0


def test_series_of_events_with_alarms_but_alarm_removed_relative_to_end():
    pytest.skip("TODO - but probably covered by the calculation relative to start")


def test_series_of_events_with_alarms_but_alarm_edited_relative_to_end():
    pytest.skip("TODO - but probably covered by the calculation relative to start")


def test_series_of_events_with_alarm_relative_to_end(alarms):
    """We check alarms relative to the end and start.

    DTSTART;TZID=Europe/London:20241004T110000
    DTEND;TZID=Europe/London:20241004T114500

    15min before start&end
    15min after start&end

    """
    q = alarms.alarm_around_event_boundaries
    assert len(q.at((2024, 10, 4, 10, 45))) == 1, "15 min before start"
    assert len(q.at((2024, 10, 4, 11, 15))) == 1, "15 min after start"
    assert len(q.at((2024, 10, 4, 11, 30))) == 1, "15 min before end"
    assert len(q.at((2024, 10, 4, 12, 0))) == 1, "15 min after end"


@pytest.mark.parametrize(
    ("dt", "trigger"),
    [
        ("20241126", datetime(2024, 11, 26, 13, 0, 0)),
        ("20241127", datetime(2024, 11, 27, 13, 0, 0)),
        ("20241128", datetime(2024, 11, 28, 13, 0, 0)),
        ("20241129", datetime(2024, 11, 29, 13, 0, 0)),
        # narrow it down
        ((2024, 11, 28, 12), None),
        ((2024, 11, 28, 12, 30), None),
        ((2024, 11, 28, 13), datetime(2024, 11, 28, 13, 0, 0)),
        ((2024, 11, 28, 13, 0), datetime(2024, 11, 28, 13, 0, 0)),
        ((2024, 11, 28, 13, 0, 0), datetime(2024, 11, 28, 13, 0, 0)),
        ((2024, 11, 28, 13, 0, 1), None),
        ((2024, 11, 28, 14), None),
    ],
)
def test_series_of_event_with_alarm_relative_to_start(alarms, dt, trigger):
    """This series of events all are preceded by an alarm.

    The alarm occurs 1h before the event starts.
    In this test, we narrow down our query time to make sure we find it.
    """
    a = alarms.alarm_recurring_and_acknowledged_at_2024_11_27_16_27.at(dt)
    if trigger is None:
        assert len(a) == 0
        return
    assert len(a) == 1, f"{dt} has {len(a)} alarms"
    event = a[0]
    assert len(event.alarms.times) == 1
    only_trigger = event.alarms.times[0].trigger
    assert only_trigger.replace(tzinfo=None) == trigger
    assert icalendar.timezone.tzid_from_dt(only_trigger) == "Europe/London"


def test_alarm_without_trigger_is_ignored_as_invalid(alarms):
    """Alarms can be malformed in many ways. This skips a few possibilities."""
    alarms.skip_bad_series = True
    q = alarms.issue_186_invalid_trigger
    e = list(q.all())
    for a in e:
        assert len(a.alarms.times) == 1
        description = a.alarms.times[0].alarm["DESCRIPTION"]
        assert description in ("correct trigger", "absolute trigger")
    assert len(e) == 2


def test_event_is_not_modified_with_2_alarms(alarms):
    """The base event should not be modified."""
    q = alarms.alarm_1_week_before_event
    assert len(q.at("20241202")) == 1, "We find the alarm"
    assert len(q.at("20241202")) == 1, "We find the alarm again"
    assert len(q.at("20241207")) == 1, "We also find the other alarm"


def test_repeating_event_is_not_modified_with_repeating_alarm(alarms):
    """The base event should not be modified."""
    q = alarms.alarm_absolute_repeat
    assert len(list(q.all())) == 3, "We find the alarms"
    assert len(list(q.all())) == 3, "We find the alarm again"


def test_repeating_event_is_not_modified(alarms):
    """The base event should not be modified."""
    q = alarms.alarm_recurring_and_acknowledged_at_2024_11_27_16_27
    assert len(q.between("20241126", "20241130")) == 4, "We find the alarms"
    assert len(q.between("20241126", "20241130")) == 4, "We find the alarm again"


EXPECTED_TRIGGERS = [
    datetime(2024, 12, 18, 8, 0),
    datetime(2024, 12, 19, 11, 0),
    datetime(2024, 12, 20, 8, 0),
    # datetime(2024, 12, 21, 8, 0),  # event without alarm
    datetime(2024, 12, 22, 8, 30),
    datetime(2024, 12, 23, 8, 0),
]
EXPECTED_STARTS = [
    datetime(2024, 12, 18, 9, 0),
    datetime(2024, 12, 19, 12, 0),
    datetime(2024, 12, 20, 9, 0),
    # datetime(2024, 12, 21, 9, 0),  # event without alarm
    datetime(2024, 12, 22, 9, 0),
    datetime(2024, 12, 23, 9, 0),
]


def test_after_with_alarms(alarms):
    """The after function checks if an event was already returned.

    This is likely to cause problems because it should be there several times.
    """
    found_triggers = []
    i = 0
    it = alarms.alarm_removed_and_moved.after(2024)
    for expected_trigger, event, expected_start in zip(
        EXPECTED_TRIGGERS, it, EXPECTED_STARTS
    ):
        print(
            f"{i} start {event.start} is {('' if event.start.replace(tzinfo=None) == expected_start else 'NOT ')}as expected"
        )
        assert len(event.alarms.times) == 1
        trigger = event.alarms.times[0].trigger.replace(tzinfo=None)
        found_triggers.append(trigger)
        print(
            f"{i} trigger {trigger} is {('' if trigger == expected_trigger else 'NOT ')}as expected"
        )
        i += 1  # noqa: SIM113
        print()
    print("\n".join(map(str, zip(found_triggers, EXPECTED_TRIGGERS))))
    assert found_triggers == EXPECTED_TRIGGERS
    with pytest.raises(StopIteration):
        next(it)


def test_all_alarms_are_present(alarms):
    """Check that we find all alarms."""
    events = alarms.alarm_several_in_one.all()
    triggers = []
    for event in events:
        assert len(event.alarms.times) == 1
        triggers.append(event.alarms.times[0].trigger - event.start)
    assert triggers == [
        timedelta(hours=-1),
        timedelta(minutes=-15),
        timedelta(minutes=15),
        timedelta(hours=1),
        timedelta(hours=2),
        timedelta(hours=3),
    ]


def test_several_alarms_occur_for_a_slightly_different_event(alarms):
    """Edited subevents have all an alarm that occurs at the same time.

    Thus, they all should appear.
    """
    events = list(alarms.alarms_at_the_same_time.all())
    summaries = {event["SUMMARY"] for event in events}
    assert summaries == {
        "event with alarm at the same time 1",
        "event with alarm at the same time 2",
        "event with alarm at the same time 3",
    }


@pytest.mark.parametrize("dt", ["20241220", "20241221", "20241222"])
def test_different_alarms_at_the_same_time_merge_into_one(alarms, dt):
    """If an event has different alarms happening at the same time,

    these alarms are in the event.
    """
    events: list[icalendar.Event] = alarms.alarms_different_in_same_event.at(dt)
    alarm_names = {
        alarm_time.alarm["DESCRIPTION"]
        for event in events
        for alarm_time in event.alarms.times
    }
    assert alarm_names >= {"Alarm 1", "Alarm 2", "Alarm 3"}
    if dt == "20241220":
        assert "Alarm 4" in alarm_names