The platform accepts app commands (D43): flags that get a QCoreApplication like the manifest commands and run prepare() and then the new hook runCommand(). The app's --ha-remove clears the device's retained topics, so HA deletes the device; uninstall.sh runs it after stopping the app and before unregistering from SteamVR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
201 lines
8.1 KiB
C++
201 lines
8.1 KiB
C++
// An app with two surfaces, run in snapshot mode: checks surface set-up
|
|
// (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.
|
|
// VRDASH_TEST_DESKTOP_NOVR=1: --desktop-novr instead of --snapshot; prepare()
|
|
// checks the mode and then fails (no window, no event loop).
|
|
// VRDASH_TEST_COMMAND=1: the app command --test-command instead of
|
|
// --snapshot: prepare(), then runCommand() without GUI, engine or surfaces;
|
|
// its result is exec()'s.
|
|
// Exit codes: 0 pass, 1 fail, 77 skipped (no GPU context).
|
|
|
|
#include <QCommandLineOption>
|
|
#include <QDir>
|
|
#include <QGuiApplication>
|
|
#include <QImage>
|
|
#include <QTemporaryDir>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "app/DashboardApplication.h"
|
|
|
|
namespace {
|
|
|
|
bool check(bool condition, const char* what) {
|
|
if (!condition) fprintf(stderr, "FAIL: %s\n", what);
|
|
return condition;
|
|
}
|
|
|
|
class TwoSurfaceApp : public DashboardApplication {
|
|
public:
|
|
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."))},
|
|
{QCommandLineOption(QStringLiteral("test-command"),
|
|
QStringLiteral("An app command."))}) {
|
|
setQmlModule(QStringLiteral("SurfacesTest"));
|
|
setDisplayName(QStringLiteral("Test App"));
|
|
setAppKey(QStringLiteral("config.key"));
|
|
addSurface({.id = QStringLiteral("red"),
|
|
.qmlRootType = QStringLiteral("Red"),
|
|
.textureSize = QSize(64, 32)});
|
|
addSurface({.id = QStringLiteral("blue"),
|
|
.qmlRootType = QStringLiteral("Blue"),
|
|
.displayName = QStringLiteral("Blue tab"),
|
|
.textureSize = QSize(32, 32)});
|
|
}
|
|
|
|
QString expectedKey; // --app-key overrides setAppKey()
|
|
DashboardMode expectedMode = DashboardMode::Snapshot;
|
|
bool failPrepare = false;
|
|
int prepareCalls = 0;
|
|
int loadingCalls = 0;
|
|
int startedCalls = 0;
|
|
QStringList commands;
|
|
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() == expectedMode, "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"));
|
|
Surface* blue = surface(QStringLiteral("blue"));
|
|
ok &= check(surfaces().size() == 2 && red && blue, "two surfaces by id");
|
|
ok &= check(surface() == red, "primary is the first");
|
|
ok &= check(surface(QStringLiteral("none")) == nullptr, "unknown id");
|
|
ok &= check(engine() != nullptr, "shared engine");
|
|
ok &= check(mode() == DashboardMode::Snapshot, "mode");
|
|
ok &= check(red && blue && red->backend() && blue->backend() &&
|
|
red->backend() != blue->backend(),
|
|
"a backend per surface");
|
|
ok &= check(appKey() == expectedKey, "app key (setAppKey / --app-key)");
|
|
ok &= check(red && red->overlayKey() == expectedKey + QLatin1String(".red"),
|
|
"overlay key = <app key>.<id>");
|
|
ok &= check(red && red->displayName() == QLatin1String("Test App"),
|
|
"tab name defaults to the app name");
|
|
ok &= check(blue && blue->displayName() == QLatin1String("Blue tab"),
|
|
"tab name from config");
|
|
ok &= check(blue && blue->textureSize() == QSize(32, 32),
|
|
"texture size from config");
|
|
ok &= check(vrRuntime() == nullptr, "no VR outside overlay mode");
|
|
}
|
|
int runCommand(const QString& name) override {
|
|
commands.append(name);
|
|
ok &= check(mode() == DashboardMode::Command, "mode in runCommand()");
|
|
ok &= check(engine() == nullptr && surfaces().isEmpty(),
|
|
"no engine and no surfaces for a command");
|
|
ok &= check(!qobject_cast<QGuiApplication*>(QCoreApplication::instance()),
|
|
"no GUI application for a command");
|
|
return 7;
|
|
}
|
|
void started() override {
|
|
++startedCalls;
|
|
const QList<Surface*> all = surfaces();
|
|
ok &= check(all.size() == 2 && all[0]->window() && all[1]->window() &&
|
|
all[0]->window() != all[1]->window(),
|
|
"a window per surface");
|
|
}
|
|
};
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[]) {
|
|
QTemporaryDir dir;
|
|
const std::string snapshot =
|
|
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");
|
|
const bool desktopNoVr =
|
|
qEnvironmentVariableIntValue("VRDASH_TEST_DESKTOP_NOVR");
|
|
const bool command = qEnvironmentVariableIntValue("VRDASH_TEST_COMMAND");
|
|
const bool failPrepare =
|
|
qEnvironmentVariableIntValue("VRDASH_TEST_PREPARE_FAIL") || desktopNoVr;
|
|
std::vector<std::string> args = {argv[0], "--test-value", "42"};
|
|
if (desktopNoVr) {
|
|
args.emplace_back("--desktop-novr");
|
|
} else if (command) {
|
|
args.emplace_back("--test-command");
|
|
} else {
|
|
args.emplace_back("--snapshot");
|
|
args.emplace_back(snapshot);
|
|
}
|
|
if (cliKey) {
|
|
args.emplace_back("--app-key");
|
|
args.emplace_back("cli.key");
|
|
}
|
|
for (int i = 1; i < argc; ++i) args.emplace_back(argv[i]);
|
|
std::vector<char*> cargs;
|
|
for (std::string& arg : args) cargs.push_back(arg.data());
|
|
int cargc = int(cargs.size());
|
|
|
|
TwoSurfaceApp app(cargc, cargs.data());
|
|
app.expectedKey =
|
|
cliKey ? QStringLiteral("cli.key") : QStringLiteral("config.key");
|
|
app.failPrepare = failPrepare;
|
|
if (desktopNoVr) app.expectedMode = DashboardMode::DesktopNoVr;
|
|
if (command) app.expectedMode = DashboardMode::Command;
|
|
const int result = app.exec();
|
|
if (command) {
|
|
bool ok = app.ok;
|
|
ok &= check(result == 7, "exec() returns runCommand()'s result");
|
|
ok &= check(
|
|
app.prepareCalls == 1 && app.loadingCalls == 0 && app.startedCalls == 0,
|
|
"prepare(), but no other hooks for a command");
|
|
ok &= check(app.commands == QStringList{QStringLiteral("test-command")},
|
|
"runCommand() once with the command's name");
|
|
fprintf(stderr, ok ? "PASS\n" : "FAILED\n");
|
|
return ok ? 0 : 1;
|
|
}
|
|
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.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")));
|
|
ok &= check(red.size() == QSize(64, 32) &&
|
|
red.pixelColor(10, 10) == QColor(255, 0, 0),
|
|
"red surface rendered");
|
|
ok &= check(blue.size() == QSize(32, 32) &&
|
|
blue.pixelColor(10, 10) == QColor(0, 0, 255),
|
|
"blue surface rendered");
|
|
fprintf(stderr, ok ? "PASS\n" : "FAILED\n");
|
|
return ok ? 0 : 1;
|
|
}
|