Dashboard creates the MQTT client and the HA device with the dummy "Uptime" sensor (D41) in overlay and both desktop modes, connects in started() and disconnects gracefully on aboutToQuit. The new platform mode --desktop-novr behaves like --desktop for now (D22). The main tab shows device, broker, connection state and last error (UI-1) through the ConnectionStatus QML singleton. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.7 KiB
C++
170 lines
6.7 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).
|
|
// Exit codes: 0 pass, 1 fail, 77 skipped (no GPU context).
|
|
|
|
#include <QCommandLineOption>
|
|
#include <QDir>
|
|
#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."))}) {
|
|
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;
|
|
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");
|
|
}
|
|
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 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 {
|
|
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;
|
|
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.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;
|
|
}
|