diff --git a/Tests/kstars_ui/CMakeLists.txt b/Tests/kstars_ui/CMakeLists.txt index d47edb9a0..1b1b93d35 100644 --- a/Tests/kstars_ui/CMakeLists.txt +++ b/Tests/kstars_ui/CMakeLists.txt @@ -1,24 +1,25 @@ SET(KSTARS_UI_TESTS_SRC + kstars_ui_tests.cpp test_ekos.cpp - kstars_ui_tests.cpp) + test_ekos_simulator.cpp) include_directories(${CFITSIO_INCLUDE_DIR}) ECM_ADD_APP_ICON(KSTARS_UI_TESTS_SRC ICONS ../../kstars/icons/16-apps-kstars.png ../../kstars/icons/32-apps-kstars.png ../../kstars/icons/48-apps-kstars.png ../../kstars/icons/64-apps-kstars.png ../../kstars/icons/128-apps-kstars.png ) QT5_ADD_RESOURCES(KSTARS_UI_TESTS_SRC ../../kstars/data/kstars.qrc) ADD_EXECUTABLE(kstars_ui_tests ${KSTARS_UI_TESTS_SRC}) TARGET_LINK_LIBRARIES(kstars_ui_tests ${TEST_LIBRARIES} ${CFITSIO_LIBRARIES}) IF (INDI_FOUND) INCLUDE_DIRECTORIES(${INDI_INCLUDE_DIR}) TARGET_LINK_LIBRARIES(kstars_ui_tests ${INDI_CLIENT_LIBRARIES} ${NOVA_LIBRARIES} z) ENDIF () diff --git a/Tests/kstars_ui/kstars_ui_tests.cpp b/Tests/kstars_ui/kstars_ui_tests.cpp index 260afbb51..016ec35a2 100644 --- a/Tests/kstars_ui/kstars_ui_tests.cpp +++ b/Tests/kstars_ui/kstars_ui_tests.cpp @@ -1,114 +1,173 @@ /* KStars UI tests Copyright (C) 2017 Csaba Kertesz This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #include "kstars_ui_tests.h" +#include "test_ekos.h" +#include "test_ekos_simulator.h" #include "auxiliary/kspaths.h" #if defined(HAVE_INDI) #include "ekos/manager.h" #include "ekos/profileeditor.h" #endif #include "kstars.h" #include #include #include #include #include #include +#include +#include #include #include -KStarsUiTests::KStarsUiTests() -{ - srand((unsigned int)time(nullptr)); - // Create writable data dir if it does not exist - QDir writableDir; +struct KStarsUiTests::_InitialConditions const KStarsUiTests::m_InitialConditions; - writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation)); - KCrash::initialize(); +KStarsUiTests::KStarsUiTests(QObject *parent): QObject(parent) +{ } void KStarsUiTests::initTestCase() { - KTipDialog::setShowOnStart(false); - K = KStars::createInstance(true); } void KStarsUiTests::cleanupTestCase() { - K->close(); - delete K; - K = nullptr; } void KStarsUiTests::init() { - KStars* const K = KStars::Instance(); - QVERIFY(K != nullptr); - KTRY_SHOW_KSTARS(K); - -#if defined(HAVE_INDI) - initEkos(); -#endif + if (KStars::Instance() != nullptr) + KTRY_SHOW_KSTARS(); } void KStarsUiTests::cleanup() { foreach (QDialog * d, KStars::Instance()->findChildren()) if (d->isVisible()) d->hide(); - -#if defined(HAVE_INDI) - cleanupEkos(); -#endif } // All QTest features are macros returning with no error code. // Therefore, in order to bail out at first failure, tests cannot use functions to run sub-tests and are required to use grouping macros too. - void KStarsUiTests::basicTest() +void KStarsUiTests::createInstanceTest() { - QVERIFY(true); + // Initialize our instance + /*QBENCHMARK_ONCE*/ { + // Create our test instance + KTipDialog::setShowOnStart(false); + KStars::createInstance(true, KStarsUiTests::m_InitialConditions.clockRunning, KStarsUiTests::m_InitialConditions.dateTime.toString()); + QApplication::processEvents(); + } + QVERIFY(KStars::Instance() != nullptr); + + // Initialize our location + GeoLocation * const g = KStars::Instance()->data()->locationNamed("Greenwich"); + QVERIFY(g != nullptr); + KStars::Instance()->data()->setLocation(*g); + QCOMPARE(KStars::Instance()->data()->geo()->lat()->Degrees(), g->lat()->Degrees()); + QCOMPARE(KStars::Instance()->data()->geo()->lng()->Degrees(), g->lng()->Degrees()); } -void KStarsUiTests::warnTest() +void KStarsUiTests::raiseKStarsTest() { - QWARN("This is a test expected to print a warning message."); + KTRY_SHOW_KSTARS(); } -void KStarsUiTests::raiseKStarsTest() +void KStarsUiTests::initialConditionsTest() { - KTRY_SHOW_KSTARS(K); + QVERIFY(KStars::Instance() != nullptr); + QVERIFY(KStars::Instance()->data() != nullptr); + QVERIFY(KStars::Instance()->data()->clock() != nullptr); + + QCOMPARE(KStars::Instance()->data()->clock()->isActive(), m_InitialConditions.clockRunning); + + QEXPECT_FAIL("", "Initial KStars clock is set from system local time, not geolocation, and is untestable for now.", Continue); + QCOMPARE(KStars::Instance()->data()->clock()->utc().toString(), m_InitialConditions.dateTime.toString()); + + QEXPECT_FAIL("", "Precision of KStars local time conversion to local time does not allow strict millisecond comparison.", Continue); + QCOMPARE(KStars::Instance()->data()->clock()->utc().toLocalTime(), m_InitialConditions.dateTime); + + // However comparison down to nearest second is expected to be OK + QCOMPARE(llround(KStars::Instance()->data()->clock()->utc().toLocalTime().toMSecsSinceEpoch()/1000.0), m_InitialConditions.dateTime.toSecsSinceEpoch()); } // We want to launch the application before running our tests // Thus we want to explicitly call QApplication::exec(), and run our tests in parallel of the event loop // We then reimplement QTEST_MAIN(KStarsUiTests); // The same will have to be done when interacting with a modal dialog: exec() in main thread, tests in timer-based thread QT_BEGIN_NAMESPACE QTEST_ADD_GPU_BLACKLIST_SUPPORT_DEFS QT_END_NAMESPACE int main(int argc, char *argv[]) { + // Create our test application QApplication app(argc, argv); app.setAttribute(Qt::AA_Use96Dpi, true); QTEST_ADD_GPU_BLACKLIST_SUPPORT - QApplication::processEvents(); - KStarsUiTests * tc = new KStarsUiTests(); QTEST_SET_MAIN_SOURCE_PATH - QTimer::singleShot(1000, &app, [=]() { return QTest::qExec(tc, argc, argv); }); - QTimer::singleShot(30000, &app, &QCoreApplication::quit); + QApplication::processEvents(); + + // Prepare our configuration + srand((unsigned int)time(nullptr)); + QDir writableDir; + writableDir.mkdir(KSPaths::writableLocation(QStandardPaths::GenericDataLocation)); + KCrash::initialize(); + + // The instance will be created (and benchmarked) as first test in KStarsUiTests + qDebug("Deferring instance creation to tests."); + + // This holds the final result of the test session + int result = 0; + + // Execute tests in sequence, eventually skipping sub-tests based on prior ones + QTimer::singleShot(1000, &app, [&] + { + qDebug("Starting tests..."); + + // This creates our KStars instance + KStarsUiTests * tc = new KStarsUiTests(); + result = QTest::qExec(tc, argc, argv); + +#if defined(HAVE_INDI) + if (!result) + { + TestEkos * ek = new TestEkos(); + result |= QTest::qExec(ek, argc, argv); + } + + if (!result) + { + TestEkosSimulator * eks = new TestEkosSimulator(); + result |= QTest::qExec(eks, argc, argv); + } +#endif + + // Done testing, successful or not + qDebug("Tests are done."); + app.quit(); + }); + + // Limit execution duration + QTimer::singleShot(5*60*1000, &app, &QCoreApplication::quit); + app.exec(); + KStars::Instance()->close(); + delete KStars::Instance(); + + return result; } diff --git a/Tests/kstars_ui/kstars_ui_tests.h b/Tests/kstars_ui/kstars_ui_tests.h index de1861c38..cd8e874da 100644 --- a/Tests/kstars_ui/kstars_ui_tests.h +++ b/Tests/kstars_ui/kstars_ui_tests.h @@ -1,65 +1,65 @@ /* KStars UI tests Copyright (C) 2017 Csaba Kertesz This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ #pragma once #include "config-kstars.h" #include #include #include #include -#include +#include #include "kstars.h" class KStars; -#define KTRY_SHOW_KSTARS(K) do { \ - QTRY_VERIFY_WITH_TIMEOUT((K)->isGUIReady(), 1000); \ +#define KTRY_SHOW_KSTARS() do { \ + KStars * const K = KStars::Instance(); \ + QVERIFY(K != nullptr); \ + QTRY_VERIFY_WITH_TIMEOUT((K)->isGUIReady(), 30000); \ (K)->raise(); \ QTRY_VERIFY_WITH_TIMEOUT((K)->isActiveWindow(), 1000); } while(false) +#define KTRY_ACTION(action_text) do { \ + QAction * const action = KStars::Instance()->actionCollection()->action(action_text); \ + QVERIFY2(action != nullptr, QString("Action '%1' is not registered and cannot be triggered").arg(action_text).toStdString().c_str()); \ + action->trigger(); } while(false) + class KStarsUiTests : public QObject { Q_OBJECT +public: + explicit KStarsUiTests(QObject *parent = nullptr); public: - KStarsUiTests(); - ~KStarsUiTests() override = default; + static struct _InitialConditions + { + QDateTime dateTime; + bool clockRunning; -private: - KStars* K {nullptr}; + _InitialConditions(): + dateTime(QDate(2020, 01, 01), QTime(23, 0, 0), Qt::UTC), + clockRunning(false) {}; + } + const m_InitialConditions; private slots: void initTestCase(); void cleanupTestCase(); void init(); void cleanup(); - void basicTest(); - void warnTest(); - void raiseKStarsTest(); - -#if defined(HAVE_INDI) -private: - QString testProfileName; - void initEkos(); - void cleanupEkos(); + void createInstanceTest(); -private slots: - // UI - void openEkosTest(); - - // Profiles - void manipulateEkosProfiles(); - void testdriveSimulatorProfile(); -#endif + void initialConditionsTest(); + void raiseKStarsTest(); }; diff --git a/Tests/kstars_ui/test_ekos.cpp b/Tests/kstars_ui/test_ekos.cpp index d183aac83..533ca9a83 100644 --- a/Tests/kstars_ui/test_ekos.cpp +++ b/Tests/kstars_ui/test_ekos.cpp @@ -1,359 +1,335 @@ /* KStars UI tests - Copyright (C) 218, 2020 + Copyright (C) 2018, 2020 Csaba Kertesz Jasem Mutlaq Eric Dejouhanet This application is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. */ -#include "config-kstars.h" +#include "test_ekos.h" #if defined(HAVE_INDI) #include "kstars_ui_tests.h" #include "ekos/manager.h" #include "ekos/profileeditor.h" #include "kstars.h" #include "auxiliary/ksmessagebox.h" #include #include #include #include #include #include #include #include #include #include -#define KTRY_ACTION(action_text) do { \ - QAction * const action = KStars::Instance()->actionCollection()->action(action_text); \ - QVERIFY(action != nullptr); \ - action->trigger(); } while(false) - -#define KVERIFY_EKOS_IS_HIDDEN() do { \ - if (Ekos::Manager::Instance() != nullptr) { \ - QVERIFY(!Ekos::Manager::Instance()->isVisible()); \ - QVERIFY(!Ekos::Manager::Instance()->isActiveWindow()); }} while(false) - -#define KVERIFY_EKOS_IS_OPENED() do { \ - QVERIFY(Ekos::Manager::Instance() != nullptr); \ - QVERIFY(Ekos::Manager::Instance()->isVisible()); \ - QVERIFY(Ekos::Manager::Instance()->isActiveWindow()); } while(false) - -#define KTRY_OPEN_EKOS() do { \ - if (Ekos::Manager::Instance() == nullptr || !Ekos::Manager::Instance()->isVisible()) { \ - KTRY_ACTION("show_ekos"); \ - QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance() != nullptr, 200); \ - QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->isVisible(), 200); \ - QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->isActiveWindow(), 200); }} while(false) - -#define KTRY_CLOSE_EKOS() do { \ - if (Ekos::Manager::Instance() != nullptr && Ekos::Manager::Instance()->isVisible()) { \ - KTRY_ACTION("show_ekos"); \ - QTRY_VERIFY_WITH_TIMEOUT(!Ekos::Manager::Instance()->isActiveWindow(), 200); \ - QTRY_VERIFY_WITH_TIMEOUT(!Ekos::Manager::Instance()->isVisible(), 200); }} while(false) - -void KStarsUiTests::initEkos() +TestEkos::TestEkos(QObject *parent): QObject(parent) +{ + +} + +void TestEkos::initTestCase() { /* No-op */ } -void KStarsUiTests::cleanupEkos() +void TestEkos::cleanupTestCase() { KTRY_CLOSE_EKOS(); } -void KStarsUiTests::openEkosTest() +void TestEkos::init() { KVERIFY_EKOS_IS_HIDDEN(); KTRY_OPEN_EKOS(); KVERIFY_EKOS_IS_OPENED(); - KTRY_CLOSE_EKOS(); - KVERIFY_EKOS_IS_HIDDEN(); } -void KStarsUiTests::testdriveSimulatorProfile() +void TestEkos::cleanup() { + KTRY_CLOSE_EKOS(); KVERIFY_EKOS_IS_HIDDEN(); - KTRY_OPEN_EKOS(); - KVERIFY_EKOS_IS_OPENED(); +} - // Because we don't want to manager the order of tests, we do the profile manipulation in three steps of the same test - // We use that poor man's shared variable to hold the result of the first (creation) and second (edition) test step. - // The ProfileEditor is exec()'d, so test code must be made asynchronous, and QTimer::singleShot is an easy way to do that. - // We use two timers, one to run the end-user test, which eventually will close the dialog, and a second one to really close if the test step fails. - bool testIsSuccessful = false; +void TestEkos::testOpenClose() +{ + /* No-op, we just use init+cleanup */ +} +void TestEkos::testSimulatorProfile() +{ Ekos::Manager * const ekos = Ekos::Manager::Instance(); // --------- First step: selecting the Simulators profile - QString const testProfileName("Simulators"); - // Verify that the test profile exists, and select it - QComboBox* profileCBox = ekos->findChild("profileCombo"); + QString const p("Simulators"); + QComboBox* profileCBox = Ekos::Manager::Instance()->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); - profileCBox->setCurrentText(testProfileName); - QCOMPARE(profileCBox->currentText(), testProfileName); + profileCBox->setCurrentText(p); + QTRY_COMPARE(profileCBox->currentText(), p); // --------- Second step: starting Ekos with the Simulators profile QString const buttonReadyToStart("media-playback-start"); QString const buttonReadyToStop("media-playback-stop"); // Check the start button icon as visual feedback about Ekos state, and click to start Ekos QPushButton* startEkos = ekos->findChild("processINDIB"); QVERIFY(startEkos != nullptr); QVERIFY(!buttonReadyToStart.compare(startEkos->icon().name())); QTest::mouseClick(startEkos, Qt::LeftButton); // --------- Third step: waiting for Ekos to finish startup // The INDI property pages automatically raised on top, but as we got a handle to the button, we continue to test as is // Wait until Ekos gives feedback on the INDI client startup - button changes to symbol "stop" - QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStop.compare(startEkos->icon().name()), 5000); + QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStop.compare(startEkos->icon().name()), 7000); // It might be that our Simulators profile is not auto-connecting and we should do that manually // We assume that by default it's not the case // We have no feedback on whether all our devices are connected, so we need to hack a delay in... QWARN("HACK HACK HACK adding delay here for devices to connect"); QTest::qWait(5000); // Verify the device connection button is unavailable QPushButton * connectDevices = ekos->findChild("connectB"); QVERIFY(connectDevices != nullptr); QVERIFY(!connectDevices->isEnabled()); + QEXPECT_FAIL("", "Ekos resets the simulation clock when starting a profile.", Continue); + QCOMPARE(llround(KStars::Instance()->data()->clock()->utc().toLocalTime().toMSecsSinceEpoch()/1000.0), KStarsUiTests::m_InitialConditions.dateTime.toSecsSinceEpoch()); + + QEXPECT_FAIL("", "Ekos resumes the simulation clock when starting a profile.", Continue); + QVERIFY(!KStars::Instance()->data()->clock()->isActive()); + // --------- Fourth step: waiting for Ekos to finish stopping // Start button that became a stop button is now disabled - we need to disconnect devices first QVERIFY(!startEkos->isEnabled()); - // Verify the device disconnection button is available - QPushButton * disconnectDevices = ekos->findChild("disconnectB"); - QVERIFY(disconnectDevices != nullptr); - - // Disconnect INDI devices - QTimer::singleShot(200, ekos, [&]() + // Verify the device disconnection button is available, disconnect devices + QPushButton * const b = ekos->findChild("disconnectB"); + QVERIFY(b != nullptr); + QVERIFY(b->isEnabled()); + QTimer::singleShot(200, Ekos::Manager::Instance(), [&] { - QTest::mouseClick(disconnectDevices, Qt::LeftButton); + QTest::mouseClick(b, Qt::LeftButton); }); QWARN("Intentionally leaving a delay here for BZ398192"); QTest::qWait(5000); // --------- Fifth step: waiting for Ekos to finish stopping // Start button that became a stop button has to be available - QTRY_VERIFY_WITH_TIMEOUT(startEkos->isEnabled(), 5000); + QTRY_VERIFY_WITH_TIMEOUT(startEkos->isEnabled(), 7000); // Hang INDI client up - QTimer::singleShot(200, ekos, [&]() + QTimer::singleShot(200, ekos, [&] { QTest::mouseClick(startEkos, Qt::LeftButton); }); - QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStart.compare(startEkos->icon().name()), 5000); + QTRY_VERIFY_WITH_TIMEOUT(!buttonReadyToStart.compare(startEkos->icon().name()), 7000); } -void KStarsUiTests::manipulateEkosProfiles() +void TestEkos::testManipulateProfiles() { - KVERIFY_EKOS_IS_HIDDEN(); - KTRY_OPEN_EKOS(); - KVERIFY_EKOS_IS_OPENED(); - // Because we don't want to manager the order of tests, we do the profile manipulation in three steps of the same test // We use that poor man's shared variable to hold the result of the first (creation) and second (edition) test step. // The ProfileEditor is exec()'d, so test code must be made asynchronous, and QTimer::singleShot is an easy way to do that. // We use two timers, one to run the end-user test, which eventually will close the dialog, and a second one to really close if the test step fails. bool testIsSuccessful = false; Ekos::Manager * const ekos = Ekos::Manager::Instance(); + QString testProfileName = QString("testUI%1").arg(rand() % 100000); // FIXME: Move this to fixtures // --------- First step: creating the profile // Because the dialog is modal, the remainder of the test is made asynchronous - QTimer::singleShot(200, ekos, [&]() + QTimer::singleShot(200, ekos, [&] { // Find the Profile Editor dialog ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); /* // If the name of the widget is unknown, use this snippet to look it up from its class if (profileEditor == nullptr) foreach (QWidget *w, QApplication::topLevelWidgets()) if (w->inherits("ProfileEditor")) profileEditor = qobject_cast (w); */ QVERIFY(profileEditor != nullptr); // Create a test profile QLineEdit * const profileNameLE = profileEditor->findChild("profileIN"); QVERIFY(nullptr != profileNameLE); - testProfileName = QString("testUI%1").arg(rand() % 100000); // FIXME: Move this to fixtures profileNameLE->setText(testProfileName); QCOMPARE(profileNameLE->text(), testProfileName); // Setting an item programmatically in a treeview combobox... QComboBox * const mountCBox = profileEditor->findChild("mountCombo"); QVERIFY(nullptr != mountCBox); QString lookup("Telescope Simulator"); // FIXME: Move this to fixtures // Match the text recursively in the model, this results in a model index with a parent QModelIndexList const list = mountCBox->model()->match(mountCBox->model()->index(0, 0), Qt::DisplayRole, QVariant::fromValue(lookup), 1, Qt::MatchRecursive); QVERIFY(0 < list.count()); QModelIndex const &item = list.first(); //QWARN(QString("Found text '%1' at #%2, parent at #%3").arg(item.data().toString()).arg(item.row()).arg(item.parent().row()).toStdString().data()); QCOMPARE(list.value(0).data().toString(), lookup); QVERIFY(!item.parent().parent().isValid()); // Now set the combobox model root to the match's parent mountCBox->setRootModelIndex(item.parent()); // And set the text as if the end-user had selected it mountCBox->setCurrentText(lookup); QCOMPARE(mountCBox->currentText(), lookup); QComboBox * const ccdCBox = profileEditor->findChild("ccdCombo"); QVERIFY(nullptr != ccdCBox); lookup = "CCD Simulator"; ccdCBox->setCurrentText(lookup); // FIXME: Move this to fixtures QCOMPARE(ccdCBox->currentText(), lookup); // Save the profile using the "Save" button QDialogButtonBox* buttons = profileEditor->findChild("dialogButtons"); QVERIFY(nullptr != buttons); QTest::mouseClick(buttons->button(QDialogButtonBox::Save), Qt::LeftButton); testIsSuccessful = true; }); // Cancel the Profile Editor dialog if the test failed - this will happen after pushing the add button below QTimer * closeDialog = new QTimer(this); closeDialog->setSingleShot(true); closeDialog->setInterval(1000); - ekos->connect(closeDialog, &QTimer::timeout, [&]() + ekos->connect(closeDialog, &QTimer::timeout, [&] { ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); if (profileEditor != nullptr) profileEditor->reject(); }); // Click on "Add profile" button, and let the async tests run on the modal dialog QPushButton* addButton = ekos->findChild("addProfileB"); QVERIFY(addButton != nullptr); QTest::mouseClick(addButton, Qt::LeftButton); // Click handler returned, stop the timer closing the dialog on failure closeDialog->stop(); delete closeDialog; // Verification of the first test step QVERIFY(testIsSuccessful); testIsSuccessful = false; // --------- Second step: editing and verifying the profile // Verify that the test profile exists, and select it QComboBox* profileCBox = ekos->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(testProfileName); QCOMPARE(profileCBox->currentText(), testProfileName); // Because the dialog is modal, the remainder of the test is made asynchronous - QTimer::singleShot(200, ekos, [&]() + QTimer::singleShot(200, ekos, [&] { // Find the Profile Editor dialog ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); QVERIFY(profileEditor != nullptr); // Verify the values set by addEkosProfileTest QLineEdit* profileNameLE = ekos->findChild("profileIN"); QVERIFY(profileNameLE != nullptr); QCOMPARE(profileNameLE->text(), profileCBox->currentText()); QComboBox* mountCBox = ekos->findChild("mountCombo"); QVERIFY(mountCBox != nullptr); QCOMPARE(mountCBox->currentText(), QString("Telescope Simulator")); QComboBox* ccdCBox = ekos->findChild("ccdCombo"); QVERIFY(ccdCBox != nullptr); QCOMPARE(ccdCBox->currentText(), QString("CCD Simulator")); // Cancel the dialog using the "Close" button QDialogButtonBox* buttons = profileEditor->findChild("dialogButtons"); QVERIFY(nullptr != buttons); QTest::mouseClick(buttons->button(QDialogButtonBox::Close), Qt::LeftButton); testIsSuccessful = true; }); // Cancel the Profile Editor dialog if the test failed - this will happen after pushing the edit button below closeDialog = new QTimer(this); closeDialog->setSingleShot(true); closeDialog->setInterval(1000); - ekos->connect(closeDialog, &QTimer::timeout, [&]() + ekos->connect(closeDialog, &QTimer::timeout, [&] { ProfileEditor* profileEditor = ekos->findChild("profileEditorDialog"); if (profileEditor != nullptr) profileEditor->reject(); }); // Click on "Edit profile" button, and let the async tests run on the modal dialog QPushButton* editButton = ekos->findChild("editProfileB"); QVERIFY(editButton != nullptr); QTest::mouseClick(editButton, Qt::LeftButton); // Click handler returned, stop the timer closing the dialog on failure closeDialog->stop(); delete closeDialog; // Verification of the second test step QVERIFY(testIsSuccessful); testIsSuccessful = false; // Verify that the test profile still exists, and select it profileCBox = ekos->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(testProfileName); QCOMPARE(profileCBox->currentText(), testProfileName); // --------- Third step: deleting the profile // The yes/no modal dialog is not really used as a blocking question, but let's keep the remainder of the test is made asynchronous - QTimer::singleShot(200, ekos, [&]() + QTimer::singleShot(200, ekos, [&] { // This trick is from https://stackoverflow.com/questions/38596785 QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() != nullptr, 1000); // This is not a regular dialog but a KStars-customized dialog, so we can't search for a yes button from QDialogButtonBox KSMessageBox * const dialog = qobject_cast (QApplication::activeModalWidget()); QVERIFY(dialog != nullptr); emit dialog->accept(); testIsSuccessful = true; }); // Click on "Remove profile" button - this will display a modal yes/no dialog QPushButton* removeButton = ekos->findChild("deleteProfileB"); QVERIFY(removeButton != nullptr); QTest::mouseClick(removeButton, Qt::LeftButton); // Pressing delete-profile triggers the open() function of the yes/no modal dialog, which returns immediately, so wait for it to disappear QTRY_VERIFY_WITH_TIMEOUT(QApplication::activeModalWidget() == nullptr, 1000); // Verification of the third test step QVERIFY(testIsSuccessful); testIsSuccessful = false; // Verify that the test profile doesn't exist anymore profileCBox = ekos->findChild("profileCombo"); QVERIFY(profileCBox != nullptr); profileCBox->setCurrentText(testProfileName); QVERIFY(profileCBox->currentText() != testProfileName); } #endif diff --git a/Tests/kstars_ui/test_ekos.h b/Tests/kstars_ui/test_ekos.h new file mode 100644 index 000000000..aadf354fb --- /dev/null +++ b/Tests/kstars_ui/test_ekos.h @@ -0,0 +1,64 @@ +/* KStars UI tests + Copyright (C) 2018, 2020 + Csaba Kertesz + Jasem Mutlaq + Eric Dejouhanet + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + */ + +#ifndef TEST_EKOS_H +#define TEST_EKOS_H + +#include "config-kstars.h" + +#if defined(HAVE_INDI) + +#include + +#define KVERIFY_EKOS_IS_HIDDEN() do { \ + if (Ekos::Manager::Instance() != nullptr) { \ + QVERIFY(!Ekos::Manager::Instance()->isVisible()); \ + QVERIFY(!Ekos::Manager::Instance()->isActiveWindow()); }} while(false) + +#define KVERIFY_EKOS_IS_OPENED() do { \ + QVERIFY(Ekos::Manager::Instance() != nullptr); \ + QVERIFY(Ekos::Manager::Instance()->isVisible()); \ + QVERIFY(Ekos::Manager::Instance()->isActiveWindow()); } while(false) + +#define KTRY_OPEN_EKOS() do { \ + if (Ekos::Manager::Instance() == nullptr || !Ekos::Manager::Instance()->isVisible()) { \ + KTRY_ACTION("show_ekos"); \ + QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance() != nullptr, 200); \ + QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->isVisible(), 200); \ + QTRY_VERIFY_WITH_TIMEOUT(Ekos::Manager::Instance()->isActiveWindow(), 200); }} while(false) + +#define KTRY_CLOSE_EKOS() do { \ + if (Ekos::Manager::Instance() != nullptr && Ekos::Manager::Instance()->isVisible()) { \ + KTRY_ACTION("show_ekos"); \ + QTRY_VERIFY_WITH_TIMEOUT(!Ekos::Manager::Instance()->isActiveWindow(), 200); \ + QTRY_VERIFY_WITH_TIMEOUT(!Ekos::Manager::Instance()->isVisible(), 200); }} while(false) + +class TestEkos: public QObject +{ + Q_OBJECT +public: + explicit TestEkos(QObject *parent = nullptr); + +private slots: + void initTestCase(); + void cleanupTestCase(); + + void init(); + void cleanup(); + + void testOpenClose(); + void testSimulatorProfile(); + void testManipulateProfiles(); +}; + +#endif +#endif // TEST_EKOS_H diff --git a/Tests/kstars_ui/test_ekos_simulator.cpp b/Tests/kstars_ui/test_ekos_simulator.cpp new file mode 100644 index 000000000..61728afab --- /dev/null +++ b/Tests/kstars_ui/test_ekos_simulator.cpp @@ -0,0 +1,151 @@ +/* KStars UI tests + Copyright (C) 2020 + Eric Dejouhanet + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + */ + +#include "test_ekos_simulator.h" + +#if defined(HAVE_INDI) + +#include "kstars_ui_tests.h" +#include "test_ekos.h" +#include "ekos/manager.h" +#include "kstars.h" +#include "ksmessagebox.h" + +TestEkosSimulator::TestEkosSimulator(QObject *parent) : QObject(parent) +{ + +} + +void TestEkosSimulator::initTestCase() +{ + KVERIFY_EKOS_IS_HIDDEN(); + KTRY_OPEN_EKOS(); + KVERIFY_EKOS_IS_OPENED(); + KTRY_EKOS_START_SIMULATORS(); + +} + +void TestEkosSimulator::cleanupTestCase() +{ + KTRY_EKOS_STOP_SIMULATORS(); + KTRY_CLOSE_EKOS(); + KVERIFY_EKOS_IS_HIDDEN(); +} + +void TestEkosSimulator::init() +{ + +} + +void TestEkosSimulator::cleanup() +{ + +} + + +void TestEkosSimulator::testMountSlew_data() +{ + QTest::addColumn("NAME"); + QTest::addColumn("RA"); + QTest::addColumn("DEC"); + + // Altitude computation taken from SchedulerJob::findAltitude + GeoLocation * const geo = KStarsData::Instance()->geo(); + KStarsDateTime const now(KStarsData::Instance()->lt()); + KSNumbers const numbers(now.djd()); + CachingDms const LST = geo->GSTtoLST(geo->LTtoUT(now).gst()); + + // Build a list of Messier objects, 5 degree over the horizon + for (int i = 1; i < 103; i++) + { + QString name = QString("M %1").arg(i); + SkyObject const * const so = KStars::Instance()->data()->objectNamed(name); + if (so != nullptr) + { + SkyObject o(*so); + o.updateCoordsNow(&numbers); + o.EquatorialToHorizontal(&LST, geo->lat()); + if (5.0 < so->alt().Degrees()) + QTest::addRow(name.toStdString().c_str()) + << name + << so->ra().toHMSString() + << so->dec().toDMSString(); + } + } +} + +void TestEkosSimulator::testMountSlew() +{ + Ekos::Manager * const ekos = Ekos::Manager::Instance(); + + QFETCH(QString, NAME); + QFETCH(QString, RA); + QFETCH(QString, DEC); + qDebug(QString("Test slewing to '%1' RA '%2' DEC '%3'").arg(NAME).arg(RA).arg(DEC).toStdString().c_str()); + +#if 0 + // In the mount tab, open the mount control + KTRY_EKOS_CLICK("mountToolBoxB"); + QTextObject * raInput = Ekos::Manager::Instance()->findChild("targetRATextObject"); + QVERIFY(raInput != nullptr); + raInput->setProperty("text", QVariant("07h 37m 30s")); + QTest::qWait(1000); + QTextObject * deInput = Ekos::Manager::Instance()->findChild("targetDETextObject"); + QVERIFY(deInput != nullptr); + deInput->setProperty("text", QVariant("-14° 31' 50")); + QTest::qWait(1000); + + // Too bad, not accessible... + QPushButton * gotoButton = Ekos::Manager::Instance()->findChild(""); + + QTest::qWait(5000); + KTRY_EKOS_CLICK("mountToolBoxB"); +#else + QVERIFY(ekos->mountModule()->abort()); + // Catch the unexpected "under horizon" and the "sun is close" dialogs + // This will not catch the altitude security interval + bool under_horizon_or_close_to_sun = false; + QTimer::singleShot(1000, [&] + { + QDialog * const dialog = qobject_cast (QApplication::activeModalWidget()); + if(dialog != nullptr) + { + under_horizon_or_close_to_sun = true; + emit dialog->reject(); + } + }); + bool const slew_result = ekos->mountModule()->slew(RA, DEC); + if (under_horizon_or_close_to_sun) + QEXPECT_FAIL(NAME.toStdString().c_str(), QString("Slew target '%1' is expected to be over the horizon during night time.").arg(NAME).toStdString().c_str(), Abort); + QVERIFY(slew_result); +#endif + + // DEC slews are precise at plus/minus one arcsecond - expected or not? + auto clampRA = [](QString v) { return CachingDms(v, false).arcsec(); }; + auto clampDE = [](QString v) { return CachingDms(v, true).arcsec(); }; + + QLineEdit * raOut = ekos->findChild("raOUT"); + QVERIFY(raOut != nullptr); + QTRY_VERIFY_WITH_TIMEOUT(abs(clampRA(RA) - clampRA(raOut->text())) < 2, 15000); + QTest::qWait(100); + if (clampRA(RA) != clampRA(raOut->text())) + QWARN(QString("Target '%1' slewed to with offset RA %2").arg(NAME).arg(RA).toStdString().c_str()); + + QLineEdit * deOut = Ekos::Manager::Instance()->findChild("decOUT"); + QVERIFY(deOut != nullptr); + QTRY_VERIFY_WITH_TIMEOUT(abs(clampDE(DEC) - clampDE(deOut->text())) < 2, 15000); + QTest::qWait(100); + if (clampRA(DEC) != clampRA(deOut->text())) + QWARN(QString("Target '%1' slewed to with coordinate offset DEC %2").arg(NAME).arg(DEC).toStdString().c_str()); + + QVERIFY(Ekos::Manager::Instance()->mountModule()->abort()); +} + +#endif diff --git a/Tests/kstars_ui/test_ekos_simulator.h b/Tests/kstars_ui/test_ekos_simulator.h new file mode 100644 index 000000000..bf1bdc016 --- /dev/null +++ b/Tests/kstars_ui/test_ekos_simulator.h @@ -0,0 +1,65 @@ +/* KStars UI tests + Copyright (C) 2020 + Eric Dejouhanet + + This application is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + */ + +#ifndef TESTEKOSSIMULATOR_H +#define TESTEKOSSIMULATOR_H + +#include "config-kstars.h" + +#ifdef HAVE_INDI + +#include +#include + +#define KTRY_EKOS_SELECT_PROFILE(profile) do { \ + QString const p(profile); \ + QComboBox* profileCBox = Ekos::Manager::Instance()->findChild("profileCombo"); \ + QVERIFY(profileCBox != nullptr); \ + profileCBox->setCurrentText(p); \ + QTRY_COMPARE(profileCBox->currentText(), p); } while(false) + +#define KTRY_EKOS_CLICK(button) do { \ + QPushButton * const b = Ekos::Manager::Instance()->findChild(button); \ + QVERIFY2(b != nullptr, QString("QPushButton '%1' does not exist and cannot be clicked").arg(button).toStdString().c_str()); \ + QVERIFY2(b->isEnabled(), QString("QPushButton '%1' is disabled and cannot be clicked").arg(button).toStdString().c_str()); \ + QTimer::singleShot(200, Ekos::Manager::Instance(), [&]() { QTest::mouseClick(b, Qt::LeftButton); }); } while(false) + +#define KTRY_EKOS_START_SIMULATORS() do { \ + KTRY_EKOS_SELECT_PROFILE("Simulators"); \ + KTRY_EKOS_CLICK("processINDIB"); \ + QWARN("HACK HACK HACK adding delay here for devices to connect"); \ + QTest::qWait(5000); } while(false) + +#define KTRY_EKOS_STOP_SIMULATORS() do { \ + KTRY_EKOS_CLICK("disconnectB"); \ + QWARN("Intentionally leaving a delay here for BZ398192"); \ + QTest::qWait(5000); \ + KTRY_EKOS_CLICK("processINDIB"); \ + QTest::qWait(1000); } while(false) + +class TestEkosSimulator : public QObject +{ + Q_OBJECT +public: + explicit TestEkosSimulator(QObject *parent = nullptr); + +private slots: + void initTestCase(); + void cleanupTestCase(); + + void init(); + void cleanup(); + + void testMountSlew_data(); + void testMountSlew(); +}; + +#endif +#endif // TESTEKOSSIMULATOR_H