File indexing completed on 2024-04-21 14:47:29

0001 /*
0002     SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 /*
0008  * This file contains unit tests for the scheduler, in particular for the
0009  * planning parts--evaluating jobs and setting proposed start/end times for them.
0010  */
0011 
0012 #include "ekos/scheduler/scheduler.h"
0013 #include "ekos/scheduler/schedulerutils.h"
0014 #include "ekos/scheduler/greedyscheduler.h"
0015 #include "ekos/scheduler/schedulerjob.h"
0016 #include "ekos/scheduler/schedulermodulestate.h"
0017 #include "indi/indiproperty.h"
0018 #include "ekos/capture/sequencejob.h"
0019 #include "ekos/capture/placeholderpath.h"
0020 #include "geolocation.h"
0021 #include "Options.h"
0022 
0023 #include <QTest>
0024 #include <memory>
0025 
0026 #include <QObject>
0027 
0028 using Ekos::SequenceJob;
0029 using Ekos::Scheduler;
0030 
0031 class TestSchedulerUnit : public QObject
0032 {
0033         Q_OBJECT
0034 
0035     public:
0036         /** @short Constructor */
0037         TestSchedulerUnit();
0038 
0039         /** @short Destructor */
0040         ~TestSchedulerUnit() override = default;
0041 
0042     private slots:
0043         void setupGeoAndTimeTest();
0044         void setupJobTest_data();
0045         void setupJobTest();
0046         void loadSequenceQueueTest();
0047         void estimateJobTimeTest();
0048         void evaluateJobsTest();
0049 
0050     private:
0051         void runSetupJob(Ekos::SchedulerJob &job,
0052                          GeoLocation *geo, KStarsDateTime *localTime, const QString &name,
0053                          const dms &ra, const dms &dec, double positionAngle, const QUrl &sequenceUrl,
0054                          const QUrl &fitsUrl, Ekos::StartupCondition sCond, const QDateTime &sTime, Ekos::CompletionCondition eCond,
0055                          const QDateTime &eTime, int eReps,
0056                          double minAlt, double minMoonSep = 0, bool enforceWeather = false, bool enforceTwilight = true,
0057                          bool enforceArtificialHorizon = true, bool track = true, bool focus = true, bool align = true, bool guide = true);
0058 };
0059 
0060 #include "testschedulerunit.moc"
0061 
0062 // The tests use this latitude/longitude, and happen around this QDateTime.
0063 GeoLocation siliconValley(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -7);
0064 KStarsDateTime midNight(QDateTime(QDate(2021, 4, 17), QTime(0, 0, 1), QTimeZone(-7 * 3600)));
0065 
0066 // At the midNight (midnight start of 4/17/2021) a star at the zenith is about
0067 // at DEC=37d33'36" ~37.56 degrees, RA=12h32'48" ~ 188.2 degrees
0068 // The tests in this file use this RA/DEC.
0069 dms midnightRA(188.2), testDEC(37.56);
0070 
0071 // These altitudes were precomputed for the above GeoLocation and time (midnight), time offset
0072 // by 12hours in the vector, one altitude per hour.
0073 // That is, altitudes[0] corresponds to -12 hours from midNight.
0074 QVector<double> svAltitudes =
0075 {
0076     -15.11, -13.92, -10.28, -4.53, // 12,1,2,3pm
0077         2.94, 11.74, 21.53, 32.05,     // 4,5,6,7pm
0078         43.10, 54.53, 66.22, 78.08,    // 8,9,10,11pm
0079         89.99,                         // midnight
0080         78.07, 66.21, 54.52, 43.09,    // 1,2,3,4am
0081         32.04, 21.52, 11.73, 2.94,     // 5,6,7,8am
0082         -4.53, -10.29, -13.92, -15.11  // 9,10,11,12
0083     };
0084 
0085 // Used to keep the "correct information" about what's stored in an .esq file
0086 // in order to test loadSequenceQueue() and processJobInfo(XML, job) which it calls.
0087 struct CaptureJobDetails
0088 {
0089     QString filter;
0090     int count;
0091     double exposure;
0092     CCDFrameType type;
0093 };
0094 
0095 // This sequence corresponds to the contents of the sequence file 9filters.esq.
0096 const QString seqFile9Filters = "9filters.esq";
0097 QList<CaptureJobDetails> details9Filters =
0098 {
0099     {"Luminance", 6,  60.0, FRAME_LIGHT},
0100     {"SII",      20,  30.0, FRAME_LIGHT},
0101     {"OIII",      7,  20.0, FRAME_LIGHT},
0102     {"H_Alpha",   5,  30.0, FRAME_LIGHT},
0103     {"Red",       7,  90.0, FRAME_LIGHT},
0104     {"Green",     7,  45.0, FRAME_LIGHT},
0105     {"Blue",      2, 120.0, FRAME_LIGHT},
0106     {"SII",       6,  30.0, FRAME_LIGHT},
0107     {"OIII",      6,  10.0, FRAME_LIGHT}
0108 };
0109 
0110 TestSchedulerUnit::TestSchedulerUnit() : QObject()
0111 {
0112     // Remove the dither-enabled option. It adds a complexity to estimating the job time.
0113     Options::setDitherEnabled(false);
0114 
0115     // Remove the setting-altitude-cutoff option.
0116     // There's some slight complexity when setting near the altitude constraint.
0117     // This is not tested yet.
0118     Options::setSettingAltitudeCutoff(0);
0119 
0120     // Setting this true winds up calling KStarsData::Instance() in the scheduler via SkyPoint::apparentCoord().
0121     // Unit tests don't instantiate KStarsData::Instance() and will crash.
0122     Options::setUseRelativistic(false);
0123 }
0124 
0125 // Tests that the doubles are within tolerance.
0126 bool compareFloat(double d1, double d2, double tolerance = .0001)
0127 {
0128     return (fabs(d1 - d2) < tolerance);
0129 }
0130 
0131 // Tests that the QDateTimes are within the tolerance in seconds.
0132 bool compareTimes(const QDateTime &t1, const QDateTime &t2, int toleranceSecs = 1)
0133 {
0134     int toleranceMsecs = toleranceSecs * 1000;
0135     if (std::abs(t1.msecsTo(t2)) >= toleranceMsecs)
0136     {
0137         QWARN(qPrintable(QString("Comparison of %1 with %2 is out of %3s tolerance.").arg(t1.toString()).arg(t2.toString()).arg(
0138                              toleranceSecs)));
0139         return false;
0140     }
0141     else return true;
0142 }
0143 
0144 // The runSetupJob() utility calls the static function Scheduler::setupJob() with all the args passed in
0145 // and tests to see that the resulting Ekos::SchedulerJob object has the values that were requested.
0146 void TestSchedulerUnit::runSetupJob(Ekos::SchedulerJob &job, GeoLocation *geo, KStarsDateTime *localTime,
0147                                     const QString &name,
0148                                     const dms &ra, const dms &dec, double positionAngle, const QUrl &sequenceUrl,
0149                                     const QUrl &fitsUrl, Ekos::StartupCondition sCond, const QDateTime &sTime,
0150                                     Ekos::CompletionCondition eCond, const QDateTime &eTime, int eReps,
0151                                     double minAlt, double minMoonSep, bool enforceWeather, bool enforceTwilight,
0152                                     bool enforceArtificialHorizon, bool track, bool focus, bool align, bool guide)
0153 {
0154     // Setup the time and geo.
0155     KStarsDateTime ut = geo->LTtoUT(*localTime);
0156     Ekos::SchedulerModuleState::setGeo(geo);
0157     Ekos::SchedulerModuleState::setLocalTime(localTime);
0158     QVERIFY(Ekos::SchedulerModuleState::hasLocalTime() && Ekos::SchedulerModuleState::hasGeo());
0159 
0160     QString group = "";
0161     Ekos::SchedulerUtils::setupJob(job, name, group, ra, dec, ut.djd(), positionAngle,
0162                                    sequenceUrl, fitsUrl,
0163                                    sCond, sTime,
0164                                    eCond, eTime, eReps,
0165                                    minAlt, minMoonSep,
0166                                    enforceWeather, enforceTwilight, enforceArtificialHorizon,
0167                                    track, focus, align, guide);
0168     QVERIFY(name == job.getName());
0169     QVERIFY(ra == job.getTargetCoords().ra0());
0170     QVERIFY(dec == job.getTargetCoords().dec0());
0171     QVERIFY(positionAngle == job.getPositionAngle());
0172     QVERIFY(sequenceUrl == job.getSequenceFile());
0173     QVERIFY(fitsUrl == job.getFITSFile());
0174     QVERIFY(minAlt == job.getMinAltitude());
0175     QVERIFY(minMoonSep == job.getMinMoonSeparation());
0176     QVERIFY(enforceWeather == job.getEnforceWeather());
0177     QVERIFY(enforceTwilight == job.getEnforceTwilight());
0178     QVERIFY(enforceArtificialHorizon == job.getEnforceArtificialHorizon());
0179 
0180     QVERIFY(sCond == job.getStartupCondition());
0181     switch (sCond)
0182     {
0183         case Ekos::START_AT:
0184             QVERIFY(sTime == job.getStartupTime());
0185             break;
0186         case Ekos::START_ASAP:
0187             QVERIFY(QDateTime() == job.getStartupTime());
0188             break;
0189     }
0190 
0191     QVERIFY(eCond == job.getCompletionCondition());
0192     switch (eCond)
0193     {
0194         case Ekos::FINISH_AT:
0195             QVERIFY(eTime == job.getCompletionTime());
0196             QVERIFY(0 == job.getRepeatsRequired());
0197             QVERIFY(0 == job.getRepeatsRemaining());
0198             break;
0199         case Ekos::FINISH_REPEAT:
0200             QVERIFY(QDateTime() == job.getCompletionTime());
0201             QVERIFY(eReps == job.getRepeatsRequired());
0202             QVERIFY(eReps == job.getRepeatsRemaining());
0203             break;
0204         case Ekos::FINISH_SEQUENCE:
0205             QVERIFY(QDateTime() == job.getCompletionTime());
0206             QVERIFY(1 == job.getRepeatsRequired());
0207             QVERIFY(1 == job.getRepeatsRemaining());
0208             break;
0209         case Ekos::FINISH_LOOP:
0210             QVERIFY(QDateTime() == job.getCompletionTime());
0211             QVERIFY(0 == job.getRepeatsRequired());
0212             QVERIFY(0 == job.getRepeatsRemaining());
0213             break;
0214     }
0215 
0216     Ekos::SchedulerJob::StepPipeline pipe = job.getStepPipeline();
0217     QVERIFY((track && (pipe & Ekos::SchedulerJob::USE_TRACK)) || (!track && !(pipe & Ekos::SchedulerJob::USE_TRACK)));
0218     QVERIFY((focus && (pipe & Ekos::SchedulerJob::USE_FOCUS)) || (!focus && !(pipe & Ekos::SchedulerJob::USE_FOCUS)));
0219     QVERIFY((align && (pipe & Ekos::SchedulerJob::USE_ALIGN)) || (!align && !(pipe & Ekos::SchedulerJob::USE_ALIGN)));
0220     QVERIFY((guide && (pipe & Ekos::SchedulerJob::USE_GUIDE)) || (!guide && !(pipe & Ekos::SchedulerJob::USE_GUIDE)));
0221 }
0222 
0223 void TestSchedulerUnit::setupGeoAndTimeTest()
0224 {
0225     Ekos::SchedulerJob job(nullptr);
0226     QVERIFY(!Ekos::SchedulerModuleState::hasLocalTime() && !Ekos::SchedulerModuleState::hasGeo());
0227     Ekos::SchedulerModuleState::setGeo(&siliconValley);
0228     Ekos::SchedulerModuleState::setLocalTime(&midNight);
0229     QVERIFY(Ekos::SchedulerModuleState::hasLocalTime() && Ekos::SchedulerModuleState::hasGeo());
0230     QVERIFY(Ekos::SchedulerModuleState::getGeo()->lat() == siliconValley.lat());
0231     QVERIFY(Ekos::SchedulerModuleState::getGeo()->lng() == siliconValley.lng());
0232     QVERIFY(job.getLocalTime() == midNight);
0233 }
0234 
0235 Q_DECLARE_METATYPE(Ekos::StartupCondition);
0236 Q_DECLARE_METATYPE(Ekos::CompletionCondition);
0237 
0238 // Test Scheduler::setupJob().
0239 // Calls runSetupJob (which calls Ekos::SchedulerJob::setupJob(...)) in a few different ways
0240 // to test different kinds of Ekos::SchedulerJob initializations.
0241 void TestSchedulerUnit::setupJobTest_data()
0242 {
0243     QTest::addColumn<Ekos::StartupCondition>("START_CONDITION");
0244     QTest::addColumn<QDateTime>("START_TIME");
0245     QTest::addColumn<Ekos::CompletionCondition>("END_CONDITION");
0246     QTest::addColumn<QDateTime>("END_TIME");
0247     QTest::addColumn<int>("REPEATS");
0248     QTest::addColumn<bool>("ENFORCE_WEATHER");
0249     QTest::addColumn<bool>("ENFORCE_TWILIGHT");
0250     QTest::addColumn<bool>("ENFORCE_ARTIFICIAL_HORIZON");
0251     QTest::addColumn<bool>("TRACK");
0252     QTest::addColumn<bool>("FOCUS");
0253     QTest::addColumn<bool>("ALIGN");
0254     QTest::addColumn<bool>("GUIDE");
0255 
0256     QTest::newRow("ASAP_TO_FINISH")
0257             << Ekos::START_ASAP << QDateTime() // start conditions
0258             << Ekos::FINISH_SEQUENCE << QDateTime() << 1 // end conditions
0259             << false  // enforce weather
0260             << true   // enforce twilight
0261             << true   // enforce artificial horizon
0262             << false  // track
0263             << true   // focus
0264             << true   // align
0265             << true;  // guide
0266 
0267     QTest::newRow("START_AT_FINISH_AT")
0268             << Ekos::START_AT // start conditions
0269             << QDateTime(QDate(2021, 4, 17), QTime(0, 1, 0), QTimeZone(-7 * 3600))
0270             << Ekos::FINISH_AT // end conditions
0271             << QDateTime(QDate(2021, 4, 17), QTime(0, 2, 0), QTimeZone(-7 * 3600))
0272             << 1
0273             << true   // enforce weather
0274             << false  // enforce twilight
0275             << true   // enforce artificial horizon
0276             << true   // track
0277             << false  // focus
0278             << true   // align
0279             << true;  // guide
0280 
0281     QTest::newRow("ASAP_TO_LOOP")
0282             << Ekos::START_ASAP << QDateTime() // start conditions
0283             << Ekos::FINISH_SEQUENCE << QDateTime() << 1 // end conditions
0284             << false  // enforce weather
0285             << false  // enforce twilight
0286             << true   // enforce artificial horizon
0287             << true   // track
0288             << true   // focus
0289             << true   // align
0290             << false; // guide
0291 }
0292 
0293 void TestSchedulerUnit::setupJobTest()
0294 {
0295     QFETCH(Ekos::StartupCondition, START_CONDITION);
0296     QFETCH(QDateTime, START_TIME);
0297     QFETCH(Ekos::CompletionCondition, END_CONDITION);
0298     QFETCH(QDateTime, END_TIME);
0299     QFETCH(int, REPEATS);
0300     QFETCH(bool, ENFORCE_WEATHER);
0301     QFETCH(bool, ENFORCE_TWILIGHT);
0302     QFETCH(bool, ENFORCE_ARTIFICIAL_HORIZON);
0303     QFETCH(bool, TRACK);
0304     QFETCH(bool, FOCUS);
0305     QFETCH(bool, ALIGN);
0306     QFETCH(bool, GUIDE);
0307 
0308     Ekos::SchedulerJob job(nullptr);
0309     runSetupJob(job, &siliconValley, &midNight, "Job1",
0310                 midnightRA, testDEC, 5.0, QUrl("1"), QUrl("2"),
0311                 START_CONDITION, START_TIME,
0312                 END_CONDITION, END_TIME, REPEATS,
0313                 30.0, 5.0, ENFORCE_WEATHER, ENFORCE_TWILIGHT,
0314                 ENFORCE_ARTIFICIAL_HORIZON, TRACK, FOCUS, ALIGN, GUIDE);
0315 }
0316 
0317 namespace
0318 {
0319 // compareCaptureSequeuce() is a utility to use the CaptureJobDetails structure as a truth value
0320 // to see if the capture sequeuce was loaded properly.
0321 void compareCaptureSequence(const QList<CaptureJobDetails> &details, const QList<Ekos::SequenceJob *> &jobs)
0322 {
0323     QVERIFY(details.size() == jobs.size());
0324     for (int i = 0; i < jobs.size(); ++i)
0325     {
0326         QVERIFY(details[i].filter == jobs[i]->getCoreProperty(SequenceJob::SJ_Filter).toString());
0327         QVERIFY(details[i].count == jobs[i]->getCoreProperty(SequenceJob::SJ_Count).toInt());
0328         QVERIFY(details[i].exposure == jobs[i]->getCoreProperty(SequenceJob::SJ_Exposure).toDouble());
0329         QVERIFY(details[i].type == jobs[i]->getFrameType());
0330     }
0331 }
0332 }  // namespace
0333 
0334 // Test Scheduler::loadSequeuceQueue().
0335 // Load sequenceQueue doesn't load all details of the sequence. It just loads what it
0336 // needs to compute a duration estimate for the job.
0337 // Hence, compareCaptureSequence just tests a few things that a capture sequence can contain.
0338 // A full test for capture sequences should be written in capture testing.
0339 void TestSchedulerUnit::loadSequenceQueueTest()
0340 {
0341     // Create a new Ekos::SchedulerJob and pass in a null moon pointer.
0342     Ekos::SchedulerJob schedJob(nullptr);
0343 
0344     QList<Ekos::SequenceJob *> jobs;
0345     bool hasAutoFocus = false;
0346     // Read in the 9 filters file.
0347     // The last arg is for logging. Use nullptr for testing.
0348     QVERIFY(Ekos::SchedulerUtils::loadSequenceQueue(seqFile9Filters, &schedJob, jobs, hasAutoFocus, nullptr));
0349     // Makes sure we have the basic details of the capture sequence were read properly.
0350     compareCaptureSequence(details9Filters, jobs);
0351 }
0352 
0353 namespace
0354 {
0355 // This utility computes the sum of time taken for all exposures in a capture sequence.
0356 double computeExposureDurations(const QList<CaptureJobDetails> &details)
0357 {
0358     double sum = 0;
0359     for (int i = 0; i < details.size(); ++i)
0360         sum += details[i].count * details[i].exposure;
0361     return sum;
0362 }
0363 }  // namespace
0364 
0365 // Test Scheduler::estimateJobTime(). Tests the estimates of job completion time.
0366 // Focuses on the non-heuristic aspects (e.g. sum of num_exposures * exposure_duration for all the
0367 // jobs in the capture sequence).
0368 void TestSchedulerUnit::estimateJobTimeTest()
0369 {
0370     // Some computations use the local time, which is normally taken from KStars::Instance()->lt()
0371     // unless this is set. The Instance does not exist when running this unit test.
0372     Ekos::SchedulerModuleState::setLocalTime(&midNight);
0373 
0374     // First test, start ASAP and finish when the sequence is done.
0375     Ekos::SchedulerJob job(nullptr);
0376     runSetupJob(job, &siliconValley, &midNight, "Job1",
0377                 midnightRA, testDEC, 5.0,
0378                 QUrl(QString("file:%1").arg(seqFile9Filters)), QUrl(""),
0379                 Ekos::START_ASAP, QDateTime(),
0380                 Ekos::FINISH_SEQUENCE, QDateTime(), 1,
0381                 30.0, 5.0, false, false);
0382 
0383     // Initial map has no previous captures.
0384     QMap<QString, uint16_t> capturedFramesCount;
0385     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0386 
0387     // The time estimate is essentially the sum of (exposure times * the number of exposures) for each filter.
0388     // There are other heuristics added to take initial track, focus, align & guide into account.
0389     // We're not testing these heuristics here, so they are set to false in the job setup above (last 4 bools).
0390     const double exposureDuration = computeExposureDurations(details9Filters);
0391     const int overhead = Ekos::SchedulerUtils::timeHeuristics(&job);
0392     QVERIFY(compareFloat(exposureDuration + overhead, job.getEstimatedTime()));
0393 
0394     // Repeat the above test, but repeat the sequence 1,2,3,4,...,10 times.
0395     for (int i = 1; i <= 10; ++i)
0396     {
0397         job.setCompletionCondition(Ekos::FINISH_REPEAT);
0398         job.setRepeatsRequired(i);
0399         QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0400         QVERIFY(compareFloat(overhead + i * exposureDuration, job.getEstimatedTime()));
0401     }
0402 
0403     // Resetting the number of repeats. This has a side-effect of changing the completion condition,
0404     // so we must change completion condition below.
0405     job.setRepeatsRequired(1);
0406 
0407     // Test again, this time looping indefinitely.
0408     // In this case the scheduler should estimate negative completion time, as the sequence doesn't
0409     // end until the user stops it (or it is interrupted by altitude or daylight). The scheduler
0410     // doesn't estimate those stopping conditions at this point.
0411     job.setCompletionCondition(Ekos::FINISH_LOOP);
0412     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0413     QVERIFY(job.getEstimatedTime() < 0);
0414 
0415     // Test again with a fixed end time. The scheduler estimates the time from "now" until the end time.
0416     // Perhaps it should estimate the max of that and the FINISH_SEQUENCE time??
0417     job.setCompletionCondition(Ekos::FINISH_AT);
0418     KStarsDateTime stopTime(QDateTime(QDate(2021, 4, 17), QTime(1, 0, 0), QTimeZone(-7 * 3600)));
0419     job.setCompletionTime(stopTime);
0420     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0421     QVERIFY(midNight.secsTo(stopTime) == job.getEstimatedTime());
0422 
0423     // Test again, similar to above but given a START_AT time as well.
0424     // Now it should return the interval between the start and end times.
0425     // Again, perhaps that should be max'd with the FINISH_SEQUENCE time?
0426     job.setStartupCondition(Ekos::START_AT);
0427     job.setStartupTime(midNight.addSecs(1800));
0428     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0429     QVERIFY(midNight.secsTo(stopTime) - 1800 == job.getEstimatedTime());
0430 
0431     // Small test of accounting for already completed captures.
0432     // 1. Explicitly load the capture jobs
0433     job.setStartupCondition(Ekos::START_ASAP);
0434     job.setCompletionCondition(Ekos::FINISH_SEQUENCE);
0435     QList<Ekos::SequenceJob *> jobs;
0436     bool hasAutoFocus = false;
0437     // The last arg is for logging. Use nullptr for testing.
0438     QVERIFY(Ekos::SchedulerUtils::loadSequenceQueue(seqFile9Filters, &job, jobs, hasAutoFocus, nullptr));
0439     // 2. Get the signiture of the first job
0440     SequenceJob *seqJob = jobs[0];
0441     seqJob->setCoreProperty(Ekos::SequenceJob::SJ_TargetName, job.getName());
0442     auto placeholderPath = Ekos::PlaceholderPath();
0443     QString signature = placeholderPath.generateSequenceFilename(*seqJob, true, true, 1, ".fits", "", false, true);
0444     seqJob->setCoreProperty(SequenceJob::SJ_Signature, signature);
0445     QString sig0 = jobs[0]->getSignature();
0446     // 3. The first job has 6 exposures, each of 20s duration. Set it up that 2 are already done.
0447     capturedFramesCount[sig0] = 2;
0448     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0449     // 4. Now expect that we have 2*60s=120s less job time, but only if we're remembering job progress.
0450     // First don't remember job progress, and the scheduler should provide the standard estimate.
0451     Options::setRememberJobProgress(false);
0452     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0453     QVERIFY(compareFloat(overhead + exposureDuration, job.getEstimatedTime()));
0454     // Next remember the progress, the job should reduce the estimate by 120s (the 2 completed exposures).
0455     Options::setRememberJobProgress(true);
0456     QVERIFY(Ekos::SchedulerUtils::estimateJobTime(&job, capturedFramesCount, nullptr));
0457     QVERIFY(compareFloat(overhead + exposureDuration - 120, job.getEstimatedTime()));
0458 }
0459 
0460 // Test Scheduler::evaluateJobs().
0461 void TestSchedulerUnit::evaluateJobsTest()
0462 {
0463     auto now = midNight;
0464     Ekos::GreedyScheduler scheduler;
0465     Ekos::SchedulerModuleState state;
0466     state.setLocalTime(&now);
0467     // The nullptr is moon pointer. Not currently tested.
0468     Ekos::SchedulerJob job(nullptr);
0469 
0470     const double _dawn = .25, _dusk = .75;
0471     QDateTime const dawn = midNight.addSecs(_dawn * 24.0 * 3600.0);
0472     QDateTime const dusk = midNight.addSecs(_dusk * 24.0 * 3600.0);
0473     const bool rescheduleErrors = true;
0474     const bool restart = true;
0475     const QMap<QString, uint16_t> capturedFrames;
0476     QList<Ekos::SchedulerJob *> jobs;
0477     const double minAltitude = 30.0;
0478 
0479     // Test 1: Evaluating an empty jobs list should return an empty list.
0480     scheduler.setParams(restart, true, rescheduleErrors, 3600, 3600);
0481     scheduler.scheduleJobs(jobs, now, capturedFrames, nullptr);
0482     QVERIFY(jobs.empty());
0483 
0484     // Test 2: Add one job to the list.
0485     // It should be on the list, and scheduled starting right away (there are no conflicting constraints)
0486     // and ending at the estimated completion interval after "now" .
0487     runSetupJob(job, &siliconValley, &midNight, "Job1",
0488                 midnightRA, testDEC, 0.0,
0489                 QUrl(QString("file:%1").arg(seqFile9Filters)), QUrl(""),
0490                 Ekos::START_ASAP, QDateTime(),
0491                 Ekos::FINISH_SEQUENCE, QDateTime(), 1,
0492                 minAltitude);
0493     jobs.append(&job);
0494 
0495     scheduler.scheduleJobs(jobs, now, capturedFrames, nullptr);
0496     // Should have the one same job on both lists.
0497     QVERIFY(jobs.size() == 1);
0498     QVERIFY(jobs[0] == &job);
0499     // The job should start now.
0500     QVERIFY(job.getStartupTime().secsTo(now) == 0);
0501     // It should finish when its exposures are done.
0502     QVERIFY(compareTimes(job.getCompletionTime(),
0503                          now.addSecs(Ekos::SchedulerUtils::timeHeuristics(&job) +
0504                                      computeExposureDurations(details9Filters))));
0505 
0506     state.calculateDawnDusk();
0507 
0508     // The job should run inside the twilight interval and have the same twilight values as Scheduler current values
0509     QVERIFY(job.runsDuringAstronomicalNightTime());
0510     QVERIFY(job.getDawnAstronomicalTwilight() == Ekos::SchedulerModuleState::Dawn());
0511     QVERIFY(job.getDuskAstronomicalTwilight() == Ekos::SchedulerModuleState::Dusk());
0512 
0513     // The job can start now, thus the next events are dawn, then dusk
0514     QVERIFY(Ekos::SchedulerModuleState::Dawn() <= Ekos::SchedulerModuleState::Dusk());
0515 
0516     jobs.clear();
0517 
0518     // Test 3: In this case, there are two jobs.
0519     // The first must wait for to get above the min altitude (which is set to 80-degrees).
0520     // The second one has no constraints, but is scheduled after the first.
0521 
0522     // Start the scheduler at 8pm but minAltitude won't be reached until after 11pm.
0523     // Job repetition takes about 45 minutes plus a little overhead.
0524     // Thus, first job, with two repetitions will be scheduled 11:10pm --> 12:43am.
0525     Ekos::SchedulerJob job1(nullptr);
0526     auto localTime8pm = midNight.addSecs(-4 * 3600);
0527     runSetupJob(job1, &siliconValley, &localTime8pm, "Job1",
0528                 midnightRA, testDEC, 0.0,
0529                 QUrl(QString("file:%1").arg(seqFile9Filters)), QUrl(""),
0530                 Ekos::START_ASAP, QDateTime(),
0531                 Ekos::FINISH_REPEAT, QDateTime(), 2,
0532                 80.0);
0533     jobs.append(&job1);
0534 
0535     // The second job has no blocking constraints, hence it will start immediately at 08:00 pm.
0536     // Since it lasts about 48 minutes, it should terminate at 08:48 pm and won't be suspended
0537     // by the first job.
0538     Ekos::SchedulerJob job2(nullptr);
0539     runSetupJob(job2, &siliconValley, &localTime8pm, "Job2",
0540                 midnightRA, testDEC, 0.0,
0541                 QUrl(QString("file:%1").arg(seqFile9Filters)), QUrl(""),
0542                 Ekos::START_ASAP, QDateTime(),
0543                 Ekos::FINISH_SEQUENCE, QDateTime(), 1,
0544                 30.0);
0545     jobs.append(&job2);
0546 
0547     scheduler.scheduleJobs(jobs, localTime8pm, capturedFrames, nullptr);
0548 
0549     QVERIFY(jobs.size() == 2);
0550     QVERIFY(jobs[0] == &job1);
0551     QVERIFY(jobs[1] == &job2);
0552     QVERIFY(compareTimes(jobs[0]->getStartupTime(), midNight.addSecs(-50 * 60), 300));
0553     QVERIFY(compareTimes(jobs[0]->getCompletionTime(), midNight.addSecs(43 * 60), 300));
0554 
0555     QVERIFY(compareTimes(jobs[1]->getStartupTime(), localTime8pm, 300));
0556     QVERIFY(compareTimes(jobs[1]->getCompletionTime(), localTime8pm.addSecs(48 * 60), 300));
0557 
0558     state.calculateDawnDusk();
0559 
0560     // The two job should run inside the twilight interval and have the same twilight values as Scheduler current values
0561     QVERIFY(jobs[0]->runsDuringAstronomicalNightTime());
0562     QVERIFY(jobs[1]->runsDuringAstronomicalNightTime());
0563     QVERIFY(jobs[0]->getDawnAstronomicalTwilight() == Ekos::SchedulerModuleState::Dawn());
0564     QVERIFY(jobs[1]->getDuskAstronomicalTwilight() == Ekos::SchedulerModuleState::Dusk());
0565 
0566     // The two job can start now, thus the next events for today are dawn, then dusk
0567     QVERIFY(Ekos::SchedulerModuleState::Dawn() <= Ekos::SchedulerModuleState::Dusk());
0568 
0569     jobs.clear();
0570 
0571     // Test 4: Similar to above, but the first job estimate will run past dawn as it has 10 repetitions.
0572     // The second job, therefore, should be initially scheduled to start "tomorrow" just after dusk.
0573 
0574     // Start the scheduler at 8pm but minAltitude won't be reached until ~11:10pm, and job3 estimated conclusion is 6:39am.
0575     Ekos::SchedulerJob job3(nullptr);
0576     Ekos::SchedulerModuleState::setLocalTime(&localTime8pm);
0577     runSetupJob(job3, &siliconValley, &localTime8pm, "Job1",
0578                 midnightRA, testDEC, 0.0,
0579                 QUrl(QString("file:%1").arg(seqFile9Filters)), QUrl(""),
0580                 Ekos::START_ASAP, QDateTime(),
0581                 Ekos::FINISH_REPEAT, QDateTime(), 10,
0582                 80.0);
0583     jobs.append(&job3);
0584 
0585     // The second job should be scheduled "tomorrow" starting after dusk
0586     Ekos::SchedulerJob job4(nullptr);
0587     runSetupJob(job4, &siliconValley, &localTime8pm, "Job2",
0588                 midnightRA, testDEC, 0.0,
0589                 QUrl(QString("file:%1").arg(seqFile9Filters)), QUrl(""),
0590                 Ekos::START_ASAP, QDateTime(),
0591                 Ekos::FINISH_SEQUENCE, QDateTime(), 1,
0592                 80.0);
0593     jobs.append(&job4);
0594 
0595     scheduler.scheduleJobs(jobs, localTime8pm, capturedFrames, nullptr);
0596 
0597     QVERIFY(jobs.size() == 2);
0598     QVERIFY(jobs[0] == &job3);
0599     QVERIFY(jobs[1] == &job4);
0600     QVERIFY(compareTimes(jobs[0]->getStartupTime(), midNight.addSecs(-50 * 60), 300));
0601     QVERIFY(compareTimes(jobs[0]->getCompletionTime(), midNight.addSecs(6 * 3600 + 39 * 60), 300));
0602 
0603     QVERIFY(compareTimes(jobs[1]->getStartupTime(), midNight.addSecs(18 * 3600 + 54 * 60), 300));
0604     QVERIFY(compareTimes(jobs[1]->getCompletionTime(), midNight.addSecs(19 * 3600 + 44 * 60), 300));
0605 
0606     jobs.clear();
0607 }
0608 
0609 QTEST_GUILESS_MAIN(TestSchedulerUnit)