/*****************************************************************************
 * $CAMITK_LICENCE_BEGIN$
 *
 * CamiTK - Computer Assisted Medical Intervention ToolKit
 * (c) 2001-2025 Univ. Grenoble Alpes, CNRS, Grenoble INP - UGA, TIMC, 38000 Grenoble, France
 *
 * Visit http://camitk.imag.fr for more information
 *
 * This file is part of CamiTK.
 *
 * CamiTK is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License version 3
 * only, as published by the Free Software Foundation.
 *
 * CamiTK is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License version 3 for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * version 3 along with CamiTK.  If not, see <http://www.gnu.org/licenses/>.
 *
 * $CAMITK_LICENCE_END$
 ****************************************************************************/

#include <QtTest>

#include <HotPlugActionExtension.h>
#include <HotPlugAction.h>
#include <HotPlugExtensionManager.h>

#include <Application.h>
#include <ExtensionManager.h>
#include <MainWindow.h>
#include <Application.h>
#include <Component.h>
#include <Action.h>
#include <Log.h>

#include <TransformEngine.h>
#include <CamiTKExtensionModel.h>

class TestPythonHotPlug : public QObject {
    Q_OBJECT
public:

    void setArg(int argc, char* argv[]) {
        this->argc = argc;
        this->argv = argv;
    }

private:
    // needs to be static otherwise memory is not kept between tests (needs inline to declare it here as well)
    static inline camitk::Application* camitkApp; // single application used for all the tests
    static inline int argc;
    static inline char** argv;

    QDir tempDir;
    bool allTestPassed;
    QDir currentTestTempDir;
    QString resourceSubdirectory; // resources subdirectory (where the test files for this qtest app are stored)

    /// write a file in currentTestTempDir using the given string
    bool writeScript(QFile& scriptFile, QString sourceCode) {
        // WriteOnly → overwrite automatically
        if (!scriptFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
            qWarning() << "Could not open file for writing:" << scriptFile.errorString();
            return false;
        }
        QTextStream out(&scriptFile);
        out << sourceCode;
        scriptFile.close();
        return true;
    }

    void testUserScript(QString extensionFileBasename, QString sourceCode, bool initShouldBeSuccessful, bool processShouldBeSuccessful = false) {
        QString extensionFilename = extensionFileBasename + ".camitk";
        QString scriptFilename = "test_action.py";

        QString camitkFileFullPath = currentTestTempDir.filePath(extensionFilename);
        QString scriptfilePath = currentTestTempDir.filePath(scriptFilename);
        QFile scriptFile(scriptfilePath);

        // create script if
        if (!sourceCode.isNull()) {
            replacePySideVersionInString(sourceCode);
            QVERIFY(writeScript(scriptFile, sourceCode));
        }

        camitk::HotPlugActionExtension* actionExtension = nullptr;

        actionExtension = camitk::HotPlugExtensionManager::load(camitkFileFullPath);

        QCOMPARE(actionExtension != nullptr, initShouldBeSuccessful);
        if (actionExtension != nullptr) {
            QCOMPARE(actionExtension->getActions().size(), 1);
            camitk::HotPlugAction* hotPlugAction = dynamic_cast<camitk::HotPlugAction*>(actionExtension->getActions().at(0));
            QVERIFY(hotPlugAction != nullptr);
            QVERIFY(hotPlugAction->update());
            QCOMPARE(hotPlugAction->apply() == camitk::Action::SUCCESS, processShouldBeSuccessful);

            //-- unload (this will delete actionExtension)
            QVERIFY(camitk::HotPlugExtensionManager::unload(camitkFileFullPath));
        }

        // check the final state
        QCOMPARE(camitk::Application::getActions().size(), 0);
        QCOMPARE(camitk::HotPlugExtensionManager::getLoadedExtensions().size(), 0);

        // should return false as ExtensionManager does not manage any .camitk extension
        QCOMPARE(camitk::ExtensionManager::unloadActionExtension(camitkFileFullPath), false);
        QCOMPARE(camitk::ExtensionManager::getActionExtensionsList().size(), 0);

        if (!sourceCode.isNull()) {
            QVERIFY(scriptFile.remove());
        }
    }

