From a34c74e453c65f19f8c5ac0e9ca8a0c83420aa45 Mon Sep 17 00:00:00 2001 From: WHC Date: Sun, 11 Oct 2026 20:41:58 +0800 Subject: [PATCH] the tool card counts, and a device run leaves a log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tool that takes minutes -- a build, a wide search, a command nobody can predict -- read as a window that had hung: the card said "running" and nothing else, forever. The chip now counts from the point the card starts running (from five seconds on, before that it is noise), which is where a person looks when a call seems stuck; the status line and the Stop button were already there. A card that appears already running (a replayed transcript) starts its clock too. --uitest: two checks for it (a card four minutes eighteen in says "running · 4:18"; a finished one stops counting). And the run now writes uitest.log in its workspace, every line and the verdict. That is the only way to read a run *on a phone*: stdout reaches nobody there, an app started from adb cannot be handed CST_APP_SHOT or any other environment (so the shots a desktop run can take are out), and logcat does not carry this build's Qt messages at all -- checked: a plain `qInfo` from the app is not in the buffer. First device run (NX789J, 105 checks): 88 ok, 17 FAIL, every failure pre-existing and unrelated to this work -- the settings dialog does not open from the gear (so nothing in it is reachable: the five dialog checks, the two provider-form ones and the four clicks that follow), Open/Share leave no status line on the device, and the clipboard-image checks have no clipboard. All the md_to_pdf and card checks pass on the phone, including "black ink on paper (darkest pixel 0)" from 4c69ef3. --- app/README.md | 7 +++++ app/qml/ToolCard.qml | 54 ++++++++++++++++++++++++++++++++++++--- app/src/ui/UiTest.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 3 deletions(-) diff --git a/app/README.md b/app/README.md index 50cdde9..7f634b0 100644 --- a/app/README.md +++ b/app/README.md @@ -150,6 +150,13 @@ creates and deletes sessions. On a machine without a screen, `CST_APP_SHOT=shot.png` makes the run save the window as a PNG when it is done; `CST_APP_DUMP=1` prints how many session rows the visual tree holds. +Every line the run prints also goes to `uitest.log` in the workspace, and the +verdict is the last line of it. That is how a run *on a phone* is read back: +stdout reaches nobody there, an app started from adb cannot be handed +environment variables, and logcat does not carry this build's Qt messages -- +so `--uitest --workspace /sdcard/cst-uitest` plus +`adb pull /sdcard/cst-uitest/uitest.log` is the whole round trip. + ## Provider, model, key The sidebar shows where the engine points (`provider · model`); the gear in the diff --git a/app/qml/ToolCard.qml b/app/qml/ToolCard.qml index ab5ec53..f2e2eef 100644 --- a/app/qml/ToolCard.qml +++ b/app/qml/ToolCard.qml @@ -29,6 +29,55 @@ Rectangle { : state === "unfinished" ? "no result" : hasResult && exitCode !== 0 ? "failed" : "done" + /* How long this call has been going. A tool that takes minutes -- a + * build, a wide search, a command nobody can predict -- must not read as + * a window that has hung: while the card runs, its chip counts. */ + property double startedAt: 0 + property int runningFor: 0 + + function elapsedText() { + const s = runningFor + if (s < 60) + return s + "s" + const m = Math.floor(s / 60) + if (m < 60) + return m + ":" + (s % 60 < 10 ? "0" : "") + (s % 60) + return Math.floor(m / 60) + "h" + (m % 60 < 10 ? "0" : "") + (m % 60) + } + + /* The clock starts when the card starts running -- and a card that is + * already running when it appears (a replayed transcript) starts one too, + * hence Component.onCompleted. */ + function noteRunning() { + if (running && startedAt === 0) + startedAt = Date.now() + else if (!running) + startedAt = 0 + } + + onRunningChanged: noteRunning() + Component.onCompleted: noteRunning() + + Timer { + interval: 1000 + repeat: true + running: card.running && card.startedAt > 0 + onTriggered: card.runningFor = Math.floor((Date.now() - card.startedAt) / 1000) + } + + /* What the chip says: the state, how long it has been running (from five + * seconds on -- before that it is noise), or the exit code once there is + * a result. "no result" is not an outcome, so no exit code belongs on + * it. */ + function chipText() { + if (running) + return runningFor >= 5 + ? stateText + " · " + elapsedText() + : stateText + return stateText + (hasResult && state !== "unfinished" + ? " · exit " + exitCode : "") + } + function lines() { const all = output.split("\n") return all.length > 500 ? all.slice(0, 500).concat(["… " + (all.length - 500) + " more lines"]) : all @@ -92,10 +141,9 @@ Rectangle { implicitHeight: chip.implicitHeight + 2 Text { id: chip + objectName: "toolChip" anchors.centerIn: parent - /* "no result" is not an outcome: no exit code belongs on it. */ - text: card.stateText + (card.hasResult && card.state !== "unfinished" - ? " · exit " + card.exitCode : "") + text: card.chipText() color: card.stateColor font.pixelSize: Theme.smallSize } diff --git a/app/src/ui/UiTest.cpp b/app/src/ui/UiTest.cpp index 6223988..fbee357 100644 --- a/app/src/ui/UiTest.cpp +++ b/app/src/ui/UiTest.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -42,9 +43,23 @@ namespace { int g_fail; +/* The run's own log file, opened once the workspace is known (see the top of + * the run below): every line stdout gets is written here as well. On a phone + * stdout reaches nobody, and an app started from adb cannot be handed + * CST_APP_SHOT or any other environment -- so `--uitest --workspace DIR` plus + * `adb pull DIR/uitest.log` is how a device run is read back. Empty when the + * file could not be made, in which case the run only prints. */ +QFile g_runLog; + void check(bool ok, const QString &what) { std::printf("%s %s\n", ok ? "ok " : "FAIL", qPrintable(what)); std::fflush(stdout); + if (g_runLog.isOpen()) { + g_runLog.write(ok ? "ok " : "FAIL"); + g_runLog.write(what.toUtf8()); + g_runLog.write("\n"); + g_runLog.flush(); + } if (!ok) g_fail++; } @@ -243,6 +258,10 @@ int runUiTest(AgentCore &core, const QString &size) { /* A file card hands its file to the system: in a test run nothing may open * for real, and the platform layer itself honours this (see ToolProvider.h). */ qputenv("CST_APP_NO_OPEN", "1"); + /* From here on the run leaves a log in the workspace next to the files it + * makes, so a device run can be read back without logcat (see g_runLog). */ + g_runLog.setFileName(core.workspace() + QStringLiteral("/uitest.log")); + g_runLog.open(QIODevice::WriteOnly | QIODevice::Truncate); const QString resumed = writeFixtureSession(core.workspace(), QStringLiteral("20260101-115959-on")); check(!resumed.isEmpty() && core.switchSession(resumed), @@ -516,6 +535,40 @@ int runUiTest(AgentCore &core, const QString &size) { QGuiApplication::setPalette(was); } + /* A tool that takes minutes -- a build, a wide search -- must not read as a + * window that has hung: while a card runs, its chip counts. */ + { + QList cards; + collectNamed(win->contentItem(), QStringLiteral("toolCard"), &cards); + check(!cards.isEmpty(), + QStringLiteral("the fixture session has a tool card to watch")); + if (!cards.isEmpty()) { + QQuickItem *card = cards.first(); + card->setProperty("state", "running"); + /* four minutes eighteen in, the point where "running" alone stopped + * being an answer */ + card->setProperty( + "startedAt", + static_cast(QDateTime::currentMSecsSinceEpoch()) - 258000.0); + card->setProperty("runningFor", 258); + QList chips; + collectNamed(card, QStringLiteral("toolChip"), &chips); + const QString counting = chips.isEmpty() + ? QString() + : chips.first()->property("text").toString(); + check(counting.contains(QLatin1String("4:18")), + QStringLiteral("a running card says how long it has been going (%1)") + .arg(counting)); + card->setProperty("state", "done"); + const QString settled = chips.isEmpty() + ? QString() + : chips.first()->property("text").toString(); + check(!settled.contains(QLatin1String("4:18")), + QStringLiteral("and a finished card stops counting (%1)") + .arg(settled)); + } + } + /* new sessions: the engine moves, the view empties, the store grows */ const int before = core.sessions().size(); clickMouse(win, QStringLiteral("newSessionButton")); @@ -954,6 +1007,12 @@ int runUiTest(AgentCore &core, const QString &size) { maybeShot(win, QString()); std::printf("--- %s\n", g_fail ? "FAILED" : "all UI checks passed"); + if (g_runLog.isOpen()) { + g_runLog.write("--- "); + g_runLog.write(g_fail ? "FAILED" : "all UI checks passed"); + g_runLog.write("\n"); + g_runLog.close(); + } return g_fail ? 1 : 0; }