Files
FrameAssistant/tests/tst_surfaces.cpp
T
matzmanandClaude Opus 5.5 0bdcaddcd5 App commands and --ha-remove (iteration 2, step 6)
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>
2026-10-06 16:32:51 +02:00

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