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:
Jarek Kobus
2021-04-28 15:59:18 +02:00
parent ff0301635e
commit 0f535703aa
6 changed files with 156 additions and 0 deletions

View File

@@ -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))

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;
};

View File

@@ -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;

View File

@@ -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);
});
}
/*!