File indexing completed on 2024-04-28 03:45:30
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)