11import pytest
22import os
3+ from collections import defaultdict
4+ from datetime import datetime , timedelta
35
46import requests
57from icalendar import Calendar
@@ -16,6 +18,19 @@ def find_ics_files(base_path='./'):
1618 ics_files .append (os .path .join (root , file ))
1719 return ics_files
1820
21+
22+ def find_ferien_ics_files (base_path = './' ):
23+ """Find only ICS files in 'Ferien' directories."""
24+ ics_files = []
25+ for root , _ , files in os .walk (base_path ):
26+ path_parts = set (os .path .normpath (root ).split (os .sep ))
27+ if 'Ferien' in path_parts :
28+ for file in files :
29+ if file .endswith ('.ics' ):
30+ ics_files .append (os .path .join (root , file ))
31+ return ics_files
32+
33+
1934@pytest .fixture (params = find_ics_files ())
2035def parsed_calendar (request ):
2136 path = request .param
@@ -26,6 +41,18 @@ def parsed_calendar(request):
2641 pytest .fail (f"Failed to parse ICS file { path } : { e } " )
2742 return cal , path
2843
44+
45+ @pytest .fixture (params = find_ferien_ics_files ())
46+ def parsed_ferien_calendar (request ):
47+ path = request .param
48+ with open (path , 'rb' ) as f :
49+ try :
50+ cal = Calendar .from_ical (f .read ())
51+ except Exception as e :
52+ pytest .fail (f"Failed to parse ICS file { path } : { e } " )
53+ return cal , path
54+
55+
2956def build_expected_calendar_name (ics_path : str ) -> str :
3057 dirname = os .path .basename (os .path .dirname (ics_path ))
3158 filename = os .path .splitext (os .path .basename (ics_path ))[0 ]
@@ -46,6 +73,85 @@ def test_calendar_name_matches_filename_with_suffix(parsed_calendar):
4673 assert cal_name == expected_name , f"Expected calendar name '{ expected_name } ', but found '{ cal_name } ' (NAME) in { path } "
4774 assert x_wr_cal_name == expected_name , f"Expected calendar name '{ expected_name } ', but found '{ x_wr_cal_name } ' (X-WR-CALNAME) in { path } "
4875
76+
77+ def test_no_close_events_with_same_title_in_ferien_calendars (parsed_ferien_calendar ):
78+ """Test that events with the same title have more than 1 day between end of one and start of next in Ferien calendars."""
79+ cal , path = parsed_ferien_calendar
80+
81+ events_by_title = defaultdict (list )
82+
83+ for component in cal .walk ():
84+ if component .name == "VEVENT" :
85+ summary = component .get ('SUMMARY' )
86+ dtstart = component .get ('DTSTART' )
87+ dtend = component .get ('DTEND' )
88+
89+ if summary is not None and dtstart is not None :
90+ title = str (summary )
91+
92+ start_date = dtstart .dt
93+ if hasattr (start_date , 'date' ):
94+ start_date = start_date .date ()
95+
96+ if dtend is not None :
97+ end_date = dtend .dt
98+ if hasattr (end_date , 'date' ):
99+ end_date = end_date .date ()
100+ else :
101+ end_date = start_date
102+
103+ events_by_title [title ].append ({
104+ 'start' : start_date ,
105+ 'end' : end_date
106+ })
107+
108+ close_events = []
109+
110+ for title , events in events_by_title .items ():
111+ if len (events ) > 1 :
112+ sorted_events = sorted (events , key = lambda x : x ['start' ])
113+
114+ for i in range (len (sorted_events ) - 1 ):
115+ event1 = sorted_events [i ]
116+ event2 = sorted_events [i + 1 ]
117+
118+ end_date1 = event1 ['end' ]
119+ start_date2 = event2 ['start' ]
120+
121+ if hasattr (end_date1 , 'toordinal' ) and hasattr (start_date2 , 'toordinal' ):
122+ days_diff = start_date2 .toordinal () - end_date1 .toordinal ()
123+ else :
124+ if hasattr (end_date1 , 'date' ):
125+ end_date1 = end_date1 .date ()
126+ if hasattr (start_date2 , 'date' ):
127+ start_date2 = start_date2 .date ()
128+ days_diff = (start_date2 - end_date1 ).days
129+
130+ if 0 <= days_diff <= 1 :
131+ close_events .append ({
132+ 'title' : title ,
133+ 'event1_start' : event1 ['start' ],
134+ 'event1_end' : event1 ['end' ],
135+ 'event2_start' : event2 ['start' ],
136+ 'event2_end' : event2 ['end' ],
137+ 'gap_days' : days_diff
138+ })
139+
140+ if close_events :
141+ error_messages = []
142+ for event in close_events :
143+ error_messages .append (
144+ f" '{ event ['title' ]} ': Event 1 ({ event ['event1_start' ]} - { event ['event1_end' ]} ) "
145+ f"and Event 2 ({ event ['event2_start' ]} - { event ['event2_end' ]} ) "
146+ f"have only { event ['gap_days' ]} days gap"
147+ )
148+
149+ error_info = "\n " .join (error_messages )
150+ pytest .fail (
151+ f"Events with same title found with 1 day or less gap in Ferien calendar { path } :\n { error_info } "
152+ )
153+
154+
49155@pytest .mark .parametrize ("ics_path" , find_ics_files ())
50156def test_icalendar_org_validator (ics_path ):
51157 with open (ics_path , 'rb' ) as f :
0 commit comments