File indexing completed on 2024-03-24 03:55:27
0001 /* 0002 This file is part of the kcalcore library. 0003 0004 SPDX-FileCopyrightText: 2005-2007 David Jarvie <djarvie@kde.org> 0005 0006 SPDX-License-Identifier: LGPL-2.0-or-later 0007 */ 0008 0009 #include "testicaltimezones.h" 0010 #include "icaltimezones_p.h" 0011 0012 #include <QDateTime> 0013 0014 #include <QTest> 0015 0016 QTEST_MAIN(ICalTimeZonesTest) 0017 0018 extern "C" { 0019 #include <libical/ical.h> 0020 } 0021 using namespace KCalendarCore; 0022 0023 static icalcomponent *loadCALENDAR(const char *vcal); 0024 0025 // First daylight savings time has an end date, takes a break for a year, 0026 // and is then replaced by another 0027 static const char *VTZ_Western = 0028 "BEGIN:VTIMEZONE\r\n" 0029 "TZID:Test-Dummy-Western\r\n" 0030 "LAST-MODIFIED:19870101T000000Z\r\n" 0031 "TZURL:http://tz.reference.net/dummies/western\r\n" 0032 "LOCATION:Zedland/Tryburgh\r\n" 0033 "X-LIC-LOCATION:Wyland/Tryburgh\r\n" 0034 "BEGIN:STANDARD\r\n" 0035 "DTSTART:19671029T020000\r\n" 0036 "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 0037 "TZOFFSETFROM:-0400\r\n" 0038 "TZOFFSETTO:-0500\r\n" 0039 "TZNAME:WST\r\n" 0040 "END:STANDARD\r\n" 0041 "BEGIN:DAYLIGHT\r\n" 0042 "DTSTART:19870405T020000\r\n" 0043 "RRULE:FREQ=YEARLY;UNTIL=19970406T070000Z;BYDAY=1SU;BYMONTH=4\r\n" 0044 "TZOFFSETFROM:-0500\r\n" 0045 "TZOFFSETTO:-0400\r\n" 0046 "TZNAME:WDT1\r\n" 0047 "END:DAYLIGHT\r\n" 0048 "BEGIN:DAYLIGHT\r\n" 0049 "DTSTART:19990425T020000\r\n" 0050 "RDATE;VALUE=DATE-TIME:20000430T020000\r\n" 0051 "TZOFFSETFROM:-0500\r\n" 0052 "TZOFFSETTO:-0400\r\n" 0053 "TZNAME:WDT2\r\n" 0054 "END:DAYLIGHT\r\n" 0055 "END:VTIMEZONE\r\n"; 0056 0057 // Standard time only 0058 static const char *VTZ_other = 0059 "BEGIN:VTIMEZONE\r\n" 0060 "TZID:Test-Dummy-Other\r\n" 0061 "TZURL:http://tz.reference.net/dummies/other\r\n" 0062 "X-LIC-LOCATION:Wyland/Tryburgh\r\n" 0063 "BEGIN:STANDARD\r\n" 0064 "DTSTART:19500101T000000\r\n" 0065 "RDATE;VALUE=DATE-TIME:19500101T000000\r\n" 0066 "TZOFFSETFROM:+0000\r\n" 0067 "TZOFFSETTO:+0300\r\n" 0068 "TZNAME:OST\r\n" 0069 "END:STANDARD\r\n" 0070 "END:VTIMEZONE\r\n"; 0071 0072 static const char *VTZ_other_DST = 0073 "BEGIN:VTIMEZONE\r\n" 0074 "TZID:Test-Dummy-Other-DST\r\n" 0075 "BEGIN:STANDARD\r\n" 0076 "DTSTART:19500101T000000\r\n" 0077 "RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11\r\n" 0078 "TZOFFSETFROM:+0000\r\n" 0079 "TZOFFSETTO:+0300\r\n" 0080 "TZNAME:OST\r\n" 0081 "END:STANDARD\r\n" 0082 "BEGIN:DAYLIGHT\r\n" 0083 "DTSTART:19500501T000000\r\n" 0084 "RRULE:FREQ=YEARLY;BYDAY=3SU;BYMONTH=5\r\n" 0085 "TZOFFSETFROM:+0200\r\n" 0086 "TZOFFSETTO:+0500\r\n" 0087 "TZNAME:DST\r\n" 0088 "END:DAYLIGHT\r\n" 0089 "END:VTIMEZONE\r\n"; 0090 0091 static const char *VTZ_Prague = 0092 "BEGIN:VTIMEZONE\r\n" 0093 "TZID:Europe/Prague\r\n" 0094 "BEGIN:DAYLIGHT\r\n" 0095 "TZNAME:CEST\r\n" 0096 "TZOFFSETFROM:+0000\r\n" 0097 "TZOFFSETTO:+0200\r\n" 0098 "DTSTART:19790401T010000\r\n" 0099 "RDATE;VALUE=DATE-TIME:19790401T010000\r\n" 0100 "END:DAYLIGHT\r\n" 0101 "BEGIN:STANDARD\r\n" 0102 "TZNAME:CET\r\n" 0103 "TZOFFSETFROM:+0200\r\n" 0104 "TZOFFSETTO:+0100\r\n" 0105 "DTSTART:19971026T030000\r\n" 0106 "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 0107 "END:STANDARD\r\n" 0108 "BEGIN:STANDARD\r\n" 0109 "TZNAME:CET\r\n" 0110 "TZOFFSETFROM:+0200\r\n" 0111 "TZOFFSETTO:+0100\r\n" 0112 "DTSTART:19790930T030000\r\n" 0113 "RRULE:FREQ=YEARLY;UNTIL=19961027T030000;BYDAY=-1SU;BYMONTH=9\r\n" 0114 "RDATE;VALUE=DATE-TIME:19950924T030000\r\n" 0115 "END:STANDARD\r\n" 0116 "BEGIN:DAYLIGHT\r\n" 0117 "TZNAME:CEST\r\n" 0118 "TZOFFSETFROM:+0100\r\n" 0119 "TZOFFSETTO:+0200\r\n" 0120 "DTSTART:19810329T020000\r\n" 0121 "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\r\n" 0122 "END:DAYLIGHT\r\n" 0123 "BEGIN:DAYLIGHT\r\n" 0124 "TZNAME:CEST\r\n" 0125 "TZOFFSETFROM:+0100\r\n" 0126 "TZOFFSETTO:+0200\r\n" 0127 "DTSTART:19800406T020000\r\n" 0128 "RDATE;VALUE=DATE-TIME:19800406T020000\r\n" 0129 "END:DAYLIGHT\r\n" 0130 "END:VTIMEZONE\r\n"; 0131 0132 // When there's an extra transition from +0000 to +0100 0133 // in 1978 (FreeBSD and old Debian), we get one more 0134 // transition and slightly different RRULEs 0135 #ifdef Q_OS_FREEBSD 0136 static const char *VTZ_PragueExtra = 0137 "BEGIN:VTIMEZONE\r\n" 0138 "TZID:Europe/Prague\r\n" 0139 "BEGIN:STANDARD\r\n" 0140 "TZNAME:CET\r\n" 0141 "TZOFFSETFROM:+0000\r\n" 0142 "TZOFFSETTO:+0100\r\n" 0143 "DTSTART:19781231T230000\r\n" 0144 "RDATE:19781231T230000\r\n" 0145 "END:STANDARD\r\n" 0146 "BEGIN:DAYLIGHT\r\n" 0147 "TZNAME:CEST\r\n" 0148 "TZOFFSETFROM:+0100\r\n" 0149 "TZOFFSETTO:+0200\r\n" 0150 "DTSTART:19810329T020000\r\n" 0151 "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3\r\n" 0152 "END:DAYLIGHT\r\n" 0153 "BEGIN:DAYLIGHT\r\n" 0154 "TZNAME:CEST\r\n" 0155 "TZOFFSETFROM:+0100\r\n" 0156 "TZOFFSETTO:+0200\r\n" 0157 "DTSTART:19790401T020000\r\n" 0158 "RDATE:19790401T020000\r\n" 0159 "RDATE:19800406T020000\r\n" 0160 "END:DAYLIGHT\r\n" 0161 "BEGIN:STANDARD\r\n" 0162 "TZNAME:CET\r\n" 0163 "TZOFFSETFROM:+0200\r\n" 0164 "TZOFFSETTO:+0100\r\n" 0165 "DTSTART:19971026T030000\r\n" 0166 "RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10\r\n" 0167 "END:STANDARD\r\n" 0168 "BEGIN:STANDARD\r\n" 0169 "TZNAME:CET\r\n" 0170 "TZOFFSETFROM:+0200\r\n" 0171 "TZOFFSETTO:+0100\r\n" 0172 "DTSTART:19790930T030000\r\n" 0173 "RRULE:FREQ=YEARLY;UNTIL=19961027T030000;BYDAY=-1SU;BYMONTH=9\r\n" 0174 "RDATE:19950924T030000\r\n" 0175 "END:STANDARD\r\n" 0176 "END:VTIMEZONE\r\n"; 0177 #endif 0178 0179 // CALENDAR component header and footer 0180 static const char *calendarHeader = 0181 "BEGIN:VCALENDAR\r\n" 0182 "PRODID:-//Libkcal//NONSGML ICalTimeZonesTest//EN\r\n" 0183 "VERSION:2.0\r\n"; 0184 static const char *calendarFooter = "END:CALENDAR\r\n"; 0185 0186 /////////////////////////// 0187 // ICalTimeZoneSource tests 0188 /////////////////////////// 0189 0190 void ICalTimeZonesTest::initTestCase() 0191 { 0192 qputenv("TZ", "Europe/Zurich"); 0193 } 0194 0195 void ICalTimeZonesTest::parse_data() 0196 { 0197 QTest::addColumn<QByteArray>("vtimezone"); 0198 QTest::addColumn<QDateTime>("onDate"); 0199 QTest::addColumn<QByteArray>("origTz"); 0200 QTest::addColumn<QByteArray>("expTz"); 0201 0202 QTest::newRow("dummy-western") << QByteArray(VTZ_Western) << QDateTime{} << QByteArray("Test-Dummy-Western") << QByteArray("America/Toronto"); 0203 QTest::newRow("dummy-other") << QByteArray(VTZ_other) << QDateTime{} << QByteArray("Test-Dummy-Other") << QByteArray("UTC+03:00"); 0204 QTest::newRow("dummy-other-dst DST") << QByteArray(VTZ_other_DST) << QDateTime({2017, 03, 10}, {}) << QByteArray("Test-Dummy-Other-DST") 0205 << QByteArray("UTC+03:00"); 0206 QTest::newRow("dummy-other-dst STD") << QByteArray(VTZ_other_DST) << QDateTime({2017, 07, 05}, {}) << QByteArray("Test-Dummy-Other-DST") 0207 << QByteArray("UTC+05:00"); 0208 QTest::newRow("dummy-other-dst DST after") << QByteArray(VTZ_other_DST) << QDateTime({2017, 12, 24}, {}) << QByteArray("Test-Dummy-Other-DST") 0209 << QByteArray("UTC+03:00"); 0210 QTest::newRow("iana") << QByteArray() << QDateTime({2017, 9, 14}, {}) << QByteArray("Europe/Zurich") << QByteArray("Europe/Zurich"); 0211 } 0212 0213 void ICalTimeZonesTest::parse() 0214 { 0215 QFETCH(QByteArray, vtimezone); 0216 QFETCH(QDateTime, onDate); 0217 QFETCH(QByteArray, origTz); 0218 QFETCH(QByteArray, expTz); 0219 0220 QByteArray calText(calendarHeader); 0221 calText += vtimezone; 0222 calText += calendarFooter; 0223 0224 auto vcalendar = loadCALENDAR(calText.constData()); 0225 0226 ICalTimeZoneCache timezones; 0227 ICalTimeZoneParser parser(&timezones); 0228 parser.parse(vcalendar); 0229 0230 icalcomponent_free(vcalendar); 0231 0232 QCOMPARE(timezones.tzForTime(onDate, origTz).id(), expTz); 0233 } 0234 0235 void ICalTimeZonesTest::write() 0236 { 0237 /* By picking a date close to the TZ transition, we avoid 0238 * picking up FreeBSD's spurious transition at the end of 0239 * 1978 (see testPragueTransitions, below). The starting date 0240 * **was** 1970, which ought to get a starting TZ transition in 0241 * 1979 (the previous one was 1949, which is out-of-scope). 0242 * However, that gets one extra transition of FreeBSD, 0243 * which fails the test. 0244 */ 0245 { 0246 auto vtimezone = ICalTimeZoneParser::vcaltimezoneFromQTimeZone(QTimeZone("Europe/Prague"), QDateTime({1979, 2, 1}, {0, 0})); 0247 QCOMPARE(vtimezone, QByteArray(VTZ_Prague).replace(";VALUE=DATE-TIME", "")); // krazy:exclude=doublequote_chars 0248 } 0249 0250 /* By picking a date which overlaps the spurious TZ transition, 0251 * we get a different output, but only on FreeBSD (and old Debian). 0252 */ 0253 { 0254 auto vtimezone = ICalTimeZoneParser::vcaltimezoneFromQTimeZone(QTimeZone("Europe/Prague"), QDateTime({1970, 1, 1}, {0, 0})); 0255 #ifdef Q_OS_FREEBSD 0256 // The result is quite different: besides the extra 0257 // transition, the RRULEs that are generated differ as well. 0258 auto expect = QByteArray(VTZ_PragueExtra); 0259 #else 0260 auto expect = QByteArray(VTZ_Prague); 0261 #endif 0262 expect.replace(";VALUE=DATE-TIME", ""); // krazy:exclude=doublequote_chars 0263 QCOMPARE(vtimezone, expect); 0264 } 0265 } 0266 0267 icalcomponent *loadCALENDAR(const char *vcal) 0268 { 0269 icalcomponent *calendar = icalcomponent_new_from_string(const_cast<char *>(vcal)); 0270 if (calendar) { 0271 if (icalcomponent_isa(calendar) == ICAL_VCALENDAR_COMPONENT) { 0272 return calendar; 0273 } 0274 icalcomponent_free(calendar); 0275 } 0276 return nullptr; 0277 } 0278 0279 void ICalTimeZonesTest::testPragueTransitions() 0280 { 0281 QTimeZone prague("Europe/Prague"); 0282 QVERIFY(prague.isValid()); 0283 0284 /* The transitions for Prague, according to tzdata version 2020a, 0285 * from 1949 to 1979, are the following, from the command 0286 * `cd /usr/share/zoneinfo ; zdump -v Europe/Prague | grep 19[47]9` 0287 * It was manually verified that there were no transitions in 0288 * intermediate years. 0289 * 0290 * There are therefore 2 transitions between june 1949 and june 1979: 0291 * - fall back to CET in october 1949 0292 * - spring forward to CEST in april 1979 0293 */ 0294 /* 0295 Europe/Prague Sat Apr 9 00:59:59 1949 UTC = Sat Apr 9 01:59:59 1949 CET isdst=0 gmtoff=3600 0296 Europe/Prague Sat Apr 9 01:00:00 1949 UTC = Sat Apr 9 03:00:00 1949 CEST isdst=1 gmtoff=7200 0297 Europe/Prague Sun Oct 2 00:59:59 1949 UTC = Sun Oct 2 02:59:59 1949 CEST isdst=1 gmtoff=7200 0298 Europe/Prague Sun Oct 2 01:00:00 1949 UTC = Sun Oct 2 02:00:00 1949 CET isdst=0 gmtoff=3600 0299 Europe/Prague Sun Apr 1 00:59:59 1979 UTC = Sun Apr 1 01:59:59 1979 CET isdst=0 gmtoff=3600 0300 Europe/Prague Sun Apr 1 01:00:00 1979 UTC = Sun Apr 1 03:00:00 1979 CEST isdst=1 gmtoff=7200 0301 Europe/Prague Sun Sep 30 00:59:59 1979 UTC = Sun Sep 30 02:59:59 1979 CEST isdst=1 gmtoff=7200 0302 Europe/Prague Sun Sep 30 01:00:00 1979 UTC = Sun Sep 30 02:00:00 1979 CET isdst=0 gmtoff=3600 0303 */ 0304 0305 const auto &transitions = prague.transitions(QDateTime({1949, 6, 6}, {0, 0}), QDateTime({1979, 6, 6}, {0, 0})); 0306 QVERIFY(transitions.count() > 0); 0307 const auto &earliest = transitions.first().atUtc; 0308 QCOMPARE(earliest.date(), QDate(1949, 10, 2)); 0309 QCOMPARE(transitions.last().atUtc.date(), QDate(1979, 4, 1)); 0310 0311 bool occursIn1978 = false; 0312 for (const auto &transition : transitions) { 0313 if (transition.atUtc.date().year() == 1978) { 0314 occursIn1978 = true; 0315 } 0316 } 0317 0318 /* Except that the FreeBSD zic (zone-info-compiler, which is separate from 0319 * zdump) produces a slightly different output file than zic on Linux, 0320 * for instance they have different sizes: 0321 * 2312 Prague-freebsd 0322 * 2338 Prague-linux 0323 * The `zdump -v` output is the same for the two files. The Linux version 0324 * contains one extra tzh_ttisgmtcnt and one extra tzh_ttisstdcnt entry. 0325 * 0326 * Whatever the precise cause, on loading the TZ file, on FreeBSD 0327 * Qt deduces an extra transition, at the end of 1978, with no change in 0328 * offzet or zone name: 0329 * QDateTime(1949-10-02 01:00:00.000 UTC Qt::UTC) "CET" 3600 0330 * QDateTime(1978-12-31 23:00:00.000 UTC Qt::UTC) "CET" 3600 0331 * QDateTime(1979-04-01 01:00:00.000 UTC Qt::UTC) "CEST" 7200 0332 * 0333 * This additional transition makes the test fail. 0334 */ 0335 #ifdef Q_OS_FREEBSD 0336 QEXPECT_FAIL("", "FreeBSD (and old Debian) zic produces extra transition", Continue); 0337 #endif 0338 QCOMPARE(transitions.count(), 2); 0339 0340 #ifdef Q_OS_FREEBSD 0341 QEXPECT_FAIL("", "FreeBSD (and old Debian) zic produces extra transition", Continue); 0342 #endif 0343 QVERIFY(!occursIn1978); 0344 } 0345 0346 #include "moc_testicaltimezones.cpp"