diff --git a/src/app/DashboardApplication.cpp b/src/app/DashboardApplication.cpp index 5d36586..2efc05f 100644 --- a/src/app/DashboardApplication.cpp +++ b/src/app/DashboardApplication.cpp @@ -128,6 +128,8 @@ struct DashboardApplication::Options { int maxFps = 60; QString appKey; // --app-key, empty: not given QString appName; // --app-name, empty: not given + // Kept for the app's own options (isOptionSet() / optionValue()). + QCommandLineParser parser; }; const char* dashboardModeName(DashboardMode mode) { @@ -165,7 +167,8 @@ void DashboardApplication::setDefaultQmlModule(const char* uri) { defaultQmlModule() = QString::fromUtf8(uri); } -DashboardApplication::DashboardApplication(int& argc, char** argv) +DashboardApplication::DashboardApplication( + int& argc, char** argv, const QList& appOptions) : m_options(std::make_unique()), m_qmlModule(defaultQmlModule()) { // When launched by SteamVR, stderr is not a TTY and Qt would log to // journald only; keep messages on stderr so they show up wherever the @@ -189,7 +192,7 @@ DashboardApplication::DashboardApplication(int& argc, char** argv) QCoreApplication::setApplicationVersion( QString::fromLatin1(AppConfig::kVersion)); - QCommandLineParser parser; + QCommandLineParser& parser = o.parser; parser.setApplicationDescription(QStringLiteral("SteamVR dashboard overlay")); parser.addHelpOption(); parser.addVersionOption(); @@ -247,6 +250,11 @@ DashboardApplication::DashboardApplication(int& argc, char** argv) parser.addOptions({desktopOption, snapshotOption, benchmarkOption, apiOption, keyOption, nameOption, fpsOption, installOption, uninstallOption, manifestOption, launchOption, logOption}); + for (const QCommandLineOption& option : appOptions) { + if (!parser.addOption(option)) + o.error = QStringLiteral("App option --%1 clashes with another option") + .arg(option.names().constFirst()); + } parser.process(*m_app); // exits for --help / --version if (parser.isSet(logOption)) { @@ -319,6 +327,8 @@ void DashboardApplication::setQuickStyle(const QString& style) { m_quickStyle = style; } +bool DashboardApplication::prepare(QString*) { return true; } + void DashboardApplication::loading() {} void DashboardApplication::started() {} @@ -329,6 +339,14 @@ QList DashboardApplication::surfaces() const { return result; } +bool DashboardApplication::isOptionSet(const QString& name) const { + return m_options->parser.isSet(name); +} + +QString DashboardApplication::optionValue(const QString& name) const { + return m_options->parser.value(name); +} + Surface* DashboardApplication::surface() const { return m_surfaces.empty() ? nullptr : m_surfaces.front().get(); } @@ -383,6 +401,7 @@ int DashboardApplication::exec() { ? m_configDisplayName : QString::fromUtf8(AppConfig::kDisplayName); QCoreApplication::setApplicationName(m_appName); + if (!prepare(&error)) return fail(error); if (!m_quickStyle.isEmpty()) QQuickStyle::setStyle(m_quickStyle); m_engine = std::make_unique(); diff --git a/src/app/DashboardApplication.h b/src/app/DashboardApplication.h index b9c33af..72508dd 100644 --- a/src/app/DashboardApplication.h +++ b/src/app/DashboardApplication.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -53,7 +54,11 @@ class DashboardApplication : public QObject { Q_OBJECT public: // Creates the Qt application object and parses the command line. - DashboardApplication(int& argc, char** argv); + // `appOptions` are the app's own command-line options: parsed and listed + // in --help together with the built-in ones (names must not clash); read + // them with isOptionSet() / optionValue(). + DashboardApplication(int& argc, char** argv, + const QList& appOptions = {}); ~DashboardApplication() override; // Runs the mode selected on the command line; returns the exit code. @@ -81,6 +86,11 @@ class DashboardApplication : public QObject { void setQuickStyle(const QString& style); // --- Lifecycle hooks --- + // First hook, once mode and app key are known: before the QML engine, the + // surfaces and the SteamVR connection exist (not called for manifest + // commands). Return false and set *error to abort: exec() then logs the + // error and returns 1. + virtual bool prepare(QString* error); // Before the QML scenes are created: the shared engine, the surfaces and // (except in desktop preview) their render backends exist. virtual void loading(); @@ -91,6 +101,9 @@ class DashboardApplication : public QObject { DashboardMode mode() const { return m_mode; } QString appKey() const { return m_appKey; } QString appName() const { return m_appName; } + // The app's own command-line options (see the constructor), by name. + bool isOptionSet(const QString& name) const; + QString optionValue(const QString& name) const; // Shared by all surfaces. QQmlEngine* engine() const { return m_engine.get(); } // All views, in addSurface() order; the primary (first) one; one by id. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 713e060..61207a2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,5 +31,7 @@ add_test(NAME surfaces_opengl COMMAND tst_surfaces --graphics-api opengl) add_test(NAME surfaces_vulkan COMMAND tst_surfaces --graphics-api vulkan) add_test(NAME surfaces_cli_key COMMAND tst_surfaces --graphics-api opengl) set_tests_properties(surfaces_cli_key PROPERTIES ENVIRONMENT "VRDASH_TEST_CLI_KEY=1") +add_test(NAME surfaces_prepare_fail COMMAND tst_surfaces --graphics-api opengl) +set_tests_properties(surfaces_prepare_fail PROPERTIES ENVIRONMENT "VRDASH_TEST_PREPARE_FAIL=1") set_tests_properties(surfaces_opengl surfaces_vulkan surfaces_cli_key PROPERTIES SKIP_RETURN_CODE 77) diff --git a/tests/tst_surfaces.cpp b/tests/tst_surfaces.cpp index e12d71e..8c1ab1b 100644 --- a/tests/tst_surfaces.cpp +++ b/tests/tst_surfaces.cpp @@ -1,8 +1,12 @@ // An app with two surfaces, run in snapshot mode: checks surface set-up -// (overlay keys, names, sizes, shared engine, backends), the lifecycle hooks -// and that every surface renders its own scene. No SteamVR involved. +// (overlay keys, names, sizes, shared engine, backends), the lifecycle hooks, +// the app's own command-line options and that every surface renders its own +// scene. No SteamVR involved. +// VRDASH_TEST_PREPARE_FAIL=1: prepare() fails, so exec() must return 1 +// without calling the other hooks. // Exit codes: 0 pass, 1 fail, 77 skipped (no GPU context). +#include #include #include #include @@ -21,7 +25,14 @@ bool check(bool condition, const char* what) { class TwoSurfaceApp : public DashboardApplication { public: - TwoSurfaceApp(int& argc, char** argv) : DashboardApplication(argc, argv) { + TwoSurfaceApp(int& argc, char** argv) + : DashboardApplication( + argc, argv, + {QCommandLineOption(QStringLiteral("test-value"), + QStringLiteral("An app option."), + QStringLiteral("value")), + QCommandLineOption(QStringLiteral("test-flag"), + QStringLiteral("An app flag, never set."))}) { setQmlModule(QStringLiteral("SurfacesTest")); setDisplayName(QStringLiteral("Test App")); setAppKey(QStringLiteral("config.key")); @@ -35,11 +46,28 @@ class TwoSurfaceApp : public DashboardApplication { } QString expectedKey; // --app-key overrides setAppKey() + bool failPrepare = false; + int prepareCalls = 0; int loadingCalls = 0; int startedCalls = 0; bool ok = true; protected: + bool prepare(QString* error) override { + ++prepareCalls; + ok &= check(loadingCalls == 0 && startedCalls == 0, "prepare() first"); + ok &= check(engine() == nullptr && surfaces().isEmpty(), + "prepare() before engine and surfaces"); + ok &= check(mode() == DashboardMode::Snapshot, "mode in prepare()"); + ok &= check(appKey() == expectedKey, "app key in prepare()"); + ok &= check( + isOptionSet(QStringLiteral("test-value")) && + optionValue(QStringLiteral("test-value")) == QLatin1String("42"), + "app option value"); + ok &= check(!isOptionSet(QStringLiteral("test-flag")), "app option unset"); + if (failPrepare) *error = QStringLiteral("prepare() failed on purpose"); + return !failPrepare; + } void loading() override { ++loadingCalls; Surface* red = surface(QStringLiteral("red")); @@ -80,7 +108,10 @@ int main(int argc, char* argv[]) { QDir(dir.path()).filePath(QStringLiteral("shot.png")).toStdString(); // VRDASH_TEST_CLI_KEY=1: also pass --app-key (must win over setAppKey()). const bool cliKey = qEnvironmentVariableIntValue("VRDASH_TEST_CLI_KEY"); - std::vector args = {argv[0], "--snapshot", snapshot}; + const bool failPrepare = + qEnvironmentVariableIntValue("VRDASH_TEST_PREPARE_FAIL"); + std::vector args = {argv[0], "--snapshot", snapshot, + "--test-value", "42"}; if (cliKey) { args.emplace_back("--app-key"); args.emplace_back("cli.key"); @@ -93,15 +124,26 @@ int main(int argc, char* argv[]) { TwoSurfaceApp app(cargc, cargs.data()); app.expectedKey = cliKey ? QStringLiteral("cli.key") : QStringLiteral("config.key"); + app.failPrepare = failPrepare; const int result = app.exec(); + if (failPrepare) { + bool ok = app.ok; + ok &= check(result == 1, "exec() returns 1 when prepare() fails"); + ok &= check( + app.prepareCalls == 1 && app.loadingCalls == 0 && app.startedCalls == 0, + "no hooks after a failed prepare()"); + fprintf(stderr, ok ? "PASS\n" : "FAILED\n"); + return ok ? 0 : 1; + } if (app.loadingCalls == 0) { fprintf(stderr, "SKIP: no render backend (no GPU context?)\n"); return 77; } bool ok = app.ok; ok &= check(result == 0, "exec() succeeded"); - ok &= check(app.loadingCalls == 1 && app.startedCalls == 1, - "each hook called once"); + ok &= check( + app.prepareCalls == 1 && app.loadingCalls == 1 && app.startedCalls == 1, + "each hook called once"); const QImage red(QDir(dir.path()).filePath(QStringLiteral("shot-red.png"))); const QImage blue(QDir(dir.path()).filePath(QStringLiteral("shot-blue.png")));