File indexing completed on 2024-05-12 05:13:47

0001 /*
0002     SPDX-FileCopyrightText: 2018 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "modelverificationpoint.h"
0008 #include "testhelper.h"
0009 
0010 #include "applicationcontroller.h"
0011 #include "favoritelocationmodel.h"
0012 #include "livedatamanager.h"
0013 #include "locationinformation.h"
0014 #include "pkpassmanager.h"
0015 #include "reservationmanager.h"
0016 #include "timelinemodel.h"
0017 #include "tripgroupmanager.h"
0018 #include "transfermanager.h"
0019 #include "weatherinformation.h"
0020 
0021 #include "weatherforecast.h"
0022 #include "weatherforecastmanager.h"
0023 
0024 #include <KItinerary/Flight>
0025 #include <KItinerary/Place>
0026 #include <KItinerary/Reservation>
0027 
0028 #include <QAbstractItemModelTester>
0029 #include <QDirIterator>
0030 #include <QUrl>
0031 #include <QtTest/qtest.h>
0032 #include <QSignalSpy>
0033 #include <QStandardPaths>
0034 
0035 void initLocale()
0036 {
0037     qputenv("LC_ALL", "en_US.utf-8");
0038     qputenv("LANG", "en_US");
0039     qputenv("TZ", "UTC");
0040 }
0041 
0042 Q_CONSTRUCTOR_FUNCTION(initLocale)
0043 
0044 class TimelineModelTest : public QObject
0045 {
0046     Q_OBJECT
0047 private Q_SLOTS:
0048     void initTestCase()
0049     {
0050         QStandardPaths::setTestModeEnabled(true);
0051     }
0052 
0053     void init()
0054     {
0055         TripGroupManager::clear();
0056     }
0057 
0058     void testElementCompare_data()
0059     {
0060         QTest::addColumn<TimelineElement>("lhs");
0061         QTest::addColumn<TimelineElement>("rhs");
0062 
0063         const auto dt = QDateTime::currentDateTimeUtc();
0064 
0065         TimelineElement group(nullptr, TimelineElement::TripGroup, dt);
0066         group.rangeType = TimelineElement::RangeBegin;
0067         QTest::newRow("transfer-group-begin") << group << TimelineElement(nullptr, TimelineElement::Transfer, dt);
0068         group.rangeType = TimelineElement::RangeEnd;
0069         QTest::newRow("transfer-group-end") << TimelineElement(nullptr, TimelineElement::Transfer, dt) << group;
0070 
0071         TimelineElement locInfo(nullptr, TimelineElement::LocationInfo, dt);
0072         TimelineElement hotel(nullptr, TimelineElement::Hotel, dt);
0073         hotel.rangeType = TimelineElement::RangeBegin;
0074         QTest::newRow("locinfo-before-hotel") << locInfo << hotel;
0075         TimelineElement flight(nullptr, TimelineElement::Flight, dt);
0076         QTest::newRow("locinfo-before-flight") << locInfo << flight;
0077 
0078         QTest::newRow("location-info-before-transfer") << TimelineElement(nullptr, TimelineElement::LocationInfo, dt) << TimelineElement(nullptr, TimelineElement::Transfer, dt);
0079     }
0080 
0081     void testElementCompare()
0082     {
0083         QFETCH(TimelineElement, lhs);
0084         QFETCH(TimelineElement, rhs);
0085         QCOMPARE(lhs < rhs, true);
0086         QCOMPARE(rhs < lhs, false);
0087     }
0088 
0089     void testModel()
0090     {
0091         PkPassManager mgr;
0092         Test::clearAll(&mgr);
0093         ReservationManager resMgr;
0094         Test::clearAll(&resMgr);
0095         auto ctrl = Test::makeAppController();
0096         ctrl->setPkPassManager(&mgr);
0097         ctrl->setReservationManager(&resMgr);
0098 
0099         TimelineModel model;
0100         QAbstractItemModelTester tester(&model);
0101         model.setReservationManager(&resMgr);
0102 
0103         QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted);
0104         QVERIFY(insertSpy.isValid());
0105         QSignalSpy updateSpy(&model, &TimelineModel::dataChanged);
0106         QVERIFY(updateSpy.isValid());
0107         QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved);
0108         QVERIFY(rmSpy.isValid());
0109 
0110         QCOMPARE(model.rowCount(), 1);
0111         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0112         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/boardingpass-v1.pkpass")));
0113         QCOMPARE(insertSpy.size(), 1);
0114         QCOMPARE(insertSpy.at(0).at(1).toInt(), 0);
0115         QCOMPARE(insertSpy.at(0).at(2).toInt(), 0);
0116         QVERIFY(updateSpy.isEmpty());
0117         QCOMPARE(model.rowCount(), 2);
0118         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0119 
0120         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/boardingpass-v2.pkpass")));
0121         QCOMPARE(insertSpy.size(), 1);
0122         QCOMPARE(updateSpy.size(), 1);
0123         QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 0);
0124         QCOMPARE(model.rowCount(), 2);
0125 
0126         Test::clearAll(&resMgr);
0127         QCOMPARE(insertSpy.size(), 1);
0128         QCOMPARE(updateSpy.size(), 1);
0129         QCOMPARE(rmSpy.size(), 1);
0130         QCOMPARE(model.rowCount(), 1);
0131     }
0132 
0133     void testNestedElements()
0134     {
0135         ReservationManager resMgr;
0136         Test::clearAll(&resMgr);
0137         auto ctrl = Test::makeAppController();
0138         ctrl->setReservationManager(&resMgr);
0139 
0140         TimelineModel model;
0141         QAbstractItemModelTester tester(&model);
0142         model.setHomeCountryIsoCode(QStringLiteral("DE"));
0143         model.setReservationManager(&resMgr);
0144 
0145         QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted);
0146         QVERIFY(insertSpy.isValid());
0147         QSignalSpy updateSpy(&model, &TimelineModel::dataChanged);
0148         QVERIFY(updateSpy.isValid());
0149         QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved);
0150         QVERIFY(rmSpy.isValid());
0151 
0152         QCOMPARE(model.rowCount(), 1);
0153         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0154         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/haus-randa-v1.json")));
0155         QCOMPARE(insertSpy.size(), 3);
0156         QCOMPARE(insertSpy.at(0).at(1).toInt(), 0);
0157         QCOMPARE(insertSpy.at(0).at(2).toInt(), 0);
0158         QCOMPARE(insertSpy.at(1).at(1).toInt(), 1);
0159         QCOMPARE(insertSpy.at(1).at(2).toInt(), 1);
0160         QVERIFY(updateSpy.isEmpty());
0161         QCOMPARE(model.rowCount(), 4);
0162         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::LocationInfo);
0163         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Hotel);
0164         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementRangeRole), TimelineElement::RangeBegin);
0165         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Hotel);
0166         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementRangeRole), TimelineElement::RangeEnd);
0167 
0168         // move end date of a hotel booking: dataChanged on RangeBegin, move (or del/ins) on RangeEnd
0169         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/haus-randa-v2.json")));
0170         QCOMPARE(insertSpy.size(), 5);
0171         QCOMPARE(updateSpy.size(), 1);
0172         QCOMPARE(rmSpy.size(), 2);
0173         QCOMPARE(updateSpy.at(0).at(0).toModelIndex().row(), 1);
0174         QCOMPARE(insertSpy.at(2).at(1).toInt(), 0);
0175         QCOMPARE(insertSpy.at(2).at(2).toInt(), 0);
0176         QCOMPARE(rmSpy.at(0).at(1), 2);
0177         QCOMPARE(model.rowCount(), 4);
0178 
0179         // delete a split element
0180         const auto resId = model.data(model.index(1, 0), TimelineModel::BatchIdRole).toString();
0181         QVERIFY(!resId.isEmpty());
0182         resMgr.removeReservation(resId);
0183         QCOMPARE(rmSpy.size(), 5);
0184         QCOMPARE(model.rowCount(), 1);
0185         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0186     }
0187 
0188     void testCountryInfos()
0189     {
0190         ReservationManager resMgr;
0191         Test::clearAll(&resMgr);
0192         auto ctrl = Test::makeAppController();
0193         ctrl->setReservationManager(&resMgr);
0194 
0195         TimelineModel model;
0196         QAbstractItemModelTester tester(&model);
0197         model.setHomeCountryIsoCode(QStringLiteral("DE"));
0198         model.setReservationManager(&resMgr);
0199 
0200         QCOMPARE(model.rowCount(), 1);
0201         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0202 
0203         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/flight-txl-lhr-sfo.json")));
0204         QCOMPARE(model.rowCount(), 5); //  2x country info, 2x flights, today marker
0205 
0206         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0207         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::LocationInfo);
0208         auto countryInfo = model.index(1, 0).data(TimelineModel::LocationInformationRole).value<LocationInformation>();
0209         QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Left);
0210         QCOMPARE(countryInfo.drivingSideDiffers(), true);
0211         QCOMPARE(countryInfo.powerPlugCompatibility(), LocationInformation::Incompatible);
0212 
0213         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0214         QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineElement::LocationInfo);
0215         countryInfo = model.index(3, 0).data(TimelineModel::LocationInformationRole).value<LocationInformation>();
0216         QCOMPARE(countryInfo.drivingSide(), KItinerary::KnowledgeDb::DrivingSide::Right);
0217         QCOMPARE(countryInfo.drivingSideDiffers(), false);
0218         QCOMPARE(countryInfo.powerPlugCompatibility(), LocationInformation::Incompatible);
0219         QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0220 
0221         // remove the GB flight should also remove the GB country info
0222         auto resId = model.index(0, 0).data(TimelineModel::BatchIdRole).toString();
0223         resMgr.removeReservation(resId);
0224         QCOMPARE(model.rowCount(), 3);
0225         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0226         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::LocationInfo);
0227         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0228 
0229         // remove the US flight should also remove the US country info
0230         resId = model.index(0, 0).data(TimelineModel::BatchIdRole).toString();
0231         resMgr.removeReservation(resId);
0232         QCOMPARE(model.rowCount(), 1);
0233         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0234     }
0235 
0236     void testWeatherElements()
0237     {
0238         using namespace KItinerary;
0239 
0240         ReservationManager resMgr;
0241         Test::clearAll(&resMgr);
0242         WeatherForecastManager weatherMgr;
0243         weatherMgr.setTestModeEnabled(true);
0244 
0245         TimelineModel model;
0246         QAbstractItemModelTester tester(&model);
0247         model.setReservationManager(&resMgr);
0248         model.setWeatherForecastManager(&weatherMgr);
0249         QCOMPARE(model.rowCount(), 1); // no weather data, as we don't know where we are
0250 
0251         // Add an element that will result in a defined location
0252         GeoCoordinates geo;
0253         geo.setLatitude(52.0f);
0254         geo.setLongitude(13.0f);
0255         Airport a;
0256         a.setGeo(geo);
0257         Flight f;
0258         f.setArrivalAirport(a);
0259         f.setDepartureTime(QDateTime(QDate(2018, 1, 1), QTime(0, 0)));
0260         FlightReservation res;
0261         res.setReservationFor(f);
0262         resMgr.addReservation(res);
0263 
0264         // if there's only <1h left in a day, we wont get a weather element
0265         auto currentDate = QDate::currentDate();
0266         if (QDateTime::currentDateTimeUtc().time().hour() >= 23) {
0267             currentDate = currentDate.addDays(1);
0268         }
0269 
0270         QCOMPARE(model.rowCount(), 11); // 1x flight, 1x today, 9x weather
0271         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0272         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0273         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::WeatherForecast);
0274         auto fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0275         QVERIFY(fc.isValid());
0276         QCOMPARE(fc.dateTime().date(), currentDate);
0277         QCOMPARE(fc.minimumTemperature(), 13.0f);
0278         QCOMPARE(fc.maximumTemperature(), 52.0f);
0279         QCOMPARE(model.index(10, 0).data(TimelineModel::ElementTypeRole), TimelineElement::WeatherForecast);
0280         fc = model.index(10, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0281         QVERIFY(fc.isValid());
0282         QCOMPARE(fc.dateTime().date(), currentDate.addDays(8));
0283 
0284         // Add a flight one day from now changing location mid-day
0285         geo.setLatitude(46.0f);
0286         geo.setLongitude(8.0f);
0287         a.setGeo(geo);
0288         f.setArrivalAirport(a);
0289         f.setDepartureTime(QDateTime(currentDate.addDays(1), QTime(12, 0)));
0290         f.setArrivalTime(QDateTime(currentDate.addDays(1), QTime(14, 0)));
0291         res.setReservationFor(f);
0292         resMgr.addReservation(res);
0293 
0294         QCOMPARE(model.rowCount(), 13); // 2x flight, 1x today, 10x weather
0295         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0296         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0297         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::WeatherForecast);
0298         fc = model.index(2, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0299         QVERIFY(fc.isValid());
0300         QCOMPARE(fc.dateTime().date(), currentDate);
0301         QCOMPARE(model.index(3, 0).data(TimelineModel::ElementTypeRole), TimelineElement::WeatherForecast);
0302         fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0303         QVERIFY(fc.isValid());
0304         QCOMPARE(fc.minimumTemperature(), 13.0f);
0305         QCOMPARE(fc.maximumTemperature(), 52.0f);
0306         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(1), QTime(0, 0)));
0307         QCOMPARE(model.index(4, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0308         QCOMPARE(model.index(5, 0).data(TimelineModel::ElementTypeRole), TimelineElement::WeatherForecast);
0309         fc = model.index(5, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0310         QVERIFY(fc.isValid());
0311         QCOMPARE(fc.minimumTemperature(), 8.0f);
0312         QCOMPARE(fc.maximumTemperature(), 46.0f);
0313         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(1), QTime(14, 0)));
0314         QCOMPARE(model.index(6, 0).data(TimelineModel::ElementTypeRole), TimelineElement::WeatherForecast);
0315         fc = model.index(6, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0316         QCOMPARE(fc.minimumTemperature(), 8.0f);
0317         QCOMPARE(fc.maximumTemperature(), 46.0f);
0318         QVERIFY(fc.isValid());
0319         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(2), QTime(0, 0)));
0320 
0321         // check we get update signals for all weather elements
0322         QSignalSpy spy(&model, &TimelineModel::dataChanged);
0323         QVERIFY(spy.isValid());
0324         Q_EMIT weatherMgr.forecastUpdated();
0325         QCOMPARE(spy.size(), 10);
0326 
0327         fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0328         QVERIFY(fc.isValid());
0329         QCOMPARE(fc.minimumTemperature(), 13.0f);
0330         QCOMPARE(fc.maximumTemperature(), 52.0f);
0331         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(1), QTime(0, 0)));
0332         fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0333         QVERIFY(fc.isValid());
0334         QCOMPARE(fc.minimumTemperature(), 8.0f);
0335         QCOMPARE(fc.maximumTemperature(), 46.0f);
0336         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(5), QTime(0, 0)));
0337 
0338         // add a location change far in the future, this must not change anything
0339         geo.setLatitude(60.0f);
0340         geo.setLongitude(11.0f);
0341         a.setGeo(geo);
0342         f.setArrivalAirport(a);
0343         f.setDepartureTime(QDateTime(currentDate.addYears(1), QTime(6, 0)));
0344         res.setReservationFor(f);
0345         resMgr.addReservation(res);
0346         QCOMPARE(model.rowCount(), 14);
0347 
0348         fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0349         QVERIFY(fc.isValid());
0350         QCOMPARE(fc.minimumTemperature(), 13.0f);
0351         QCOMPARE(fc.maximumTemperature(), 52.0f);
0352         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(1), QTime(0, 0)));
0353         fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0354         QVERIFY(fc.isValid());
0355         QCOMPARE(fc.minimumTemperature(), 8.0f);
0356         QCOMPARE(fc.maximumTemperature(), 46.0f);
0357         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(5), QTime(0, 0)));
0358 
0359         // result is the same when data hasn't been added incrementally
0360         model.setReservationManager(nullptr);
0361         model.setReservationManager(&resMgr);
0362         QCOMPARE(model.rowCount(), 14);
0363 
0364         fc = model.index(3, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0365         QVERIFY(fc.isValid());
0366         QCOMPARE(fc.minimumTemperature(), 13.0f);
0367         QCOMPARE(fc.maximumTemperature(), 52.0f);
0368         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(1), QTime(0, 0)));
0369         fc = model.index(9, 0).data(TimelineModel::WeatherForecastRole).value<WeatherInformation>().forecast;
0370         QVERIFY(fc.isValid());
0371         QCOMPARE(fc.minimumTemperature(), 8.0f);
0372         QCOMPARE(fc.maximumTemperature(), 46.0f);
0373         QCOMPARE(fc.dateTime(), QDateTime(currentDate.addDays(5), QTime(0, 0)));
0374 
0375         // clean up
0376         auto resId = model.index(13, 0).data(TimelineModel::BatchIdRole).toString();
0377         resMgr.removeReservation(resId);
0378         resId = model.index(4, 0).data(TimelineModel::BatchIdRole).toString();
0379         resMgr.removeReservation(resId);
0380         QCOMPARE(model.rowCount(), 11);
0381 
0382         // test case: two consecutive location changes, the first one to an unknown location
0383         // result: the weather element before the first location change ends with the start of that
0384         // result 2: we get a second weather element the same day after the second location change
0385         // TODO
0386     }
0387 
0388     // multi-day event at different location from last known location, but no explicit transfers/location changes
0389     // should produce weather elements on event site
0390     void testWeatherNoLocationChange()
0391     {
0392         ReservationManager resMgr;
0393         Test::clearAll(&resMgr);
0394         auto ctrl = Test::makeAppController();
0395         ctrl->setReservationManager(&resMgr);
0396         WeatherForecastManager weatherMgr;
0397         weatherMgr.setTestModeEnabled(true);
0398 
0399         TimelineModel model;
0400         QAbstractItemModelTester tester(&model);
0401         model.setHomeCountryIsoCode(QStringLiteral("DE"));
0402         model.setCurrentDateTime(QDateTime({2023, 3, 16}, {12, 34}));
0403         model.setReservationManager(&resMgr);
0404         model.setWeatherForecastManager(&weatherMgr);
0405 
0406         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/weather-no-location-change.json")));
0407         ModelVerificationPoint vp0(QLatin1StringView(SOURCE_DIR "/data/weather-no-location-change.model"));
0408         vp0.setRoleFilter({TimelineModel::BatchIdRole});
0409         QVERIFY(vp0.verify(&model));
0410     }
0411 
0412     void testMultiTraveller()
0413     {
0414         using namespace KItinerary;
0415 
0416         ReservationManager resMgr;
0417         Test::clearAll(&resMgr);
0418         auto ctrl = Test::makeAppController();
0419         ctrl->setReservationManager(&resMgr);
0420         TimelineModel model;
0421         QAbstractItemModelTester tester(&model);
0422         model.setReservationManager(&resMgr);
0423         QCOMPARE(model.rowCount(), 1); // 1x TodayMarker
0424 
0425         QSignalSpy insertSpy(&model, &TimelineModel::rowsInserted);
0426         QVERIFY(insertSpy.isValid());
0427         QSignalSpy updateSpy(&model, &TimelineModel::dataChanged);
0428         QVERIFY(updateSpy.isValid());
0429         QSignalSpy rmSpy(&model, &TimelineModel::rowsRemoved);
0430         QVERIFY(rmSpy.isValid());
0431 
0432         // full import at runtime
0433         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/google-multi-passenger-flight.json")));
0434         QCOMPARE(model.rowCount(), 3); // 2x Flight, 1x TodayMarger
0435         QCOMPARE(insertSpy.count(), 2);
0436         QCOMPARE(updateSpy.count(), 2);
0437         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0438         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0439         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0440         QCOMPARE(resMgr.reservationsForBatch(model.index(0, 0).data(TimelineModel::BatchIdRole).toString()).size(), 2);
0441         QCOMPARE(resMgr.reservationsForBatch(model.index(1, 0).data(TimelineModel::BatchIdRole).toString()).size(), 2);
0442 
0443         // already existing data
0444         model.setReservationManager(nullptr);
0445         model.setReservationManager(&resMgr);
0446         QCOMPARE(model.rowCount(), 3); // 2x Flight, 1x TodayMarger
0447         QCOMPARE(model.index(0, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0448         QCOMPARE(model.index(1, 0).data(TimelineModel::ElementTypeRole), TimelineElement::Flight);
0449         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TodayMarker);
0450         QCOMPARE(resMgr.reservationsForBatch(model.index(0, 0).data(TimelineModel::BatchIdRole).toString()).size(), 2);
0451         QCOMPARE(resMgr.reservationsForBatch(model.index(1, 0).data(TimelineModel::BatchIdRole).toString()).size(), 2);
0452 
0453         // update splits element
0454         updateSpy.clear();
0455         insertSpy.clear();
0456         auto resId = model.index(1, 0).data(TimelineModel::BatchIdRole).toString();
0457         QVERIFY(!resId.isEmpty());
0458         auto res = resMgr.reservation(resId).value<FlightReservation>();
0459         auto flight = res.reservationFor().value<Flight>();
0460         flight.setDepartureTime(flight.departureTime().addDays(1));
0461         res.setReservationFor(flight);
0462         resMgr.updateReservation(resId, res);
0463         QCOMPARE(model.rowCount(), 4);
0464         QCOMPARE(updateSpy.count(), 1);
0465         QCOMPARE(insertSpy.count(), 1);
0466         QCOMPARE(rmSpy.count(), 0);
0467 
0468         // update merges two elements
0469         updateSpy.clear();
0470         insertSpy.clear();
0471         rmSpy.clear();
0472         flight.setDepartureTime(flight.departureTime().addDays(-1));
0473         res.setReservationFor(flight);
0474         resMgr.updateReservation(resId, res);
0475         QCOMPARE(model.rowCount(), 3);
0476         QCOMPARE(updateSpy.count(), 1);
0477         QCOMPARE(rmSpy.count(), 1);
0478         QCOMPARE(insertSpy.count(), 0);
0479 
0480         // removal of merged items
0481         updateSpy.clear();
0482         rmSpy.clear();
0483         Test::clearAll(&resMgr);
0484         QCOMPARE(model.rowCount(), 1);
0485         QCOMPARE(rmSpy.count(), 2);
0486         QCOMPARE(updateSpy.count(), 2);
0487     }
0488 
0489     void testDayChange()
0490     {
0491         ReservationManager resMgr;
0492         Test::clearAll(&resMgr);
0493         auto ctrl = Test::makeAppController();
0494         ctrl->setReservationManager(&resMgr);
0495         WeatherForecastManager weatherMgr;
0496         weatherMgr.setTestModeEnabled(true);
0497 
0498         TimelineModel model;
0499         QAbstractItemModelTester tester(&model);
0500         model.setHomeCountryIsoCode(QStringLiteral("DE"));
0501         model.setCurrentDateTime(QDateTime({2196, 10, 14}, {12, 34}));
0502         model.setReservationManager(&resMgr);
0503         model.setWeatherForecastManager(&weatherMgr);
0504 
0505         ModelVerificationPoint vp0(QLatin1StringView(SOURCE_DIR "/data/timeline/daychange-r0.model"));
0506         vp0.setRoleFilter({TimelineModel::BatchIdRole});
0507         QVERIFY(vp0.verify(&model));
0508 
0509         // changing the day should move the today marker
0510         model.setCurrentDateTime(QDateTime({2196, 10, 15}, {0, 15}));
0511         ModelVerificationPoint vp1(QLatin1StringView(SOURCE_DIR "/data/timeline/daychange-r1.model"));
0512         vp1.setRoleFilter({TimelineModel::BatchIdRole});
0513         QVERIFY(vp1.verify(&model));
0514 
0515         // load something to define the current location, so we get weather
0516         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/flight-txl-lhr-sfo.json")));
0517         ModelVerificationPoint vp2(QLatin1StringView(SOURCE_DIR "/data/timeline/daychange-r2.model"));
0518         vp2.setRoleFilter({TimelineModel::BatchIdRole});
0519         QVERIFY(vp2.verify(&model));
0520 
0521         // changing the day should move the today marker and weather one day forward
0522         model.setCurrentDateTime(QDateTime({2196, 10, 16}, {19, 30}));
0523         ModelVerificationPoint vp3(QLatin1StringView(SOURCE_DIR "/data/timeline/daychange-r3.model"));
0524         vp3.setRoleFilter({TimelineModel::BatchIdRole});
0525         QVERIFY(vp3.verify(&model));
0526     }
0527 
0528     void testContent_data()
0529     {
0530         QTest::addColumn<QString>("baseName");
0531 
0532         QDirIterator it(QLatin1StringView(SOURCE_DIR "/data/timeline/"), {QLatin1StringView("*.json")});
0533         while (it.hasNext()) {
0534             it.next();
0535             const auto baseName = it.fileInfo().baseName();
0536             QTest::newRow(baseName.toUtf8().constData()) << baseName;
0537         }
0538     }
0539 
0540     void testContent()
0541     {
0542         QFETCH(QString, baseName);
0543         ReservationManager resMgr;
0544         Test::clearAll(&resMgr);
0545         auto ctrl = Test::makeAppController();
0546         ctrl->setReservationManager(&resMgr);
0547         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/timeline/") + baseName + QLatin1StringView(".json")));
0548         TripGroupManager groupMgr;
0549         groupMgr.setReservationManager(&resMgr);
0550         WeatherForecastManager weatherMgr;
0551         weatherMgr.setTestModeEnabled(true);
0552 
0553         FavoriteLocationModel favLocModel;
0554         while (favLocModel.rowCount()) {
0555             favLocModel.removeLocation(0);
0556         }
0557         FavoriteLocation favLoc;
0558         favLoc.setLatitude(52.51860f);
0559         favLoc.setLongitude(13.37630f);
0560         favLoc.setName(QStringLiteral("Home"));
0561         favLocModel.setFavoriteLocations({favLoc});
0562         QCOMPARE(favLocModel.rowCount(), 1);
0563 
0564         LiveDataManager liveMgr;
0565 
0566         TransferManager::clear();
0567         TransferManager transferMgr;
0568         transferMgr.overrideCurrentDateTime(QDateTime({1996, 10, 14}, {12, 34}));
0569         transferMgr.setReservationManager(&resMgr);
0570         transferMgr.setTripGroupManager(&groupMgr);
0571         transferMgr.setFavoriteLocationModel(&favLocModel);
0572         transferMgr.setLiveDataManager(&liveMgr);
0573 
0574         TimelineModel model;
0575         QAbstractItemModelTester tester(&model);
0576         model.setHomeCountryIsoCode(QStringLiteral("DE"));
0577         model.setCurrentDateTime(QDateTime({1996, 10, 14}, {12, 34}));
0578         model.setReservationManager(&resMgr);
0579         model.setTripGroupManager(&groupMgr);
0580         model.setWeatherForecastManager(&weatherMgr);
0581         model.setTransferManager(&transferMgr);
0582 
0583         // check state is correct for data imported at the start
0584         ModelVerificationPoint vp(QLatin1StringView(SOURCE_DIR "/data/timeline/") + baseName + QLatin1StringView(".model"));
0585         vp.setRoleFilter({TimelineModel::BatchIdRole, TimelineModel::TripGroupIdRole});
0586         vp.setJsonPropertyFilter({QStringLiteral("reservationId")});
0587         QVERIFY(vp.verify(&model));
0588 
0589         // retry with loading during runtime
0590         Test::clearAll(&resMgr);
0591         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/data/timeline/") + baseName + QLatin1StringView(".json")));
0592         QVERIFY(vp.verify(&model));
0593     }
0594 
0595     void testCurrentReservation()
0596     {
0597         ReservationManager resMgr;
0598         Test::clearAll(&resMgr);
0599         auto ctrl = Test::makeAppController();
0600         ctrl->setReservationManager(&resMgr);
0601 
0602         TimelineModel model;
0603         model.setCurrentDateTime(QDateTime({2017, 8, 1}, {23, 0}, QTimeZone("Europe/Zurich")));
0604         QAbstractItemModelTester tester(&model);
0605         model.setReservationManager(&resMgr);
0606 
0607         QSignalSpy currentResChangedSpy(&model, &TimelineModel::currentBatchChanged);
0608 
0609         ctrl->importFromUrl(QUrl::fromLocalFile(QLatin1StringView(SOURCE_DIR "/../tests/randa2017.json")));
0610         QCOMPARE(model.rowCount(), 13);
0611         QVERIFY(!currentResChangedSpy.empty());
0612 
0613         model.setCurrentDateTime(QDateTime({2017, 8, 1}, {23, 0}, QTimeZone("Europe/Zurich")));
0614         QVERIFY(model.currentBatchId().isEmpty());
0615 
0616         model.setCurrentDateTime(QDateTime({2017, 9, 9}, {23, 0}, QTimeZone("Europe/Zurich")));
0617         QVERIFY(!model.currentBatchId().isEmpty());
0618         QCOMPARE(model.currentBatchId(), model.index(1, 0).data(TimelineModel::BatchIdRole).toString());
0619 
0620         model.setCurrentDateTime(QDateTime({2017, 9, 10}, {14, 0}, QTimeZone("Europe/Zurich")));
0621         QCOMPARE(model.index(2, 0).data(TimelineModel::ElementTypeRole), TimelineElement::TrainTrip);
0622         QVERIFY(!model.currentBatchId().isEmpty());
0623         QCOMPARE(model.currentBatchId(), model.index(2, 0).data(TimelineModel::BatchIdRole).toString());
0624 
0625         model.setCurrentDateTime(QDateTime({2017, 9, 10}, {14, 5}, QTimeZone("Europe/Zurich")));
0626         QVERIFY(!model.currentBatchId().isEmpty());
0627         QCOMPARE(model.currentBatchId(), model.index(3, 0).data(TimelineModel::BatchIdRole).toString());
0628 
0629         model.setCurrentDateTime(QDateTime({2017, 9, 10}, {20, 0}, QTimeZone("Europe/Zurich")));
0630         QVERIFY(model.currentBatchId().isEmpty());
0631 
0632         model.setCurrentDateTime(QDateTime({2019, 1, 1}, {0, 0}, QTimeZone("Europe/Zurich")));
0633         QVERIFY(model.currentBatchId().isEmpty());
0634     }
0635 };
0636 
0637 QTEST_GUILESS_MAIN(TimelineModelTest)
0638 
0639 #include "timelinemodeltest.moc"