    /// replace PySideX in String depending on the current Qt Version
    // FIXME Remove this when all supported OS are using Qt6
    void replacePySideVersionInString(QString& sourceString) {
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
        const QString pysideVersion = "PySide6";
#else
        const QString pysideVersion = "PySide2";
#endif
        sourceString.replace("PySideX", pysideVersion);
    }

    /// replace PySideX in String depending on the current Qt Version
    // FIXME Remove this when all supported OS are using Qt6
    void replacePySideVersionInFile(QString sourceFilePath) {
        QFile file(sourceFilePath);
        QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), "Failed to open file for reading");
        QString content = QTextStream(&file).readAll();
        file.close();

        int numberOfPySideRef = content.count("PySideX");
        if (numberOfPySideRef > 0) {
            replacePySideVersionInString(content);

            // ensure writability
            QVERIFY(file.setPermissions(file.permissions() | QFile::WriteUser | QFile::WriteOwner));
            QVERIFY(QFileInfo(file).isWritable());

            if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) {
                QFAIL(qPrintable(QString("open WriteOnly file %1 (to replace %2 PySideX occurence) failed: %3; error=%4; perms=%5")
                                 .arg(sourceFilePath)
                                 .arg(numberOfPySideRef)
                                 .arg(file.errorString())
                                 .arg(int(file.error()))
                                 .arg(int(QFileInfo(sourceFilePath).permissions()))));
            }

            QTextStream(&file) << content;
            file.close();

            // Verify the replacement worked
            QVERIFY2(file.open(QIODevice::ReadOnly | QIODevice::Text), "Failed to open file for verification");
            content = QTextStream(&file).readAll();
            file.close();

#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
            const QString pysideVersion = "PySide6";
#else
            const QString pysideVersion = "PySide2";
