1 /*  KStars scheduler operations tests
2     SPDX-FileCopyrightText: 2021 Hy Murveit <hy@murveit.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 #include <QFile>
8 
9 #include "test_ekos_scheduler_ops.h"
10 #include "ekos/scheduler/scheduler.h"
11 #include "ekos/scheduler/schedulerjob.h"
12 #include "skymapcomposite.h"
13 
14 #if defined(HAVE_INDI)
15 
16 #include "artificialhorizoncomponent.h"
17 #include "kstars_ui_tests.h"
18 #include "test_ekos.h"
19 #include "test_ekos_simulator.h"
20 #include "linelist.h"
21 #include "mockmodules.h"
22 #include "Options.h"
23 
24 #define QWAIT_TIME 10
25 
26 using Ekos::Scheduler;
27 
TestEkosSchedulerOps(QObject * parent)28 TestEkosSchedulerOps::TestEkosSchedulerOps(QObject *parent) : QObject(parent)
29 {
30 }
31 
initTestCase()32 void TestEkosSchedulerOps::initTestCase()
33 {
34     // This gets executed at the start of testing
35 
36     disableSkyMap();
37 }
38 
cleanupTestCase()39 void TestEkosSchedulerOps::cleanupTestCase()
40 {
41     // This gets executed at the end of testing
42 }
43 
init()44 void TestEkosSchedulerOps::init()
45 {
46     // This gets executed at the start of each of the individual tests.
47     testTimer.start();
48 
49     focuser.reset(new Ekos::MockFocus);
50     mount.reset(new Ekos::MockMount);
51     capture.reset(new Ekos::MockCapture);
52     align.reset(new Ekos::MockAlign);
53     guider.reset(new Ekos::MockGuide);
54     ekos.reset(new Ekos::MockEkos);
55 
56     scheduler.reset(new Scheduler(Ekos::MockEkos::mockPath, "org.kde.kstars.MockEkos"));
57     // These org.kde.* interface strings are set up in the various .xml files.
58     scheduler->setFocusInterfaceString("org.kde.kstars.MockEkos.MockFocus");
59     scheduler->setMountInterfaceString("org.kde.kstars.MockEkos.MockMount");
60     scheduler->setCaptureInterfaceString("org.kde.kstars.MockEkos.MockCapture");
61     scheduler->setAlignInterfaceString("org.kde.kstars.MockEkos.MockAlign");
62     scheduler->setGuideInterfaceString("org.kde.kstars.MockEkos.MockGuide");
63     scheduler->setFocusPathString(Ekos::MockFocus::mockPath);
64     scheduler->setMountPathString(Ekos::MockMount::mockPath);
65     scheduler->setCapturePathString(Ekos::MockCapture::mockPath);
66     scheduler->setAlignPathString(Ekos::MockAlign::mockPath);
67     scheduler->setGuidePathString(Ekos::MockGuide::mockPath);
68 
69     // Let's not deal with the dome for now.
70     scheduler->unparkDomeCheck->setChecked(false);
71 
72     // For now don't deal with files that were left around from previous testing.
73     // Should put these is a temporary directory that will be removed, if we generate
74     // them at all.
75     Options::setRememberJobProgress(false);
76 
77     // This allows testing of the shutdown.
78     Options::setStopEkosAfterShutdown(true);
79 
80     // define ASAP as default startup condition
81     m_startupCondition.type = SchedulerJob::START_ASAP;
82 }
83 
cleanup()84 void TestEkosSchedulerOps::cleanup()
85 {
86     // This gets executed at the end of each of the individual tests.
87 
88     // The signal and/or dbus communications seems to get confused
89     // without explicit resetting of these objects.
90     focuser.reset();
91     mount.reset();
92     capture.reset();
93     align.reset();
94     guider.reset();
95     ekos.reset();
96     scheduler.reset();
97     fprintf(stderr, "Test took %.1fs\n", testTimer.elapsed() / 1000.0);
98 }
99 
disableSkyMap()100 void TestEkosSchedulerOps::disableSkyMap()
101 {
102     Options::setShowAsteroids(false);
103     Options::setShowComets(false);
104     Options::setShowSupernovae(false);
105     Options::setShowDeepSky(false);
106     Options::setShowEcliptic(false);
107     Options::setShowEquator(false);
108     Options::setShowLocalMeridian(false);
109     Options::setShowGround(false);
110     Options::setShowHorizon(false);
111     Options::setShowFlags(false);
112     Options::setShowOther(false);
113     Options::setShowMilkyWay(false);
114     Options::setShowSolarSystem(false);
115     Options::setShowStars(false);
116     Options::setShowSatellites(false);
117     Options::setShowHIPS(false);
118     Options::setShowTerrain(false);
119 }
120 
121 // When checking that something happens near a certain time, the tolerance of the
122 // check is affected by how often the scheduler iterates. E.g. if the scheduler only
123 // runs once a minute (to speed up simulation), it is unreasonable to check for 1 second
124 // tolerances. This function returns the max of the tolerance passed in and 3 times
125 // the scheduler's iteration period to compensate for that.
timeTolerance(int seconds)126 int TestEkosSchedulerOps::timeTolerance(int seconds)
127 {
128     const int tolerance = std::max(seconds, 3 * (scheduler->m_UpdatePeriodMs / 1000));
129     return tolerance;
130 }
131 
132 // Thos tests an empty scheduler job and makes sure dbus communications
133 // work between the scheduler and the mock modules.
testBasics()134 void TestEkosSchedulerOps::testBasics()
135 {
136     QVERIFY(scheduler->focusInterface.isNull());
137     QVERIFY(scheduler->mountInterface.isNull());
138     QVERIFY(scheduler->captureInterface.isNull());
139     QVERIFY(scheduler->alignInterface.isNull());
140     QVERIFY(scheduler->guideInterface.isNull());
141 
142     ekos->addModule(QString("Focus"));
143     ekos->addModule(QString("Mount"));
144     ekos->addModule(QString("Capture"));
145     ekos->addModule(QString("Align"));
146     ekos->addModule(QString("Guide"));
147 
148     // Allow Qt to pass around the messages.
149     // Wait time is set short (10ms) for longer tests where the scheduler is
150     // iterating and can miss on one iteration. Here we make it longer
151     // for a more stable test.
152 
153     // Not sure why processEvents() doesn't always work. Would be quicker that way.
154     //qApp->processEvents();
155     QTest::qWait(10 * QWAIT_TIME);
156 
157     QVERIFY(!scheduler->focusInterface.isNull());
158     QVERIFY(!scheduler->mountInterface.isNull());
159     QVERIFY(!scheduler->captureInterface.isNull());
160     QVERIFY(!scheduler->alignInterface.isNull());
161     QVERIFY(!scheduler->guideInterface.isNull());
162 
163     // Verify the mocks can use the DBUS.
164     QVERIFY(!focuser->isReset);
165     scheduler->focusInterface->call(QDBus::AutoDetect, "resetFrame");
166 
167     //qApp->processEvents();  // this fails, is it because dbus calls are on a separate thread?
168     QTest::qWait(10 * QWAIT_TIME);
169 
170     QVERIFY(focuser->isReset);
171 
172     // Run the scheduler with nothing setup. Should quickly exit.
173     scheduler->init();
174     QVERIFY(scheduler->timerState == Scheduler::RUN_WAKEUP);
175     int sleepMs = scheduler->runSchedulerIteration();
176     QVERIFY(scheduler->timerState == Scheduler::RUN_SCHEDULER);
177     sleepMs = scheduler->runSchedulerIteration();
178     QVERIFY(sleepMs == 1000);
179     QVERIFY(scheduler->timerState == Scheduler::RUN_SHUTDOWN);
180     sleepMs = scheduler->runSchedulerIteration();
181     QVERIFY(scheduler->timerState == Scheduler::RUN_NOTHING);
182 }
183 
184 // Runs the scheduler for a number of iterations between 1 and the arg "iterations".
185 // Each iteration it  increments the simulated clock (currentUTime, which is in Universal
186 // Time) by *sleepMs, then runs the scheduler, then calls fcn().
187 // If fcn() returns true, it stops iterating and returns true.
188 // It returns false if it completes all the with fnc() returning false.
iterateScheduler(const QString & label,int iterations,int * sleepMs,KStarsDateTime * currentUTime,std::function<bool ()> fcn)189 bool TestEkosSchedulerOps::iterateScheduler(const QString &label, int iterations,
190         int *sleepMs, KStarsDateTime* currentUTime, std::function<bool ()> fcn)
191 {
192     fprintf(stderr, "\n----------------------------------------\n");
193     fprintf(stderr, "Starting iterateScheduler(%s)\n",  label.toLatin1().data());
194 
195     for (int i = 0; i < iterations; ++i)
196     {
197         //qApp->processEvents();
198         QTest::qWait(QWAIT_TIME); // this takes ~10ms per iteration!
199         // Is there a way to speed up the above?
200         // I didn't reduce it, because the basic test fails to call a dbus function
201         // with less than 10ms wait time.
202 
203         *currentUTime = currentUTime->addSecs(*sleepMs / 1000.0);
204         KStarsData::Instance()->changeDateTime(*currentUTime); // <-- 175ms
205         *sleepMs = scheduler->runSchedulerIteration();
206         fprintf(stderr, "current time LT %s UT %s\n",
207                 KStarsData::Instance()->lt().toString().toLatin1().data(),
208                 KStarsData::Instance()->ut().toString().toLatin1().data());
209         if (fcn())
210         {
211             fprintf(stderr, "IterateScheduler %s returning TRUE at %s %s after %d iterations\n",
212                     label.toLatin1().data(),
213                     KStarsData::Instance()->lt().toString().toLatin1().data(),
214                     KStarsData::Instance()->ut().toString().toLatin1().data(), i + 1);
215             return true;
216         }
217     }
218     fprintf(stderr, "IterateScheduler %s returning FALSE at %s %s after %d iterations\n",
219             label.toLatin1().data(),
220             KStarsData::Instance()->lt().toString().toLatin1().data(),
221             KStarsData::Instance()->ut().toString().toLatin1().data(), iterations);
222     return false;
223 }
224 
225 // Sets up the scheduler in a particular location (geo) and a UTC start time.
initScheduler(const GeoLocation & geo,const QDateTime & startUTime,QTemporaryDir * dir,const QVector<QString> & esls,const QVector<QString> & esqs)226 void TestEkosSchedulerOps::initScheduler(const GeoLocation &geo, const QDateTime &startUTime, QTemporaryDir *dir,
227         const QVector<QString> &esls, const QVector<QString> &esqs)
228 {
229     KStarsData::Instance()->geo()->setLat(*(geo.lat()));
230     KStarsData::Instance()->geo()->setLong(*(geo.lng()));
231     // Note, the actual TZ would be -7 as there is a DST correction for these dates.
232     KStarsData::Instance()->geo()->setTZ0(geo.TZ0());
233 
234     KStarsDateTime currentUTime(startUTime);
235     KStarsData::Instance()->changeDateTime(currentUTime);
236     KStarsData::Instance()->clock()->setManualMode(true);
237 
238     fprintf(stderr, "Starting up with geo %.3f %.3f, local time: %s\n",
239             KStarsData::Instance()->geo()->lat()->Degrees(),
240             KStarsData::Instance()->geo()->lng()->Degrees(),
241             KStarsData::Instance()->lt().toString().toLatin1().data());
242 
243     QVERIFY(dir->isValid());
244     QVERIFY(dir->autoRemove());
245 
246     for (int i = 0; i < esls.size(); ++i)
247     {
248         const QString eslFile = dir->filePath(QString("test%1.esl").arg(i));
249         const QString esqFile = dir->filePath(QString("test%1.esq").arg(i));
250 
251         QVERIFY(TestEkosSchedulerHelper::writeSimpleSequenceFiles(esls[i], eslFile, esqs[i], esqFile));
252         scheduler->load(i == 0, QString("file://%1").arg(eslFile));
253         QVERIFY(scheduler->jobs.size() == (i + 1));
254         scheduler->jobs[i]->setSequenceFile(QUrl(QString("file://%1").arg(esqFile)));
255         fprintf(stderr, "seq file: %s \"%s\"\n", esqFile.toLatin1().data(), QString("file://%1").arg(esqFile).toLatin1().data());
256     }
257 
258     scheduler->evaluateJobs(false);
259     scheduler->init();
260     QVERIFY(scheduler->timerState == Scheduler::RUN_WAKEUP);
261 }
262 
startupJob(const GeoLocation & geo,const QDateTime & startUTime,QTemporaryDir * dir,const QString & esl,const QString & esq,const QDateTime & wakeupTime,KStarsDateTime & endUTime,int & endSleepMs)263 void TestEkosSchedulerOps::startupJob(
264     const GeoLocation &geo, const QDateTime &startUTime,
265     QTemporaryDir *dir, const QString &esl, const QString &esq,
266     const QDateTime &wakeupTime, KStarsDateTime &endUTime, int &endSleepMs)
267 {
268     QVector<QString> esls, esqs;
269     esls.push_back(esl);
270     esqs.push_back(esq);
271     startupJobs(geo, startUTime, dir, esls, esqs, wakeupTime, endUTime, endSleepMs);
272 }
273 
startupJobs(const GeoLocation & geo,const QDateTime & startUTime,QTemporaryDir * dir,const QVector<QString> & esls,const QVector<QString> & esqs,const QDateTime & wakeupTime,KStarsDateTime & endUTime,int & endSleepMs)274 void TestEkosSchedulerOps::startupJobs(
275     const GeoLocation &geo, const QDateTime &startUTime,
276     QTemporaryDir *dir, const QVector<QString> &esls, const QVector<QString> &esqs,
277     const QDateTime &wakeupTime, KStarsDateTime &endUTime, int &endSleepMs)
278 {
279     initScheduler(geo, startUTime, dir, esls, esqs);
280     KStarsDateTime currentUTime(startUTime);
281 
282     int sleepMs = 0;
283 
284     QVERIFY(scheduler->timerState == Scheduler::RUN_WAKEUP);
285     QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, &currentUTime, [&]() -> bool
286     {
287         return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
288     }));
289 
290     if (wakeupTime.isValid())
291     {
292         // This is the sequence when it goes to sleep, then wakes up later to start.
293 
294         QVERIFY(iterateScheduler("Wait for RUN_WAKEUP", 10, &sleepMs, &currentUTime, [&]() -> bool
295         {
296             return (scheduler->timerState == Scheduler::RUN_WAKEUP);
297         }));
298 
299         // Verify that it's near the original start time.
300         const qint64 delta_t = KStarsData::Instance()->ut().secsTo(startUTime);
301         QVERIFY2(std::abs(delta_t) < timeTolerance(60),
302                  QString("Delta to original time %1 too large, failing.").arg(delta_t).toLatin1());
303 
304         QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, &currentUTime, [&]() -> bool
305         {
306             return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
307         }));
308 
309         // Verify that it wakes up at the right time, after the twilight constraint
310         // and the stars rises above 30 degrees. See the time comment above.
311         QVERIFY(std::abs(KStarsData::Instance()->ut().secsTo(wakeupTime)) < timeTolerance(60));
312     }
313     else
314     {
315         // This is the sequence when it can start-up right away.
316 
317         // Verify that it's near the original start time.
318         const qint64 delta_t = KStarsData::Instance()->ut().secsTo(startUTime);
319         QVERIFY2(std::abs(delta_t) < timeTolerance(60),
320                  QString("Delta to original time %1 too large, failing.").arg(delta_t).toLatin1());
321 
322         QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, &currentUTime, [&]() -> bool
323         {
324             return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
325         }));
326     }
327     // When the scheduler starts up, it sends connectDevices to Ekos
328     // which sets Indi --> Ekos::Success,
329     // and then it sends start() to Ekos which sets Ekos --> Ekos::Success
330     bool sentOnce = false, readyOnce = false;
331     QVERIFY(iterateScheduler("Wait for Indi and Ekos", 30, &sleepMs, &currentUTime, [&]() -> bool
332     {
333         if ((scheduler->indiState == Scheduler::INDI_READY) &&
334                 (scheduler->ekosState == Scheduler::EKOS_READY))
335             return true;
336         //else if (scheduler->m_EkosCommunicationStatus == Ekos::Success)
337         else if (ekos->ekosStatus() == Ekos::Success)
338         {
339             // Once Ekos is woken up, say mount and capture are ready.
340             if (!sentOnce)
341             {
342                 // Add the modules once ekos is started up.
343                 sentOnce = true;
344                 ekos->addModule("Focus");
345                 ekos->addModule("Capture");
346                 ekos->addModule("Mount");
347                 ekos->addModule("Align");
348                 ekos->addModule("Guide");
349             }
350             else if (scheduler->mountInterface != nullptr &&
351                      scheduler->captureInterface != nullptr && !readyOnce)
352             {
353                 // Can't send the ready messages until the devices are registered.
354                 readyOnce = true;
355                 mount->sendReady();
356                 capture->sendReady();
357             }
358         }
359         return false;
360     }));
361 
362     endUTime = currentUTime;
363     endSleepMs = sleepMs;
364 }
365 
startModules(KStarsDateTime & currentUTime,int & sleepMs)366 void TestEkosSchedulerOps::startModules(KStarsDateTime &currentUTime, int &sleepMs)
367 {
368     QVERIFY(iterateScheduler("Wait for MountTracking", 30, &sleepMs, &currentUTime, [&]() -> bool
369     {
370         if (mount->status() == ISD::Telescope::MOUNT_SLEWING)
371             mount->setStatus(ISD::Telescope::MOUNT_TRACKING);
372         else if (mount->status() == ISD::Telescope::MOUNT_TRACKING)
373             return true;
374         return false;
375     }));
376 
377     QVERIFY(iterateScheduler("Wait for Focus", 30, &sleepMs, &currentUTime, [&]() -> bool
378     {
379         if (focuser->status() == Ekos::FOCUS_PROGRESS)
380             focuser->setStatus(Ekos::FOCUS_COMPLETE);
381         else if (focuser->status() == Ekos::FOCUS_COMPLETE)
382             return true;
383         return false;
384     }));
385 
386     QVERIFY(iterateScheduler("Wait for Align", 30, &sleepMs, &currentUTime, [&]() -> bool
387     {
388         if (align->status() == Ekos::ALIGN_PROGRESS)
389             align->setStatus(Ekos::ALIGN_COMPLETE);
390         else if (align->status() == Ekos::ALIGN_COMPLETE)
391             return true;
392         return false;
393     }));
394 
395     QVERIFY(iterateScheduler("Wait for Guide", 30, &sleepMs, &currentUTime, [&]() -> bool
396     {
397         return (guider->status() == Ekos::GUIDE_GUIDING);
398     }));
399     QVERIFY(guider->connected);
400 }
401 
402 // Roughly compare the slew coordinates sent to the mount to Deneb's.
403 // Rough comparison because these will have been converted to JNow.
404 // Should be called after simulated slew has been completed.
checkLastSlew(const SkyObject * targetObject)405 bool TestEkosSchedulerOps::checkLastSlew(const SkyObject* targetObject)
406 {
407     constexpr double halfDegreeInHours = 1 / (15 * 2.0);
408     bool success = (fabs(mount->lastRaHoursSlew - targetObject->ra().Hours()) < halfDegreeInHours) &&
409                    (fabs(mount->lastDecDegreesSlew - targetObject->dec().Degrees()) < 0.5);
410     if (!success)
411         fprintf(stderr, "Expected slew RA: %f DEC: %F but got %f %f\n",
412                 targetObject->ra().Hours(), targetObject->dec().Degrees(),
413                 mount->lastRaHoursSlew, mount->lastDecDegreesSlew);
414     return success;
415 }
416 
417 // Utility to print the state of the current scheduler job list.
printJobs(const QString & label)418 void TestEkosSchedulerOps::printJobs(const QString &label)
419 {
420     fprintf(stderr, "%-30s: ", label.toLatin1().data());
421     for (int i = 0; i < scheduler->jobs.size(); ++i)
422     {
423         fprintf(stderr, "(%d) %s %-15s ", i, scheduler->jobs[i]->getName().toLatin1().data(),
424                 SchedulerJob::jobStatusString(scheduler->jobs[i]->getState()).toLatin1().data());
425     }
426     fprintf(stderr, "\n");
427 }
428 
initJob(const KStarsDateTime & startUTime,const KStarsDateTime & jobStartUTime)429 void TestEkosSchedulerOps::initJob(const KStarsDateTime &startUTime, const KStarsDateTime &jobStartUTime)
430 {
431     KStarsDateTime currentUTime(startUTime);
432     int sleepMs = 0;
433 
434     // wait for the scheduler select the configured job for execution
435     QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, &currentUTime, [&]() -> bool
436     {
437         return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
438     }));
439 
440     // wait until the scheduler turns to the wakeup mode sleeping until the startup condition is met
441     QVERIFY(iterateScheduler("Wait for RUN_WAKEUP", 10, &sleepMs, &currentUTime, [&]() -> bool
442     {
443         return (scheduler->timerState == Scheduler::RUN_WAKEUP);
444     }));
445 
446     // wait until the scheduler starts the job
447     QVERIFY(iterateScheduler("Wait for RUN_SCHEDULER", 2, &sleepMs, &currentUTime, [&]() -> bool
448     {
449         return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
450     }));
451 
452     // check the distance from the expected start time
453     int delta = KStars::Instance()->data()->ut().secsTo(jobStartUTime);
454     // real offset should be maximally 5 min off the configured offset
455     QVERIFY2(std::abs(delta) < 300,
456              QString("wrong startup time: %1 secs distance to planned %2.").arg(delta).arg(jobStartUTime.toString(
457                          Qt::ISODate)).toLocal8Bit());
458 }
459 
460 // This tests a simple scheduler job.
461 // The job initializes Ekos and Indi, slews, plate-solves, focuses, starts guiding, and
462 // captures. Capture completes and the scheduler shuts down.
runSimpleJob(const GeoLocation & geo,const SkyObject * targetObject,const QDateTime & startUTime,const QDateTime & wakeupTime,bool enforceArtificialHorizon)463 void TestEkosSchedulerOps::runSimpleJob(const GeoLocation &geo, const SkyObject *targetObject, const QDateTime &startUTime,
464                                         const QDateTime &wakeupTime, bool enforceArtificialHorizon)
465 {
466     KStarsDateTime currentUTime;
467     int sleepMs = 0;
468 
469     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
470 
471     startupJob(geo, startUTime, &dir, TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, {true, true, true, true},
472                                                                                 false, enforceArtificialHorizon),
473                TestEkosSchedulerHelper::getDefaultEsqContent(), wakeupTime, currentUTime, sleepMs);
474     startModules(currentUTime, sleepMs);
475     QVERIFY(checkLastSlew(targetObject));
476 
477     QVERIFY(iterateScheduler("Wait for Capturing", 30, &sleepMs, &currentUTime, [&]() -> bool
478     {
479         return (scheduler->currentJob != nullptr &&
480                 scheduler->currentJob->getStage() == SchedulerJob::STAGE_CAPTURING);
481     }));
482 
483     // Tell the scheduler that capture is done.
484     capture->setStatus(Ekos::CAPTURE_COMPLETE);
485 
486     QVERIFY(iterateScheduler("Wait for Abort Guider", 30, &sleepMs, &currentUTime, [&]() -> bool
487     {
488         return (guider->status() == Ekos::GUIDE_ABORTED);
489     }));
490     QVERIFY(iterateScheduler("Wait for Shutdown", 30, &sleepMs, &currentUTime, [&]() -> bool
491     {
492         return (scheduler->shutdownState == Scheduler::SHUTDOWN_COMPLETE);
493     }));
494 
495     // Here the scheduler sends a message to ekosInterface to disconnectDevices,
496     // which will cause indi --> IDLE,
497     // and then calls stop() which will cause ekos --> IDLE
498     // This will cause the scheduler to shutdown.
499     QVERIFY(iterateScheduler("Wait for Scheduler Complete", 30, &sleepMs, &currentUTime, [&]() -> bool
500     {
501         return (scheduler->timerState == Scheduler::RUN_NOTHING);
502     }));
503 }
504 
testSimpleJob()505 void TestEkosSchedulerOps::testSimpleJob()
506 {
507     GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8);
508     SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName("Altair");
509 
510     // Setup an initial time.
511     // Note that the start time is 3pm local (10pm UTC - 7 TZ).
512     // Altair, the target, should be at about -40 deg altitude at this time,.
513     // The dawn/dusk constraints are 4:03am and 10:12pm (lst=13:43)
514     // At 10:12pm it should have an altitude of about 14 degrees, still below the 30-degree constraint.
515     // It achieves 30-degrees altitude at about 23:35.
516     QDateTime startUTime(QDateTime(QDate(2021, 6, 13), QTime(22, 0, 0), Qt::UTC));
517     const QDateTime wakeupTime(QDate(2021, 6, 14), QTime(06, 35, 0), Qt::UTC);
518     runSimpleJob(geo, targetObject, startUTime, wakeupTime, true);
519 }
520 
521 // This test has the same start as testSimpleJob, except that it but runs in NYC
522 // instead of silicon valley. This makes sure testing doesn't depend on timezone.
testTimeZone()523 void TestEkosSchedulerOps::testTimeZone()
524 {
525     GeoLocation geo(dms(-74, 0), dms(40, 42, 0), "NYC", "NY", "USA", -5);
526     KStarsDateTime startUTime(QDateTime(QDate(2021, 6, 13), QTime(22, 0, 0), Qt::UTC));
527 
528     scheduler->setUpdateInterval(5000);
529     KStarsDateTime currentUTime;
530     int sleepMs = 0;
531 
532     // It crosses 30-degrees altitude around the same time locally, but that's
533     // 3 hours earlier UTC.
534     const QDateTime wakeupTime(QDate(2021, 6, 14), QTime(03, 26, 0), Qt::UTC);
535     SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName("Altair");
536     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
537     startupJob(geo, startUTime, &dir, TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, {true, true, true, true}, false, true),
538                TestEkosSchedulerHelper::getDefaultEsqContent(), wakeupTime, currentUTime, sleepMs);
539     startModules(currentUTime, sleepMs);
540     QVERIFY(checkLastSlew(targetObject));
541 }
542 
testDawnShutdown()543 void TestEkosSchedulerOps::testDawnShutdown()
544 {
545     // This test will iterate the scheduler every 40 simulated seconds (to save testing time).
546     scheduler->setUpdateInterval(40000);
547 
548     // At this geo/date, Dawn is calculated = .1625 of a day = 3:53am local = 10:52 UTC
549     // If we started at 23:35 local time, as before, it's a little over 4 hours
550     // or over 4*3600 iterations. Too many? Instead we start at 3am local.
551 
552     GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8);
553     QVector<SkyObject*> targetObjects;
554     targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Altair"));
555 
556     // We'll start the scheduler at 3am local time.
557     QDateTime startUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 0, 0), Qt::UTC));
558     // The job should start at 3:12am local time.
559     QDateTime startJobUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 12, 0), Qt::UTC));
560     // The job should be interrupted at the pre-dawn time, which is about 3:53am
561     QDateTime preDawnUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 53, 0), Qt::UTC));
562     // Consider pre-dawn security range
563     preDawnUTime = preDawnUTime.addSecs(-60.0 * abs(Options::preDawnTime()));
564 
565     KStarsDateTime currentUTime;
566     int sleepMs = 0;
567     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
568 
569     runUntilFirstShutdown(geo, targetObjects, startUTime, startJobUTime, preDawnUTime, currentUTime, sleepMs, dir);
570     parkAndSleep(currentUTime, sleepMs);
571 
572     const QDateTime restartTime(QDate(2021, 6, 15), QTime(06, 31, 0), Qt::UTC);
573     wakeupAndRestart(restartTime, currentUTime, sleepMs);
574 }
575 
576 // Set up the target objects to run in the scheduler (in the order they're given)
577 // Scheduler running at location geo.
578 // Start the scheduler at startSchedulerUTime.
579 // Expect the first job to be interrupted at interruptUTime.
580 // Expect the first job to start running at startJobUTime.
581 // currentUTime and sleepMs can be set up as: KStarsDateTime currentUTime; int sleepMs = 0; and
582 // their latest values are returned, if you want to continue the simulation after this call.
583 // Similarly, dir is passed in so the temporary directory continues to exist for continued simulation.
runUntilFirstShutdown(const GeoLocation & geo,const QVector<SkyObject * > targetObjects,const QDateTime & startSchedulerUTime,const QDateTime & startJobUTime,const QDateTime & interruptUTime,KStarsDateTime & currentUTime,int & sleepMs,QTemporaryDir & dir)584 void TestEkosSchedulerOps::runUntilFirstShutdown(const GeoLocation &geo, const QVector<SkyObject*> targetObjects,
585         const QDateTime &startSchedulerUTime, const QDateTime &startJobUTime, const QDateTime &interruptUTime,
586         KStarsDateTime &currentUTime, int &sleepMs, QTemporaryDir &dir)
587 {
588     const QDateTime wakeupTime;  // Not valid--it starts up right away.
589     QVector<QString> esls, esqs;
590     for (int i = 0; i < targetObjects.size(); ++i)
591     {
592         esls.push_back(TestEkosSchedulerHelper::getSchedulerFile(targetObjects[i], m_startupCondition, {true, true, true, true}, true, true));
593         esqs.push_back(TestEkosSchedulerHelper::getDefaultEsqContent());
594     }
595     startupJobs(geo, startSchedulerUTime, &dir, esls, esqs, wakeupTime, currentUTime, sleepMs);
596     startModules(currentUTime, sleepMs);
597     QVERIFY(checkLastSlew(targetObjects[0]));
598 
599     QVERIFY(iterateScheduler("Wait for Job Startup", 10, &sleepMs, &currentUTime, [&]() -> bool
600     {
601         return (scheduler->timerState == Scheduler::RUN_JOBCHECK);
602     }));
603 
604     double delta = KStarsData::Instance()->ut().secsTo(startJobUTime);
605     QVERIFY2(std::abs(delta) < timeTolerance(60),
606              QString("Unexpected difference to job statup time: %1 secs").arg(delta).toLocal8Bit());
607 
608     // We should be unparked at this point.
609     QVERIFY(mount->parkStatus() == ISD::PARK_UNPARKED);
610 
611     // Wait until the job stops processing,
612     // hen scheduler state JOBCHECK changes to RUN_SCHEDULER.
613     QVERIFY(iterateScheduler("Wait for Job Interruption", 700, &sleepMs, &currentUTime, [&]() -> bool
614     {
615         return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
616     }));
617 
618     delta = KStarsData::Instance()->ut().secsTo(interruptUTime);
619     QVERIFY2(std::abs(delta) < timeTolerance(60),
620              QString("Unexpected difference to interrupt time: %1 secs").arg(delta).toLocal8Bit());
621 
622     // It should start to shutdown now.
623     QVERIFY(iterateScheduler("Wait for Guide Abort", 30, &sleepMs, &currentUTime, [&]() -> bool
624     {
625         return (guider->status() == Ekos::GUIDE_ABORTED);
626     }));
627 }
628 
parkAndSleep(KStarsDateTime & currentUTime,int & sleepMs)629 void TestEkosSchedulerOps::parkAndSleep(KStarsDateTime &currentUTime, int &sleepMs)
630 {
631     QVERIFY(iterateScheduler("Wait for Parked", 30, &sleepMs, &currentUTime, [&]() -> bool
632     {
633         return (mount->parkStatus() == ISD::PARK_PARKED);
634     }));
635 
636     QVERIFY(iterateScheduler("Wait for Sleep State", 30, &sleepMs, &currentUTime, [&]() -> bool
637     {
638         return (scheduler->timerState == Scheduler::RUN_WAKEUP);
639     }));
640 }
641 
wakeupAndRestart(const QDateTime & restartTime,KStarsDateTime & currentUTime,int & sleepMs)642 void TestEkosSchedulerOps::wakeupAndRestart(const QDateTime &restartTime, KStarsDateTime &currentUTime, int &sleepMs)
643 {
644     // Make sure it wakes up at the proper time.
645     QVERIFY(iterateScheduler("Wait for Wakeup Tomorrow", 30, &sleepMs, &currentUTime, [&]() -> bool
646     {
647         return (scheduler->timerState == Scheduler::RUN_SCHEDULER);
648     }));
649 
650     QVERIFY(std::abs(KStarsData::Instance()->ut().secsTo(restartTime)) < timeTolerance(60));
651 
652     // Verify the job starts up again, and the mount is once-again unparked.
653     bool readyOnce = false;
654     QVERIFY(iterateScheduler("Wait for Job Startup & Unparked", 50, &sleepMs, &currentUTime, [&]() -> bool
655     {
656         if (scheduler->mountInterface != nullptr &&
657                 scheduler->captureInterface != nullptr && !readyOnce)
658         {
659             // Send a ready signal since the scheduler expects it.
660             readyOnce = true;
661             mount->sendReady();
662             capture->sendReady();
663         }
664         return (scheduler->timerState == Scheduler::RUN_JOBCHECK &&
665                 mount->parkStatus() == ISD::PARK_UNPARKED);
666     }));
667 }
668 
testCulminationStartup()669 void TestEkosSchedulerOps::testCulminationStartup()
670 {
671     // culmination offset
672     const int offset = -60;
673 
674     // obtain location and target from test data
675     QFETCH(QString, location);
676     QFETCH(QString, target);
677     GeoLocation * const geo = KStars::Instance()->data()->locationNamed(location);
678     SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName(target);
679 
680     // move forward in 20s steps
681     scheduler->setUpdateInterval(20000);
682 
683     // determine the transit time (UTC) for a fixed date
684     KStarsDateTime midnight(QDate(2021, 7, 11), QTime(0, 0, 0), Qt::UTC);
685     QTime transitTimeUT = targetObject->transitTimeUT(midnight, geo);
686     KStarsDateTime transitUT(midnight.date(), transitTimeUT, Qt::UTC);
687 
688     // select start time three hours before transit
689     KStarsDateTime startUTime(midnight.date(), transitTimeUT.addSecs(-3600 * 3), Qt::UTC);
690 
691     // define culmination offset of 1h as startup condition
692     m_startupCondition.type = SchedulerJob::START_CULMINATION;
693     m_startupCondition.culminationOffset = -60;
694 
695     // check whether the job startup is offset minutes before the calculated transit
696     KStarsDateTime const jobStartUTime = transitUT.addSecs(60 * offset);
697     // initialize the the scheduler
698     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
699     QVector<QString> esqVector;
700     esqVector.push_back(TestEkosSchedulerHelper::getDefaultEsqContent());
701     QVector<QString> eslVector;
702     eslVector.push_back(TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, {true, true, true, true}, false, true));
703     initScheduler(*geo, startUTime, &dir, eslVector, esqVector);
704     // verify if the job starts at the expected time
705     initJob(startUTime, jobStartUTime);
706 }
707 
testFixedDateStartup()708 void TestEkosSchedulerOps::testFixedDateStartup()
709 {
710     GeoLocation * const geo = KStars::Instance()->data()->locationNamed("Heidelberg");
711     SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName("Rasalhague");
712 
713     KStarsDateTime jobStartUTime(QDate(2021, 7, 12), QTime(1, 0, 0), Qt::UTC);
714     KStarsDateTime jobStartLocalTime(QDate(2021, 7, 12), QTime(3, 0, 0), Qt::UTC);
715     // scheduler starts one hour earlier than lead time
716     KStarsDateTime startUTime = jobStartUTime.addSecs(-1 * 3600 - Options::leadTime() * 60);
717 
718     // define culmination offset of 1h as startup condition
719     m_startupCondition.type = SchedulerJob::START_AT;
720     m_startupCondition.atLocalDateTime = jobStartLocalTime;
721 
722     // initialize the the scheduler
723     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
724     QVector<QString> esqVector;
725     esqVector.push_back(TestEkosSchedulerHelper::getDefaultEsqContent());
726     QVector<QString> eslVector;
727     eslVector.push_back(TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, {true, true, true, true}, false, true));
728     initScheduler(*geo, startUTime, &dir, eslVector, esqVector);
729     // verify if the job starts at the expected time
730     initJob(startUTime, jobStartUTime);
731 }
732 
testTwilightStartup_data()733 void TestEkosSchedulerOps::testTwilightStartup_data()
734 {
735     QTest::addColumn<QString>("city");
736     QTest::addColumn<QString>("state");
737     QTest::addColumn<QString>("target");
738     QTest::addColumn<QString>("startTimeUTC");
739     QTest::addColumn<QString>("jobStartTimeUTC");
740 
741     QTest::newRow("SF")
742             << "San Francisco" << "California" << "Rasalhague"
743             << "Sun Jun 13 20:00:00 2021 GMT" <<  "Mon Jun 14 05:28:00 2021 GMT";
744 
745     QTest::newRow("Melbourne")
746             << "Melbourne" << "Victoria" << "Rasalhague"
747             << "Sun Jun 13 02:00:00 2021 GMT" <<  "Mon Jun 13 08:42:00 2021 GMT";
748 }
749 
testTwilightStartup()750 void TestEkosSchedulerOps::testTwilightStartup()
751 {
752     QFETCH(QString, city);
753     QFETCH(QString, state);
754     QFETCH(QString, target);
755     QFETCH(QString, startTimeUTC);
756     QFETCH(QString, jobStartTimeUTC);
757 
758     SkyObject *targetObject = KStars::Instance()->data()->skyComposite()->findByName(target);
759     GeoLocation * const geoPtr = KStars::Instance()->data()->locationNamed(city, state, "");
760     GeoLocation &geo = *geoPtr;
761 
762     const KStarsDateTime startUTime(QDateTime::fromString(startTimeUTC));
763     const KStarsDateTime jobStartUTime(QDateTime::fromString(jobStartTimeUTC));
764 
765     // move forward in 20s steps
766     scheduler->setUpdateInterval(20000);
767     // define culmination offset of 1h as startup condition
768     m_startupCondition.type = SchedulerJob::START_ASAP;
769 
770     // initialize the the scheduler
771     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
772     QVector<QString> esqVector;
773     esqVector.push_back(TestEkosSchedulerHelper::getDefaultEsqContent());
774     QVector<QString> eslVector;
775     // 3rd arg is the true for twilight enforced. 0 is minAltitude.
776     eslVector.push_back(TestEkosSchedulerHelper::getSchedulerFile(targetObject, m_startupCondition, {true, true, true, true}, true, false, 0));
777     initScheduler(geo, startUTime, &dir, eslVector, esqVector);
778     initJob(startUTime, jobStartUTime);
779 }
addHorizonConstraint(ArtificialHorizon * horizon,const QString & name,bool enabled,const QVector<double> & azimuths,const QVector<double> & altitudes)780 void addHorizonConstraint(ArtificialHorizon *horizon, const QString &name, bool enabled,
781                           const QVector<double> &azimuths, const QVector<double> &altitudes)
782 {
783     std::shared_ptr<LineList> pointList(new LineList);
784     for (int i = 0; i < azimuths.size(); ++i)
785     {
786         std::shared_ptr<SkyPoint> skyp1(new SkyPoint);
787         skyp1->setAlt(altitudes[i]);
788         skyp1->setAz(azimuths[i]);
789         pointList->append(skyp1);
790     }
791     horizon->addRegion(name, enabled, pointList, false);
792 }
793 
testArtificialHorizonConstraints()794 void TestEkosSchedulerOps::testArtificialHorizonConstraints()
795 {
796     // In testSimpleJob, above, the wakeup time for the job was 11:35pm local time, and it used a 30-degrees min altitude.
797     // Now let's add an artificial horizon constraint for 40-degrees at the azimuths where the object will be.
798     // It should now wakeup and start processing at about 00:27am
799 
800     ArtificialHorizon horizon;
801     addHorizonConstraint(&horizon, "r1", true, QVector<double>({100, 120}), QVector<double>({40, 40}));
802     SchedulerJob::setHorizon(&horizon);
803 
804     GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8);
805     QVector<SkyObject*> targetObjects;
806     targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Altair"));
807     QDateTime startUTime(QDateTime(QDate(2021, 6, 13), QTime(22, 0, 0), Qt::UTC));
808 
809     const QDateTime wakeupTime(QDate(2021, 6, 14), QTime(07, 27, 0), Qt::UTC);
810     runSimpleJob(geo, targetObjects[0], startUTime, wakeupTime, true);
811 
812     // Uncheck enforce artificial horizon and the wakeup time should go back to it's original time,
813     // even though the artificial horizon is still there and enabled.
814     init(); // Reset the scheduler.
815     const QDateTime originalWakeupTime(QDate(2021, 6, 14), QTime(06, 35, 0), Qt::UTC);
816     runSimpleJob(geo, targetObjects[0], startUTime, originalWakeupTime, /* enforce artificial horizon */false);
817 
818     // Re-check enforce artificial horizon, but remove the constraint, and the wakeup time also goes back to it's original time.
819     init(); // Reset the scheduler.
820     ArtificialHorizon emptyHorizon;
821     SchedulerJob::setHorizon(&emptyHorizon);
822     runSimpleJob(geo, targetObjects[0], startUTime, originalWakeupTime, /* enforce artificial horizon */ true);
823 
824     // Testing that the artificial horizon constraint will end a job
825     // when the altitude of the running job is below the artificial horizon at the
826     // target's azimuth.
827     //
828     // This repeats testDawnShutdown() above, except that an artifical horizon
829     // constraint is added so that the job doesn't reach dawn but rather is interrupted
830     // at 3:19 local time. That's the time the azimuth reaches 175.
831 
832     init(); // Reset the scheduler.
833     scheduler->setUpdateInterval(40000);
834     ArtificialHorizon shutdownHorizon;
835     // Note, just putting a constraint at 175->180 will fail this test because Altair will
836     // cross past 180 and the scheduler will want to restart it before dawn.
837     addHorizonConstraint(&shutdownHorizon, "h", true,
838                          QVector<double>({175, 200}), QVector<double>({70, 70}));
839     SchedulerJob::setHorizon(&shutdownHorizon);
840 
841     // We'll start the scheduler at 3am local time.
842     startUTime = QDateTime(QDate(2021, 6, 14), QTime(10, 0, 0), Qt::UTC);
843     // The job should start at 3:12am local time.
844     QDateTime startJobUTime(QDate(2021, 6, 14), QTime(10, 12, 0), Qt::UTC);
845     // The job should be interrupted by the horizon limit, which is reached about 3:19am local.
846     QDateTime horizonStopUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 19, 0), Qt::UTC));
847 
848     KStarsDateTime currentUTime;
849     int sleepMs = 0;
850     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
851     runUntilFirstShutdown(geo, targetObjects, startUTime, startJobUTime, horizonStopUTime, currentUTime, sleepMs, dir);
852     parkAndSleep(currentUTime, sleepMs);
853 
854     const QDateTime restartTime(QDate(2021, 6, 15), QTime(06, 31, 0), Qt::UTC);
855     wakeupAndRestart(restartTime, currentUTime, sleepMs);
856 }
857 
858 // Similar to the above testArtificialHorizonConstraints test,
859 // Schedule Altair and give it an artificial horizon constraint that will stop it at 3:19am.
860 // However, here we also have a second job, Deneb, and test to see that the 2nd job will
861 // start up after Altair stops and run until dawn.
test2ndJobRunsAfter1stHitsAltitudeConstraint()862 void TestEkosSchedulerOps::test2ndJobRunsAfter1stHitsAltitudeConstraint()
863 {
864 #ifdef TWO_JOB_TEST
865     // This test will iterate the scheduler every 40 simulated seconds (to save testing time).
866     scheduler->setUpdateInterval(40000);
867 
868     // Make sure that Altair is the first job, and Deneb the 2nd.
869     // If we allowed sorting, Deneb would go first.
870     Options::setSortSchedulerJobs(false);
871 
872     GeoLocation geo(dms(-122, 10), dms(37, 26, 30), "Silicon Valley", "CA", "USA", -8);
873     QVector<SkyObject*> targetObjects;
874     targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Altair"));
875     targetObjects.push_back(KStars::Instance()->data()->skyComposite()->findByName("Deneb"));
876 
877     ArtificialHorizon shutdownHorizon;
878     addHorizonConstraint(&shutdownHorizon, "h", true,
879                          QVector<double>({175, 200}), QVector<double>({70, 70}));
880     SchedulerJob::setHorizon(&shutdownHorizon);
881 
882     // Start the scheduler in the afternoon, about 3pm local.
883     const QDateTime startUTime = QDateTime(QDate(2021, 6, 13), QTime(20, 0, 0), Qt::UTC);
884     // The first job should actually start at 11:45pm local.
885     QDateTime startJobUTime(QDateTime(QDate(2021, 6, 14), QTime(6, 45, 0), Qt::UTC));
886     // Set the stop interrupt time at 3:19am local.
887     QDateTime horizonStopUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 19, 0), Qt::UTC));
888 
889     KStarsDateTime currentUTime;
890     int sleepMs = 0;
891     QTemporaryDir dir(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
892 
893     runUntilFirstShutdown(geo, targetObjects, startUTime, startJobUTime, horizonStopUTime, currentUTime, sleepMs, dir);
894 
895     // Now we should see the Deneb job startup.
896 
897     QVERIFY(iterateScheduler("Wait for MountSlewing", 30, &sleepMs, &currentUTime, [&]() -> bool
898     {
899         if (mount->status() == ISD::Telescope::MOUNT_SLEWING)
900             return true;
901         return false;
902     }));
903     mount->setStatus(ISD::Telescope::MOUNT_TRACKING);
904 
905     // All the modules should be active.
906     startModules(currentUTime, sleepMs);
907 
908     // Make sure the 2nd slew was to Deneb.
909     QVERIFY(checkLastSlew(targetObjects[1]));
910 
911     // Wait for the Deneb job to run.
912     QVERIFY(iterateScheduler("Wait for Job Startup", 10, &sleepMs, &currentUTime, [&]() -> bool
913     {
914         return (scheduler->timerState == Scheduler::RUN_JOBCHECK);
915     }));
916 
917     // This should run through dawn.
918     // The time should be the pre-dawn time, which is about 3:53am
919     QDateTime preDawnUTime(QDateTime(QDate(2021, 6, 14), QTime(10, 53, 0), Qt::UTC));
920     // Consider pre-dawn security range
921     preDawnUTime = preDawnUTime.addSecs(-60.0 * abs(Options::preDawnTime()));
922 
923     QVERIFY(iterateScheduler("Wait for Guide Abort", 1000, &sleepMs, &currentUTime, [&]() -> bool
924     {
925         return (guider->status() == Ekos::GUIDE_ABORTED);
926     }));
927 
928     double delta = KStarsData::Instance()->ut().secsTo(preDawnUTime);
929     QVERIFY2(std::abs(delta) < timeTolerance(60), QString("Unexpected difference to dawn: %1 secs").arg(delta).toLocal8Bit());
930 
931     parkAndSleep(currentUTime, sleepMs);
932 
933     // Wake up tomorrow, and the first job should be scheduled and running again.
934     const QDateTime restartTime(QDate(2021, 6, 15), QTime(06, 31, 0), Qt::UTC);
935     wakeupAndRestart(restartTime, currentUTime, sleepMs);
936     startModules(currentUTime, sleepMs);
937 
938     QVERIFY(checkLastSlew(targetObjects[0]));
939 #endif
940 }
941 
prepareTestData(QList<QString> locationList,QList<QString> targetList)942 void TestEkosSchedulerOps::prepareTestData(QList<QString> locationList, QList<QString> targetList)
943 {
944 #if QT_VERSION < QT_VERSION_CHECK(5,9,0)
945     QSKIP("Bypassing fixture test on old Qt");
946     Q_UNUSED(locationList)
947 #else
948     QTest::addColumn<QString>("location"); /*!< location the KStars test is running */
949     QTest::addColumn<QString>("target");   /*!< scheduled target */
950     for (QString location : locationList)
951         for (QString target : targetList)
952             QTest::newRow(QString("loc= \"%1\", target=\"%2\"").arg(location).arg(target).toLocal8Bit())
953                     << location << target;
954 #endif
955 }
956 
957 /* *********************************************************************************
958  *
959  * Test data
960  *
961  * ********************************************************************************* */
testCulminationStartup_data()962 void TestEkosSchedulerOps::testCulminationStartup_data()
963 {
964     prepareTestData({"Heidelberg", "New York"}, {"Rasalhague"});
965 }
966 
967 
968 QTEST_KSTARS_MAIN(TestEkosSchedulerOps)
969 
970 #endif // HAVE_INDI
971 
972 
973