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>
This commit is contained in:
2026-10-06 16:32:51 +02:00
co-authored by Claude Opus 5.5
parent 148ae663cf
commit 0bdcaddcd5
12 changed files with 219 additions and 47 deletions
+9 -7
View File
@@ -112,19 +112,21 @@ into Qt events.
- `src/app/DashboardApplication` is the **base class of every app** (in `vrdashboard_core`):
CLI, logging, SteamVR connection, backend selection, surfaces, controller navigation, and the
modes (overlay, `--desktop`, `--desktop-novr`, `--snapshot`, `--benchmark`, manifest
install).
install, app commands).
- **Inheritance for the app, components for the surfaces** (the user's decision, after
trying composition): the app derives from `DashboardApplication`. In its constructor it
configures (`addSurface`, optional `setAppKey`/`setDisplayName`) and passes its own
command-line options (`QCommandLineOption` list) to the base constructor. It overrides
the hooks `prepare(QString*)` (first; mode and app key known, no engine/surfaces/SteamVR
yet; returning false ends `exec()` with exit code 1), `loading()` (backends + shared
engine exist, QML not loaded) and `started()` (everything runs). In the hooks, protected
command-line options and its commands (two `QCommandLineOption` lists, D38, D43) to the
base constructor. It overrides the hooks `prepare(QString*)` (first; mode and app key
known, no engine/surfaces/SteamVR yet; returning false ends `exec()` with exit code 1),
`loading()` (backends + shared engine exist, QML not loaded), `started()` (everything
runs) and `runCommand(name)` (mode `Command`, after `prepare()`, nothing else exists). In the hooks, protected
accessors give the running state: `mode()`, `appKey()`, `appName()`, `isOptionSet()`,
`optionValue()`, `engine()`, `surfaces()`, `surface()` (primary), `surface(id)`,
`vrRuntime()`, `navigation()`. The app is `src/Dashboard.h/.cpp`.
- The base constructor creates the `QGuiApplication` (`QCoreApplication` for manifest
commands), so derived members can be QObjects with timers.
- The base constructor creates the `QGuiApplication` (`QCoreApplication` for manifest and
app commands: decided from the raw `argv` before parsing), so derived members can be
QObjects with timers.
- All QML and surfaces are torn down **at the end of `exec()`**, while the derived object
and its members (e.g. QML singletons) still exist.
- `src/app/Surface` is **one view**: its QML root (from the app's module), renderer, render
+1
View File
@@ -135,6 +135,7 @@ How the pieces fit together: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
| `--max-fps <fps>` | Frame-rate cap (default 60); rendering only happens on change |
| `--install-manifest [--launch]` | Register with SteamVR (auto-start) and optionally start it now; works over SSH |
| `--uninstall-manifest` | Unregister |
| `--ha-remove` | Remove this device from Home Assistant (clears its retained MQTT topics; needs the settings and the broker) and exit; `uninstall.sh` calls it. Works over SSH |
| `--manifest <path>` | Manifest for the two options above (default: the app's own) |
| `--log-file <path>` | Also log to a file (relative paths: relative to the install folder) |
+7 -1
View File
@@ -41,6 +41,9 @@ Your app derives from it:
`setDisplayName()`, `setQuickStyle()`, `setQmlModule()`). The app's own command-line
options go to the base constructor as a list of `QCommandLineOption`s; they are parsed and
listed in `--help` with the built-in ones, and read with `isOptionSet()`/`optionValue()`.
Its commands (flags such as `--ha-remove`) go there as a second list: like the manifest
commands they get a `QCoreApplication` (no display needed, works over SSH), and `exec()`
runs `prepare()` and then `runCommand(name)` in mode `Command` instead of a mode.
- **Hooks:**
- `prepare(QString* error)`: first, once mode and app key are known, before the QML
engine, the surfaces and SteamVR exist (not for manifest commands). Returning `false`
@@ -48,10 +51,13 @@ Your app derives from it:
- `loading()`: the shared QML engine, the surfaces and their render backends exist; QML
isn't loaded yet. Set up what the QML needs here.
- `started()`: everything runs. Connect to surfaces and input here.
- `runCommand(name)`: one of the app's commands, after `prepare()`; no QML engine,
surfaces or SteamVR. Its result is the exit code.
- **Accessors:** `mode()`, `appKey()`, `appName()`, `isOptionSet()`, `optionValue()`,
`engine()`, `surfaces()`, `surface()` (the first), `surface(id)`, `vrRuntime()`,
`navigation()`.
- **Lifetime:** the base constructor creates the `QGuiApplication`, so your members can be
- **Lifetime:** the base constructor creates the `QGuiApplication` (`QCoreApplication` for
commands), so your members can be
QObjects with timers. All QML and surfaces are torn down at the end of `exec()`, while your
object and its members still exist.
+7 -1
View File
@@ -385,6 +385,8 @@ a `QGuiApplication` (no display needed, works over SSH), and `exec()` calls
- `exec()`: after the manifest check, an app command runs `prepare()` and then a new hook
`virtual int runCommand(const QString& name)`, whose result is the exit code. No engine,
surfaces or SteamVR.
- The mode is `DashboardMode::Command` (so `prepare()` can tell); the name of the first app
command given goes to `runCommand()`. Tested in `tst_surfaces` (`surfaces_command`).
- Moving the manifest commands onto this mechanism is possible later, not part of iteration 2.
### `--ha-remove`
@@ -396,7 +398,11 @@ delete).
- An app command (D43): loads the settings, connects, publishes **empty retained payloads**
to discovery (HA then deletes the device and its entities), device availability, and each
entity's state and availability topics; disconnects.
- Exit code 0 on success; ≠ 0 with a warning if the broker can't be reached within 10 s.
- Its client uses the client id `frameassistant_<device id>_remove` (doesn't knock a running
instance off the broker) and no will. The `HaDevice` is created only after the connection
is up: a device that sees the connection come up publishes itself first.
- Exit code 0 on success; 1 with an error if the broker can't be reached within 10 s, or the
removal isn't acknowledged within 10 s.
- `uninstall.sh`: `pkill` → `sleep 1` → `--ha-remove` (a failure doesn't stop the uninstall) →
`--uninstall-manifest`.
- Also usable by hand, e.g. after a device name change without `ha_unique_id`:
+8 -1
View File
@@ -25,7 +25,8 @@ CLAUDE.md ("Working rules").
- **Implementation** follows the order in DESIGN.md (steps 1–7). Steps 1 (libmosquitto in
CMake and the containers), 2 (`MqttClient`, `MosquittoClient`, `tst_mqtt`) 3
(`HaEntity`, `HaSensor`, `HaDevice`, unit tests) and 4 (app wiring, `--desktop-novr`,
UI-1) and 5 (TLS) done; next: step 6 (app commands, `--ha-remove`, `uninstall.sh`).
UI-1) 5 (TLS) and 6 (app commands, `--ha-remove`, `uninstall.sh`) done; next: step 7
(packaging check, docs).
## Next steps
@@ -178,6 +179,12 @@ Known limitation: the bundled-Qt package does not support WebEngine in this iter
validation messages.
- Still open: which CA store the Frame's OpenSSL uses by default (for `ca_file` empty);
check on the device.
- Step 6: app commands in the platform (D43: second option list, `QCoreApplication`, mode
`Command`, hook `runCommand()`; `tst_surfaces` `surfaces_command`); `--ha-remove` in the
app; `uninstall.sh` stops the app, runs `--ha-remove` (failure doesn't stop it) and
unregisters. Checked end to end against a local mosquitto, without a display: retained
discovery, availability, state and entity availability before, none after (exit 0);
broker down → exit 1 after 10 s; missing settings → exit 1.
### 2026-10-05: requirements approved
+9 -3
View File
@@ -64,9 +64,14 @@ EOF
cat > "$out/uninstall.sh" <<EOF
#!/bin/sh
# Unregisters $name from SteamVR and stops it. The folder can be deleted afterwards.
# Stops $name, removes its device from Home Assistant (clears its retained MQTT
# topics) and unregisters it from SteamVR. The folder can be deleted afterwards.
cd "\$(dirname "\$0")" || exit 1
pkill -f "/bin/$exe( |\\$)" 2>/dev/null
mkdir -p logs
# Stopped first, so it can't publish itself again; the broker sends its last will at once.
pkill -f "/bin/$exe( |\\$)" 2>/dev/null && sleep 1
./bin/$exe --ha-remove --log-file logs/install.log ||
echo "Could not remove the device from Home Assistant (see logs/install.log); unregistering anyway." >&2
exec ./bin/$exe --uninstall-manifest --log-file logs/install.log
EOF
chmod +x "$out/install.sh" "$out/uninstall.sh"
@@ -92,7 +97,8 @@ chmod +x "$out/install.sh" "$out/uninstall.sh"
echo "Install / update: ./install.sh (SteamVR must be running)"
echo " SteamVR remembers this folder's location: after moving it,"
echo " run ./install.sh again."
echo "Uninstall: ./uninstall.sh"
echo "Uninstall: ./uninstall.sh (also removes the device from Home Assistant;"
echo " needs the settings file and the MQTT broker)"
echo "Logs: logs/$exe.log (the app), logs/install.log (install.sh);"
echo " run ./bin/$exe --help for options"
echo
+81 -23
View File
@@ -3,6 +3,8 @@
#include <QCommandLineOption>
#include <QCoreApplication>
#include <QDebug>
#include <QEventLoop>
#include <QTimer>
#include "ha/HaDevice.h"
#include "ha/HaSensor.h"
@@ -11,10 +13,13 @@
namespace {
const QString kConfigOption = QStringLiteral("config");
const QString kHaRemoveCommand = QStringLiteral("ha-remove");
// How often the dummy sensor's value is updated.
constexpr int kUptimeIntervalMs = 10000;
// How long a normal exit waits for the broker to acknowledge "offline".
constexpr int kDisconnectTimeoutMs = 2000;
// --ha-remove: how long it may take to connect, and then to publish.
constexpr int kRemoveTimeoutMs = 10000;
} // namespace
@@ -24,7 +29,11 @@ Dashboard::Dashboard(int& argc, char** argv)
{QCommandLineOption(kConfigOption,
QStringLiteral("Settings file (default: %1).")
.arg(Config::defaultPath()),
QStringLiteral("path"))}) {
QStringLiteral("path"))},
{QCommandLineOption(
kHaRemoveCommand,
QStringLiteral("Remove this device from Home Assistant (clears "
"its retained MQTT topics) and exit."))}) {
addSurface({.qmlRootType = QStringLiteral("Main")});
}
@@ -33,7 +42,7 @@ Dashboard::~Dashboard() = default;
bool Dashboard::prepare(QString* error) {
// Snapshot and benchmark only render the UI: no settings needed.
if (mode() != DashboardMode::Overlay && mode() != DashboardMode::Desktop &&
mode() != DashboardMode::DesktopNoVr)
mode() != DashboardMode::DesktopNoVr && mode() != DashboardMode::Command)
return true;
const QString path = isOptionSet(kConfigOption) ? optionValue(kConfigOption)
: Config::defaultPath();
@@ -46,30 +55,38 @@ bool Dashboard::prepare(QString* error) {
return true;
}
MosquittoClient::Settings Dashboard::mqttSettings() const {
MosquittoClient::Settings settings;
settings.broker = m_config->mqtt;
settings.clientId = QStringLiteral("frameassistant_") + m_config->deviceId;
settings.will = HaDevice::lastWill(m_config->deviceId);
return settings;
}
HaDevice::Info Dashboard::deviceInfo() const {
return {.name = m_config->deviceName,
.id = m_config->deviceId,
.discoveryPrefix = m_config->discoveryPrefix,
.softwareVersion = QCoreApplication::applicationVersion()};
}
HaSensor* Dashboard::addEntities(HaDevice& device) {
return device.addSensor({.id = QStringLiteral("uptime"),
.name = QStringLiteral("Uptime"),
.deviceClass = QStringLiteral("duration"),
.unit = QStringLiteral("s"),
.category = QStringLiteral("diagnostic")});
}
void Dashboard::loading() {
if (!m_config) return;
const Config& config = *m_config;
MosquittoClient::Settings settings;
settings.broker = config.mqtt;
settings.clientId = QStringLiteral("frameassistant_") + config.deviceId;
settings.will = HaDevice::lastWill(config.deviceId);
m_mqtt = std::make_unique<MosquittoClient>(settings);
m_mqtt = std::make_unique<MosquittoClient>(mqttSettings());
m_device = std::make_unique<HaDevice>(deviceInfo(), m_mqtt.get());
m_uptime = addEntities(*m_device);
m_device = std::make_unique<HaDevice>(
HaDevice::Info{.name = config.deviceName,
.id = config.deviceId,
.discoveryPrefix = config.discoveryPrefix,
.softwareVersion = QCoreApplication::applicationVersion()},
m_mqtt.get());
m_uptime = m_device->addSensor({.id = QStringLiteral("uptime"),
.name = QStringLiteral("Uptime"),
.deviceClass = QStringLiteral("duration"),
.unit = QStringLiteral("s"),
.category = QStringLiteral("diagnostic")});
m_status.setDevice(
config.deviceName,
QStringLiteral("%1:%2").arg(config.mqtt.host).arg(config.mqtt.port));
const MqttConfig& broker = m_config->mqtt;
m_status.setDevice(m_config->deviceName,
QStringLiteral("%1:%2").arg(broker.host).arg(broker.port));
connect(m_mqtt.get(), &MqttClient::connectionChanged, &m_status,
&ConnectionStatus::setConnection);
}
@@ -91,3 +108,44 @@ void Dashboard::started() {
});
m_mqtt->connectToBroker();
}
int Dashboard::runCommand(const QString& name) {
if (name == kHaRemoveCommand) return removeFromHa();
return DashboardApplication::runCommand(name);
}
int Dashboard::removeFromHa() {
MosquittoClient::Settings settings = mqttSettings();
// Its own client id, so it doesn't knock a running instance off the
// broker; no will, since it publishes nothing to keep.
settings.clientId += QStringLiteral("_remove");
settings.will = {};
MosquittoClient client(settings);
QEventLoop loop;
QTimer::singleShot(kRemoveTimeoutMs, &loop, &QEventLoop::quit);
connect(&client, &MqttClient::connectionChanged, &loop,
[&loop](MqttClient::State state) {
if (state == MqttClient::State::Connected) loop.quit();
});
client.connectToBroker();
loop.exec();
if (client.state() != MqttClient::State::Connected) {
qCritical().noquote() << "Cannot remove the device from Home Assistant: "
"no connection to the broker";
return 1;
}
// Created only now: a device that saw the connection come up would
// publish itself first.
HaDevice device(deviceInfo(), &client);
addEntities(device);
const bool published = device.removeFromHa();
const bool acknowledged = client.disconnectFromBroker(kRemoveTimeoutMs);
if (!published || !acknowledged) {
qCritical().noquote() << "Removing the device from Home Assistant failed";
return 1;
}
qInfo().noquote() << "Removed device" << m_config->deviceName
<< "(" + m_config->deviceId + ") from Home Assistant";
return 0;
}
+12 -2
View File
@@ -7,10 +7,10 @@
#include "ConnectionStatus.h"
#include "app/DashboardApplication.h"
#include "config/Config.h"
#include "ha/HaDevice.h"
#include "mqtt/MosquittoClient.h"
class HaDevice;
class HaSensor;
class MosquittoClient;
// The dashboard app. Its UI is the QML module set up in src/CMakeLists.txt
// (QML_URI FrameAssistant, files in src/qml/).
@@ -30,8 +30,18 @@ class Dashboard : public DashboardApplication {
void loading() override;
// Connects to the broker and starts the sensors.
void started() override;
// --ha-remove (docs/DESIGN.md, "--ha-remove").
int runCommand(const QString& name) override;
private:
// The settings for the MQTT client and the HA device.
MosquittoClient::Settings mqttSettings() const;
HaDevice::Info deviceInfo() const;
// Adds the entities to `device` (the same for the app and --ha-remove);
// returns the dummy sensor.
static HaSensor* addEntities(HaDevice& device);
int removeFromHa();
std::optional<Config> m_config; // none in snapshot/benchmark mode
// The device uses the client: declared after it, so destroyed before it.
std::unique_ptr<MosquittoClient> m_mqtt;
+33 -6
View File
@@ -115,6 +115,7 @@ std::unique_ptr<RenderBackend> startBackend(GraphicsApi api, bool apiExplicit,
struct DashboardApplication::Options {
QString error; // set: exec() reports it and fails
bool manifestCommand = false;
QString appCommand; // name of the app command given, if any
bool install = false;
bool launch = false;
QString manifest;
@@ -144,6 +145,8 @@ const char* dashboardModeName(DashboardMode mode) {
return "snapshot";
case DashboardMode::Benchmark:
return "benchmark";
case DashboardMode::Command:
return "command";
}
return "?";
}
@@ -170,7 +173,8 @@ void DashboardApplication::setDefaultQmlModule(const char* uri) {
}
DashboardApplication::DashboardApplication(
int& argc, char** argv, const QList<QCommandLineOption>& appOptions)
int& argc, char** argv, const QList<QCommandLineOption>& appOptions,
const QList<QCommandLineOption>& appCommands)
: m_options(std::make_unique<Options>()), 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
@@ -178,14 +182,24 @@ DashboardApplication::DashboardApplication(
if (!qEnvironmentVariableIsSet("QT_FORCE_STDERR_LOGGING"))
qputenv("QT_FORCE_STDERR_LOGGING", "1");
// Manifest commands only talk to SteamVR: no GUI (and no display
// connection) needed, so they also work over SSH.
// Manifest commands only talk to SteamVR, app commands do their own thing:
// no GUI (and no display connection) needed, so they also work over SSH.
// Decided before parsing: the parser needs the application object.
Options& o = *m_options;
o.manifestCommand = std::any_of(argv + 1, argv + argc, [](const char* arg) {
return qstrcmp(arg, "--install-manifest") == 0 ||
qstrcmp(arg, "--uninstall-manifest") == 0;
});
if (o.manifestCommand)
const bool appCommand =
std::any_of(argv + 1, argv + argc, [&appCommands](const char* arg) {
for (const QCommandLineOption& command : appCommands) {
for (const QString& name : command.names()) {
if (QLatin1String(arg) == QStringLiteral("--") + name) return true;
}
}
return false;
});
if (o.manifestCommand || appCommand)
m_app = std::make_unique<QCoreApplication>(argc, argv);
else
m_app = std::make_unique<QGuiApplication>(argc, argv);
@@ -257,7 +271,7 @@ DashboardApplication::DashboardApplication(
benchmarkOption, apiOption, keyOption, nameOption,
fpsOption, installOption, uninstallOption, manifestOption,
launchOption, logOption});
for (const QCommandLineOption& option : appOptions) {
for (const QCommandLineOption& option : appOptions + appCommands) {
if (!parser.addOption(option))
o.error = QStringLiteral("App option --%1 clashes with another option")
.arg(option.names().constFirst());
@@ -283,6 +297,12 @@ DashboardApplication::DashboardApplication(
? resolvePath(parser.value(manifestOption))
: shareDir() + QStringLiteral("/manifest.vrmanifest");
if (o.manifestCommand) return;
for (const QCommandLineOption& command : appCommands) {
if (parser.isSet(command)) {
o.appCommand = command.names().constFirst();
return;
}
}
if (parser.isSet(apiOption)) {
if (!parseGraphicsApi(parser.value(apiOption), &o.graphicsApi))
@@ -338,6 +358,10 @@ void DashboardApplication::setQuickStyle(const QString& style) {
bool DashboardApplication::prepare(QString*) { return true; }
int DashboardApplication::runCommand(const QString& name) {
return fail(QStringLiteral("Command --%1 is not implemented").arg(name));
}
void DashboardApplication::loading() {}
void DashboardApplication::started() {}
@@ -400,7 +424,7 @@ int DashboardApplication::exec() {
QString error;
if (!validateConfig(&error)) return fail(error);
m_mode = o.mode;
m_mode = o.appCommand.isEmpty() ? o.mode : DashboardMode::Command;
m_appKey = !o.appKey.isEmpty() ? o.appKey
: !m_configAppKey.isEmpty()
? m_configAppKey
@@ -411,6 +435,7 @@ int DashboardApplication::exec() {
: QString::fromUtf8(AppConfig::kDisplayName);
QCoreApplication::setApplicationName(m_appName);
if (!prepare(&error)) return fail(error);
if (m_mode == DashboardMode::Command) return runCommand(o.appCommand);
if (!m_quickStyle.isEmpty()) QQuickStyle::setStyle(m_quickStyle);
m_engine = std::make_unique<QQmlEngine>();
@@ -432,6 +457,8 @@ int DashboardApplication::exec() {
case DashboardMode::Overlay:
result = runOverlay();
break;
case DashboardMode::Command:
break; // handled above
}
// Still within exec(): the derived object (and what it owns) outlives all
// QML. Surfaces (their QML scenes, renderers, backends) before the engine.
+18 -2
View File
@@ -20,7 +20,15 @@ class VrRuntime;
// Desktop and DesktopNoVr both show desktop windows; DesktopNoVr promises
// never to connect to SteamVR (for apps that use SteamVR data otherwise).
enum class DashboardMode { Overlay, Desktop, DesktopNoVr, Snapshot, Benchmark };
// Command: one of the app's commands runs (runCommand()); no GUI.
enum class DashboardMode {
Overlay,
Desktop,
DesktopNoVr,
Snapshot,
Benchmark,
Command
};
const char* dashboardModeName(DashboardMode mode);
// Base class of a dashboard app. It owns everything app- and surface-related:
@@ -59,8 +67,12 @@ class DashboardApplication : public QObject {
// `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().
// `appCommands` are the app's commands (flags, e.g. --ha-remove): like the
// manifest commands they need no display (QCoreApplication), and exec()
// runs prepare() and then runCommand() instead of a mode.
DashboardApplication(int& argc, char** argv,
const QList<QCommandLineOption>& appOptions = {});
const QList<QCommandLineOption>& appOptions = {},
const QList<QCommandLineOption>& appCommands = {});
~DashboardApplication() override;
// Runs the mode selected on the command line; returns the exit code.
@@ -98,6 +110,10 @@ class DashboardApplication : public QObject {
virtual void loading();
// Everything runs; called right before the event loop starts.
virtual void started();
// Command mode: runs the app command `name` (an appCommands option, see
// the constructor) after prepare(); no QML engine, surfaces or SteamVR.
// Returns the exit code. The default fails.
virtual int runCommand(const QString& name);
// --- The running app (valid in the hooks and until exec() returns) ---
DashboardMode mode() const { return m_mode; }
+2
View File
@@ -33,6 +33,8 @@ 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")
add_test(NAME surfaces_command COMMAND tst_surfaces)
set_tests_properties(surfaces_command PROPERTIES ENVIRONMENT "VRDASH_TEST_COMMAND=1")
add_test(NAME surfaces_desktop_novr COMMAND tst_surfaces)
set_tests_properties(surfaces_desktop_novr PROPERTIES
ENVIRONMENT "VRDASH_TEST_DESKTOP_NOVR=1;QT_QPA_PLATFORM=offscreen")
+32 -1
View File
@@ -6,10 +6,14 @@
// 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>
@@ -34,7 +38,9 @@ class TwoSurfaceApp : public DashboardApplication {
QStringLiteral("An app option."),
QStringLiteral("value")),
QCommandLineOption(QStringLiteral("test-flag"),
QStringLiteral("An app flag, never set."))}) {
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"));
@@ -53,6 +59,7 @@ class TwoSurfaceApp : public DashboardApplication {
int prepareCalls = 0;
int loadingCalls = 0;
int startedCalls = 0;
QStringList commands;
bool ok = true;
protected:
@@ -94,6 +101,15 @@ class TwoSurfaceApp : public DashboardApplication {
"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();
@@ -113,11 +129,14 @@ int main(int argc, char* argv[]) {
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);
@@ -136,7 +155,19 @@ int main(int argc, char* argv[]) {
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");