File indexing completed on 2024-04-21 14:47:28
0001 /* KStars scheduler operations tests 0002 SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include <QFile> 0008 0009 #include <chrono> 0010 #include <ctime> 0011 0012 #include "test_ekos_scheduler_ops.h" 0013 #include "test_ekos_scheduler_helper.h" 0014 #include "ekos/scheduler/scheduler.h" 0015 #include "ekos/scheduler/schedulermodulestate.h" 0016 #include "ekos/scheduler/schedulerjob.h" 0017 #include "ekos/scheduler/greedyscheduler.h" 0018 #include "ekos/scheduler/schedulerprocess.h" 0019 0020 #include "skymapcomposite.h" 0021 0022 #if defined(HAVE_INDI) 0023 0024 #include "artificialhorizoncomponent.h" 0025 #include "kstars_ui_tests.h" 0026 #include "test_ekos.h" 0027 #include "test_ekos_simulator.h" 0028 #include "linelist.h" 0029 #include "mockmodules.h" 0030 #include "Options.h" 0031 0032 #define QWAIT_TIME 10 0033 0034 #define DEFAULT_TOLERANCE 300 0035 #define DEFAULT_ITERATIONS 50 0036 0037 using Ekos::Scheduler; 0038 0039 // Use this class to temporarily modify the scheduler's update interval 0040 // by creating a WithInterval variable in a scope. 0041 // The constructor sets the scheduler's update interval to the number of ms 0042 // in the 1st arg, and restore it at the end of the scope. For example: 0043 // { 0044 // WithInterval interval(10000, scheduler) 0045 // // scheduler update interval is now 10000 0046 // ... 0047 // } 0048 // // scheduler update interval is back to the original value. 0049 class WithInterval 0050 { 0051 public: 0052 WithInterval(int interval, QSharedPointer<Ekos::Scheduler> &_scheduler) : scheduler(_scheduler) 0053 { 0054 keepInterval = scheduler->moduleState()->updatePeriodMs(); 0055 scheduler->moduleState()->setUpdatePeriodMs(interval); 0056 } 0057 ~WithInterval() 0058 { 0059 scheduler->moduleState()->setUpdatePeriodMs(keepInterval); 0060 } 0061 private: 0062 int keepInterval; 0063 QSharedPointer<Ekos::Scheduler> scheduler; 0064 }; 0065 0066 TestEkosSchedulerOps::TestEkosSchedulerOps(QObject *parent) : QObject(parent) 0067 { 0068 } 0069 0070 void TestEkosSchedulerOps::initTestCase() 0071 { 0072 0073 QDBusConnection::sessionBus().registerObject("/MockKStars", this); 0074 QDBusConnection::sessionBus().registerService("org.kde.mockkstars"); 0075 0076 // This gets executed at the start of testing 0077 0078 disableSkyMap(); 0079 } 0080 0081 void TestEkosSchedulerOps::cleanupTestCase() 0082 { 0083 // This gets executed at the end of testing 0084 } 0085 0086 void TestEkosSchedulerOps::init() 0087 { 0088 // This gets executed at the start of each of the individual tests. 0089 testTimer.start(); 0090 0091 focuser.reset(new Ekos::MockFocus); 0092 mount.reset(new Ekos::MockMount); 0093 capture.reset(new Ekos::MockCapture); 0094 align.reset(new Ekos::MockAlign); 0095 guider.reset(new Ekos::MockGuide); 0096 ekos.reset(new Ekos::MockEkos); 0097 0098 scheduler.reset(new Scheduler("/MockKStars/MockEkos/Scheduler", "org.kde.mockkstars", 0099 Ekos::MockEkos::mockPath, "org.kde.mockkstars.MockEkos")); 0100 // These org.kde.* interface strings are set up in the various .xml files. 0101 scheduler->setFocusInterfaceString("org.kde.mockkstars.MockEkos.MockFocus"); 0102 scheduler->setMountInterfaceString("org.kde.mockkstars.MockEkos.MockMount"); 0103 scheduler->setCaptureInterfaceString("org.kde.mockkstars.MockEkos.MockCapture"); 0104 scheduler->setAlignInterfaceString("org.kde.mockkstars.MockEkos.MockAlign"); 0105 scheduler->setGuideInterfaceString("org.kde.mockkstars.MockEkos.MockGuide"); 0106 scheduler->setFocusPathString(Ekos::MockFocus::mockPath); 0107 scheduler->setMountPathString(Ekos::MockMount::mockPath); 0108 scheduler->setCapturePathString(Ekos::MockCapture::mockPath); 0109 scheduler->setAlignPathString(Ekos::MockAlign::mockPath); 0110 scheduler->setGuidePathString(Ekos::MockGuide::mockPath); 0111 0112 // Let's not deal with the dome for now. 0113 scheduler->schedulerUnparkDome->setChecked(false); 0114 0115 // For now don't deal with files that were left around from previous testing. 0116 // Should put these is a temporary directory that will be removed, if we generate 0117 // them at all. 0118 Options::setRememberJobProgress(false); 0119 0120 // This allows testing of the shutdown. 0121 Options::setStopEkosAfterShutdown(true); 0122 Options::setDitherEnabled(false); 0123 0124 // define START_ASAP and FINISH_SEQUENCE as default startup/completion conditions. 0125 m_startupCondition.type = Ekos::START_ASAP; 0126 m_completionCondition.type = Ekos::FINISH_SEQUENCE; 0127 0128 Options::setDawnOffset(0); 0129 Options::setDuskOffset(0); 0130 Options::setSettingAltitudeCutoff(0); 0131 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 0132 Options::setGreedyScheduling(true); 0133 } 0134 0135 void TestEkosSchedulerOps::cleanup() 0136 { 0137 // This gets executed at the end of each of the individual tests. 0138 0139 // The signal and/or dbus communications seems to get confused 0140 // without explicit resetting of these objects. 0141 focuser.reset(); 0142 mount.reset(); 0143 capture.reset(); 0144 align.reset(); 0145 guider.reset(); 0146 ekos.reset(); 0147 scheduler.reset(); 0148 fprintf(stderr, "Test took %.1fs\n", testTimer.elapsed() / 1000.0); 0149 } 0150 0151 void TestEkosSchedulerOps::disableSkyMap() 0152 { 0153 Options::setShowAsteroids(false); 0154 Options::setShowComets(false); 0155 Options::setShowSupernovae(false); 0156 Options::setShowDeepSky(false); 0157 Options::setShowEcliptic(false); 0158 Options::setShowEquator(false); 0159 Options::setShowLocalMeridian(false); 0160 Options::setShowGround(false); 0161 Options::setShowHorizon(false); 0162 Options::setShowFlags(false); 0163 Options::setShowOther(false); 0164 Options::setShowMilkyWay(false); 0165 Options::setShowSolarSystem(false); 0166 Options::setShowStars(false); 0167 Options::setShowSatellites(false); 0168 Options::setShowHIPS(false); 0169 Options::setShowTerrain(false); 0170 } 0171 0172 // When checking that something happens near a certain time, the tolerance of the 0173 // check is affected by how often the scheduler iterates. E.g. if the scheduler only 0174 // runs once a minute (to speed up simulation), it is unreasonable to check for 1 second 0175 // tolerances. This function returns the max of the tolerance passed in and 3 times 0176 // the scheduler's iteration period to compensate for that. 0177 int TestEkosSchedulerOps::timeTolerance(int seconds) 0178 { 0179 const int tolerance = std::max(seconds, 3 * (scheduler->moduleState()->updatePeriodMs() / 1000)); 0180 return tolerance; 0181 } 0182 0183 // Thos tests an empty scheduler job and makes sure dbus communications 0184 // work between the scheduler and the mock modules. 0185 void TestEkosSchedulerOps::testBasics() 0186 { 0187 QVERIFY(scheduler->process()->focusInterface().isNull()); 0188 QVERIFY(scheduler->process()->mountInterface().isNull()); 0189 QVERIFY(scheduler->process()->captureInterface().isNull()); 0190 QVERIFY(scheduler->process()->alignInterface().isNull()); 0191 QVERIFY(scheduler->process()->guideInterface().isNull()); 0192 0193 ekos->addModule(QString("Focus")); 0194 ekos->addModule(QString("Mount")); 0195 ekos->addModule(QString("Capture")); 0196 ekos->addModule(QString("Align")); 0197 ekos->addModule(QString("Guide")); 0198 0199 // Allow Qt to pass around the messages. 0200 // Wait time is set short (10ms) for longer tests where the scheduler is 0201 // iterating and can miss on one iteration. Here we make it longer 0202 // for a more stable test. 0203 0204 // Not sure why processEvents() doesn't always work. Would be quicker that way. 0205 //qApp->processEvents(); 0206 QTest::qWait(10 * QWAIT_TIME); 0207 0208 QVERIFY(!scheduler->process()->focusInterface().isNull()); 0209 QVERIFY(!scheduler->process()->mountInterface().isNull()); 0210 QVERIFY(!scheduler->process()->captureInterface().isNull()); 0211 QVERIFY(!scheduler->process()->alignInterface().isNull()); 0212 QVERIFY(!scheduler->process()->guideInterface().isNull()); 0213 0214 // Verify the mocks can use the DBUS. 0215 QVERIFY(!focuser->isReset); 0216 scheduler->process()->focusInterface()->call(QDBus::AutoDetect, "resetFrame"); 0217 0218 //qApp->processEvents(); // this fails, is it because dbus calls are on a separate thread? 0219 QTest::qWait(10 * QWAIT_TIME); 0220 0221 QVERIFY(focuser->isReset); 0222 0223 // Run the scheduler with nothing setup. Should quickly exit. 0224 scheduler->moduleState()->init(); 0225 QVERIFY(scheduler->moduleState()->timerState() == Ekos::RUN_WAKEUP); 0226 int sleepMs = scheduler->process()->runSchedulerIteration(); 0227 QVERIFY(scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0228 sleepMs = scheduler->process()->runSchedulerIteration(); 0229 QVERIFY(sleepMs == 1000); 0230 QVERIFY(scheduler->moduleState()->timerState() == Ekos::RUN_SHUTDOWN); 0231 sleepMs = scheduler->process()->runSchedulerIteration(); 0232 QVERIFY(scheduler->moduleState()->timerState() == Ekos::RUN_NOTHING); 0233 } 0234 0235 // Runs the scheduler for a number of iterations between 1 and the arg "iterations". 0236 // Each iteration it increments the simulated clock (currentUTime, which is in Universal 0237 // Time) by *sleepMs, then runs the scheduler, then calls fcn(). 0238 // If fcn() returns true, it stops iterating and returns true. 0239 // It returns false if it completes all the with fnc() returning false. 0240 bool TestEkosSchedulerOps::iterateScheduler(const QString &label, int iterations, 0241 int *sleepMs, KStarsDateTime* currentUTime, std::function<bool ()> fcn) 0242 { 0243 fprintf(stderr, "\n----------------------------------------\n"); 0244 fprintf(stderr, "Starting iterateScheduler(%s)\n", label.toLatin1().data()); 0245 0246 for (int i = 0; i < iterations; ++i) 0247 { 0248 //qApp->processEvents(); 0249 QTest::qWait(QWAIT_TIME); // this takes ~10ms per iteration! 0250 // Is there a way to speed up the above? 0251 // I didn't reduce it, because the basic test fails to call a dbus function 0252 // with less than 10ms wait time. 0253 0254 *currentUTime = currentUTime->addSecs(*sleepMs / 1000.0); 0255 KStarsData::Instance()->changeDateTime(*currentUTime); // <-- 175ms 0256 *sleepMs = scheduler->process()->runSchedulerIteration(); 0257 fprintf(stderr, "current time LT %s UT %s\n", 0258 KStarsData::Instance()->lt().toString().toLatin1().data(), 0259 KStarsData::Instance()->ut().toString().toLatin1().data()); 0260 if (fcn()) 0261 { 0262 fprintf(stderr, "IterateScheduler %s returning TRUE at %s %s after %d iterations\n", 0263 label.toLatin1().data(), 0264 KStarsData::Instance()->lt().toString().toLatin1().data(), 0265 KStarsData::Instance()->ut().toString().toLatin1().data(), i + 1); 0266 return true; 0267 } 0268 } 0269 fprintf(stderr, "IterateScheduler %s returning FALSE at %s %s after %d iterations\n", 0270 label.toLatin1().data(), 0271 KStarsData::Instance()->lt().toString().toLatin1().data(), 0272 KStarsData::Instance()->ut().toString().toLatin1().data(), iterations); 0273 return false; 0274 } 0275 0276 // Sets up the scheduler in a particular location (geo) and a UTC start time. 0277 void TestEkosSchedulerOps::initTimeGeo(const GeoLocation &geo, const QDateTime &startUTime) 0278 { 0279 KStarsData::Instance()->geo()->setLat(*(geo.lat())); 0280 KStarsData::Instance()->geo()->setLong(*(geo.lng())); 0281 // Note, the actual TZ would be -7 as there is a DST correction for these dates. 0282 KStarsData::Instance()->geo()->setTZ0(geo.TZ0()); 0283 0284 KStarsDateTime currentUTime(startUTime); 0285 KStarsData::Instance()->changeDateTime(currentUTime); 0286 KStarsData::Instance()->clock()->setManualMode(true); 0287 } 0288 0289 QString TestEkosSchedulerOps::writeFiles(const QString &label, QTemporaryDir &dir, 0290 const QVector<TestEkosSchedulerHelper::CaptureJob> &captureJob, 0291 const QString &schedulerXML) 0292 { 0293 // nanoseconds since epoch will be tacked on to the label to keep them unique. 0294 long int nn = std::chrono::duration_cast<std::chrono::nanoseconds>( 0295 std::chrono::system_clock::now().time_since_epoch()).count(); 0296 0297 const QString eslFilename = dir.filePath(QString("%1-%2.esl").arg(label).arg(nn)); 0298 const QString esqFilename = dir.filePath(QString("%1-%2.esq").arg(label).arg(nn)); 0299 const QString eslContents = QString(schedulerXML); 0300 const QString esqContents = TestEkosSchedulerHelper::getEsqContent(captureJob); 0301 0302 TestEkosSchedulerHelper::writeSimpleSequenceFiles(eslContents, eslFilename, esqContents, esqFilename); 0303 return eslFilename; 0304 } 0305 0306 void TestEkosSchedulerOps::initFiles(QTemporaryDir *dir, const QVector<QString> &esls, const QVector<QString> &esqs) 0307 { 0308 QVERIFY(dir->isValid()); 0309 QVERIFY(dir->autoRemove()); 0310 0311 for (int i = 0; i < esls.size(); ++i) 0312 { 0313 const QString eslFile = dir->filePath(QString("test%1.esl").arg(i)); 0314 const QString esqFile = dir->filePath(QString("test%1.esq").arg(i)); 0315 0316 QVERIFY(TestEkosSchedulerHelper::writeSimpleSequenceFiles(esls[i], eslFile, esqs[i], esqFile)); 0317 scheduler->load(i == 0, QString("file://%1").arg(eslFile)); 0318 QVERIFY(scheduler->moduleState()->jobs().size() == (i + 1)); 0319 scheduler->moduleState()->jobs()[i]->setSequenceFile(QUrl(QString("file://%1").arg(esqFile))); 0320 fprintf(stderr, "seq file: %s \"%s\"\n", esqFile.toLatin1().data(), QString("file://%1").arg(esqFile).toLatin1().data()); 0321 } 0322 } 0323 0324 // Sets up the scheduler in a particular location (geo) and a UTC start time. 0325 void TestEkosSchedulerOps::initScheduler(const GeoLocation &geo, const QDateTime &startUTime, QTemporaryDir *dir, 0326 const QVector<QString> &esls, const QVector<QString> &esqs) 0327 { 0328 initTimeGeo(geo, startUTime); 0329 initFiles(dir, esls, esqs); 0330 scheduler->process()->evaluateJobs(false); 0331 scheduler->moduleState()->init(); 0332 QVERIFY(scheduler->moduleState()->timerState() == Ekos::RUN_WAKEUP); 0333 } 0334 0335 void TestEkosSchedulerOps::startupJob( 0336 const GeoLocation &geo, const QDateTime &startUTime, 0337 QTemporaryDir *dir, const QString &esl, const QString &esq, 0338 const QDateTime &wakeupTime, KStarsDateTime &endUTime, int &endSleepMs) 0339 { 0340 QVector<QString> esls, esqs; 0341 esls.push_back(esl); 0342 esqs.push_back(esq); 0343 startupJobs(geo, startUTime, dir, esls, esqs, wakeupTime, endUTime, endSleepMs); 0344 } 0345 0346 void TestEkosSchedulerOps::startupJobs( 0347 const GeoLocation &geo, const QDateTime &startUTime, 0348 QTemporaryDir *dir, const QVector<QString> &esls, const QVector<QString> &esqs, 0349 const QDateTime &wakeupTime, KStarsDateTime &endUTime, int &endSleepMs) 0350 { 0351 initScheduler(geo, startUTime, dir, esls, esqs); 0352 { 0353 // Have the scheduler update quickly when running these init routines. 0354 WithInterval interval(1000, scheduler); 0355 0356 KStarsDateTime currentUTime(startUTime); 0357 int sleepMs = 0; 0358 QVERIFY(scheduler->moduleState()->timerState() == Ekos::RUN_WAKEUP); 0359 QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, ¤tUTime, [&]() -> bool 0360 { 0361 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0362 })); 0363 0364 if (wakeupTime.isValid()) 0365 { 0366 // This is the sequence when it goes to sleep, then wakes up later to start. 0367 0368 QVERIFY(iterateScheduler("Wait for RUN_WAKEUP", 10, &sleepMs, ¤tUTime, [&]() -> bool 0369 { 0370 return (scheduler->moduleState()->timerState() == Ekos::RUN_WAKEUP); 0371 })); 0372 0373 // Verify that it's near the original start time. 0374 const qint64 delta_t = KStarsData::Instance()->ut().secsTo(startUTime); 0375 QVERIFY2(std::abs(delta_t) < timeTolerance(300), 0376 QString("Delta to original time %1 too large, failing.").arg(delta_t).toLatin1()); 0377 0378 QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, ¤tUTime, [&]() -> bool 0379 { 0380 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0381 })); 0382 0383 // Verify that it wakes up at the right time, after the twilight constraint 0384 // and the stars rises above 30 degrees. See the time comment above. 0385 QVERIFY(std::abs(KStarsData::Instance()->ut().secsTo(wakeupTime)) < timeTolerance(DEFAULT_TOLERANCE)); 0386 } 0387 else 0388 { 0389 // check if there is a job scheduled 0390 bool scheduled_job = false; 0391 foreach (Ekos::SchedulerJob *sched_job, scheduler->moduleState()->jobs()) 0392 if (sched_job->state == Ekos::SCHEDJOB_SCHEDULED) 0393 scheduled_job = true; 0394 if (scheduled_job) 0395 { 0396 // This is the sequence when it can start-up right away. 0397 0398 // Verify that it's near the original start time. 0399 const qint64 delta_t = KStarsData::Instance()->ut().secsTo(startUTime); 0400 QVERIFY2(std::abs(delta_t) < timeTolerance(DEFAULT_TOLERANCE), 0401 QString("Delta to original time %1 too large, failing.").arg(delta_t).toLatin1()); 0402 0403 QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, ¤tUTime, [&]() -> bool 0404 { 0405 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0406 })); 0407 } 0408 else 0409 // if there is no job scheduled, we're done 0410 { 0411 return; 0412 } 0413 } 0414 // When the scheduler starts up, it sends connectDevices to Ekos 0415 // which sets Indi --> Ekos::Success, 0416 // and then it sends start() to Ekos which sets Ekos --> Ekos::Success 0417 bool sentOnce = false, readyOnce = false; 0418 QVERIFY(iterateScheduler("Wait for Indi and Ekos", 30, &sleepMs, ¤tUTime, [&]() -> bool 0419 { 0420 if ((scheduler->moduleState()->indiState() == Ekos::INDI_READY) && 0421 (scheduler->moduleState()->ekosState() == Ekos::EKOS_READY)) 0422 { 0423 return true; 0424 } 0425 //else if (scheduler->m_EkosCommunicationStatus == Ekos::Success) 0426 else if (ekos->ekosStatus() == Ekos::Success) 0427 { 0428 // Once Ekos is woken up, say mount and capture are ready. 0429 if (!sentOnce) 0430 { 0431 // Add the modules once ekos is started up. 0432 sentOnce = true; 0433 ekos->addModule("Focus"); 0434 ekos->addModule("Capture"); 0435 ekos->addModule("Mount"); 0436 ekos->addModule("Align"); 0437 ekos->addModule("Guide"); 0438 } 0439 else if (scheduler->process()->mountInterface() != nullptr && 0440 scheduler->process()->captureInterface() != nullptr && !readyOnce) 0441 { 0442 // Can't send the ready messages until the devices are registered. 0443 readyOnce = true; 0444 mount->sendReady(); 0445 capture->sendReady(); 0446 } 0447 } 0448 return false; 0449 })); 0450 0451 endUTime = currentUTime; 0452 endSleepMs = sleepMs; 0453 } 0454 } 0455 0456 void TestEkosSchedulerOps::startModules(KStarsDateTime ¤tUTime, int &sleepMs) 0457 { 0458 WithInterval interval(1000, scheduler); 0459 QVERIFY(iterateScheduler("Wait for MountTracking", 30, &sleepMs, ¤tUTime, [&]() -> bool 0460 { 0461 if (mount->status() == ISD::Mount::MOUNT_SLEWING) 0462 mount->setStatus(ISD::Mount::MOUNT_TRACKING); 0463 else if (mount->status() == ISD::Mount::MOUNT_TRACKING) 0464 return true; 0465 return false; 0466 })); 0467 0468 QVERIFY(iterateScheduler("Wait for Focus", 30, &sleepMs, ¤tUTime, [&]() -> bool 0469 { 0470 if (focuser->status() == Ekos::FOCUS_PROGRESS) 0471 focuser->setStatus(Ekos::FOCUS_COMPLETE); 0472 else if (focuser->status() == Ekos::FOCUS_COMPLETE) 0473 return true; 0474 return false; 0475 })); 0476 0477 QVERIFY(iterateScheduler("Wait for Align", 30, &sleepMs, ¤tUTime, [&]() -> bool 0478 { 0479 if (align->status() == Ekos::ALIGN_PROGRESS) 0480 align->setStatus(Ekos::ALIGN_COMPLETE); 0481 else if (align->status() == Ekos::ALIGN_COMPLETE) 0482 return true; 0483 return false; 0484 })); 0485 0486 QVERIFY(iterateScheduler("Wait for Guide", 30, &sleepMs, ¤tUTime, [&]() -> bool 0487 { 0488 return (guider->status() == Ekos::GUIDE_GUIDING); 0489 })); 0490 QVERIFY(guider->connected); 0491 } 0492 0493 // Roughly compare the slew coordinates sent to the mount to Deneb's. 0494 // Rough comparison because these will have been converted to JNow. 0495 // Should be called after simulated slew has been completed. 0496 bool TestEkosSchedulerOps::checkLastSlew(const SkyObject* targetObject) 0497 { 0498 constexpr double halfDegreeInHours = 1 / (15 * 2.0); 0499 bool success = (fabs(mount->lastRaHoursSlew - targetObject->ra().Hours()) < halfDegreeInHours) && 0500 (fabs(mount->lastDecDegreesSlew - targetObject->dec().Degrees()) < 0.5); 0501 if (!success) 0502 fprintf(stderr, "Expected slew RA: %f DEC: %F but got %f %f\n", 0503 targetObject->ra().Hours(), targetObject->dec().Degrees(), 0504 mount->lastRaHoursSlew, mount->lastDecDegreesSlew); 0505 return success; 0506 } 0507 0508 // Utility to print the state of the current scheduler job list. 0509 void TestEkosSchedulerOps::printJobs(const QString &label) 0510 { 0511 fprintf(stderr, "%-30s: ", label.toLatin1().data()); 0512 for (int i = 0; i < scheduler->moduleState()->jobs().size(); ++i) 0513 { 0514 fprintf(stderr, "(%d) %s %-15s ", i, scheduler->moduleState()->jobs()[i]->getName().toLatin1().data(), 0515 Ekos::SchedulerJob::jobStatusString(scheduler->moduleState()->jobs()[i]->getState()).toLatin1().data()); 0516 } 0517 fprintf(stderr, "\n"); 0518 } 0519 0520 void TestEkosSchedulerOps::initJob(const KStarsDateTime &startUTime, const KStarsDateTime &jobStartUTime) 0521 { 0522 KStarsDateTime currentUTime(startUTime); 0523 int sleepMs = 0; 0524 0525 // wait for the scheduler select the configured job for execution 0526 QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, ¤tUTime, [&]() -> bool 0527 { 0528 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0529 })); 0530 0531 // wait until the scheduler turns to the wakeup mode sleeping until the startup condition is met 0532 QVERIFY(iterateScheduler("Wait for RUN_WAKEUP", 10, &sleepMs, ¤tUTime, [&]() -> bool 0533 { 0534 return (scheduler->moduleState()->timerState() == Ekos::RUN_WAKEUP); 0535 })); 0536 0537 // wait until the scheduler starts the job 0538 QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, ¤tUTime, [&]() -> bool 0539 { 0540 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0541 })); 0542 0543 // check the distance from the expected start time 0544 qint64 delta = KStars::Instance()->data()->ut().secsTo(jobStartUTime); 0545 // real offset should be maximally 5 min off the configured offset 0546 QVERIFY2(std::abs(delta) < 300, 0547 QString("wrong startup time: %1 secs distance to planned %2.").arg(delta).arg(jobStartUTime.toString( 0548 Qt::ISODate)).toLocal8Bit()); 0549 } 0550 0551 // This tests a simple scheduler job. 0552 // The job initializes Ekos and Indi, slews, plate-solves, focuses, starts guiding, and 0553 // captures. Capture completes and the scheduler shuts down. 0554 void TestEkosSchedulerOps::runSimpleJob(const GeoLocation &geo, const SkyObject *targetObject, const QDateTime &startUTime, 0555 const QDateTime &wakeupTime, bool enforceArtificialHorizon) 0556 { 0557 KStarsDateTime currentUTime; 0558 int sleepMs = 0; 0559 0560 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 0561 0562 startupJob(geo, startUTime, &dir, TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, 0563 m_completionCondition, {true, true, true, true}, 0564 false, enforceArtificialHorizon), 0565 TestEkosSchedulerHelper::getDefaultEsqContent(), wakeupTime, currentUTime, sleepMs); 0566 startModules(currentUTime, sleepMs); 0567 QVERIFY(checkLastSlew(targetObject)); 0568 0569 QVERIFY(iterateScheduler("Wait for Capturing", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0570 { 0571 return (scheduler->activeJob() != nullptr && 0572 scheduler->activeJob()->getStage() == Ekos::SCHEDSTAGE_CAPTURING); 0573 })); 0574 0575 { 0576 WithInterval interval(1000, scheduler); 0577 0578 // Tell the scheduler that capture is done. 0579 capture->setStatus(Ekos::CAPTURE_COMPLETE); 0580 0581 QVERIFY(iterateScheduler("Wait for Abort Guider", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0582 { 0583 return (guider->status() == Ekos::GUIDE_ABORTED); 0584 })); 0585 QVERIFY(iterateScheduler("Wait for Shutdown", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0586 { 0587 return (scheduler->moduleState()->shutdownState() == Ekos::SHUTDOWN_COMPLETE); 0588 })); 0589 0590 // Here the scheduler sends a message to ekosInterface to disconnectDevices, 0591 // which will cause indi --> IDLE, 0592 // and then calls stop() which will cause ekos --> IDLE 0593 // This will cause the scheduler to shutdown. 0594 QVERIFY(iterateScheduler("Wait for Scheduler Complete", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0595 { 0596 return (scheduler->moduleState()->timerState() == Ekos::RUN_NOTHING); 0597 })); 0598 } 0599 } 0600 0601 void TestEkosSchedulerOps::testSimpleJob() 0602 { 0603 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 0604 SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName("Altair"); 0605 0606 // Setup an initial time. 0607 // Note that the start time is 3pm local (10pm UTC - 7 TZ). 0608 // Altair, the target, should be at about -40 deg altitude at this time,. 0609 // The dawn/dusk constraints are 4:03am and 10:12pm (lst=13:43) 0610 // At 10:12pm it should have an altitude of about 14 degrees, still below the 30-degree constraint. 0611 // It achieves 30-degrees altitude at about 23:35. 0612 QDateTime startUTime(QDateTime(QDate(2021, 6, 13), QTime(22, 0, 0), Qt::UTC)); 0613 const QDateTime wakeupTime(QDate(2021, 6, 14), QTime(06, 35, 0), Qt::UTC); 0614 runSimpleJob(geo, targetObject, startUTime, wakeupTime, true); 0615 QVERIFY(checkLastSlew(targetObject)); 0616 } 0617 0618 // This test has the same start as testSimpleJob, except that it but runs in NYC 0619 // instead of silicon valley. This makes sure testing doesn't depend on timezone. 0620 void TestEkosSchedulerOps::testTimeZone() 0621 { 0622 WithInterval interval(5000, scheduler); 0623 GeoLocation geo(dms(-74, 0), dms(40, 42, 0), "NYC", "NY", "USA", -5); 0624 SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName("Altair"); 0625 KStarsDateTime startUTime(QDateTime(QDate(2021, 6, 13), QTime(22, 0, 0), Qt::UTC)); 0626 0627 // It crosses 30-degrees altitude around the same time locally, but that's 0628 // 3 hours earlier UTC. 0629 const QDateTime wakeupTime(QDate(2021, 6, 14), QTime(03, 26, 0), Qt::UTC); 0630 0631 runSimpleJob(geo, targetObject, startUTime, wakeupTime, true); 0632 QVERIFY(checkLastSlew(targetObject)); 0633 } 0634 0635 void TestEkosSchedulerOps::testDawnShutdown() 0636 { 0637 // remove the various options that play with the dawn/dusk times 0638 Options::setDawnOffset(0); 0639 Options::setDuskOffset(0); 0640 Options::setSettingAltitudeCutoff(0); 0641 Options::setPreDawnTime(0); 0642 0643 // This test will iterate the scheduler every 40 simulated seconds (to save testing time). 0644 WithInterval interval(40000, scheduler); 0645 0646 // At this geo/date, Dawn is calculated = .1625 of a day = 3:53am local = 10:52 UTC 0647 // If we started at 23:35 local time, as before, it's a little over 4 hours 0648 // or over 4*3600 iterations. Too many? Instead we start at 3am local. 0649 0650 // According to https://www.timeanddate.com/sun/usa/san-francisco?month=6&year=2021 0651 // astronomical dawn for SF was 3:52am on 6/14/2021 0652 0653 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 0654 QVector<SkyObject*> targetObjects; 0655 targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Altair")); 0656 0657 // We'll start the scheduler at 3am local time. 0658 QDateTime startUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 10, 0), Qt::UTC)); 0659 // The job should start at 3:12am local time. 0660 QDateTime startJobUTime = startUTime.addSecs(180); 0661 // The job should be interrupted at the pre-dawn time, which is about 3:53am 0662 QDateTime preDawnUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 53, 0), Qt::UTC)); 0663 // Consider pre-dawn security range 0664 preDawnUTime = preDawnUTime.addSecs(-60.0 * abs(Options::preDawnTime())); 0665 0666 KStarsDateTime currentUTime; 0667 int sleepMs = 0; 0668 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 0669 0670 startup(geo, targetObjects, startUTime, currentUTime, sleepMs, dir); 0671 slewAndRun(targetObjects[0], startJobUTime, preDawnUTime, currentUTime, sleepMs, DEFAULT_TOLERANCE); 0672 parkAndSleep(currentUTime, sleepMs); 0673 0674 const QDateTime restartTime(QDate(2021, 6, 15), QTime(06, 31, 0), Qt::UTC); 0675 wakeupAndRestart(restartTime, currentUTime, sleepMs); 0676 } 0677 0678 // Expect the job to start running at startJobUTime. 0679 // Check that the correct slew was made 0680 // Expect the job to be interrupted at interruptUTime (if the time is valid) 0681 void TestEkosSchedulerOps::slewAndRun(SkyObject *object, const QDateTime &startUTime, const QDateTime &interruptUTime, 0682 KStarsDateTime ¤tUTime, int &sleepMs, int tolerance, const QString &label) 0683 { 0684 QVERIFY(iterateScheduler("Wait for Job Startup", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0685 { 0686 return (scheduler->moduleState()->timerState() == Ekos::RUN_JOBCHECK); 0687 })); 0688 0689 double delta = KStarsData::Instance()->ut().secsTo(startUTime); 0690 QVERIFY2(std::abs(delta) < timeTolerance(tolerance), 0691 QString("Unexpected difference to job statup time: %1 secs (%2 vs %3) %4") 0692 .arg(delta).arg(KStarsData::Instance()->ut().toString("MM/dd hh:mm")) 0693 .arg(startUTime.toString("MM/dd hh:mm")).arg(label).toLocal8Bit()); 0694 0695 // We should be unparked at this point. 0696 QVERIFY(mount->parkStatus() == ISD::PARK_UNPARKED); 0697 0698 QVERIFY(iterateScheduler("Wait for MountTracking", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0699 { 0700 if (mount->status() == ISD::Mount::MOUNT_SLEWING) 0701 mount->setStatus(ISD::Mount::MOUNT_TRACKING); 0702 else if (mount->status() == ISD::Mount::MOUNT_TRACKING) 0703 return true; 0704 return false; 0705 })); 0706 0707 QVERIFY(checkLastSlew(object)); 0708 0709 if (interruptUTime.isValid()) 0710 { 0711 // Wait until the job stops processing, 0712 // When scheduler state JOBCHECK changes to RUN_SCHEDULER. 0713 QVERIFY(iterateScheduler("Wait for Job Interruption", 1000, &sleepMs, ¤tUTime, [&]() -> bool 0714 { 0715 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0716 })); 0717 0718 delta = KStarsData::Instance()->ut().secsTo(interruptUTime); 0719 QVERIFY2(std::abs(delta) < timeTolerance(tolerance), 0720 QString("Unexpected difference to interrupt time: %1 secs (%2 vs %3) %4") 0721 .arg(delta).arg(KStarsData::Instance()->ut().toString("MM/dd hh:mm")) 0722 .arg(interruptUTime.toString("MM/dd hh:mm")).arg(label).toLocal8Bit()); 0723 0724 // It should start to shutdown now. 0725 QVERIFY(iterateScheduler("Wait for Guide Abort", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0726 { 0727 return (guider->status() == Ekos::GUIDE_ABORTED); 0728 })); 0729 } 0730 } 0731 0732 // Set up the target objects to run in the scheduler (in the order they're given) 0733 // Scheduler running at location geo. 0734 // Start the scheduler at startSchedulerUTime. 0735 // currentUTime and sleepMs can be set up as: KStarsDateTime currentUTime; int sleepMs = 0; and 0736 // their latest values are returned, if you want to continue the simulation after this call. 0737 // Similarly, dir is passed in so the temporary directory continues to exist for continued simulation. 0738 void TestEkosSchedulerOps::startup(const GeoLocation &geo, const QVector<SkyObject*> targetObjects, 0739 const QDateTime &startSchedulerUTime, KStarsDateTime ¤tUTime, int &sleepMs, QTemporaryDir &dir) 0740 { 0741 const QDateTime wakeupTime; // Not valid--it starts up right away. 0742 QVector<QString> esls, esqs; 0743 auto schedJob200x60 = QVector<TestEkosSchedulerHelper::CaptureJob>(1, {200, 60, "Red", "."}); 0744 auto esqContent = TestEkosSchedulerHelper::getEsqContent(schedJob200x60); 0745 for (int i = 0; i < targetObjects.size(); ++i) 0746 { 0747 esls.push_back(TestEkosSchedulerHelper::getSchedulerFile(targetObjects[i], m_startupCondition, m_completionCondition, {true, true, true, true}, 0748 true, true)); 0749 esqs.push_back(esqContent); 0750 } 0751 startupJobs(geo, startSchedulerUTime, &dir, esls, esqs, wakeupTime, currentUTime, sleepMs); 0752 startModules(currentUTime, sleepMs); 0753 } 0754 0755 void TestEkosSchedulerOps::parkAndSleep(KStarsDateTime ¤tUTime, int &sleepMs) 0756 { 0757 QVERIFY(iterateScheduler("Wait for Parked", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0758 { 0759 return (mount->parkStatus() == ISD::PARK_PARKED); 0760 })); 0761 0762 QVERIFY(iterateScheduler("Wait for Sleep State", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0763 { 0764 return (scheduler->moduleState()->timerState() == Ekos::RUN_WAKEUP); 0765 })); 0766 } 0767 0768 void TestEkosSchedulerOps::wakeupAndRestart(const QDateTime &restartTime, KStarsDateTime ¤tUTime, int &sleepMs) 0769 { 0770 // Make sure it wakes up at the proper time. 0771 QVERIFY(iterateScheduler("Wait for Wakeup Tomorrow", DEFAULT_ITERATIONS, &sleepMs, ¤tUTime, [&]() -> bool 0772 { 0773 return (scheduler->moduleState()->timerState() == Ekos::RUN_SCHEDULER); 0774 })); 0775 0776 fprintf(stderr, "Times instance %s vs reference %s, diff %lld\n", 0777 KStarsData::Instance()->ut().toString().toLatin1().data(), 0778 restartTime.toString().toLatin1().data(), 0779 KStarsData::Instance()->ut().secsTo(restartTime)); 0780 QVERIFY(std::abs(KStarsData::Instance()->ut().secsTo(restartTime)) < timeTolerance(DEFAULT_TOLERANCE)); 0781 0782 { 0783 WithInterval interval(1000, scheduler); 0784 // Verify the job starts up again, and the mount is once-again unparked. 0785 bool readyOnce = false; 0786 QVERIFY(iterateScheduler("Wait for Job Startup & Unparked", 50, &sleepMs, ¤tUTime, [&]() -> bool 0787 { 0788 if (scheduler->process()->mountInterface() != nullptr && 0789 scheduler->process()->captureInterface() != nullptr && !readyOnce) 0790 { 0791 // Send a ready signal since the scheduler expects it. 0792 readyOnce = true; 0793 mount->sendReady(); 0794 capture->sendReady(); 0795 } 0796 return (scheduler->moduleState()->timerState() == Ekos::RUN_JOBCHECK && 0797 mount->parkStatus() == ISD::PARK_UNPARKED); 0798 })); 0799 } 0800 } 0801 0802 void TestEkosSchedulerOps::testTwilightStartup_data() 0803 { 0804 QTest::addColumn<QString>("city"); 0805 QTest::addColumn<QString>("state"); 0806 QTest::addColumn<QString>("target"); 0807 QTest::addColumn<QString>("startTimeUTC"); 0808 QTest::addColumn<QString>("jobStartTimeUTC"); 0809 0810 QTest::newRow("SF") 0811 << "San Francisco" << "California" << "Rasalhague" 0812 << "Sun Jun 13 20:00:00 2021 GMT" << "Mon Jun 14 05:28:00 2021 GMT"; 0813 0814 QTest::newRow("Melbourne") 0815 << "Melbourne" << "Victoria" << "Arcturus" 0816 << "Sun Jun 13 02:00:00 2021 GMT" << "Mon Jun 13 08:42:00 2021 GMT"; 0817 } 0818 0819 void TestEkosSchedulerOps::testTwilightStartup() 0820 { 0821 QFETCH(QString, city); 0822 QFETCH(QString, state); 0823 QFETCH(QString, target); 0824 QFETCH(QString, startTimeUTC); 0825 QFETCH(QString, jobStartTimeUTC); 0826 0827 SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName(target); 0828 GeoLocation * const geoPtr = KStars::Instance()->data()->locationNamed(city, state, ""); 0829 GeoLocation &geo = *geoPtr; 0830 0831 const KStarsDateTime startUTime(QDateTime::fromString(startTimeUTC)); 0832 const KStarsDateTime jobStartUTime(QDateTime::fromString(jobStartTimeUTC)); 0833 0834 // move forward in 20s steps 0835 WithInterval interval(20000, scheduler); 0836 // define culmination offset of 1h as startup condition 0837 m_startupCondition.type = Ekos::START_ASAP; 0838 // initialize the the scheduler 0839 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 0840 QVector<QString> esqVector; 0841 esqVector.push_back(TestEkosSchedulerHelper::getDefaultEsqContent()); 0842 QVector<QString> eslVector; 0843 // 3rd arg is the true for twilight enforced. 0 is minAltitude. 0844 eslVector.push_back(TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, m_completionCondition, {true, true, true, true}, 0845 true, false, 0)); 0846 initScheduler(geo, startUTime, &dir, eslVector, esqVector); 0847 initJob(startUTime, jobStartUTime); 0848 } 0849 0850 void addHorizonConstraint(ArtificialHorizon *horizon, const QString &name, bool enabled, 0851 const QVector<double> &azimuths, const QVector<double> &altitudes, bool ceiling = false) 0852 { 0853 std::shared_ptr<LineList> pointList(new LineList); 0854 for (int i = 0; i < azimuths.size(); ++i) 0855 { 0856 std::shared_ptr<SkyPoint> skyp1(new SkyPoint); 0857 skyp1->setAlt(altitudes[i]); 0858 skyp1->setAz(azimuths[i]); 0859 pointList->append(skyp1); 0860 } 0861 horizon->addRegion(name, enabled, pointList, ceiling); 0862 } 0863 0864 void TestEkosSchedulerOps::testArtificialHorizonConstraints() 0865 { 0866 // In testSimpleJob, above, the wakeup time for the job was 11:35pm local time, and it used a 30-degrees min altitude. 0867 // Now let's add an artificial horizon constraint for 40-degrees at the azimuths where the object will be. 0868 // It should now wakeup and start processing at about 00:27am 0869 0870 ArtificialHorizon horizon; 0871 addHorizonConstraint(&horizon, "r1", true, QVector<double>({100, 120}), QVector<double>({40, 40})); 0872 Ekos::SchedulerJob::setHorizon(&horizon); 0873 0874 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 0875 QVector<SkyObject*> targetObjects; 0876 targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Altair")); 0877 QDateTime startUTime(QDateTime(QDate(2021, 6, 13), QTime(22, 0, 0), Qt::UTC)); 0878 0879 const QDateTime wakeupTime(QDate(2021, 6, 14), QTime(07, 27, 0), Qt::UTC); 0880 runSimpleJob(geo, targetObjects[0], startUTime, wakeupTime, true); 0881 0882 // Uncheck enforce artificial horizon and the wakeup time should go back to it's original time, 0883 // even though the artificial horizon is still there and enabled. 0884 init(); // Reset the scheduler. 0885 const QDateTime originalWakeupTime(QDate(2021, 6, 14), QTime(06, 35, 0), Qt::UTC); 0886 runSimpleJob(geo, targetObjects[0], startUTime, originalWakeupTime, /* enforce artificial horizon */false); 0887 0888 // Re-check enforce artificial horizon, but remove the constraint, and the wakeup time also goes back to it's original time. 0889 init(); // Reset the scheduler. 0890 ArtificialHorizon emptyHorizon; 0891 Ekos::SchedulerJob::setHorizon(&emptyHorizon); 0892 runSimpleJob(geo, targetObjects[0], startUTime, originalWakeupTime, /* enforce artificial horizon */ true); 0893 0894 // Testing that the artificial horizon constraint will end a job 0895 // when the altitude of the running job is below the artificial horizon at the 0896 // target's azimuth. 0897 // 0898 // This repeats testDawnShutdown() above, except that an artifical horizon 0899 // constraint is added so that the job doesn't reach dawn but rather is interrupted 0900 // at 3:19 local time. That's the time the azimuth reaches 175. 0901 0902 init(); // Reset the scheduler. 0903 { 0904 WithInterval interval(40000, scheduler); 0905 ArtificialHorizon shutdownHorizon; 0906 // Note, just putting a constraint at 175->180 will fail this test because Altair will 0907 // cross past 180 and the scheduler will want to restart it before dawn. 0908 addHorizonConstraint(&shutdownHorizon, "h", true, 0909 QVector<double>({175, 200}), QVector<double>({70, 70})); 0910 Ekos::SchedulerJob::setHorizon(&shutdownHorizon); 0911 0912 // We'll start the scheduler at 3am local time. 0913 startUTime = QDateTime(QDate(2021, 6, 14), QTime(10, 0, 0), Qt::UTC); 0914 // The job should start at 3:12am local time. 0915 QDateTime startJobUTime = startUTime.addSecs(120); 0916 // The job should be interrupted by the horizon limit, which is reached about 3:19am local. 0917 QDateTime horizonStopUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 19, 0), Qt::UTC)); 0918 0919 KStarsDateTime currentUTime; 0920 int sleepMs = 0; 0921 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 0922 0923 startup(geo, targetObjects, startUTime, currentUTime, sleepMs, dir); 0924 slewAndRun(targetObjects[0], startJobUTime, horizonStopUTime, currentUTime, sleepMs, DEFAULT_TOLERANCE, 0925 "Horizon slewAndRun"); 0926 parkAndSleep(currentUTime, sleepMs); 0927 const QDateTime restartTime(QDate(2021, 6, 15), QTime(06, 31, 0), Qt::UTC); 0928 wakeupAndRestart(restartTime, currentUTime, sleepMs); 0929 } 0930 } 0931 0932 // Similar to the above testArtificialHorizonConstraints test, 0933 // Schedule Altair and give it an artificial horizon constraint that will stop it at 3:19am. 0934 // However, here we also have a second job, Deneb, and test to see that the 2nd job will 0935 // start up after Altair stops and run until dawn. 0936 0937 // Test of running the full scheduler with the greedy algorithm. 0938 // This is the schedule that Greedy predicts 0939 // Deneb starts 06/13 22:48 done: 2760/12225 s stops 23:34 2760s (interrupted) (Tcomp 02:11 Tint 23:34 Tconstraint 03:53) 0940 // Altair starts 06/13 23:34 done: 12225/12225 s stops 02:57 12225s (completion) (Tcomp 02:57 Tint Tconstraint 03:20) 0941 // Deneb starts 06/14 02:57 done: 6059/12225 s stops 03:52 3299s (constraint) (Tcomp 05:35 Tint Tconstraint 03:52) 0942 // Deneb starts 06/14 22:43 done: 12225/12225 s stops 00:26 6166s (completion) (Tcomp 00:26 Tint Tconstraint 03:52) 0943 // however the code below doesn't simulate completion, so instead the Altair should end at its 3:20 constraint time 0944 0945 void TestEkosSchedulerOps::testGreedySchedulerRun() 0946 { 0947 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 0948 // This test will iterate the scheduler every 40 simulated seconds (to save testing time). 0949 WithInterval interval(40000, scheduler); 0950 0951 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 0952 QVector<SkyObject*> targetObjects; 0953 constexpr int altairIndex = 0, denebIndex = 1; 0954 targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Altair")); 0955 targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Deneb")); 0956 0957 ArtificialHorizon shutdownHorizon; 0958 addHorizonConstraint(&shutdownHorizon, "h", true, 0959 QVector<double>({175, 200}), QVector<double>({70, 70})); 0960 Ekos::SchedulerJob::setHorizon(&shutdownHorizon); 0961 0962 // Start the scheduler about 9pm local 0963 const QDateTime startUTime = QDateTime(QDate(2021, 6, 14), QTime(4, 0, 0), Qt::UTC); 0964 0965 QDateTime d1Start (QDateTime(QDate(2021, 6, 14), QTime( 5, 50, 0), Qt::UTC)); // 10:48pm 0966 QDateTime a1Start (QDateTime(QDate(2021, 6, 14), QTime( 6, 34, 0), Qt::UTC)); // 11:34pm 0967 QDateTime d2Start (QDateTime(QDate(2021, 6, 14), QTime(10, 20, 0), Qt::UTC)); // 3:20am 0968 QDateTime d2End (QDateTime(QDate(2021, 6, 14), QTime(10, 53, 0), Qt::UTC)); // 3:53am 0969 QDateTime d3Start (QDateTime(QDate(2021, 6, 15), QTime( 5, 44, 0), Qt::UTC)); // 10:43pm the next day 0970 0971 KStarsDateTime currentUTime; 0972 int sleepMs = 0; 0973 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 0974 0975 startup(geo, targetObjects, startUTime, currentUTime, sleepMs, dir); 0976 slewAndRun(targetObjects[denebIndex], d1Start, a1Start, currentUTime, sleepMs, 600, "Greedy job #1"); 0977 slewAndRun(targetObjects[altairIndex], a1Start, d2Start, currentUTime, sleepMs, 600, "Greedy job #2"); 0978 slewAndRun(targetObjects[denebIndex], d2Start, d2End, currentUTime, sleepMs, 600, "Greedy job #3"); 0979 0980 parkAndSleep(currentUTime, sleepMs); 0981 wakeupAndRestart(d3Start, currentUTime, sleepMs); 0982 startModules(currentUTime, sleepMs); 0983 slewAndRun(targetObjects[denebIndex], d3Start.addSecs(500), QDateTime(), currentUTime, sleepMs, 600, "Greedy job #4"); 0984 } 0985 0986 // Check if already existing captures are recognized properly and schedules are 0987 // recognized are started properly or as completed. 0988 void TestEkosSchedulerOps::testRememberJobProgress() 0989 { 0990 // turn on remember job progress 0991 Options::setRememberJobProgress(true); 0992 QVERIFY(Options::rememberJobProgress()); 0993 0994 // a well known place and target :) 0995 GeoLocation geo(dms(9, 45, 54), dms(49, 6, 22), "Schwaebisch Hall", "Baden-Wuerttemberg", "Germany", +1); 0996 SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName("Kocab"); 0997 0998 // Take 20:00 GMT (incl. +1h DST) and invalid wakeup time since the scheduler will start immediately 0999 QDateTime startUTime(QDateTime(QDate(2021, 10, 30), QTime(18, 0, 0), Qt::UTC)); 1000 const QDateTime wakeupTime; 1001 KStarsDateTime currentUTime; 1002 int sleepMs = 0; 1003 1004 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1005 QDir fits_dir(dir.path() + "/images"); 1006 1007 QVector<TestEkosSchedulerHelper::CaptureJob> capture_jobs = QVector<TestEkosSchedulerHelper::CaptureJob>(); 1008 1009 // parse test data to create the capture jobs 1010 QFETCH(QString, jobs); 1011 if (jobs != "") 1012 { 1013 for (QString value : jobs.split(",")) 1014 { 1015 QVERIFY(value.indexOf(":") > -1); 1016 QString filter = value.left(value.indexOf(":")).trimmed(); 1017 int count = value.right(value.length() - value.indexOf(":") - 1).toInt(); 1018 capture_jobs.append({1000, count, filter, fits_dir.absolutePath()}); 1019 } 1020 } 1021 1022 // parse test data to create the existing frame files 1023 QFETCH(QString, frames); 1024 if (frames != "") 1025 { 1026 for (QString value : frames.split(",")) 1027 { 1028 QVERIFY(value.indexOf(":") > -1); 1029 QString filter = value.left(value.indexOf(":")).trimmed(); 1030 int count = value.right(value.length() - value.indexOf(":") - 1).toInt(); 1031 QDir img_dir(fits_dir); 1032 img_dir.mkpath("Kocab/Light/" + filter); 1033 1034 // create files 1035 for (int i = 0; i < count; i++) 1036 { 1037 QFile frame; 1038 frame.setFileName(QString(img_dir.absolutePath() + "/Kocab/Light/" + filter + "/Kocab_Light_%1_%2.fits").arg(filter).arg( 1039 i)); 1040 frame.open(QIODevice::WriteOnly | QIODevice::Text); 1041 frame.close(); 1042 } 1043 } 1044 } 1045 1046 // start up the scheduler job 1047 QFETCH(int, iterations); 1048 1049 TestEkosSchedulerHelper::CompletionCondition completionCondition; 1050 completionCondition.type = Ekos::FINISH_REPEAT; 1051 completionCondition.repeat = iterations; 1052 startupJob(geo, startUTime, &dir, 1053 TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, completionCondition, {true, true, true, true}, 1054 false, false, sleepMs, nullptr, {false, false, false, false}), 1055 TestEkosSchedulerHelper::getEsqContent(capture_jobs), wakeupTime, currentUTime, sleepMs); 1056 1057 // fetch the expected result from the test data 1058 QFETCH(bool, scheduled); 1059 1060 // verify if the job is scheduled as expected 1061 QVERIFY(scheduler->moduleState()->jobs()[0]->getState() == (scheduled ? Ekos::SCHEDJOB_SCHEDULED : 1062 Ekos::SCHEDJOB_COMPLETE)); 1063 } 1064 1065 void TestEkosSchedulerOps::loadGreedySchedule( 1066 bool first, const QString &targetName, 1067 const TestEkosSchedulerHelper::StartupCondition &startupCondition, 1068 const TestEkosSchedulerHelper::CompletionCondition &completionCondition, 1069 QTemporaryDir &dir, const QVector<TestEkosSchedulerHelper::CaptureJob> &captureJob, int minAltitude, 1070 const TestEkosSchedulerHelper::ScheduleSteps steps, bool enforceTwilight, bool enforceHorizon, int errorDelay) 1071 { 1072 SkyObject *object = KStars::Instance()->data()->skyComposite()->findByName(targetName); 1073 QVERIFY(object != nullptr); 1074 const QString schedulerXML = 1075 TestEkosSchedulerHelper::getSchedulerFile( 1076 object, startupCondition, completionCondition, steps, enforceTwilight, enforceHorizon, minAltitude, 1077 nullptr, {false, false, true, false}, errorDelay); 1078 1079 // Write the scheduler and sequence files. 1080 QString f1 = writeFiles(targetName, dir, captureJob, schedulerXML); 1081 scheduler->load(first, QString("file://%1").arg(f1)); 1082 } 1083 1084 struct SPlan 1085 { 1086 QString name; 1087 QString start; 1088 QString stop; 1089 }; 1090 1091 bool checkSchedule(const QVector<SPlan> &ref, const QList<Ekos::GreedyScheduler::JobSchedule> &schedule, int tolerance) 1092 { 1093 bool result = true; 1094 if (schedule.size() != ref.size()) 1095 { 1096 qCInfo(KSTARS_EKOS_TEST) << QString("Sizes don't match %1 vs ref %2").arg(schedule.size()).arg(ref.size()); 1097 return false; 1098 } 1099 for (int i = 0; i < ref.size(); ++i) 1100 { 1101 QDateTime startTime = QDateTime::fromString(ref[i].start, "yyyy/MM/dd hh:mm"); 1102 QDateTime stopTime = QDateTime::fromString(ref[i].stop, "yyyy/MM/dd hh:mm"); 1103 if (!startTime.isValid() || !stopTime.isValid()) 1104 { 1105 qCInfo(KSTARS_EKOS_TEST) << QString("Reference start or stop time invalid: %1 %2").arg(ref[i].start).arg(ref[i].stop); 1106 result = false; 1107 } 1108 else if (!schedule[i].startTime.isValid() || !schedule[i].stopTime.isValid()) 1109 { 1110 qCInfo(KSTARS_EKOS_TEST) << QString("Scheduled start or stop time %1 invalid.").arg(i); 1111 result = false; 1112 } 1113 else if ((ref[i].name != schedule[i].job->getName()) || 1114 (std::abs(schedule[i].startTime.secsTo(startTime)) > tolerance) || 1115 (std::abs(startTime.secsTo(stopTime) - schedule[i].startTime.secsTo(schedule[i].stopTime)) > tolerance)) 1116 { 1117 qCInfo(KSTARS_EKOS_TEST) 1118 << QString("Mismatch on entry %1: ref \"%2\" \"%3\" \"%4\" result \"%5\" \"%6\" \"%7\"") 1119 .arg(i) 1120 .arg(ref[i].name, startTime.toString(), stopTime.toString(), 1121 schedule[i].job->getName(), 1122 schedule[i].startTime.toString(), 1123 schedule[i].stopTime.toString()); 1124 result = false; 1125 } 1126 } 1127 return result; 1128 } 1129 1130 void TestEkosSchedulerOps::testGreedy() 1131 { 1132 // Allow 10 minutes of slop in the schedule. The scheduler simulates every 2 minutes, 1133 // so 10 minutes is approx 5 of these timesteps. 1134 constexpr int checkScheduleTolerance = 600; 1135 1136 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1137 1138 // Setup geo and an artificial horizon. 1139 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 1140 ArtificialHorizon shutdownHorizon; 1141 addHorizonConstraint(&shutdownHorizon, "h", true, QVector<double>({175, 200}), QVector<double>({70, 70})); 1142 Ekos::SchedulerJob::setHorizon(&shutdownHorizon); 1143 // Start the scheduler about 9pm local 1144 const QDateTime startUTime = QDateTime(QDate(2021, 6, 14), QTime(4, 0, 0), Qt::UTC); 1145 initTimeGeo(geo, startUTime); 1146 1147 auto schedJob200x60 = QVector<TestEkosSchedulerHelper::CaptureJob>(1, {200, 60, "Red", "."}); 1148 auto schedJob400x60 = QVector<TestEkosSchedulerHelper::CaptureJob>(1, {400, 60, "Red", "."}); 1149 1150 TestEkosSchedulerHelper::StartupCondition asapStartupCondition, atStartupCondition; 1151 TestEkosSchedulerHelper::CompletionCondition finishCompletionCondition, loopCompletionCondition; 1152 TestEkosSchedulerHelper::CompletionCondition repeat2CompletionCondition, atCompletionCondition; 1153 asapStartupCondition.type = Ekos::START_ASAP; 1154 atStartupCondition.type = Ekos::START_AT; 1155 atStartupCondition.atLocalDateTime = QDateTime(QDate(2021, 6, 14), QTime(1, 0, 0), Qt::LocalTime); 1156 finishCompletionCondition.type = Ekos::FINISH_SEQUENCE; 1157 loopCompletionCondition.type = Ekos::FINISH_LOOP; 1158 repeat2CompletionCondition.type = Ekos::FINISH_REPEAT; 1159 repeat2CompletionCondition.repeat = 2; 1160 atCompletionCondition.type = Ekos::FINISH_AT; 1161 atCompletionCondition.atLocalDateTime = QDateTime(QDate(2021, 6, 14), QTime(3, 30, 0), Qt::LocalTime); 1162 1163 // Write the scheduler and sequence files. 1164 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1165 1166 // Altair is scheduled with first priority, but doesn't clear constraints immediately. 1167 // Deneb is also scheduled, but with twice as many captures. Both start ASAP and run to comletion. 1168 // Deneb runs first, gets interrupted by Altair which runs to completion. 1169 // Then Deneb runs for the rest of the night, and also again the next evening before it comletes. 1170 loadGreedySchedule(true, "Altair", asapStartupCondition, finishCompletionCondition, dir, schedJob200x60, 30); 1171 loadGreedySchedule(false, "Deneb", asapStartupCondition, finishCompletionCondition, dir, schedJob400x60, 30); 1172 scheduler->process()->evaluateJobs(false); 1173 QVERIFY(checkSchedule( 1174 { 1175 {"Deneb", "2021/06/13 22:48", "2021/06/13 23:35"}, 1176 {"Altair", "2021/06/13 23:35", "2021/06/14 02:59"}, 1177 {"Deneb", "2021/06/14 02:59", "2021/06/14 03:53"}, 1178 {"Deneb", "2021/06/14 22:44", "2021/06/15 03:48"}}, 1179 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1180 1181 // Disable greedy scheduling, and Deneb should NOT run until Altair is done. 1182 Options::setGreedyScheduling(false); 1183 scheduler->process()->evaluateJobs(false); 1184 QVERIFY(checkSchedule( 1185 { 1186 {"Altair", "2021/06/13 23:34", "2021/06/14 03:00"}, 1187 {"Deneb", "2021/06/14 03:01", "2021/06/14 03:53"}, 1188 {"Deneb", "2021/06/14 22:44", "2021/06/15 03:52"}}, 1189 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1190 Options::setGreedyScheduling(true); 1191 1192 // As above, except Altair has completion condition repeat 2. It should run longer. 1193 // This makes a mess of things, as Altair can't complete during the first night, running into an artificial horizon constraint. 1194 // It also can't start the 2nd night as early as Deneb, so the 2nd night is Deneb, Altair (completing), Deneb, and Deneb finishes the 3rd night. 1195 loadGreedySchedule(true, "Altair", asapStartupCondition, repeat2CompletionCondition, dir, schedJob200x60, 30); 1196 loadGreedySchedule(false, "Deneb", asapStartupCondition, finishCompletionCondition, dir, schedJob400x60, 30); 1197 scheduler->process()->evaluateJobs(false); 1198 QVERIFY(checkSchedule( 1199 { 1200 {"Deneb", "2021/06/13 22:48", "2021/06/13 23:35"}, 1201 {"Altair", "2021/06/13 23:35", "2021/06/14 03:21"}, 1202 {"Deneb", "2021/06/14 03:22", "2021/06/14 03:53"}, 1203 {"Deneb", "2021/06/14 22:44", "2021/06/14 23:30"}, 1204 {"Altair", "2021/06/14 23:31", "2021/06/15 02:29"}, 1205 {"Deneb", "2021/06/15 02:30", "2021/06/15 03:53"}}, 1206 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1207 1208 // Again disable greedy scheduling, and Deneb should NOT run until Altair is done. 1209 Options::setGreedyScheduling(false); 1210 scheduler->process()->evaluateJobs(false); 1211 QVERIFY(checkSchedule( 1212 { 1213 {"Altair", "2021/06/13 23:34", "2021/06/14 03:18"}, 1214 {"Altair", "2021/06/14 23:30", "2021/06/15 02:32"}, 1215 {"Deneb", "2021/06/15 02:33", "2021/06/15 03:53"}}, 1216 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1217 Options::setGreedyScheduling(true); 1218 1219 // Now we're using START_AT 6/14 1am for Altair (but not repeating twice). 1220 // Deneb will run until then (1am) the 1st night. Altair will run until it hits the horizon constraint. 1221 // Deneb runs through the end of the night, and again the next night until it completes. 1222 loadGreedySchedule(true, "Altair", atStartupCondition, finishCompletionCondition, dir, schedJob200x60, 30); 1223 loadGreedySchedule(false, "Deneb", asapStartupCondition, finishCompletionCondition, dir, schedJob400x60, 30); 1224 scheduler->process()->evaluateJobs(false); 1225 QVERIFY(checkSchedule( 1226 { 1227 {"Deneb", "2021/06/13 22:48", "2021/06/14 01:00"}, 1228 {"Altair", "2021/06/14 01:00", "2021/06/14 03:21"}, 1229 {"Deneb", "2021/06/14 03:22", "2021/06/14 03:53"}, 1230 {"Deneb", "2021/06/14 22:44", "2021/06/15 02:44"}}, 1231 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1232 1233 // Again disable greedy scheduling, and Deneb should NOT run until Altair is done. 1234 Options::setGreedyScheduling(false); 1235 scheduler->process()->evaluateJobs(false); 1236 QVERIFY(checkSchedule( 1237 { 1238 {"Altair", "2021/06/14 01:00", "2021/06/14 03:21"}, 1239 {"Deneb", "2021/06/14 03:22", "2021/06/14 03:53"}, 1240 {"Deneb", "2021/06/14 22:44", "2021/06/15 03:53"}}, 1241 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1242 Options::setGreedyScheduling(true); 1243 1244 // We again use START_AT 6/14 1am for Altair, but force Deneb to complete by 3:30am on 6/14. 1245 // So we get the same first two lines as above, but now Deneb stops on the 3rd line at 3:30. 1246 loadGreedySchedule(true, "Altair", atStartupCondition, finishCompletionCondition, dir, schedJob200x60, 30); 1247 loadGreedySchedule(false, "Deneb", asapStartupCondition, atCompletionCondition, dir, schedJob400x60, 30); 1248 scheduler->process()->evaluateJobs(false); 1249 QVERIFY(checkSchedule( 1250 { 1251 {"Deneb", "2021/06/13 22:48", "2021/06/14 01:00"}, 1252 {"Altair", "2021/06/14 01:00", "2021/06/14 03:21"}, 1253 {"Deneb", "2021/06/14 03:22", "2021/06/14 03:30"}}, 1254 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1255 1256 // Again disable greedy scheduling, and Deneb should NOT run until Altair is done. 1257 Options::setGreedyScheduling(false); 1258 scheduler->process()->evaluateJobs(false); 1259 QVERIFY(checkSchedule( 1260 { 1261 {"Altair", "2021/06/14 01:00", "2021/06/14 03:21"}, 1262 {"Deneb", "2021/06/14 03:22", "2021/06/14 03:30"}}, 1263 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1264 Options::setGreedyScheduling(true); 1265 1266 // We have the same Altair constraints, but this time allow Deneb to run forever. 1267 // It will look like the 3rd test, except Deneb keeps running through the end of the simulated time (2 days). 1268 loadGreedySchedule(true, "Altair", atStartupCondition, finishCompletionCondition, dir, schedJob200x60, 30); 1269 loadGreedySchedule(false, "Deneb", asapStartupCondition, loopCompletionCondition, dir, schedJob400x60, 30); 1270 scheduler->process()->evaluateJobs(false); 1271 QVERIFY(checkSchedule( 1272 { 1273 {"Deneb", "2021/06/13 22:48", "2021/06/14 01:00"}, 1274 {"Altair", "2021/06/14 01:00", "2021/06/14 03:21"}, 1275 {"Deneb", "2021/06/14 03:22", "2021/06/14 03:53"}, 1276 {"Deneb", "2021/06/14 22:44", "2021/06/15 03:52"}}, 1277 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1278 1279 // Again disable greedy scheduling, and Deneb should NOT run until Altair is done. 1280 Options::setGreedyScheduling(false); 1281 scheduler->process()->evaluateJobs(false); 1282 QVERIFY(checkSchedule( 1283 { 1284 {"Altair", "2021/06/14 01:00", "2021/06/14 03:19"}, 1285 {"Deneb", "2021/06/14 03:20", "2021/06/14 03:52"}, 1286 {"Deneb", "2021/06/14 22:44", "2021/06/15 03:52"}}, 1287 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1288 Options::setGreedyScheduling(true); 1289 1290 // Altair stars asap, Deneb has an at 1am startup and loop finish. 1291 // Altair should start when it's able (about 23:35) but get interrupted by deneb which is higher priority 1292 // because of its startat. Altair will start up again the next evening because Deneb's startat will have expired. 1293 loadGreedySchedule(true, "Altair", asapStartupCondition, finishCompletionCondition, dir, schedJob200x60, 30); 1294 loadGreedySchedule(false, "Deneb", atStartupCondition, loopCompletionCondition, dir, schedJob400x60, 30); 1295 scheduler->process()->evaluateJobs(false); 1296 QVERIFY(checkSchedule( 1297 { 1298 {"Altair", "2021/06/13 23:34", "2021/06/14 01:00"}, 1299 {"Deneb", "2021/06/14 01:00", "2021/06/14 03:52"}, 1300 {"Altair", "2021/06/14 23:30", "2021/06/15 01:31"}}, 1301 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1302 1303 // Again disable greedy scheduling. Nothing should change as no jobs were running before higher priority ones. 1304 Options::setGreedyScheduling(false); 1305 scheduler->process()->evaluateJobs(false); 1306 QVERIFY(checkSchedule( 1307 { 1308 {"Altair", "2021/06/13 23:34", "2021/06/14 01:00"}, 1309 {"Deneb", "2021/06/14 01:00", "2021/06/14 03:52"}, 1310 {"Altair", "2021/06/14 23:30", "2021/06/15 01:31"}}, 1311 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1312 Options::setGreedyScheduling(true); 1313 } 1314 1315 void TestEkosSchedulerOps::testGroups() 1316 { 1317 // Allow 10 minutes of slop in the schedule. The scheduler simulates every 2 minutes, 1318 // so 10 minutes is approx 5 of these timesteps. 1319 constexpr int checkScheduleTolerance = 600; 1320 1321 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1322 1323 // Setup geo and an artificial horizon. 1324 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 1325 ArtificialHorizon shutdownHorizon; 1326 addHorizonConstraint(&shutdownHorizon, "h", true, QVector<double>({175, 200}), QVector<double>({70, 70})); 1327 Ekos::SchedulerJob::setHorizon(&shutdownHorizon); 1328 // Start the scheduler about 9pm local 1329 const QDateTime startUTime = QDateTime(QDate(2021, 6, 14), QTime(4, 0, 0), Qt::UTC); 1330 initTimeGeo(geo, startUTime); 1331 1332 // About a 30-minute job 1333 auto schedJob30minutes = QVector<TestEkosSchedulerHelper::CaptureJob>( 1334 {{180, 4, "Red", "."}, {180, 6, "Blue", "."}}); 1335 1336 TestEkosSchedulerHelper::StartupCondition asapStartupCondition, atStartupCondition; 1337 TestEkosSchedulerHelper::CompletionCondition finishCompletionCondition, loopCompletionCondition; 1338 TestEkosSchedulerHelper::CompletionCondition repeat2CompletionCondition, atCompletionCondition; 1339 asapStartupCondition.type = Ekos::START_ASAP; 1340 finishCompletionCondition.type = Ekos::FINISH_SEQUENCE; 1341 loopCompletionCondition.type = Ekos::FINISH_LOOP; 1342 repeat2CompletionCondition.type = Ekos::FINISH_REPEAT; 1343 repeat2CompletionCondition.repeat = 2; 1344 1345 // Write the scheduler and sequence files. 1346 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1347 1348 // Create 3 jobs in the same group, of the same target, each should last ~30 minutes (not counting repetitions). 1349 // the first job just runs the 30-minute sequence, the 2nd repeats forever, the 3rd repeats twice. 1350 // Given the grouping we should see jobs scheduled about every 30 minutes 1351 // orderdc as: J1, J2, J3, J2 (skips the completed J1), J3, J2 (looing forever as J3 is now done). 1352 loadGreedySchedule(true, "Altair", asapStartupCondition, finishCompletionCondition, dir, schedJob30minutes, 30); 1353 scheduler->moduleState()->jobs().last()->setName("J1finish"); 1354 scheduler->moduleState()->jobs().last()->setGroup("group1"); 1355 loadGreedySchedule(false, "Altair", asapStartupCondition, loopCompletionCondition, dir, schedJob30minutes, 30); 1356 scheduler->moduleState()->jobs().last()->setName("J2loop"); 1357 scheduler->moduleState()->jobs().last()->setGroup("group1"); 1358 loadGreedySchedule(false, "Altair", asapStartupCondition, repeat2CompletionCondition, dir, schedJob30minutes, 30); 1359 scheduler->moduleState()->jobs().last()->setName("J3repeat2"); 1360 scheduler->moduleState()->jobs().last()->setGroup("group1"); 1361 scheduler->process()->evaluateJobs(false); 1362 1363 QVERIFY(checkSchedule( 1364 { 1365 {"J1finish", "2021/06/13 23:34", "2021/06/14 00:10"}, 1366 {"J2loop", "2021/06/14 00:11", "2021/06/14 00:47"}, 1367 {"J3repeat2", "2021/06/14 00:48", "2021/06/14 01:24"}, 1368 {"J2loop", "2021/06/14 01:25", "2021/06/14 01:55"}, 1369 {"J3repeat2", "2021/06/14 01:56", "2021/06/14 02:26"}, 1370 {"J2loop", "2021/06/14 02:27", "2021/06/14 03:19"}, 1371 {"J2loop", "2021/06/14 23:31", "2021/06/15 03:15"}}, 1372 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1373 1374 // Now do the same thing, but this time disable the group scheduling (by not assigning groups). 1375 // This time J1 should run then J2 will just run/repeat forever. 1376 loadGreedySchedule(true, "Altair", asapStartupCondition, finishCompletionCondition, dir, schedJob30minutes, 30); 1377 scheduler->moduleState()->jobs().last()->setName("J1finish"); 1378 loadGreedySchedule(false, "Altair", asapStartupCondition, loopCompletionCondition, dir, schedJob30minutes, 30); 1379 scheduler->moduleState()->jobs().last()->setName("J2loop"); 1380 loadGreedySchedule(false, "Altair", asapStartupCondition, repeat2CompletionCondition, dir, schedJob30minutes, 30); 1381 scheduler->moduleState()->jobs().last()->setName("J3repeat2"); 1382 scheduler->process()->evaluateJobs(false); 1383 1384 QVERIFY(checkSchedule( 1385 { 1386 {"J1finish", "2021/06/13 23:34", "2021/06/14 00:10"}, 1387 {"J2loop", "2021/06/14 00:11", "2021/06/14 03:19"}, 1388 {"J2loop", "2021/06/14 23:30", "2021/06/15 03:16"}}, 1389 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1390 } 1391 1392 void TestEkosSchedulerOps::testGreedyAborts() 1393 { 1394 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1395 1396 // Allow 10 minutes of slop in the schedule. The scheduler simulates every 2 minutes, 1397 // so 10 minutes is approx 5 of these timesteps. 1398 constexpr int checkScheduleTolerance = 600; 1399 1400 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1401 Ekos::SchedulerJob::setHorizon(nullptr); 1402 1403 // Setup geo and an artificial horizon. 1404 GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8); 1405 1406 // Start the scheduler about 9pm local 1407 const QDateTime startUTime = QDateTime(QDate(2022, 2, 28), QTime(10, 29, 26), Qt::UTC); 1408 initTimeGeo(geo, startUTime); 1409 1410 auto schedJob200x60 = QVector<TestEkosSchedulerHelper::CaptureJob>(1, {200, 60, "Red", "."}); 1411 TestEkosSchedulerHelper::StartupCondition asapStartupCondition; 1412 TestEkosSchedulerHelper::CompletionCondition loopCompletionCondition; 1413 asapStartupCondition.type = Ekos::START_ASAP; 1414 loopCompletionCondition.type = Ekos::FINISH_LOOP; 1415 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1416 1417 // Parameters related to resheduling aborts and the delay times are in the scheduler .esl file created in loadGreedyScheduler. 1418 const TestEkosSchedulerHelper::ScheduleSteps steps = {true, true, true, true}; 1419 const bool enforceTwilight = true; 1420 const bool enforceHorizon = true; 1421 const int errorDelay = 3600; 1422 1423 loadGreedySchedule(true, "M 104", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 36, steps, 1424 enforceTwilight, enforceHorizon, errorDelay); 1425 loadGreedySchedule(false, "NGC 3628", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 45, steps, 1426 enforceTwilight, enforceHorizon, errorDelay); 1427 loadGreedySchedule(false, "M 5", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 30, steps, 1428 enforceTwilight, enforceHorizon, errorDelay); 1429 loadGreedySchedule(false, "M 42", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 42, steps, 1430 enforceTwilight, enforceHorizon, errorDelay); 1431 1432 // start the scheduler at 1am 1433 KStarsDateTime evalUTime(QDate(2022, 2, 28), QTime(9, 00, 00), Qt::UTC); 1434 KStarsData::Instance()->changeDateTime(evalUTime); 1435 1436 scheduler->process()->evaluateJobs(false); 1437 1438 QVERIFY(checkSchedule( 1439 { 1440 {"M 104", "2022/02/28 01:00", "2022/02/28 03:52"}, 1441 {"M 5", "2022/02/28 03:52", "2022/02/28 05:14"}, 1442 {"M 42", "2022/02/28 19:31", "2022/02/28 20:43"}, 1443 {"NGC 3628", "2022/02/28 22:02", "2022/03/01 00:38"}, 1444 {"M 104", "2022/03/01 00:39", "2022/03/01 03:49"}, 1445 {"M 5", "2022/03/01 03:50", "2022/03/01 05:12"}, 1446 {"M 42", "2022/03/01 19:31", "2022/03/01 20:39"}, 1447 {"NGC 3628", "2022/03/01 21:58", "2022/03/02 00:34"}, 1448 {"M 104", "2022/03/02 00:35", "2022/03/02 03:45"}}, 1449 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1450 1451 // Now load the same schedule, but set the M104 job to have been aborted a minute before. 1452 loadGreedySchedule(true, "M 104", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 36, steps, 1453 enforceTwilight, enforceHorizon, errorDelay); 1454 loadGreedySchedule(false, "NGC 3628", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 45, steps, 1455 enforceTwilight, enforceHorizon, errorDelay); 1456 loadGreedySchedule(false, "M 5", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 30, steps, 1457 enforceTwilight, enforceHorizon, errorDelay); 1458 loadGreedySchedule(false, "M 42", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 42, steps, 1459 enforceTwilight, enforceHorizon, errorDelay); 1460 1461 // Otherwise time changes below will trigger reschedules and mess up test. 1462 scheduler->moduleState()->setSchedulerState(Ekos::SCHEDULER_RUNNING); 1463 1464 // Find the M 104 job and make it aborted at 12:59am 1465 KStarsDateTime abortUTime(QDate(2022, 2, 28), QTime(8, 59, 00), Qt::UTC); 1466 KStarsData::Instance()->changeDateTime(abortUTime); 1467 1468 Ekos::SchedulerJob *m104Job = nullptr; 1469 foreach (auto &job, scheduler->moduleState()->jobs()) 1470 if (job->getName() == "M 104") 1471 { 1472 m104Job = job; 1473 m104Job->setState(Ekos::SCHEDJOB_ABORTED); 1474 } 1475 QVERIFY(m104Job != nullptr); 1476 1477 // start the scheduler at 1am 1478 KStarsData::Instance()->changeDateTime(evalUTime); 1479 1480 scheduler->process()->evaluateJobs(false); 1481 1482 // The M104 job is no longer the first job, since aborted jobs are delayed an hour, 1483 QVERIFY(checkSchedule( 1484 { 1485 {"NGC 3628", "2022/02/28 01:00", "2022/02/28 02:00"}, 1486 {"M 104", "2022/02/28 02:00", "2022/02/28 03:52"}, 1487 {"M 5", "2022/02/28 03:52", "2022/02/28 05:14"}, 1488 {"M 42", "2022/02/28 19:31", "2022/02/28 20:43"}, 1489 {"NGC 3628", "2022/02/28 22:02", "2022/03/01 00:38"}, 1490 {"M 104", "2022/03/01 00:39", "2022/03/01 03:49"}, 1491 {"M 5", "2022/03/01 03:50", "2022/03/01 05:12"}, 1492 {"M 42", "2022/03/01 19:31", "2022/03/01 20:39"}, 1493 {"NGC 3628", "2022/03/01 21:58", "2022/03/02 00:34"}, 1494 {"M 104", "2022/03/02 00:35", "2022/03/02 03:45"}}, 1495 1496 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1497 auto ngc3628 = scheduler->process()->getGreedyScheduler()->getSchedule()[0].job; 1498 QVERIFY(ngc3628->getName() == "NGC 3628"); 1499 1500 // And ngc3628 should not be preempted right away, 1501 QDateTime localTime(QDate(2022, 2, 28), QTime(1, 00, 00), Qt::LocalTime); 1502 bool keepRunning = scheduler->process()->getGreedyScheduler()->checkJob(scheduler->moduleState()->jobs(), localTime, 1503 ngc3628); 1504 QVERIFY(keepRunning); 1505 1506 // nor in a half-hour. 1507 auto newTime = evalUTime.addSecs(1800); 1508 KStarsData::Instance()->changeDateTime(newTime); 1509 localTime = localTime.addSecs(1800); 1510 keepRunning = scheduler->process()->getGreedyScheduler()->checkJob(scheduler->moduleState()->jobs(), localTime, ngc3628); 1511 QVERIFY(keepRunning); 1512 1513 // But if we wait until 2am, m104 should preempt it, 1514 newTime = newTime.addSecs(1800); 1515 KStarsData::Instance()->changeDateTime(newTime); 1516 localTime = localTime.addSecs(1800); 1517 keepRunning = scheduler->process()->getGreedyScheduler()->checkJob(scheduler->moduleState()->jobs(), localTime, ngc3628); 1518 QVERIFY(!keepRunning); 1519 1520 // and M104 should be scheduled to start running "now" (2am). 1521 scheduler->process()->evaluateJobs(false); 1522 auto newSchedule = scheduler->process()->getGreedyScheduler()->getSchedule(); 1523 QVERIFY(newSchedule.size() > 0); 1524 QVERIFY(newSchedule[0].job->getName() == "M 104"); 1525 QVERIFY(std::abs(newSchedule[0].startTime.secsTo( 1526 QDateTime(QDate(2022, 2, 28), QTime(2, 00, 00), Qt::LocalTime))) < 200); 1527 } 1528 1529 void TestEkosSchedulerOps::testArtificialCeiling() 1530 { 1531 constexpr int checkScheduleTolerance = 600; 1532 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1533 1534 ArtificialHorizon horizon; 1535 QVector<double> az1 = {259.0, 260.0, 299.0, 300.0, 330.0, 0.0, 30.0, 70.0, 71.0, 90.0, 120.0, 150.0, 180.0, 210.0, 240.0, 259.99}; 1536 QVector<double> alt1 = { 90.0, 26.0, 26.0, 26.0, 26.0, 26.0, 26.0, 26.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0, 90.0}; 1537 addHorizonConstraint(&horizon, "floor", true, az1, alt1); 1538 1539 QVector<double> az2 = {260.0, 300.0, 330.0, 0.0, 30.0, 70.0}; 1540 QVector<double> alt2 = { 66.0, 66.0, 66.0, 66.0, 66.0, 66.0}; 1541 addHorizonConstraint(&horizon, "ceiling", true, az2, alt2, true); // last true --> ceiling 1542 1543 // Setup geo and an artificial horizon. 1544 GeoLocation geo(dms(-75, 40), dms(45, 40), "New York", "NY", "USA", -5); 1545 Ekos::SchedulerJob::setHorizon(&horizon); 1546 // Start the scheduler about 9pm local 1547 const QDateTime startUTime = QDateTime(QDate(2022, 8, 21), QTime(16, 0, 0), Qt::UTC); 1548 initTimeGeo(geo, startUTime); 1549 1550 auto schedJob200x60 = QVector<TestEkosSchedulerHelper::CaptureJob>(1, {200, 60, "Red", "."}); 1551 auto schedJob400x60 = QVector<TestEkosSchedulerHelper::CaptureJob>(1, {400, 60, "Red", "."}); 1552 1553 TestEkosSchedulerHelper::StartupCondition asapStartupCondition; 1554 TestEkosSchedulerHelper::CompletionCondition loopCompletionCondition; 1555 asapStartupCondition.type = Ekos::START_ASAP; 1556 loopCompletionCondition.type = Ekos::FINISH_LOOP; 1557 1558 // Write the scheduler and sequence files. 1559 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1560 1561 loadGreedySchedule(true, "theta Bootis", asapStartupCondition, loopCompletionCondition, dir, schedJob200x60, 0, 1562 {true, true, true, true}, false, true); // min alt = 0, don't enforce twilight 1563 scheduler->process()->evaluateJobs(false); 1564 1565 // There are no altitude constraints, just an artificial horizon with 2 lines, the top a ceiling. 1566 // It should scheduler from "now" until the star reaches the ceiling, then shut off until it lowers 1567 // back into the window, then shutoff again when the star goes below the artificial horizon, and 1568 // then start-up again when it once again raises above the artificial horizon into the window. 1569 QVERIFY(checkSchedule( 1570 { 1571 {"HD 126660", "2022/08/21 12:00", "2022/08/21 15:05"}, 1572 {"HD 126660", "2022/08/21 19:50", "2022/08/22 00:34"}, 1573 {"HD 126660", "2022/08/22 10:19", "2022/08/22 15:03"}, 1574 {"HD 126660", "2022/08/22 19:46", "2022/08/23 00:30"}, 1575 {"HD 126660", "2022/08/23 10:15", "2022/08/23 14:59"}}, 1576 scheduler->process()->getGreedyScheduler()->getSchedule(), checkScheduleTolerance)); 1577 } 1578 1579 void TestEkosSchedulerOps::testSettingAltitudeBug() 1580 { 1581 Options::setDawnOffset(0); 1582 Options::setDuskOffset(0); 1583 Options::setSettingAltitudeCutoff(3); 1584 Options::setPreDawnTime(0); 1585 1586 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1587 Ekos::SchedulerJob::setHorizon(nullptr); 1588 1589 GeoLocation geo(dms(9, 45, 54), dms(49, 6, 22), "Schwaebisch Hall", "Baden-Wuerttemberg", "Germany", +1); 1590 const QDateTime time1 = QDateTime(QDate(2022, 3, 7), QTime(21, 28, 55), Qt::UTC); //22:28 local 1591 initTimeGeo(geo, time1); 1592 1593 auto wolfgangJob = QVector<TestEkosSchedulerHelper::CaptureJob>( 1594 { 1595 {360, 3, "L", "."}, {360, 1, "R", "."}, {360, 1, "G", "."}, 1596 {360, 1, "B", "."}, {360, 2, "L", "."}}); 1597 1598 // Guessing he was using 40minute offsets 1599 Options::setDawnOffset(.666); 1600 Options::setDuskOffset(-.666); 1601 1602 TestEkosSchedulerHelper::StartupCondition asapStartupCondition; 1603 TestEkosSchedulerHelper::CompletionCondition loopCompletionCondition; 1604 asapStartupCondition.type = Ekos::START_ASAP; 1605 loopCompletionCondition.type = Ekos::FINISH_LOOP; 1606 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1607 1608 loadGreedySchedule(true, "NGC 2359", asapStartupCondition, loopCompletionCondition, dir, wolfgangJob, 20); 1609 loadGreedySchedule(false, "NGC 2392", asapStartupCondition, loopCompletionCondition, dir, wolfgangJob, 20); 1610 loadGreedySchedule(false, "M 101", asapStartupCondition, loopCompletionCondition, dir, wolfgangJob, 20); 1611 1612 scheduler->process()->evaluateJobs(false); 1613 1614 // In the log with bug, the original schedule had 2359 running 20:30 -> 23:04 1615 // "Greedy Scheduler plan for the next 48 hours (0.075)s:" 1616 // "NGC 2359 03/07 20:30 --> 23:04 altitude 19.8 < 20.0" 1617 // "NGC 2392 03/07 23:05 --> 02:23 altitude 19.7 < 20.0" 1618 // "M 101 03/08 02:23 --> 05:43 twilight" 1619 // "NGC 2359 03/08 19:22 --> 22:58 altitude 19.9 < 20.0" 1620 // "NGC 2392 03/08 22:59 --> 02:17 altitude 19.9 < 20.0" 1621 // "M 101 03/09 02:18 --> 05:40 twilight" 1622 // "NGC 2359 03/09 19:23 --> 22:55 altitude 19.8 < 20.0" 1623 // 1624 // but the job was stopped at 22:28, and when the schedule was made then 1625 // it could no longer run ngc2359. 1626 // 1627 // "Greedy Scheduler plan for the next 48 hours (0.079)s:" 1628 // "NGC 2392 03/07 22:28 --> 02:22 altitude 19.7 < 20.0" 1629 // "M 101 03/08 02:23 --> 05:43 twilight" 1630 // "NGC 2359 03/08 19:22 --> 22:58 altitude 19.9 < 20.0" 1631 // "NGC 2392 03/08 22:59 --> 02:17 altitude 19.9 < 20.0" 1632 // "M 101 03/09 02:18 --> 05:42 twilight" 1633 // "NGC 2359 03/09 19:23 --> 22:55 altitude 19.8 < 20.0" 1634 // 1635 // The issue was that settingAltitudeCutoff was being applied to the running 1636 // job, preempting it. The intention of that parameter is to stop new jobs 1637 // from being scheduled near their altitude cutoff times, not to preempt existing ones. 1638 1639 KStarsDateTime time2(QDate(2022, 3, 7), QTime(19, 30, 00), Qt::UTC); //20:30 local 1640 initTimeGeo(geo, time2); 1641 scheduler->process()->evaluateJobs(false); 1642 1643 // This is fixed, and now, when re-evaluated at 22:28 it should not be preempted. 1644 auto greedy = scheduler->process()->getGreedyScheduler(); 1645 Ekos::SchedulerJob *job2359 = scheduler->moduleState()->jobs()[0]; 1646 auto time1Local = (Qt::UTC == time1.timeSpec() ? geo.UTtoLT(KStarsDateTime(time1)) : time1); 1647 QVERIFY(greedy->checkJob(scheduler->moduleState()->jobs(), time1Local, job2359)); 1648 } 1649 1650 // This creates empty/dummy fits files to be discovered inside estimateJobTime, when it 1651 // tries to figure out how of of the job is already completed. 1652 // Only useful when Options::rememberJobProgress() is true. 1653 void TestEkosSchedulerOps::makeFitsFiles(const QString &base, int num) 1654 { 1655 QFile f("/dev/null"); 1656 QFileInfo info(base); 1657 info.dir().mkpath("."); 1658 1659 for (int i = 0; i < num; ++i) 1660 { 1661 QString newName = QString("%1_%2.fits").arg(base).arg(i); 1662 QVERIFY(f.copy(newName)); 1663 } 1664 } 1665 1666 // In this test, we simulate pre-existing captures with the rememberJobProgress option, 1667 // and make sure Greedy estimates the job completion time properly. 1668 void TestEkosSchedulerOps::testEstimateTimeBug() 1669 { 1670 Options::setDawnOffset(0); 1671 Options::setDuskOffset(0); 1672 Options::setSettingAltitudeCutoff(3); 1673 Options::setPreDawnTime(0); 1674 Options::setRememberJobProgress(true); 1675 Options::setDitherEnabled(true); 1676 Options::setDitherFrames(1); 1677 1678 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1679 Ekos::SchedulerJob::setHorizon(nullptr); 1680 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1681 1682 GeoLocation geo(dms(9, 45, 54), dms(49, 6, 22), "Schwaebisch Hall", "Baden-Wuerttemberg", "Germany", +1); 1683 const QDateTime time1 = QDateTime(QDate(2022, 3, 20), QTime(18, 52, 48), Qt::UTC); //19:52 local 1684 initTimeGeo(geo, time1); 1685 1686 auto path = dir.filePath("."); 1687 auto jobLRGB = QVector<TestEkosSchedulerHelper::CaptureJob>( 1688 {{360, 3, "L", path}, {360, 1, "R", path}, {360, 1, "G", path}, {360, 1, "B", path}, {360, 2, "L", path}}); 1689 auto jobNB = QVector<TestEkosSchedulerHelper::CaptureJob>({{600, 1, "Ha", path}, {600, 1, "OIII", path}}); 1690 1691 // Guessing he was using 40minute offsets 1692 Options::setDawnOffset(.666); 1693 Options::setDuskOffset(-.666); 1694 1695 TestEkosSchedulerHelper::StartupCondition asapStartupCondition; 1696 TestEkosSchedulerHelper::CompletionCondition loopCompletionCondition; 1697 asapStartupCondition.type = Ekos::START_ASAP; 1698 loopCompletionCondition.type = Ekos::FINISH_LOOP; 1699 TestEkosSchedulerHelper::CompletionCondition repeat9; 1700 repeat9.type = Ekos::FINISH_REPEAT; 1701 repeat9.repeat = 9; 1702 1703 makeFitsFiles(QString("%1%2").arg(path, "/NGC_2359/Light/L/NGC_2359_Light_L"), 41); 1704 makeFitsFiles(QString("%1%2").arg(path, "/NGC_2359/Light/R/NGC_2359_Light_R"), 8); 1705 makeFitsFiles(QString("%1%2").arg(path, "/NGC_2359/Light/G/NGC_2359_Light_G"), 8); 1706 makeFitsFiles(QString("%1%2").arg(path, "/NGC_2359/Light/B/NGC_2359_Light_B"), 8); 1707 makeFitsFiles(QString("%1%2").arg(path, "/NGC_2359/Light/Ha/NGC_2359_Light_Ha"), 12); 1708 makeFitsFiles(QString("%1%2").arg(path, "/NGC_2359/Light/OIII/NGC_2359_Light_OIII"), 11); 1709 1710 // Not focusing in these schedule steps. 1711 TestEkosSchedulerHelper::ScheduleSteps steps = {true, false, true, true}; 1712 1713 loadGreedySchedule(true, "NGC 2359", asapStartupCondition, repeat9, dir, jobLRGB, 20, steps); 1714 loadGreedySchedule(false, "NGC 2359", asapStartupCondition, loopCompletionCondition, dir, jobNB, 20, steps); 1715 loadGreedySchedule(false, "M 53", asapStartupCondition, loopCompletionCondition, dir, jobLRGB, 20, steps); 1716 1717 scheduler->process()->evaluateJobs(false); 1718 1719 // The first (LRGB) version of NGC 2359 is mostly completed and should just run for about 45 minutes. 1720 // At that point, the narrowband NGC2359 and LRGB M53 jobs run. 1721 QVERIFY(checkSchedule( 1722 { 1723 {"NGC 2359", "2022/03/20 19:52", "2022/03/20 20:38"}, 1724 {"NGC 2359", "2022/03/20 20:39", "2022/03/20 22:11"}, 1725 {"M 53", "2022/03/20 22:12", "2022/03/21 05:14"}, 1726 {"NGC 2359", "2022/03/21 19:45", "2022/03/21 22:07"}, 1727 {"M 53", "2022/03/21 22:08", "2022/03/22 05:12"}, 1728 {"NGC 2359", "2022/03/22 19:47", "2022/03/22 22:03"}}, 1729 scheduler->process()->getGreedyScheduler()->getSchedule(), 300)); 1730 } 1731 1732 // A helper for setting up the esl and esq files for the test below. 1733 // The issue is the test loads an esl file, and that file has in it the name of the esq files 1734 // it needs to load. These files are put in temporary directories, so the contents needs 1735 // to modified to reflect the locations of the esq files. 1736 namespace 1737 { 1738 QString setupMessierFiles(QTemporaryDir &dir, const QString &eslFilename, const QString esqFilename) 1739 { 1740 QString esq = esqFilename; 1741 QString esl = eslFilename; 1742 1743 const QString esqPath(dir.filePath(esq)); 1744 const QString eslPath(dir.filePath(esl)); 1745 1746 // Confused about where the test runs... 1747 if (!QFile::exists(esq) || !QFile::exists(esl)) 1748 { 1749 esq = QString("../Tests/kstars_ui/%1").arg(esqFilename); 1750 esl = QString("../Tests/kstars_ui/%1").arg(eslFilename); 1751 qCInfo(KSTARS_EKOS_TEST) << QString("Didn't find the files, looking in %1 %2").arg(esq, esl); 1752 if (!QFile::exists(esq) || !QFile::exists(esl)) 1753 return QString(); 1754 } 1755 1756 // Copy the sequence file to the temp direcotry 1757 QFile::copy(esq, esqPath); 1758 1759 // Read and modify the esl file: chage __ESQ_FILE__ to the value of esqPath, and write it out to the temp directory. 1760 QFile eslFile(esl); 1761 if (!eslFile.open(QFile::ReadOnly | QFile::Text)) 1762 return QString(); 1763 QTextStream in(&eslFile); 1764 TestEkosSchedulerHelper::writeFile(eslPath, in.readAll().replace(QString("__ESQ_FILE__"), esqPath)); 1765 1766 if (!QFile::exists(esqPath) || !QFile::exists(eslPath)) 1767 return QString(); 1768 1769 return eslPath; 1770 } 1771 } // namespace 1772 1773 void TestEkosSchedulerOps::testGreedyMessier() 1774 { 1775 QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX"); 1776 QString esl0Path = setupMessierFiles(dir, "Messier-1-40-alt0.esl", "Messier-1-40.esq"); 1777 QString esl30Path = setupMessierFiles(dir, "Messier-1-40-alt30.esl", "Messier-1-40.esq"); 1778 if (esl0Path.isEmpty() || esl30Path.isEmpty()) 1779 { 1780 QSKIP("Skipping test because of missing esq or esl files"); 1781 return; 1782 } 1783 1784 Options::setDawnOffset(0); 1785 Options::setDuskOffset(0); 1786 Options::setSettingAltitudeCutoff(3); 1787 Options::setPreDawnTime(0); 1788 Options::setRememberJobProgress(false); 1789 Options::setSchedulerAlgorithm(Ekos::ALGORITHM_GREEDY); 1790 1791 GeoLocation geo(dms(9, 45, 54), dms(49, 6, 22), "Schwaebisch Hall", "Baden-Wuerttemberg", "Germany", +1); 1792 const QDateTime time1 = QDateTime(QDate(2022, 3, 7), QTime(21, 28, 55), Qt::UTC); //22:28 local 1793 initTimeGeo(geo, time1); 1794 1795 qCInfo(KSTARS_EKOS_TEST) << QString("Calculate schedule with no artificial horizon and 0 min altitude."); 1796 Ekos::SchedulerJob::setHorizon(nullptr); 1797 scheduler->load(true, QString("file://%1").arg(esl0Path)); 1798 const QVector<SPlan> scheduleMinAlt0 = 1799 { 1800 {"M 39", "2022/03/07 22:28", "2022/03/07 22:39"}, 1801 {"M 33", "2022/03/07 22:40", "2022/03/07 22:50"}, 1802 {"M 32", "2022/03/07 22:51", "2022/03/07 23:02"}, 1803 {"M 31", "2022/03/07 23:03", "2022/03/07 23:13"}, 1804 {"M 34", "2022/03/07 23:14", "2022/03/07 23:25"}, 1805 {"M 1", "2022/03/07 23:26", "2022/03/07 23:36"}, 1806 {"M 35", "2022/03/07 23:37", "2022/03/07 23:48"}, 1807 {"M 38", "2022/03/07 23:49", "2022/03/07 23:59"}, 1808 {"M 36", "2022/03/08 00:00", "2022/03/08 00:11"}, 1809 {"M 40", "2022/03/08 00:12", "2022/03/08 00:22"}, 1810 {"M 3", "2022/03/08 00:23", "2022/03/08 00:34"}, 1811 {"M 13", "2022/03/08 00:35", "2022/03/08 00:45"}, 1812 {"M 29", "2022/03/08 00:46", "2022/03/08 00:57"}, 1813 {"M 5", "2022/03/08 00:58", "2022/03/08 01:08"}, 1814 {"M 12", "2022/03/08 01:09", "2022/03/08 01:20"}, 1815 {"M 27", "2022/03/08 01:23", "2022/03/08 01:34"}, 1816 {"M 10", "2022/03/08 01:35", "2022/03/08 01:45"}, 1817 {"M 14", "2022/03/08 01:46", "2022/03/08 01:57"}, 1818 {"M 4", "2022/03/08 02:04", "2022/03/08 02:14"}, 1819 {"M 9", "2022/03/08 02:15", "2022/03/08 02:26"}, 1820 {"M 11", "2022/03/08 02:39", "2022/03/08 02:49"}, 1821 {"M 19", "2022/03/08 02:50", "2022/03/08 03:01"}, 1822 {"M 26", "2022/03/08 03:02", "2022/03/08 03:12"}, 1823 {"M 16", "2022/03/08 03:13", "2022/03/08 03:24"}, 1824 {"M 23", "2022/03/08 03:25", "2022/03/08 03:35"}, 1825 {"M 17", "2022/03/08 03:36", "2022/03/08 03:47"}, 1826 {"M 15", "2022/03/08 03:50", "2022/03/08 04:01"}, 1827 {"M 18", "2022/03/08 04:02", "2022/03/08 04:12"}, 1828 {"M 24", "2022/03/08 04:13", "2022/03/08 04:24"}, 1829 {"M 21", "2022/03/08 04:25", "2022/03/08 04:35"}, 1830 {"M 20", "2022/03/08 04:36", "2022/03/08 04:47"}, 1831 {"M 2", "2022/03/08 04:56", "2022/03/08 05:04"}, 1832 {"M 25", "2022/03/09 03:21", "2022/03/09 03:32"}, 1833 {"M 8", "2022/03/09 03:33", "2022/03/09 03:43"}, 1834 {"M 28", "2022/03/09 03:49", "2022/03/09 04:00"}, 1835 {"M 6", "2022/03/09 04:03", "2022/03/09 04:14"}, 1836 {"M 22", "2022/03/09 04:15", "2022/03/09 04:25"}, 1837 {"M 2", "2022/03/09 04:51", "2022/03/09 04:54"}, 1838 {"M 7", "2022/03/09 04:55", "2022/03/09 05:01"} 1839 }; 1840 QVERIFY(checkSchedule(scheduleMinAlt0, scheduler->process()->getGreedyScheduler()->getSchedule(), 300)); 1841 1842 qCInfo(KSTARS_EKOS_TEST) << QString("Calculate schedule with no artificial horizon and 30 min altitude."); 1843 Ekos::SchedulerJob::setHorizon(nullptr); 1844 scheduler->load(true, QString("file://%1").arg(esl30Path)); 1845 const QVector<SPlan> scheduleMinAlt30 = 1846 { 1847 {"M 1", "2022/03/07 22:28", "2022/03/07 22:39"}, 1848 {"M 35", "2022/03/07 22:40", "2022/03/07 22:50"}, 1849 {"M 38", "2022/03/07 22:51", "2022/03/07 23:02"}, 1850 {"M 36", "2022/03/07 23:03", "2022/03/07 23:13"}, 1851 {"M 40", "2022/03/07 23:14", "2022/03/07 23:25"}, 1852 {"M 3", "2022/03/07 23:26", "2022/03/07 23:36"}, 1853 {"M 13", "2022/03/08 00:23", "2022/03/08 00:34"}, 1854 {"M 5", "2022/03/08 01:43", "2022/03/08 01:53"}, 1855 {"M 12", "2022/03/08 03:40", "2022/03/08 03:51"}, 1856 {"M 29", "2022/03/08 03:56", "2022/03/08 04:06"}, 1857 {"M 39", "2022/03/08 04:15", "2022/03/08 04:26"}, 1858 {"M 10", "2022/03/08 04:27", "2022/03/08 04:37"}, 1859 {"M 27", "2022/03/08 04:38", "2022/03/08 04:49"}, 1860 {"M 14", "2022/03/08 04:50", "2022/03/08 05:00"}, 1861 {"M 34", "2022/03/08 20:03", "2022/03/08 20:14"} 1862 }; 1863 QVERIFY(checkSchedule(scheduleMinAlt30, scheduler->process()->getGreedyScheduler()->getSchedule(), 300)); 1864 // TODO: verify this test data. 1865 1866 // The timing was affected by calculating horizon constraints. 1867 // Measure the time with and without a realistic artificial horizon. 1868 ArtificialHorizon largeHorizon; 1869 addHorizonConstraint( 1870 &largeHorizon, "h", true, QVector<double>( 1871 { 1872 // vector of azimuths 1873 67.623611, 71.494167, 73.817778, 75.726667, 77.536944, 79.640000, 81.505278, 82.337778, 83.820000, 84.479444, 1874 86.375556, 89.347500, 91.982500, 93.771667, 95.124722, 95.747778, 97.303889, 100.735278, 104.573611, 106.721389, 1875 108.360278, 110.640833, 111.963611, 114.940556, 116.497500, 118.858611, 119.981389, 122.832500, 124.695278, 125.882778, 1876 127.580278, 129.888889, 130.668333, 132.550833, 133.389167, 133.892222, 138.481111, 139.192778, 140.057500, 141.234722, 1877 142.308333, 144.151944, 145.714167, 146.290833, 149.275278, 151.138056, 152.107500, 153.526389, 154.321667, 155.640000, 1878 156.685833, 156.302778, 157.421667, 160.331389, 161.091389, 160.952778, 161.975556, 162.564167, 164.866944, 166.906389, 1879 167.750000, 167.782778, 169.212500, 170.241944, 170.642500, 172.948056, 174.382778, 174.738333, 175.333056, 175.878889, 1880 177.345000, 178.390278, 177.411111, 180.062500, 177.540278, 177.981111, 179.459444, 180.363056, 182.301667, 184.176111, 1881 185.036944, 188.303611, 190.110833, 191.809444, 196.293889, 197.398889, 196.634722, 196.238889, 198.553056, 199.896389, 1882 205.868333, 207.224722, 231.645278, 258.324167, 277.260833, 292.470833, 302.961111, 308.996389, 309.027500, 312.311667, 1883 313.423333, 316.827500, 316.471111, 322.656944, 329.775278, 330.606944, 333.355278, 340.709167, 342.927222, 344.010000, 1884 345.696389, 347.886111, 349.058611, 351.998889, 353.010278, 357.548611, 359.510278, 359.320278, 363.102500, 369.171389, 1885 371.129444, 372.717778, 375.897500, 379.531944, 380.118333, 383.015278, 385.493333, 32.556944, 35.456667, 35.773889, 1886 38.304167, 43.844722, 52.575556, 55.080000, 57.086667, 67.523333, 68.458056}), 1887 QVector<double>( 1888 { 1889 // vector of altitudes 1890 22.721111, 21.776944, 20.672222, 25.656667, 1891 27.865000, 29.283889, 29.107778, 27.240556, 26.704722, 28.126111, 28.722222, 29.286111, 28.931944, 26.810000, 1892 24.275278, 21.858333, 20.081944, 20.608333, 21.693056, 22.915833, 26.003333, 29.119167, 28.771667, 24.334444, 1893 22.909444, 21.960278, 20.691389, 24.635000, 24.556667, 22.429167, 24.333056, 24.526667, 24.417222, 26.273611, 1894 25.870833, 24.805556, 27.208333, 29.074167, 31.087500, 30.648333, 28.023889, 27.469722, 27.212222, 26.296667, 1895 24.987222, 23.888333, 25.336667, 25.510556, 24.482222, 24.494444, 23.861667, 22.288889, 19.551667, 20.260278, 1896 21.554722, 22.983056, 24.001111, 27.154722, 28.523056, 27.060278, 24.611944, 22.693333, 23.140833, 22.666389, 1897 20.984722, 27.248611, 27.015000, 24.659444, 23.478611, 21.943056, 20.511389, 21.130000, 23.937778, 23.787778, 1898 29.003056, 35.778056, 37.504167, 41.471111, 43.260556, 42.360833, 39.985000, 31.131389, 32.261111, 31.051944, 1899 32.915000, 31.470000, 30.330556, 29.764722, 28.692500, 24.937222, 24.907222, 41.375556, 44.511389, 43.528611, 1900 38.659722, 33.497500, 27.334444, 24.551667, 21.920278, 22.558333, 27.221111, 27.113611, 30.526667, 31.531667, 1901 25.062778, 27.808889, 24.439167, 23.505278, 22.182222, 23.534444, 22.537778, 23.492222, 22.485000, 23.643611, 1902 25.677222, 23.192778, 19.320556, 16.915556, 18.304444, 18.866389, 18.523889, 23.704167, 24.008611, 25.070000, 1903 28.784444, 30.421389, 35.479444, 33.950833, 35.842778, 34.506667, 34.424722, 28.031944, 30.806389, 29.865833, 1904 22.579722, 22.644444, 22.672222})); 1905 1906 qCInfo(KSTARS_EKOS_TEST) << QString("Calculate schedule with large artificial horizon."); 1907 Ekos::SchedulerJob::setHorizon(&largeHorizon); 1908 scheduler->load(true, QString("file://%1").arg(esl30Path)); 1909 const QVector<SPlan> scheduleAHMinAlt30 = 1910 { 1911 {"M 35", "2022/03/07 22:28", "2022/03/07 22:39"}, 1912 {"M 38", "2022/03/07 22:40", "2022/03/07 22:50"}, 1913 {"M 36", "2022/03/07 22:51", "2022/03/07 23:02"}, 1914 {"M 40", "2022/03/07 23:03", "2022/03/07 23:13"}, 1915 {"M 3", "2022/03/07 23:14", "2022/03/07 23:25"}, 1916 {"M 13", "2022/03/08 00:24", "2022/03/08 00:34"}, 1917 {"M 5", "2022/03/08 01:43", "2022/03/08 01:54"}, 1918 {"M 12", "2022/03/08 03:41", "2022/03/08 03:51"}, 1919 {"M 29", "2022/03/08 03:56", "2022/03/08 04:07"}, 1920 {"M 39", "2022/03/08 04:16", "2022/03/08 04:26"}, 1921 {"M 10", "2022/03/08 04:27", "2022/03/08 04:38"}, 1922 {"M 27", "2022/03/08 04:39", "2022/03/08 04:49"}, 1923 {"M 14", "2022/03/08 04:50", "2022/03/08 05:01"}, 1924 {"M 34", "2022/03/08 20:03", "2022/03/08 20:13"}, 1925 {"M 1", "2022/03/08 20:14", "2022/03/08 20:25"} 1926 }; 1927 QVERIFY(checkSchedule(scheduleAHMinAlt30, scheduler->process()->getGreedyScheduler()->getSchedule(), 300)); 1928 // TODO: verify this test data. 1929 1930 } 1931 1932 void TestEkosSchedulerOps::prepareTestData(QList<QString> locationList, QList<QString> targetList) 1933 { 1934 #if QT_VERSION < QT_VERSION_CHECK(5,9,0) 1935 QSKIP("Bypassing fixture test on old Qt"); 1936 Q_UNUSED(locationList) 1937 #else 1938 QTest::addColumn<QString>("location"); /*!< location the KStars test is running */ 1939 QTest::addColumn<QString>("target"); /*!< scheduled target */ 1940 for (QString location : locationList) 1941 for (QString target : targetList) 1942 QTest::newRow(QString("loc= \"%1\", target=\"%2\"").arg(location).arg(target).toLocal8Bit()) 1943 << location << target; 1944 #endif 1945 } 1946 1947 /* ********************************************************************************* 1948 * 1949 * Test data 1950 * 1951 * ********************************************************************************* */ 1952 void TestEkosSchedulerOps::testCulminationStartup_data() 1953 { 1954 prepareTestData({"Heidelberg", "New York"}, {"Rasalhague"}); 1955 } 1956 1957 void TestEkosSchedulerOps::testRememberJobProgress_data() 1958 { 1959 #if QT_VERSION < QT_VERSION_CHECK(5,9,0) 1960 QSKIP("Bypassing fixture test on old Qt"); 1961 #else 1962 QTest::addColumn<QString>("jobs"); /*!< Capture sequences */ 1963 QTest::addColumn<QString>("frames"); /*!< Existing frames */ 1964 QTest::addColumn<int>("iterations"); /*!< Existing frames */ 1965 QTest::addColumn<bool>("scheduled"); /*!< Expected result: scheduled (true) or completed (false) */ 1966 1967 QTest::newRow("{Red:2}, scheduled=true") << "Red:2" << "Red:1" << 1 << true; 1968 QTest::newRow("{Red:2}, scheduled=false") << "Red:2" << "Red:2" << 1 << false; 1969 QTest::newRow("{Red:2, Red:1}, scheduled=true") << "Red:2, Red:1" << "Red:2" << 1 << true; 1970 QTest::newRow("{Red:2, Green:1, Red:1}, scheduled=true") << "Red:2, Green:1, Red:1" << "Red:4" << 1 << true; 1971 QTest::newRow("{Red:3, Green:1, Red:2}, 3x, scheduled=true") << "Red:3, Green:1, Red:2" << "Red:14, Green:3" << 3 << true; 1972 QTest::newRow("{Red:3, Green:1, Red:2}, 3x, scheduled=false") << "Red:3, Green:1, Red:2" << "Red:15, Green:3" << 3 << false; 1973 1974 #endif 1975 } 1976 1977 QTEST_KSTARS_MAIN(TestEkosSchedulerOps) 1978 1979 #endif // HAVE_INDI 1980 1981