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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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 ¤tUTime, int &sleepMs)
367 {
368 QVERIFY(iterateScheduler("Wait for MountTracking", 30, &sleepMs, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> bool
487 {
488 return (guider->status() == Ekos::GUIDE_ABORTED);
489 }));
490 QVERIFY(iterateScheduler("Wait for Shutdown", 30, &sleepMs, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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 ¤tUTime, 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> bool
624 {
625 return (guider->status() == Ekos::GUIDE_ABORTED);
626 }));
627 }
628
parkAndSleep(KStarsDateTime & currentUTime,int & sleepMs)629 void TestEkosSchedulerOps::parkAndSleep(KStarsDateTime ¤tUTime, int &sleepMs)
630 {
631 QVERIFY(iterateScheduler("Wait for Parked", 30, &sleepMs, ¤tUTime, [&]() -> bool
632 {
633 return (mount->parkStatus() == ISD::PARK_PARKED);
634 }));
635
636 QVERIFY(iterateScheduler("Wait for Sleep State", 30, &sleepMs, ¤tUTime, [&]() -> 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 ¤tUTime, int &sleepMs)
643 {
644 // Make sure it wakes up at the proper time.
645 QVERIFY(iterateScheduler("Wait for Wakeup Tomorrow", 30, &sleepMs, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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, ¤tUTime, [&]() -> 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