#endif
            QCOMPARE(content.count(pysideVersion), numberOfPySideRef);
        }
        QVERIFY(!content.contains("PySideX"));
    }

    /// copy the .camitk extension file to the current test temp directory
    void copyExtensionFile(QString resourcePrefix) {
        QString extensionFilePath = ":/" + resourcePrefix + "/extensionfile";
        QFile extensionFile(extensionFilePath);
        QVERIFY(extensionFile.exists());
        // copy the extension file to the current test directory
        QString camitkFileFullPath = currentTestTempDir.filePath(resourcePrefix + ".camitk");
        CAMITK_INFO_ALT("Copying '" + extensionFilePath + "' to '" + camitkFileFullPath + "'")
        QVERIFY(extensionFile.copy(camitkFileFullPath));
        replacePySideVersionInFile(camitkFileFullPath);
        QCOMPARE(currentTestTempDir.count() - 2, 1); // n-2 files counting for . and .. compare to 1 extension file
    }

    /// setup a directory with all the files declared under the given resource prefix.
    /// @param resourcePrefix is a prefix defined in the .qrc file
    /// @param durationBetweenActionApplyCalls if not 0, wait for the given duration in ms between two action->apply() call
    ///        (this is useful if the script action has a timer, so that it leaves the timer enough time to starts and ticks
    ///         e.g. for the timer state test. It uses QTest::qWait(), to wait for durationBetweenActionApplyCalls msecs.
    ///         While waiting, events will be processed and your test will stay responsive to user interface events or
    ///         network communication.)
    void testPythonHotPlug(QString resourcePrefix, int durationBetweenActionApplyCalls = 0, int initialActionCount = 0, int initialActionExtensionCount = 0) {
        copyExtensionFile(resourcePrefix);
        QString camitkFileFullPath = currentTestTempDir.filePath(resourcePrefix + ".camitk");

        // copy all python action files to the current test directory
        QString fullPrefix = ":/" + resourcePrefix + resourceSubdirectory + "/";
        QStringList pythonFiles = QDir(fullPrefix).entryList();
        QVERIFY(pythonFiles.size() >= 1); // at least one .py action script
        CAMITK_INFO_ALT("Found " + QString::number(pythonFiles.size()) + " action files:\n- " + pythonFiles.join("\n- "))

        for (QString pythonFile : pythonFiles) {
            QString pythonFilePath = currentTestTempDir.filePath(pythonFile);
            CAMITK_INFO_ALT("Copying '" + fullPrefix + pythonFile + "' to '" + pythonFilePath + "'")
            QVERIFY(QFile::copy(fullPrefix + pythonFile, pythonFilePath));
            replacePySideVersionInFile(pythonFilePath);
        }

        currentTestTempDir.refresh(); // WARNING: this is REALLY required otherwise the count is not updated
        QCOMPARE(currentTestTempDir.count() - 2, pythonFiles.size() + 1); // n-2 files counting for . and .. compare to all python files + extension file

        // check the initial state
        QCOMPARE(camitk::HotPlugExtensionManager::getLoadedExtensions().size(), 0);
        QCOMPARE(camitk::Application::getActions().size(), initialActionCount);

        //-- load (create the instances and check requirements)
        // instantiate all actions and call init
        camitk::HotPlugActionExtension* actionExtension = nullptr;
        actionExtension = camitk::HotPlugExtensionManager::load(camitkFileFullPath);
        QVERIFY(actionExtension != nullptr);

        QVERIFY(actionExtension->getName() != "");
        QVERIFY(actionExtension->getDescription() != "");
        QCOMPARE(actionExtension->getLocation(), camitkFileFullPath);

        // test without the application
        QVERIFY(actionExtension->getActions().size() > 0);
        for (auto a : actionExtension->getActions()) {
            camitk::HotPlugAction* hotPlugAction = dynamic_cast<camitk::HotPlugAction*>(a);
            QVERIFY(hotPlugAction != nullptr);
            QVERIFY(hotPlugAction->update());
        }

        // compare instantiated actions in the action extension with the expected values from the data model
        TransformEngine transformEngine;
        CamiTKExtensionModel camitkExtensionModel(camitkFileFullPath);
        VariantDataModel& dataModel = camitkExtensionModel.getModel();
        QString extensionName = transformEngine.transformToString("$title(name)$", QJsonObject::fromVariantMap(dataModel.getValue().toMap()));
        QCOMPARE(actionExtension->getName(), extensionName);
        QCOMPARE(actionExtension->getDescription(), dataModel["description"].toString());
        QCOMPARE(actionExtension->getActions().size(), dataModel["actions"].size());
        for (int i = 0; i < dataModel["actions"].size(); i++) {
            VariantDataModel& actionDataModel = dataModel["actions"][i];
            QString actionName = transformEngine.transformToString("$title(name)$", QJsonObject::fromVariantMap(actionDataModel.getValue().toMap()));
            camitk::Action* a = actionExtension->getActions().at(i);
            QVERIFY(a != nullptr);
            QCOMPARE(a->getDescription(), actionDataModel["description"].toString());
            QCOMPARE(a->getComponentClassName(), actionDataModel["componentClass"].toString());
            QCOMPARE(a->getFamily(), actionDataModel["classification"]["family"].toString());
            QVERIFY(a->getWidget() != nullptr);
            // the python action should have at least opened one component of the proper type for a
            // use it to set the current action targets (if the action requires one)
            // there should be at least one of the required type
            if (!a->getComponentClassName().isEmpty()) {
                camitk::ComponentList allTopLevels = camitk::Application::getTopLevelComponents();
                QVERIFY(allTopLevels.size() > 0);
                camitk::ComponentList potentialTargets;
                for (camitk::Component* c : allTopLevels) {
                    if (c->getHierarchy().contains(a->getComponentClassName())) {
                        potentialTargets.append(c);
                    }
                }
                a->setInputComponents(potentialTargets);
                QVERIFY(a->getTargets().size() > 0);
            }
            // run the action
            QCOMPARE(a->apply(), camitk::Action::SUCCESS);

            if (durationBetweenActionApplyCalls > 0) {
                // Simulate time: wait the given amount of ms and process events (if the action has timers)
                QTest::qWait(durationBetweenActionApplyCalls); // waits and runs event loop
            }
        }

        // compare instantiated actions registered in the Application with the expected values from the data model
        QCOMPARE(camitk::Application::getActions().size(), initialActionCount + dataModel["actions"].size());
        for (int i = 0; i < dataModel["actions"].size(); i++) {
            VariantDataModel& actionDataModel = dataModel["actions"][i];
            QString actionName = transformEngine.transformToString("$title(name)$", QJsonObject::fromVariantMap(actionDataModel.getValue().toMap()));
            camitk::Action* a = camitk::Application::getAction(actionName);
            QVERIFY(a != nullptr);
            QCOMPARE(a->getDescription(), actionDataModel["description"].toString());
            QCOMPARE(a->getComponentClassName(), actionDataModel["componentClass"].toString());
            QCOMPARE(a->getFamily(), actionDataModel["classification"]["family"].toString());
            // run the action again
            QCOMPARE(a->apply(), camitk::Action::SUCCESS);
        }

        //-- unload (this will delete actionExtension)
        QVERIFY(camitk::HotPlugExtensionManager::unload(camitkFileFullPath));

        // check the final state
        QCOMPARE(camitk::Application::getActions().size(), initialActionCount);

        QCOMPARE(camitk::HotPlugExtensionManager::getLoadedExtensions().size(), 0);

        // should return false as ExtensionManager does not manage any .camitk extension
        QCOMPARE(camitk::ExtensionManager::unloadActionExtension(camitkFileFullPath), false);
        QCOMPARE(camitk::ExtensionManager::getActionExtensionsList().size(), initialActionExtensionCount);
    }

