forked from qt-creator/qt-creator
Implement scenario player
A scenario player may be used for testing Creator crashes which can't be easily tested with "-test <plugin>" option. Some crashes are triggered when Creator unloaded plugins and left the main function. This may happen due to some other threads may still be running. This scenario can't be tested using plugin tests, since when the test finishes, Creator still has its plugins loaded. Also it's not possible to quit Creator from inside the plugin test, as if we do it, we couldn't report the test result. The follow up patches will introduce the first test scenario and provide automatic test for testing against regression in StringTable. The scenario player may be potentially used for other purposes, including automatic presentation of features (yeah!). However, most probably the API should be further developed for other purposed. This is just a starting idea. Change-Id: I0f5c3c028f35a5cdf9130c2cf315dd4b68e81126 Reviewed-by: hjk <hjk@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
@@ -41,6 +41,7 @@ const char *OptionsParser::NO_LOAD_OPTION = "-noload";
|
||||
const char *OptionsParser::LOAD_OPTION = "-load";
|
||||
const char *OptionsParser::TEST_OPTION = "-test";
|
||||
const char *OptionsParser::NOTEST_OPTION = "-notest";
|
||||
const char *OptionsParser::SCENARIO_OPTION = "-scenario";
|
||||
const char *OptionsParser::PROFILE_OPTION = "-profile";
|
||||
const char *OptionsParser::NO_CRASHCHECK_OPTION = "-no-crashcheck";
|
||||
|
||||
@@ -85,6 +86,8 @@ bool OptionsParser::parse()
|
||||
#ifdef WITH_TESTS
|
||||
if (checkForTestOptions())
|
||||
continue;
|
||||
if (checkForScenarioOption())
|
||||
continue;
|
||||
#endif
|
||||
if (checkForAppOption())
|
||||
continue;
|
||||
@@ -167,6 +170,28 @@ bool OptionsParser::checkForTestOptions()
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OptionsParser::checkForScenarioOption()
|
||||
{
|
||||
if (m_currentArg == QLatin1String(SCENARIO_OPTION)) {
|
||||
if (nextToken(RequiredToken)) {
|
||||
if (!m_pmPrivate->m_requestedScenario.isEmpty()) {
|
||||
if (m_errorString) {
|
||||
*m_errorString = QCoreApplication::translate("PluginManager",
|
||||
"Can't request scenario \"%1\" as the scenario \"%1\" was already requested.")
|
||||
.arg(m_currentArg, m_pmPrivate->m_requestedScenario);
|
||||
}
|
||||
m_hasError = true;
|
||||
} else {
|
||||
// It's called before we register scenarios, so we don't check if the requested
|
||||
// scenario was already registered yet.
|
||||
m_pmPrivate->m_requestedScenario = m_currentArg;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool OptionsParser::checkForLoadOption()
|
||||
{
|
||||
if (m_currentArg != QLatin1String(LOAD_OPTION))
|
||||
|
@@ -48,6 +48,7 @@ public:
|
||||
static const char *LOAD_OPTION;
|
||||
static const char *TEST_OPTION;
|
||||
static const char *NOTEST_OPTION;
|
||||
static const char *SCENARIO_OPTION;
|
||||
static const char *PROFILE_OPTION;
|
||||
static const char *NO_CRASHCHECK_OPTION;
|
||||
|
||||
@@ -58,6 +59,7 @@ private:
|
||||
bool checkForLoadOption();
|
||||
bool checkForNoLoadOption();
|
||||
bool checkForTestOptions();
|
||||
bool checkForScenarioOption();
|
||||
bool checkForAppOption();
|
||||
bool checkForPluginOption();
|
||||
bool checkForProfilingOption();
|
||||
|
@@ -61,6 +61,7 @@
|
||||
#ifdef WITH_TESTS
|
||||
#include <utils/hostosinfo.h>
|
||||
#include <QTest>
|
||||
#include <QThread>
|
||||
#endif
|
||||
|
||||
#include <functional>
|
||||
@@ -748,6 +749,9 @@ void PluginManager::formatOptions(QTextStream &str, int optionIndentation, int d
|
||||
formatOption(str, QString::fromLatin1(OptionsParser::NOTEST_OPTION),
|
||||
QLatin1String("plugin"), QLatin1String("Exclude all of the plugin's tests from the test run"),
|
||||
optionIndentation, descriptionIndentation);
|
||||
formatOption(str, QString::fromLatin1(OptionsParser::SCENARIO_OPTION),
|
||||
QString("scenarioname"), QLatin1String("Run given scenario"),
|
||||
optionIndentation, descriptionIndentation);
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -787,6 +791,96 @@ bool PluginManager::testRunRequested()
|
||||
return !d->testSpecs.empty();
|
||||
}
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
// Called in plugin initialization, the scenario function will be called later, from main
|
||||
bool PluginManager::registerScenario(const QString &scenarioId, std::function<bool()> scenarioStarter)
|
||||
{
|
||||
if (d->m_scenarios.contains(scenarioId)) {
|
||||
const QString warning = QString("Can't register scenario \"%1\" as the other scenario was "
|
||||
"already registered with this name.").arg(scenarioId);
|
||||
qWarning("%s", qPrintable(warning));
|
||||
return false;
|
||||
}
|
||||
|
||||
d->m_scenarios.insert(scenarioId, scenarioStarter);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Called from main
|
||||
bool PluginManager::isScenarioRequested()
|
||||
{
|
||||
return !d->m_requestedScenario.isEmpty();
|
||||
}
|
||||
|
||||
// Called from main (may be squashed with the isScenarioRequested: runScenarioIfRequested).
|
||||
// Returns false if scenario couldn't run (e.g. no Qt version set)
|
||||
bool PluginManager::runScenario()
|
||||
{
|
||||
if (d->m_isScenarioRunning) {
|
||||
qWarning("Scenario is already running. Can't run scenario recursively.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (d->m_requestedScenario.isEmpty()) {
|
||||
qWarning("Can't run any scenario since no scenario was requested.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!d->m_scenarios.contains(d->m_requestedScenario)) {
|
||||
const QString warning = QString("Requested scenario \"%1\" was not registered.").arg(d->m_requestedScenario);
|
||||
qWarning("%s", qPrintable(warning));
|
||||
return false;
|
||||
}
|
||||
|
||||
d->m_isScenarioRunning = true;
|
||||
// The return value comes now from scenarioStarted() function. It may fail e.g. when
|
||||
// no Qt version is set. Initializing the scenario may take some time, that's why
|
||||
// waitForScenarioFullyInitialized() was added.
|
||||
bool ret = d->m_scenarios[d->m_requestedScenario]();
|
||||
|
||||
QMutexLocker locker(&d->m_scenarioMutex);
|
||||
d->m_scenarioFullyInitialized = true;
|
||||
d->m_scenarioWaitCondition.wakeAll();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Called from scenario point (and also from runScenario - don't run scenarios recursively).
|
||||
// This may be called from non-main thread. We assume that m_requestedScenario
|
||||
// may only be changed from the main thread.
|
||||
bool PluginManager::isScenarioRunning(const QString &scenarioId)
|
||||
{
|
||||
return d->m_isScenarioRunning && d->m_requestedScenario == scenarioId;
|
||||
}
|
||||
|
||||
// This may be called from non-main thread.
|
||||
bool PluginManager::finishScenario()
|
||||
{
|
||||
if (!d->m_isScenarioRunning)
|
||||
return false; // Can't finish not running scenario
|
||||
|
||||
if (d->m_isScenarioFinished.exchange(true))
|
||||
return false; // Finish was already called before. We return false, as we didn't finish it right now.
|
||||
|
||||
QMetaObject::invokeMethod(d, []() { emit m_instance->scenarioFinished(0); });
|
||||
return true; // Finished successfully.
|
||||
}
|
||||
|
||||
// Waits until the running scenario is fully initialized
|
||||
void PluginManager::waitForScenarioFullyInitialized()
|
||||
{
|
||||
if (QThread::currentThread() == qApp->thread()) {
|
||||
qWarning("The waitForScenarioFullyInitialized() function can't be called from main thread.");
|
||||
return;
|
||||
}
|
||||
QMutexLocker locker(&d->m_scenarioMutex);
|
||||
if (d->m_scenarioFullyInitialized)
|
||||
return;
|
||||
|
||||
d->m_scenarioWaitCondition.wait(&d->m_scenarioMutex);
|
||||
}
|
||||
#endif
|
||||
|
||||
/*!
|
||||
\internal
|
||||
*/
|
||||
@@ -867,6 +961,14 @@ void PluginManagerPrivate::nextDelayedInitialize()
|
||||
#ifdef WITH_TESTS
|
||||
if (PluginManager::testRunRequested())
|
||||
startTests();
|
||||
else if (PluginManager::isScenarioRequested()) {
|
||||
if (PluginManager::runScenario()) {
|
||||
const QString info = QString("Successfully started scenario \"%1\"...").arg(d->m_requestedScenario);
|
||||
qInfo("%s", qPrintable(info));
|
||||
} else {
|
||||
QMetaObject::invokeMethod(this, []() { emit m_instance->scenarioFinished(1); });
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
delayedInitializeTimer->start();
|
||||
|
@@ -121,6 +121,18 @@ public:
|
||||
|
||||
static bool testRunRequested();
|
||||
|
||||
#ifdef WITH_TESTS
|
||||
static bool registerScenario(const QString &scenarioId, std::function<bool()> scenarioStarter);
|
||||
static bool isScenarioRequested();
|
||||
static bool runScenario();
|
||||
static bool isScenarioRunning(const QString &scenarioId);
|
||||
// static void triggerScenarioPoint(const QVariant pointData); // ?? called from scenario point
|
||||
static bool finishScenario();
|
||||
static void waitForScenarioFullyInitialized();
|
||||
// signals:
|
||||
// void scenarioPointTriggered(const QVariant pointData); // ?? e.g. in StringTable::GC() -> post a call to quit into main thread and sleep for 5 seconds in the GC thread
|
||||
#endif
|
||||
|
||||
static void profilingReport(const char *what, const PluginSpec *spec = nullptr);
|
||||
|
||||
static QString platformName();
|
||||
@@ -139,6 +151,7 @@ signals:
|
||||
void pluginsChanged();
|
||||
void initializationDone();
|
||||
void testsFinished(int failedTests);
|
||||
void scenarioFinished(int exitCode);
|
||||
|
||||
friend class Internal::PluginManagerPrivate;
|
||||
};
|
||||
|
@@ -30,11 +30,13 @@
|
||||
#include <utils/algorithm.h>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QMutex>
|
||||
#include <QObject>
|
||||
#include <QReadWriteLock>
|
||||
#include <QScopedPointer>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QWaitCondition>
|
||||
|
||||
#include <queue>
|
||||
|
||||
@@ -143,6 +145,14 @@ public:
|
||||
bool m_isInitializationDone = false;
|
||||
bool enableCrashCheck = true;
|
||||
|
||||
QHash<QString, std::function<bool()>> m_scenarios;
|
||||
QString m_requestedScenario;
|
||||
std::atomic_bool m_isScenarioRunning = false; // if it's running, the running one is m_requestedScenario
|
||||
std::atomic_bool m_isScenarioFinished = false; // if it's running, the running one is m_requestedScenario
|
||||
bool m_scenarioFullyInitialized = false;
|
||||
QMutex m_scenarioMutex;
|
||||
QWaitCondition m_scenarioWaitCondition;
|
||||
|
||||
private:
|
||||
PluginManager *q;
|
||||
|
||||
|
@@ -210,6 +210,10 @@ ICore::ICore(MainWindow *mainwindow)
|
||||
qWarning("Test run was not successful: %d test(s) failed.", failedTests);
|
||||
QCoreApplication::exit(failedTests);
|
||||
});
|
||||
connect(PluginManager::instance(), &PluginManager::scenarioFinished, [this] (int exitCode) {
|
||||
emit coreAboutToClose();
|
||||
QCoreApplication::exit(exitCode);
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
|
Reference in New Issue
Block a user