1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
|
# devtools
Small, shared wrappers around Gradle and adb whose only job is to make a build or
test result **impossible to misread**.
These exist because of a specific, repeated failure: concluding that something
passed or failed by reading console text, when the text was about a different
project, a stale result, or a command that never ran. Every script here reports
from exit codes and structured artifacts instead, and fails loudly rather than
defaulting to something plausible.
Invoke by absolute path — `/workspace/devtools/bin/<script>`. Nothing needs to be
on `PATH`, which is itself one less piece of ambient state to get wrong.
---
## The rule these encode
> **Never conclude from grepped stdout. Conclude from exit codes and structured
> artifacts.**
Corollaries, each learned the hard way:
| Trap | What it looks like | Guard |
| --- | --- | --- |
| cwd drift | `cd x && cp y .` fails; the next line runs Gradle in the old directory and prints `BUILD SUCCESSFUL` about the wrong project | `gradle-verify` takes the project dir as an argument, resolves it absolutely, and passes `-p`. A missing dir is fatal, never a fallback. |
| Stale test results | Gradle reports `UP-TO-DATE`, runs nothing, and the XML on disk describes older code | `gradle-test` compares result mtimes against a marker written at run start and refuses stale numbers |
| Zero tests | A test source set that won't compile leaves a green build with nothing executed | `gradle-test` fails when the parsed test count is 0 |
| `cmd \| tail; echo $?` | Reports `tail`'s status, not the command's | Redirect to a file, then check `$?`. Or use `PIPESTATUS[0]`. |
| `... \| grep -q` under `pipefail` | `grep -q` exits on first match and SIGPIPEs upstream; the pipeline reports failure despite matching | Do membership tests in pure shell (`case`), not pipelines |
| `set -e` + a command that legitimately fails | `pid=$(pidof x)` aborts the whole script when the process is gone — i.e. exactly when you are trying to detect a crash | `|| true` on any substitution whose non-zero status is meaningful data |
| Discarded command output | `adb install ... >/dev/null` hides "filename doesn't end .apk"; the old build stays installed and is silently what gets tested | `android-launch-check --apk` checks the install output for `Success` |
| `monkey` as a launch check | Reports a healthy app dead and a dead app healthy; lands on whatever activity is foreground | `android-launch-check` uses `am start -W` against a resolved activity and samples the pid repeatedly |
| Stale **analysis** reports | An invalid detekt config aborts the run *before analysing anything*, leaving the previous XML in place. The findings look current and the line numbers never move | `gradle-report` names the rejected config and refuses any report older than the run |
| Blind `input tap X Y` | Coordinates are a guess about a layout. One tap landed in a different app's settings; the screenshot afterwards was reported as the app under test | `android-ui` verifies the foreground package before acting, and taps by label rather than coordinate |
| Tapping the first of several matches | Every row in a list carries the same "Play" button; the first match is rarely the one meant | `android-ui tap` refuses when a label matches more than one place |
| Publishing without verifying | A published APK went stale for eight weeks; installing it threw `A migration from 11 to 6 was required but not found` | `publish-apk` checks the host, proves the file is really an APK, byte-compares what landed, and confirms what the server serves |
---
## Scripts
### `gradle-verify <project-dir> <task...> [-- <gradle args>]`
Runs Gradle against an explicitly named project. Exits with Gradle's exit code.
Prints the absolute project directory actually used, so the output is
self-describing.
```
/workspace/devtools/bin/gradle-verify /workspace/nav/android-app compileDebugKotlin
```
### `gradle-test <project-dir> [task] [--allow-stale] [-- <gradle args>]`
Runs the test task, then reports counts parsed from the JUnit XML. Auto-detects
`testDebugUnitTest` for Android projects and `test` otherwise.
Fails on: non-zero Gradle exit, any failure or error in the XML, **zero tests
executed**, **no XML at all**, or **results that predate this run**.
```
/workspace/devtools/bin/gradle-test /workspace/nav/android-app -- --rerun-tasks
```
`--rerun-tasks` is usually what you want when the answer matters — otherwise
Gradle may skip the task and the script will (correctly) refuse the stale
results.
### `android-launch-check <package> [--apk <path>] [--serial <s>] [--settle <secs>]`
Answers "does this build actually start and stay up", and shows the exception if
not. Optionally installs an APK first, verifying the install genuinely succeeded.
Exit codes: `0` launched and stayed up · `1` crashed or died · `2` operator error
(no device, package not installed, unusable filename).
```
/workspace/devtools/bin/android-launch-check org.terst.feedreader \
--apk /path/to/app-debug.apk --settle 15
```
---
## Tests
```
/workspace/devtools/test/run-tests.sh
```
12 cases, mostly negative — each drives a script into one failure mode and
asserts it is caught. A wrapper that only ever reports PASS is worse than no
wrapper, so the negative cases are the point.
### `gradle-report <project-dir> <detekt|lint> [-- <gradle args>]`
Runs a static-analysis task and reports from its XML, refusing to speak if the
report is older than the run.
The failure this exists for is subtle: detekt rejects an invalid config property
*before* analysing anything, exits non-zero, and leaves the previous run's XML
untouched. Read that file and you get a plausible list of findings describing
code that no longer exists — which is exactly what happened three times in one
session, the giveaway being line numbers that never moved after a refactor.
```
gradle-report /workspace/listen detekt -- --rerun-tasks
```
Prints `CONFIG INVALID` and names the offending property when the config is
rejected; prints `STALE` when the report predates the run; otherwise lists the
findings and passes only on zero.
### `android-ui <package> <dump|tap|shot|foreground> [args]`
Drives an app on a device without guessing at coordinates.
```
android-ui org.terst.listen foreground # is the right app even in front?
android-ui org.terst.listen dump # what is on screen, with tap points
android-ui org.terst.listen tap "Settings" # tap by label, not by coordinate
android-ui org.terst.listen shot /tmp/x.png # screenshot, foreground verified
```
Every command verifies the foreground package first. `tap` refuses when a label
matches more than one element, because in a list where every row has the same
button the first match is rarely the one meant, and prefers a clickable node
when a label and its button are separate nodes.
The parsing lives in `lib/uinodes.py` rather than inline in the shell script:
the first version embedded it as `python3 -c` inside a bash `case` block, and
the nested quoting silently mangled the regexes — the dump reported one element
and a tap landed on the wrong one.
### `publish-apk <apk> <published-name> [--host H] [--dir D]`
Publishes a built APK to the static site, checking each thing that has gone
wrong before: that this is the deploy host and not a box resembling it, that the
file is genuinely an APK rather than an HTML error page, that what landed is
byte-identical to what was tested, and that the web server serves those same
bytes. Backs up any existing file first, keeping the two most recent (`--keep N`) — each backup is
a full copy of a large APK in a public directory, and four publishes in one evening left
448MB of them.
```
publish-apk app/build/outputs/apk/debug/app-debug.apk listen-debug.apk
```
From another machine — the laptop — send it to the real host rather than
writing locally:
```
PUBLISH_SSH=user@tungsten publish-apk app/build/outputs/apk/debug/app-debug.apk listen-debug.apk
```
The host check still applies: without `PUBLISH_SSH` a build from elsewhere is
refused outright, and with it the file is copied to a temporary name and moved
into place only once the transfer completes. The remote copy is hash-compared
against the source and the served bytes are checked, same as a local publish.
|