private slots:

    // called once before any tests are run.
    void initTestCase() {
        // the resource subdirectory corresponding to this this qtest app (see also corresponding .qrc)
        resourceSubdirectory = "/pythonhotplug";
        allTestPassed = false;

        // Ensure all log messages are visible in the standard output
        camitk::Log::getLogger()->setLogLevel(camitk::InterfaceLogger::TRACE);
        camitk::Log::getLogger()->setMessageBoxLevel(camitk::InterfaceLogger::NONE);
        // no time stamp for reproducible log diff
        camitk::Log::getLogger()->setTimeStampInformation(false);

        // Create a new default application+main window, without loading any extension, no console redirection
        camitkApp = new camitk::Application("TestPythonHotPlug", argc, argv, false, false);
        camitk::MainWindow* defaultMainWindow = camitkApp->getMainWindow();
        defaultMainWindow->redirectToConsole(false);

        // now load only the component and viewer extensions (no action)
        camitk::ExtensionManager::autoload(camitk::ExtensionManager::COMPONENT);
        camitk::ExtensionManager::autoload(camitk::ExtensionManager::VIEWER);

        defaultMainWindow->show();
        QVERIFY(QTest::qWaitForWindowExposed(defaultMainWindow));

        // minimize window to avoid disturbance when the test is done by a dev, not on CI
        defaultMainWindow->setWindowState(Qt::WindowMinimized);

        // Simulate time: wait 500ms and process events (if the action has timers)
        QTest::qWait(500); // waits and runs event loop

        // create temporary location for all the tests
        QString tempPath = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/CamiTKExtensionCheck_" + QDateTime::currentDateTime().toString("yyyyMMddHHmmss");
        QVERIFY(QDir().mkpath(tempPath));
        tempDir.setPath(tempPath);

        // init global test status
        allTestPassed = true;
    }

    // called once after all tests have been run.
    void cleanupTestCase() {
        if (allTestPassed) {
            qDebug().noquote().nospace() << "Removing test application temp directory recursively: " << tempDir.absolutePath();
            tempDir.removeRecursively();
        }
        else {
            qDebug().noquote().nospace() << "Temporary test application directory not removed: " << tempDir.path();
        }
    }

    // called before each test function
    void init() {
        // Create temporary dir just for the current test using the current test name
        QString tempPath = tempDir.filePath(QTest::currentTestFunction());
        QDir dir;
        QVERIFY(dir.mkpath(tempPath));

        QFileInfo dirInfo(tempPath);
        QVERIFY(dirInfo.isDir());
        QVERIFY(dirInfo.isWritable());

        // QFile tempDir = QFile(tempPath);
        // QFile::Permissions originalPermissions = tempDir.permissions();
        // QFile::Permissions writablePermissions = originalPermissions | QFile::WriteOwner;
        // QVERIFY2(QFile::setPermissions(tempPath, writablePermissions),
        //          qPrintable(QString("Could not grant write permission to temp directory: %1").arg(tempPath)));
        
        // QVERIFY2(tempDir.exists() && tempDir.isWritable(), "Failed to make directory writable");

        currentTestTempDir.setPath(tempPath);
    }

    // called after each test function.
    void cleanup() {
        if (!QTest::currentTestFailed()) {
            qDebug().noquote().nospace() << "Removing current test temp directory recursively: " << currentTestTempDir.absolutePath();
            currentTestTempDir.removeRecursively();
        }
        else {
            allTestPassed = false;
            qDebug().noquote().nospace() << "Temporary directory not removed: " << currentTestTempDir.path();
        }
    }

    void image() {
        testPythonHotPlug("image");
    }

    void mesh() {
        testPythonHotPlug("mesh");
    }

    void state() {
        testPythonHotPlug("state", 2000);
        // Note: 500ms (default in the actions that uses timers)
        // are not enough to make sure a QTimer inside python has time to tick at least once
        // Benchmark using:
        // for i in {1..50}; do echo "======== $i in $(pwd) ========"; ctest -VVV -R pythonhotplug-state >> /tmp/ctest-log-500ms; done
        // shows that when durationBetweenActionApplyCalls is set to 500ms, under CPU high loads, about 20%
        // of the time the timer has not started ticking inside the 500ms
        // → use 2000ms even if this means slower test time to ensure at least one tick is performed
    }

    void qt() {
        // test qt application is visible from python
        copyExtensionFile("scriptQtTest");

        CAMITK_INFO(QString("Qt version (C++): %1").arg(qVersion()));
        CAMITK_INFO("Prefix (C++): " + QLibraryInfo::location(QLibraryInfo::PrefixPath));
        CAMITK_INFO("Plugins (C++): " + QLibraryInfo::location(QLibraryInfo::PluginsPath));

        // Check that Python is seeing the QApplication created by C++"
        testUserScript("scriptQtTest", R"python(import camitk
def init(self:camitk.Action):
    from PySideX.QtCore import QLibraryInfo, qVersion
    print("Qt version (Python):", qVersion())
    print("Prefix (Python):", QLibraryInfo.location(QLibraryInfo.PrefixPath))
    print("Plugins (Python):", QLibraryInfo.location(QLibraryInfo.PluginsPath))

    from PySideX.QtWidgets import QApplication
    print(QApplication.instance())
    return QApplication.instance() is not None
)python", true);
    }

    void userScriptValidity() {
        // A script is valid if
        // - it python file exists
        // - it does have no syntax error
        // - it does not have any other python error (NameError etc...)
        // - the process() method exists and is callable
        // - if init() or process() returns a value that can be cast to a boolean, 
        //   this boolean must be true
        copyExtensionFile("scriptTest");        

        CAMITK_INFO_ALT("=== Test 1 === Should generate a CamiTK warning: `Python script '.../userScript/test_action.py' not found")
        testUserScript("scriptTest", QString(), false);

        CAMITK_INFO_ALT("=== Test 2 === Should generate a python interpreter exception `SyntaxError: invalid syntax (test_action.py, line 3)`")
        testUserScript("scriptTest", R"python(import camitk
def init(self:camitk.Action):
    syntax_error:
)python", false);

        CAMITK_INFO_ALT("=== Test 3 === Should generate a python interpreter exception `NameError: name 'false' is not defined`")
        testUserScript("scriptTest", R"python(import camitk
def init(self:camitk.Action):
    return false
)python", false);

        CAMITK_INFO_ALT("=== Test 4 === Should generate a python interpreter exception `NameError: name 'x' is not defined`")
        testUserScript("scriptTest", R"python(import camitk
def init(self:camitk.Action):
    print(f"{x}")
)python", false);

        CAMITK_INFO_ALT("=== Test 5 === Should generate no interpreter exception, but the method returns bad status and generates a CamiTK warning `Error during 'init()'`")
        // When the python script runs into an error, the python code can report it by returning False
        // A False returned value means there was an error, and the application should report it as a warning.
        testUserScript("scriptTest", R"python(import camitk
def init(self:camitk.Action):
    return False
        )python", false);

        CAMITK_INFO_ALT("=== Test 6 === Initialization ok: No init() method → no error; but generates a `missing function 'process(self)' error` when apply() is called")
        testUserScript("scriptTest", R"python(import camitk
)python", true, false);

        CAMITK_INFO_ALT("=== Test 7 === Initialization ok: no value return, no check of the boolean status; but generates a `missing function 'process(self)' error` when apply() is called")
        testUserScript("scriptTest", R"python(import camitk
def init(self:camitk.Action):
    pass
        )python", true, false);

        CAMITK_INFO_ALT("=== Test 8 === Initialization ok: there is a proper bool return value and it is True; but generates a `missing function 'process(self)' error` when apply() is called")
        testUserScript("scriptTest", R"python(import camitk
def init(self:camitk.Action):
    return True
        )python", true, false);

        CAMITK_INFO_ALT("=== Test 9 === Initialization ok: no method; process ok: no value return, no check of the boolean status")
        testUserScript("scriptTest", R"python(import camitk
def process(self:camitk.Action):
    pass
        )python", true, true);

        CAMITK_INFO_ALT("=== Test 11 === Initialization ok: no method; but generates a CamiTK error `Error during call to method process() of action 'Test Action'` when apply() is called as the method returns False")
        testUserScript("scriptTest", R"python(import camitk
def process(self:camitk.Action):
    return False
        )python", true, false);

        CAMITK_INFO_ALT("=== Test 12 === Initialization ok: no method; process ok: there is a proper bool return value and it is True")
        testUserScript("scriptTest", R"python(import camitk
def process(self:camitk.Action):
    return True
        )python", true, true);

        CAMITK_INFO_ALT("=== Test 13 === Initialization ok: no method; but generates a CamiTK warning `Python script '.../userScriptValidity/test_action.py' of action 'Test Action': 'process' is not a function` as process is a symbol but not callable.")
        testUserScript("scriptTest", R"python(import camitk
process = True
        )python", true, false);
    }

    void pipelineAndTransformation() {
        testPythonHotPlug("pipelineAndTransformation");
    }

    void meshPoints() {
        // use 2s of waiting before moving to the next action, to make sure the timer
        // as at least one tick
        testPythonHotPlug("meshPoints", 2000);
    }

    void transformationManager() {
        testPythonHotPlug("transformationManager");
    }

    void actionPipeline() {
        // the pipeline action requires three actions:
        // - "Threshold (VTK)"
        // - "Reconstruction"
        // - "Mesh Projection"
        // → use autoload instead of looking for the proper extension dll/so/dynlib to load
        camitk::ExtensionManager::autoload(camitk::ExtensionManager::ACTION);
        // the initialActionCount and initialActionExtensionCount must be provided as they are not the default 0
        testPythonHotPlug("actionPipeline", 0, camitk::Application::getActions().size(), camitk::ExtensionManager::getActionExtensionsList().size());
    }
    // NOTE: DO NOT FORGET TO ADD ANY NEW TEST IN THE MAIN (see below)
};

