File indexing completed on 2024-04-28 15:18:53

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"