watch-gradle-tests
Skill455 starsChanged 33 days agoMITView on GitHub
Watch a Gradle instrumented-test run to a verdict without hand-writing greps. Use whenever running :app:testEmulatorDebugAndroidTest or :app:connectedDebugAndroidTest in the background.
In this repository6 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- add-stringsSkill
- device-screenshotsSkill
- investigating-bug-reportsSkill
- watch-prSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 64
A watcher that can exit silently is not a watcher: If yours can end without printing, make the last thing it does print where the run got …
- Distinctive: line 78
All three exited 0. Silence from a monitor is indistinguishable from silence from a healthy run, so a red suite was reported as green until …
- Distinctive: line 93
A stall is a result: No output for eight minutes gets reported, with a diagnosis, rather than looking like a slow test for another twenty.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Watching a test run to a verdict
The emulator suite takes upwards of ten minutes, so it gets backgrounded, so something has to report on it. Do not write that something fresh each time. Use the script.
# 1. Start the run, redirected to a file.
./gradlew :app:testEmulatorDebugAndroidTest --console=plain > tmp/run.log 2>&1
# 2. Prove the watcher sees the log before trusting it. One command, always.
python .claude/skills/watch-gradle-tests/watch_tests.py tmp/run.log --once
# 3. Arm a Monitor on the same script with no --once.
python .claude/skills/watch-gradle-tests/watch_tests.py tmp/run.logEach line it prints is one event: FAILED <Class>.<method> as each failure appears,
STALLED … if the log stops growing, and FINISHED + VERDICT at the end.
Give every run its own log file
Reusing one name races the watcher against the run that is starting. Arm a Monitor while the
previous run's log is still on disk and it reads that — matches its BUILD line, prints its
verdict, and reports a finished run that has not started. The XML age in VERDICT is the only
hint, and "2 min old" looks perfectly current.
That cost a wrong conclusion here: a stale verdict was read as the new run's, its crash trace pointed at a line number the fix had already moved, and the obvious inference — "the APK did not rebuild" — was wrong twice over.
log=tmp/run-$(date +%H%M%S).log
./gradlew :app:testEmulatorDebugAndroidTest --console=plain > "$log" 2>&1 &
python .claude/skills/watch-gradle-tests/watch_tests.py "$log"Never wrap --once in your own sleep loop
That is the shape step 3 exists to replace, and it looks close enough to right to pass review:
# WRONG - and this exact loop cost 1h22m
for i in $(seq 1 110); do
grep -qE "BUILD SUCCESSFUL|BUILD FAILED" tmp/run.log && { ...--once; break; }
sleep 20
doneIt waits for a terminal line and nothing else, so it is blind to the run stopping without
one — which is the failure worth catching. A suite hung 25 minutes on a single test produced no
new output and no verdict, so the loop sat silent, then hit its own limit and exited 0 with no
output at all: indistinguishable from success. Meanwhile watch mode would have said
STALLED no output for 8 min, at 424/687 seventeen minutes earlier.
Two rules follow, and they are the same rule twice:
- Watch progress, not just completion. "Still running" and "wedged" look identical unless something is measuring the gap between outputs.
- A watcher that can exit silently is not a watcher. If yours can end without printing, make the last thing it does print where the run got to.
Why this exists
Three hand-written monitors in one afternoon each matched nothing, and each looked like a green run:
| What was written | Why it matched nothing |
grep "(0 skipped)" | the run had 5 skipped |
grep "\S+ \[testEmulator\]" | there is no space before [testEmulator] |
... | head -25 | truncated before the verdict, and head's exit code hid it |
All three exited 0. Silence from a monitor is indistinguishable from silence from a healthy
run, so a red suite was reported as green until the log was read by hand. That is the whole
argument for a script: the patterns get fixed once, and --once proves they still match before
anything depends on them.
What it knows that a grep does not
- The XML has the last word. The console can truncate, interleave, or count a retry
(
605/600is a real line from this repo).verdict_from_xmlreadsapp/build/outputs/androidTest-results/, and scopes to the newest run's own directory — AGP keepsconnected/andmanagedDevice/side by side and clears neither, so summing everything reported a 22-test run as 74. It always prints the results' age, because a verdict that is quietly ten minutes old is the same bug wearing a coat. - No results at all is not zero failures. A run that dies before any test reports writes no XML, and reading that as success is exactly how a red build gets called green.
- A stall is a result. No output for eight minutes gets reported, with a diagnosis, rather than looking like a slow test for another twenty.
The two silent hangs, which need opposite fixes
The script separates them by whether any test has reported. Do not guess between them — the distinction is in the output.
| Symptom | Cause | Fix |
No test ever reports, no managed AVD in adb devices | leaked managed-device slots — MDLockCount above zero with nothing running | ./gradlew --stop && rm ~/.android/avd/gradle-managed/active_gradle_devices |
| Tests ran, then stopped mid-suite | the device died under them | adb logcat -b crash — the emulator's Bluetooth stack aborting has done this here |
connectedDebugAndroidTest fails to install or hangs at once | stale ADB bridge inside the Gradle daemon | restart the emulator and ./gradlew --stop — either alone leaves it broken |
Killing a run is what leaks a slot, and every Ctrl-C adds one. Prefer letting a run finish; if
you must kill one, clear the count in the same breath rather than meeting it next time.
Related
AGENTS.md, "Building and testing" — the managed device, and why it beats a hand-started emulator..claude/skills/watch-pr/— the same discipline for CI: prove the poll body emits before arming a Monitor on it.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