int main(int argc, char* argv[]) {
    // Hook to satisfy the CI QtTestDiscoveryTest cmake macro
    //
    // Analysis:
    // As TestPythonHotplug needs to instantiate a CamiTK Application (which inherits QApplication)
    // it cannot use the QTEST_MAIN(TestPythonHotPlug) macro (as it creates a QApplication and there
    // only can be one, qapp is a singleton).
    //
    // Workaround:
    // This main() simulates the QTEST_MAIN during CMake configure test discovery.
    // This main() returns the lists of tests as expected from a classic QTEST_MAIN if the argument is
    // -datatags  (as it is done by QtTestDiscoveryTest during CMake Configuration).
    //
    // Consequence:
    // -> Each new test has to be added manually to be listed by CMake

    if (QString(argv[1]) == "-datatags") {
        std::cout << "TestPythonHotPlug image" << std::endl;
        std::cout << "TestPythonHotPlug mesh" << std::endl;
        std::cout << "TestPythonHotPlug state" << std::endl;
        std::cout << "TestPythonHotPlug qt" << std::endl;
        std::cout << "TestPythonHotPlug userScriptValidity" << std::endl;
        std::cout << "TestPythonHotPlug meshPoints" << std::endl;
        std::cout << "TestPythonHotPlug transformationManager" << std::endl;
        std::cout << "TestPythonHotPlug actionPipeline" << std::endl;
        return 0;
    }
    else {
        // Real tests when launched
        TestPythonHotPlug testPythonApp;
        testPythonApp.setArg(argc, argv);

        return QTest::qExec(&testPythonApp, argc, argv);
    }
}

#include "TestPythonHotPlug.moc"