1 /*  KStars UI tests
2     SPDX-FileCopyrightText: 2020 Eric Dejouhanet <eric.dejouhanet@gmail.com>
3 
4     SPDX-License-Identifier: GPL-2.0-or-later
5 */
6 
7 
8 #include "test_ekos_capture.h"
9 
10 #if defined(HAVE_INDI)
11 
12 #include "kstars_ui_tests.h"
13 #include "test_ekos.h"
14 #include "test_ekos_simulator.h"
15 
TestEkosCapture(QObject * parent)16 TestEkosCapture::TestEkosCapture(QObject *parent) : QObject(parent)
17 {
18 }
19 
initTestCase()20 void TestEkosCapture::initTestCase()
21 {
22     KVERIFY_EKOS_IS_HIDDEN();
23     KTRY_OPEN_EKOS();
24     KVERIFY_EKOS_IS_OPENED();
25     KTRY_EKOS_START_SIMULATORS();
26 
27     // HACK: Reset clock to initial conditions
28     KHACK_RESET_EKOS_TIME();
29 }
30 
cleanupTestCase()31 void TestEkosCapture::cleanupTestCase()
32 {
33     KTRY_EKOS_STOP_SIMULATORS();
34     KTRY_CLOSE_EKOS();
35     KVERIFY_EKOS_IS_HIDDEN();
36 }
37 
init()38 void TestEkosCapture::init()
39 {
40     Ekos::Manager * const ekos = Ekos::Manager::Instance();
41 
42     // Wait for Capture to come up, switch to Focus tab
43     QTRY_VERIFY_WITH_TIMEOUT(ekos->captureModule() != nullptr, 5000);
44     KTRY_EKOS_GADGET(QTabWidget, toolsWidget);
45     toolsWidget->setCurrentWidget(ekos->captureModule());
46     QTRY_COMPARE_WITH_TIMEOUT(toolsWidget->currentWidget(), ekos->captureModule(), 1000);
47 }
48 
cleanup()49 void TestEkosCapture::cleanup()
50 {
51     Ekos::Manager::Instance()->captureModule()->clearSequenceQueue();
52     KTRY_CAPTURE_GADGET(QTableWidget, queueTable);
53     QTRY_VERIFY_WITH_TIMEOUT(queueTable->rowCount() == 0, 2000);
54 }
55 
searchFITS(QDir const & dir) const56 QStringList TestEkosCapture::searchFITS(QDir const &dir) const
57 {
58     QStringList list = dir.entryList(QDir::Files);
59 
60     //foreach (auto &f, list)
61     //    QWARN(QString(dir.path()+'/'+f).toStdString().c_str());
62 
63     foreach (auto &d, dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs))
64         list.append(searchFITS(QDir(dir.path() + '/' + d)));
65 
66     return list;
67 }
68 
testAddCaptureJob()69 void TestEkosCapture::testAddCaptureJob()
70 {
71     KTRY_CAPTURE_GADGET(QDoubleSpinBox, captureExposureN);
72     KTRY_CAPTURE_GADGET(QSpinBox, captureCountN);
73     KTRY_CAPTURE_GADGET(QSpinBox, captureDelayN);
74     KTRY_CAPTURE_GADGET(QComboBox, captureFilterS);
75     KTRY_CAPTURE_GADGET(QComboBox, captureTypeS);
76     KTRY_CAPTURE_GADGET(QPushButton, addToQueueB);
77     KTRY_CAPTURE_GADGET(QTableWidget, queueTable);
78 
79     // These are the expected exhaustive list of frame names
80     QString frameTypes[] = {"Light", "Bias", "Dark", "Flat"};
81     int const frameTypeCount = sizeof(frameTypes) / sizeof(frameTypes[0]);
82 
83     // Verify our assumption about those frame types is correct
84     QTRY_COMPARE_WITH_TIMEOUT(captureTypeS->count(), frameTypeCount, 5000);
85     for (QString &frameType: frameTypes)
86         if(captureTypeS->findText(frameType) < 0)
87             QFAIL(qPrintable(QString("Frame '%1' expected by the test is not in the Capture frame list").arg(frameType)));
88 
89     // These are the expected exhaustive list of filter names from the default Filter Wheel Simulator
90     QString filterTypes[] = {"Red", "Green", "Blue", "H_Alpha", "OIII", "SII", "LPR", "Luminance"};
91     int const filterTypeCount = sizeof(filterTypes) / sizeof(filterTypes[0]);
92 
93     // Verify our assumption about those filters is correct - but wait for properties to be read from the device
94     QTRY_COMPARE_WITH_TIMEOUT(captureFilterS->count(), filterTypeCount, 5000);
95     for (QString &filterType: filterTypes)
96         if(captureFilterS->findText(filterType) < 0)
97             QFAIL(qPrintable(QString("Filter '%1' expected by the test is not in the Capture filter list").arg(filterType)));
98 
99     // Add a few capture jobs
100     int const job_count = 50;
101     QWARN("When clicking the 'Add' button, immediately starting to fill the next job overwrites the job being added.");
102     for (int i = 0; i < job_count; i++)
103     {
104         captureExposureN->setValue(((double)i) / 10.0);
105         captureCountN->setValue(i);
106         captureDelayN->setValue(i);
107         KTRY_CAPTURE_COMBO_SET(captureTypeS, frameTypes[i % frameTypeCount]);
108         KTRY_CAPTURE_COMBO_SET(captureFilterS, filterTypes[i % filterTypeCount]);
109         KTRY_CAPTURE_CLICK(addToQueueB);
110         // Wait for the job to be added, else the next loop will overwrite the current job
111         QTRY_COMPARE_WITH_TIMEOUT(queueTable->rowCount(), i + 1, 100);
112     }
113 
114     // Count the number of rows
115     QVERIFY(queueTable->rowCount() == job_count);
116 
117     // Check first capture job item, which could not accept exposure duration 0 and count 0
118     QWARN("This test assumes that minimal exposure is 0.01 for the CCD Simulator.");
119     queueTable->setCurrentCell(0, 1);
120     QTRY_VERIFY_WITH_TIMEOUT(queueTable->currentRow() == 0, 1000);
121 
122     // It actually takes time before all signals syncing UI are processed, so wait for situation to settle
123     QTRY_COMPARE_WITH_TIMEOUT(captureExposureN->value(), 0.01, 1000);
124     QTRY_COMPARE_WITH_TIMEOUT(captureCountN->value(), 1, 1000);
125     QTRY_COMPARE_WITH_TIMEOUT(captureDelayN->value(), 0, 1000);
126     QTRY_COMPARE_WITH_TIMEOUT(captureTypeS->currentText(), frameTypes[0], 1000);
127     QTRY_COMPARE_WITH_TIMEOUT(captureFilterS->currentText(), filterTypes[0], 1000);
128 
129     // Select a few cells and verify the feedback on the left side UI
130     srand(42);
131     for (int index = 1; index < job_count / 2; index += rand() % 4 + 1)
132     {
133         QVERIFY(index < queueTable->rowCount());
134         queueTable->setCurrentCell(index, 1);
135         QTRY_VERIFY_WITH_TIMEOUT(queueTable->currentRow() == index, 1000);
136 
137         // It actually takes time before all signals syncing UI are processed, so wait for situation to settle
138         QTRY_VERIFY_WITH_TIMEOUT(std::fabs(captureExposureN->value() - static_cast<double>(index) / 10.0) < 0.1, 1000);
139         QTRY_COMPARE_WITH_TIMEOUT(captureCountN->value(), index, 1000);
140         QTRY_COMPARE_WITH_TIMEOUT(captureDelayN->value(), index, 1000);
141         QTRY_COMPARE_WITH_TIMEOUT(captureTypeS->currentText(), frameTypes[index % frameTypeCount], 1000);
142         QTRY_COMPARE_WITH_TIMEOUT(captureFilterS->currentText(), filterTypes[index % filterTypeCount], 1000);
143     }
144 
145     // Remove all the rows
146     // TODO: test edge cases with the selected row
147     KTRY_CAPTURE_GADGET(QPushButton, removeFromQueueB);
148     for (int i = job_count; 0 < i; i--)
149     {
150         KTRY_CAPTURE_CLICK(removeFromQueueB);
151         QVERIFY(i - 1 == queueTable->rowCount());
152     }
153 }
154 
testCaptureToTemporary()155 void TestEkosCapture::testCaptureToTemporary()
156 {
157     QTemporaryDir destination;
158     QVERIFY(destination.isValid());
159     QVERIFY(destination.autoRemove());
160 
161     // Add five exposures
162     KTRY_CAPTURE_ADD_LIGHT(0.1, 5, 0, "Red", destination.path());
163 
164     // Start capturing and wait for procedure to end (visual icon changing)
165     KTRY_CAPTURE_GADGET(QPushButton, startB);
166     QCOMPARE(startB->icon().name(), QString("media-playback-start"));
167     KTRY_CAPTURE_CLICK(startB);
168     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-stop"), 500);
169     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-start"), 30000);
170 
171     QWARN("Test capturing to temporary is no longer valid since we don't create temporary files any more.");
172     //    QWARN("When storing to a recognized system temporary folder, only one FITS file is created.");
173     //    QTRY_VERIFY_WITH_TIMEOUT(searchFITS(QDir(destination.path())).count() == 1, 1000);
174     //    QCOMPARE(searchFITS(QDir(destination.path()))[0], QString("Light_005.fits"));
175 }
176 
testCaptureSingle()177 void TestEkosCapture::testCaptureSingle()
178 {
179     // We cannot use a system temporary due to what testCaptureToTemporary marks
180     QTemporaryDir destination(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
181     QVERIFY(destination.isValid());
182     QVERIFY(destination.autoRemove());
183 
184     // Add an exposure
185     KTRY_CAPTURE_ADD_LIGHT(0.5, 1, 0, "Red", destination.path());
186 
187     // Start capturing and wait for procedure to end (visual icon changing)
188     KTRY_CAPTURE_GADGET(QPushButton, startB);
189     QCOMPARE(startB->icon().name(), QString("media-playback-start"));
190     KTRY_CAPTURE_CLICK(startB);
191     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-stop"), 500);
192     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-start"), 2000);
193 
194     // Verify a FITS file was created
195     QTRY_VERIFY_WITH_TIMEOUT(searchFITS(QDir(destination.path())).count() == 1, 1000);
196     QVERIFY(searchFITS(QDir(destination.path()))[0].startsWith("Light_"));
197     QVERIFY(searchFITS(QDir(destination.path()))[0].endsWith("_001.fits"));
198 
199     // Reset sequence state - this makes a confirmation dialog appear
200     volatile bool dialogValidated = false;
201     QTimer::singleShot(200, [&]
202     {
203         QDialog * const dialog = qobject_cast <QDialog*> (QApplication::activeModalWidget());
204         if(dialog != nullptr)
205         {
206             QTest::mouseClick(dialog->findChild<QDialogButtonBox*>()->button(QDialogButtonBox::Yes), Qt::LeftButton);
207             dialogValidated = true;
208         }
209     });
210     KTRY_CAPTURE_CLICK(resetB);
211     QTRY_VERIFY_WITH_TIMEOUT(dialogValidated, 1000);
212 
213     // Capture again
214     QCOMPARE(startB->icon().name(), QString("media-playback-start"));
215     KTRY_CAPTURE_CLICK(startB);
216     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-stop"), 500);
217     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-start"), 2000);
218 
219     // Verify an additional FITS file was created - asynchronously eventually
220     QTRY_VERIFY_WITH_TIMEOUT(searchFITS(QDir(destination.path())).count() == 2, 2000);
221     QVERIFY(searchFITS(QDir(destination.path()))[0].startsWith("Light_"));
222     QVERIFY(searchFITS(QDir(destination.path()))[0].endsWith("_001.fits"));
223     QVERIFY(searchFITS(QDir(destination.path()))[1].startsWith("Light_"));
224     QVERIFY(searchFITS(QDir(destination.path()))[1].endsWith("_002.fits"));
225 
226     // TODO: test storage options
227 }
228 
testCaptureMultiple()229 void TestEkosCapture::testCaptureMultiple()
230 {
231     // We cannot use a system temporary due to what testCaptureToTemporary marks
232     QTemporaryDir destination(QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/test-XXXXXX");
233     QVERIFY(destination.isValid());
234     QVERIFY(destination.autoRemove());
235 
236     // Add a few exposures
237     KTRY_CAPTURE_ADD_LIGHT(0.5, 1, 0, "Red", destination.path());
238     KTRY_CAPTURE_ADD_LIGHT(0.7, 2, 0, "SII", destination.path());
239     KTRY_CAPTURE_ADD_LIGHT(0.2, 5, 0, "Green", destination.path());
240     KTRY_CAPTURE_ADD_LIGHT(0.9, 2, 0, "Luminance", destination.path());
241     KTRY_CAPTURE_ADD_LIGHT(0.5, 1, 1, "H_Alpha", destination.path());
242     QWARN("A sequence of exposures under 1 second will always take 1 second to capture each of them.");
243     //size_t const duration = (500+0)*1+(700+0)*2+(200+0)*5+(900+0)*2+(500+1000)*1;
244     size_t const duration = 1000 * (1 + 2 + 5 + 2 + 1);
245     size_t const count = 1 + 2 + 5 + 2 + 1;
246 
247     // Start capturing and wait for procedure to end (visual icon changing) - leave enough time for frames to store
248     KTRY_CAPTURE_GADGET(QPushButton, startB);
249     QCOMPARE(startB->icon().name(), QString("media-playback-start"));
250     KTRY_CAPTURE_CLICK(startB);
251     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-stop"), 500);
252     QTRY_COMPARE_WITH_TIMEOUT(startB->icon().name(), QString("media-playback-start"), duration * 2);
253 
254     // Verify the proper number of FITS file were created
255     QTRY_VERIFY_WITH_TIMEOUT(searchFITS(QDir(destination.path())).count() == count, 1000);
256 
257     // Reset sequence state - this makes a confirmation dialog appear
258     volatile bool dialogValidated = false;
259     QTimer::singleShot(200, [&]
260     {
261         QDialog * const dialog = qobject_cast <QDialog*> (QApplication::activeModalWidget());
262         if(dialog != nullptr)
263         {
264             QTest::mouseClick(dialog->findChild<QDialogButtonBox*>()->button(QDialogButtonBox::Yes), Qt::LeftButton);
265             dialogValidated = true;
266         }
267     });
268     KTRY_CAPTURE_CLICK(resetB);
269     QTRY_VERIFY_WITH_TIMEOUT(dialogValidated, 500);
270 
271     // Capture again
272     KTRY_CAPTURE_CLICK(startB);
273     QTRY_VERIFY_WITH_TIMEOUT(!startB->icon().name().compare("media-playback-stop"), 500);
274     QTRY_VERIFY_WITH_TIMEOUT(!startB->icon().name().compare("media-playback-start"), duration * 2);
275 
276     // Verify the proper number of additional FITS file were created again
277     QTRY_VERIFY_WITH_TIMEOUT(searchFITS(QDir(destination.path())).count() == 2 * count, 1000);
278 
279     // TODO: test storage options
280 }
281 
282 QTEST_KSTARS_MAIN(TestEkosCapture)
283 
284 #endif // HAVE_INDI
285