How long a book takes to appear depends on the network it is opened on, and none of that is visible to the unit tests: it only shows up on a real device, against a server that is really not answering. Added a tests/ directory for checks of that kind, with two to start. One asserts that a book opens promptly both with the radios off and with a network whose server swallows what it is sent, the second being the case that had stopped opening books altogether. The other prints a table of opening times across four conditions, which is what showed that the radios being off was never the slow case. Each check reads the app's own database for its answer rather than watching the screen, times against the device's clock so that talking to it over a cable is not counted, and puts back everything it changed however it exits. Two details matter more than they look. The address used to stand in for a server that never answers is one reserved for documentation, because only the address is changed and the app goes on sending the credentials it already had — a private address would be perfectly capable of belonging to somebody. And every value read off the device is quoted before being sent back to it, since a book whose file name contains an apostrophe would otherwise end the command and run the remainder. They are meant to be run unattended, by a person or by an agent, and say what they did through their exit code. `make e2e` runs them all; they are kept out of `make check` because they need a device that a checkout does not have. |
||
|---|---|---|
| .. | ||
| lib | ||
| bench-open | ||
| offline-open | ||
| README.md | ||
| run-all | ||
tests/
End-to-end scenarios that run against a real device or emulator over
adb. They exist for the behaviour that only shows up on a device —
timing, connectivity, the reader actually appearing — which is
everything the JVM unit tests in app/src/test cannot see.
They are meant to be run by a person or by an agent, unattended:
tests/run-all # every scenario, on the only attached device
tests/run-all -s emulator-5554 # or a named one
tests/offline-open --help # one scenario, and what it does
Every scenario prints one PASS/FAIL/SKIP line per check and exits
0 when it passed, 3 when it skipped itself, and anything else when
it failed. An agent can branch on the exit code alone. Skipping is kept
separate on purpose: a device that tested nothing must not be reported
as a device that passed.
What is here
| Scenario | What it proves |
|---|---|
offline-open |
A book opens promptly with no network, and — the case that regressed — with a network whose server is unroutable. |
bench-open |
Not pass/fail: prints how long a book takes to open under each network condition. |
Preconditions
- A device or emulator with a debug build installed (
make install). The scenarios read the app's database throughrun-as, which a release build does not allow. - A seeded library.
make resetgives you one. - For anything touching sync, a connected server row in
remote_server. A scenario with no server to work with skips itself rather than failing.
make emulator boots the default AVD; make run-bg installs and
launches without scrcpy.
Writing a new one
Copy the shape of offline-open:
#!/usr/bin/env bash
#
# tests/my-scenario -- one line on what must be true.
#
# ... prose on why this is worth a device test ...
#
# Usage: tests/my-scenario [-s SERIAL]
source "$(dirname "$(readlink -f "$0")")/lib/device.sh"
parse_common_args "$@"
require_device
# ... checks ...
report
--help prints the header comment, so the documentation cannot drift
away from the code. Make the file executable — run-all picks up
whatever is executable in this directory.
What lib/device.sh gives you
die,step,notefor output;ok,bad,skip,reportfor results.reportexits non-zero if anything calledbad.adb— a wrapper pinned to the chosen serial. Always use it.query "<sql>"— SQL against the app's database, andsql_quoteto build a literal. Use it for anything that came off the device: a configured server address is text the reader typed, and one apostrophe pasted straight into a statement ends the string and runs whatever follows.shell_quote— the same care for the device's shell. Everything handed toadb shellis re-parsed there, so a book title with an apostrophe in its file name would otherwise end the argument and run the rest as the shell user.set_server_url "<url>"— points the connected server somewhere, with the app stopped first and the result checked afterwards.airplane_mode_state—onoroff, to read before you change it.device_now_ms— the device's clock, so adb round-trips are not counted as time the app spent working.pick_downloaded_book— setsBOOK_URLandBOOK_LOCAL_URI.time_book_open URL LOCAL_URI [CAP_MS]— milliseconds, ortimeout.expect_open_under LABEL MEASURED BUDGET_MS.airplane_mode on|off.
Two things to get right
Use the database as the oracle. Screen scraping is slow and lies.
Book-open timing keys off books.last_opened_at, which the reader
writes one line before it shows the page, so it is the honest end of
"the book opened".
Restore what you changed, from a trap. These run against a device someone else will use next. Anything a scenario sets — airplane mode, a server address, app data — is put back on the way out, however the script leaves:
trap restore EXIT
Put back what was there, not what you assume: read airplane mode
before turning it on, so a device that had it on keeps it. Stop the app
before writing to its database, and check the value afterwards rather
than announcing success — leaving a blackhole address behind quietly
breaks the next thing anyone does with the device. set_server_url
does all three.
Point a server nowhere real. To make a server unreachable these
scenarios change only its address, so the app keeps the credentials it
had and sends them to whatever answers. Use 192.0.2.1 (TEST-NET-1,
reserved by RFC 5737 and routed nowhere), never a private 10.x or
192.168.x address, which on somebody's network is perfectly capable
of answering.
Related
hack/e2e-* holds the older interactive UI walkthroughs (deletion,
opening from another app, upload), which drive the screen with input tap and need a visible device. Scenarios here are the headless,
assertion-shaped ones. See hack/README.md.