mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat(folio): ask which android device to run on when none is named
This commit is contained in:
1 parent
8c31e2bf6f
commit
419ed9d1c0
2 files changed
+116
-27
No files matched your search
@@ -23,15 +23,17 @@ example sanderling runs its property-based specs against.
|
|||||||
## Android
|
## Android
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
just install # asks which device, then builds + installs
|
||||||
ANDROID_DEVICE=emulator-5554 just install # build + install on that device
|
ANDROID_DEVICE=emulator-5554 just install # build + install on that device
|
||||||
ANDROID_DEVICE=emulator-5554 just uninstall
|
ANDROID_DEVICE=emulator-5554 just uninstall
|
||||||
just clean
|
just clean
|
||||||
```
|
```
|
||||||
|
|
||||||
`ANDROID_DEVICE` is the serial `adb devices` reports. Every recipe that
|
`ANDROID_DEVICE` is the serial `adb devices` reports. Every recipe that
|
||||||
installs, uninstalls or fuzzes refuses to run without it, unless the only
|
installs, uninstalls or fuzzes acts on that serial. Without it they print the
|
||||||
device adb can see is a single emulator on the local adb server. The refusal
|
online devices and ask which one to use, and pick on their own only when the
|
||||||
prints what adb currently sees.
|
one device adb can see is an emulator on the local adb server. With no terminal
|
||||||
|
to ask on, a CI job say, the ask becomes a refusal that prints what adb sees.
|
||||||
|
|
||||||
A run installs the app, clears its state and drives it, which is not something
|
A run installs the app, clears its state and drives it, which is not something
|
||||||
to do to a handset that happens to be the one thing plugged in. An emulator is
|
to do to a handset that happens to be the one thing plugged in. An emulator is
|
||||||
@@ -73,9 +75,9 @@ password: ledger123
|
|||||||
ANDROID_DEVICE=emulator-5554 just test
|
ANDROID_DEVICE=emulator-5554 just test
|
||||||
```
|
```
|
||||||
|
|
||||||
The same naming rule as `just install` applies. If nothing is attached at all,
|
The same naming rule as `just install` applies, and `just test` asks once for
|
||||||
`just test` boots a bootable AVD and runs against that. With multiple AVDs,
|
the whole run. If nothing is attached at all, `just test` boots a bootable AVD
|
||||||
pick one:
|
and runs against that. With multiple AVDs, pick one:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
AVD=Pixel_7 just test
|
AVD=Pixel_7 just test
|
||||||
@@ -90,14 +92,19 @@ DURATION=5m
|
|||||||
|
|
||||||
The device does not have to be attached to this machine. `ADB_SERVER_SOCKET`
|
The device does not have to be attached to this machine. `ADB_SERVER_SOCKET`
|
||||||
aims adb at another host's adb server, and `ANDROID_DEVICE` names the serial
|
aims adb at another host's adb server, and `ANDROID_DEVICE` names the serial
|
||||||
that server reports. A remote server is shared, so `ANDROID_DEVICE` is required
|
that server reports. A remote server is shared, so nothing there is ever picked
|
||||||
there even when it holds only one device:
|
without being named or asked about, one device on it or twenty:
|
||||||
|
|
||||||
```
|
```
|
||||||
ADB_SERVER_SOCKET=tcp:10.0.0.5:5037
|
ADB_SERVER_SOCKET=tcp:10.0.0.5:5037
|
||||||
ANDROID_DEVICE=emulator-5556
|
ANDROID_DEVICE=emulator-5556
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The older `ANDROID_ADB_SERVER_ADDRESS` / `ANDROID_ADB_SERVER_PORT` pair works
|
||||||
|
too, at the same precedence the adb CLI gives it. A remote server is never
|
||||||
|
auto-booted against: when it reports no device, `just test` says so rather than
|
||||||
|
starting a local emulator that server will never see.
|
||||||
|
|
||||||
Gradle only assembles the APK. The install goes through adb, which reads those
|
Gradle only assembles the APK. The install goes through adb, which reads those
|
||||||
variables, so a remote server needs nothing else. Gradle's own `installDebug`
|
variables, so a remote server needs nothing else. Gradle's own `installDebug`
|
||||||
cannot be used here: its adb client only ever dials loopback.
|
cannot be used here: its adb client only ever dials loopback.
|
||||||
|
|||||||
+101
-19
@@ -39,9 +39,56 @@ _android-home:
|
|||||||
echo "could not locate Android SDK (set ANDROID_HOME)" >&2
|
echo "could not locate Android SDK (set ANDROID_HOME)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
# Ask which of a list of targets to act on. Callers set FOLIO_PICK_ITEMS to
|
||||||
|
# "value<TAB>label" lines and read one line back: "picked <value>", "cancelled"
|
||||||
|
# or "no-tty". The answer goes to stdout rather than an exit code so a caller
|
||||||
|
# reading it does not have to sieve just's own failure line out of the menu.
|
||||||
|
_pick header:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if ! { exec 3</dev/tty; } 2>/dev/null; then
|
||||||
|
echo "no-tty"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
values=()
|
||||||
|
labels=()
|
||||||
|
while IFS=$'\t' read -r value label; do
|
||||||
|
[[ -n "$value" ]] || continue
|
||||||
|
values+=("$value")
|
||||||
|
labels+=("${label:-$value}")
|
||||||
|
done <<<"$FOLIO_PICK_ITEMS"
|
||||||
|
echo "{{header}}" >&2
|
||||||
|
for i in "${!values[@]}"; do
|
||||||
|
printf ' %d) %s\n' "$((i + 1))" "${labels[$i]}" >&2
|
||||||
|
done
|
||||||
|
while :; do
|
||||||
|
printf 'folio: number (enter to cancel): ' >&2
|
||||||
|
read -r -u 3 reply || break
|
||||||
|
[[ -n "$reply" ]] || break
|
||||||
|
if [[ "$reply" =~ ^[0-9]+$ ]] && (( reply >= 1 && reply <= ${#values[@]} )); then
|
||||||
|
echo "picked ${values[$((reply - 1))]}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "folio: not one of the numbers above." >&2
|
||||||
|
done
|
||||||
|
echo "cancelled"
|
||||||
|
|
||||||
|
# Print where adb has been aimed when that is not this machine's default
|
||||||
|
# server, and nothing when it is. Read the way the adb CLI reads it, so the
|
||||||
|
# recipes that only ever guess at a local emulator can tell the difference.
|
||||||
|
_adb-server:
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -n "${ADB_SERVER_SOCKET:-}" ]]; then
|
||||||
|
echo "${ADB_SERVER_SOCKET}"
|
||||||
|
elif [[ -n "${ANDROID_ADB_SERVER_ADDRESS:-}${ANDROID_ADB_SERVER_PORT:-}" ]]; then
|
||||||
|
echo "tcp:${ANDROID_ADB_SERVER_ADDRESS:-localhost}:${ANDROID_ADB_SERVER_PORT:-5037}"
|
||||||
|
fi
|
||||||
|
|
||||||
# Ensure an Android device is online. If none is connected and no AVD is
|
# Ensure an Android device is online. If none is connected and no AVD is
|
||||||
# provided, boot the first AVD whose system image is actually installed
|
# provided, boot the first AVD whose system image is actually installed
|
||||||
# (headless) and wait for it to finish booting.
|
# (headless) and wait for it to finish booting. A remote adb server is left
|
||||||
|
# alone: an emulator booted here would never appear on it.
|
||||||
_ensure-device:
|
_ensure-device:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@@ -50,6 +97,11 @@ _ensure-device:
|
|||||||
if "$adb" devices | awk 'NR>1 && $2=="device"{f=1} END{exit !f}'; then
|
if "$adb" devices | awk 'NR>1 && $2=="device"{f=1} END{exit !f}'; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
server="$(just _adb-server)"
|
||||||
|
if [[ -n "$server" ]]; then
|
||||||
|
echo "no device is online on the adb server at $server, and an emulator booted here would not appear on it" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
if [[ -n "{{avd}}" ]]; then
|
if [[ -n "{{avd}}" ]]; then
|
||||||
exit 0 # sanderling boots the named AVD itself
|
exit 0 # sanderling boots the named AVD itself
|
||||||
fi
|
fi
|
||||||
@@ -79,15 +131,16 @@ _ensure-device:
|
|||||||
echo "emulator did not finish booting in time (see /tmp/folio-emulator.log)" >&2
|
echo "emulator did not finish booting in time (see /tmp/folio-emulator.log)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
# Print the serial every device-affecting recipe must act on, or refuse. A run
|
# Print the serial every device-affecting recipe must act on. A run installs
|
||||||
# installs the app, clears its state and fuzzes it, so the target is never
|
# the app, clears its state and fuzzes it, so the target is never inferred from
|
||||||
# inferred from "whatever adb resolved to": the one case it picks on its own is
|
# "whatever adb resolved to": the one case it picks on its own is a single
|
||||||
# a single emulator on the local adb server, which is cheap to rebuild.
|
# emulator on the local adb server, which is cheap to rebuild. Anything else is
|
||||||
|
# asked about when there is a terminal to ask on, and refused when there is not.
|
||||||
_require-device:
|
_require-device:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
adb="$(just _android-home)/platform-tools/adb"
|
adb="$(just _android-home)/platform-tools/adb"
|
||||||
listing="$("$adb" devices)"
|
listing="$("$adb" devices -l)"
|
||||||
online="$(echo "$listing" | awk 'NR>1 && $2=="device"{print $1}')"
|
online="$(echo "$listing" | awk 'NR>1 && $2=="device"{print $1}')"
|
||||||
count="$(printf '%s' "$online" | grep -c . || true)"
|
count="$(printf '%s' "$online" | grep -c . || true)"
|
||||||
|
|
||||||
@@ -106,6 +159,12 @@ _require-device:
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model_of() {
|
||||||
|
printf '%s\n' "$listing" | awk -v serial="$1" '$1 == serial {
|
||||||
|
for (i = 3; i <= NF; i++) if ($i ~ /^model:/) { sub(/^model:/, "", $i); print $i; exit }
|
||||||
|
}'
|
||||||
|
}
|
||||||
|
|
||||||
if [[ -n "{{android_device}}" ]]; then
|
if [[ -n "{{android_device}}" ]]; then
|
||||||
if printf '%s\n' "$online" | grep -qxF "{{android_device}}"; then
|
if printf '%s\n' "$online" | grep -qxF "{{android_device}}"; then
|
||||||
echo "{{android_device}}"
|
echo "{{android_device}}"
|
||||||
@@ -116,13 +175,33 @@ _require-device:
|
|||||||
if [[ "$count" -eq 0 ]]; then
|
if [[ "$count" -eq 0 ]]; then
|
||||||
refuse "no device is online."
|
refuse "no device is online."
|
||||||
fi
|
fi
|
||||||
if [[ -z "${ADB_SERVER_SOCKET:-}" && "$count" -eq 1 && "$online" =~ ^emulator-[0-9]+$ ]]; then
|
if [[ -z "$(just _adb-server)" && "$count" -eq 1 && "$online" =~ ^emulator-[0-9]+$ ]]; then
|
||||||
echo "$online"
|
echo "$online"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
refuse "refusing to install on and fuzz a device nobody named. A run installs
|
|
||||||
the app, clears its state and drives it, so the only target it picks on its
|
items="$(printf '%s\n' "$online" | while read -r serial; do
|
||||||
own is a single emulator on the local adb server."
|
[[ -n "$serial" ]] || continue
|
||||||
|
model="$(model_of "$serial")"
|
||||||
|
printf '%s\t%s%s\n' "$serial" "$serial" "${model:+ $model}"
|
||||||
|
done)"
|
||||||
|
answer="$(FOLIO_PICK_ITEMS="$items" just _pick "folio: a run installs the app, clears its state and drives it. Pick the device:")"
|
||||||
|
case "$answer" in
|
||||||
|
"picked "*)
|
||||||
|
serial="${answer#picked }"
|
||||||
|
echo "folio: using $serial (ANDROID_DEVICE=$serial in examples/folio/.env skips this)" >&2
|
||||||
|
echo "$serial"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
cancelled)
|
||||||
|
echo "folio: cancelled." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
refuse "refusing to install on and fuzz a device nobody named, and there is no
|
||||||
|
terminal here to ask on. A run installs the app, clears its state and drives
|
||||||
|
it, so the only target it picks on its own is a single emulator on the local
|
||||||
|
adb server."
|
||||||
|
|
||||||
# Run folio's own unit tests. Named test-unit because `test` is the fuzz run.
|
# Run folio's own unit tests. Named test-unit because `test` is the fuzz run.
|
||||||
test-unit:
|
test-unit:
|
||||||
@@ -138,15 +217,16 @@ build:
|
|||||||
export ANDROID_HOME="$(just _android-home)"
|
export ANDROID_HOME="$(just _android-home)"
|
||||||
./gradlew :app:androidApp:assembleDebug
|
./gradlew :app:androidApp:assembleDebug
|
||||||
|
|
||||||
# Build and install the folio APK on a running emulator/device. Gradle only
|
# Build and install the folio APK on a running emulator/device, on the serial
|
||||||
# assembles: adb does the install because it reads ADB_SERVER_SOCKET, so the
|
# given or the one picked by _require-device. Gradle only assembles: adb does
|
||||||
# device may live on a remote adb server, which AGP's loopback-only client
|
# the install because it reads ADB_SERVER_SOCKET, so the device may live on a
|
||||||
# cannot reach.
|
# remote adb server, which AGP's loopback-only client cannot reach.
|
||||||
install: _ensure-device
|
install serial="": _ensure-device
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export ANDROID_HOME="$(just _android-home)"
|
export ANDROID_HOME="$(just _android-home)"
|
||||||
serial="$(just _require-device)"
|
serial="{{serial}}"
|
||||||
|
[[ -n "$serial" ]] || serial="$(just _require-device)"
|
||||||
export ANDROID_SERIAL="$serial"
|
export ANDROID_SERIAL="$serial"
|
||||||
./gradlew :app:androidApp:assembleDebug
|
./gradlew :app:androidApp:assembleDebug
|
||||||
"$ANDROID_HOME/platform-tools/adb" install -r "{{apk}}"
|
"$ANDROID_HOME/platform-tools/adb" install -r "{{apk}}"
|
||||||
@@ -217,12 +297,13 @@ ios-device:
|
|||||||
|
|
||||||
# Run 'sanderling test' against the folio app. Uses a connected device if one is
|
# Run 'sanderling test' against the folio app. Uses a connected device if one is
|
||||||
# online; otherwise boots AVD=<name> when provided, or auto-boots a bootable AVD.
|
# online; otherwise boots AVD=<name> when provided, or auto-boots a bootable AVD.
|
||||||
# Depends on install so the run always fuzzes the current build, matching
|
# Installs before it runs so the run always fuzzes the current build, matching
|
||||||
# test-ios which rebuilds and reinstalls the app first.
|
# test-ios which rebuilds and reinstalls the app first.
|
||||||
test: install
|
test: _ensure-device
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
serial="$(just _require-device)"
|
serial="$(just _require-device)"
|
||||||
|
just install "$serial"
|
||||||
device_flag=(--device "$serial")
|
device_flag=(--device "$serial")
|
||||||
if [[ -n "{{avd}}" ]]; then
|
if [[ -n "{{avd}}" ]]; then
|
||||||
device_flag+=(--avd "{{avd}}")
|
device_flag+=(--avd "{{avd}}")
|
||||||
@@ -239,10 +320,11 @@ test: install
|
|||||||
# Run 'sanderling test' with the LLM action generator instead of the seeded
|
# Run 'sanderling test' with the LLM action generator instead of the seeded
|
||||||
# fuzzer. Needs OPENROUTER_API_KEY (or OPENAI_API_KEY) in the environment; the
|
# fuzzer. Needs OPENROUTER_API_KEY (or OPENAI_API_KEY) in the environment; the
|
||||||
# model is configured by generator = llm({...}) in spec.ts.
|
# model is configured by generator = llm({...}) in spec.ts.
|
||||||
test-llm: install
|
test-llm: _ensure-device
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
serial="$(just _require-device)"
|
serial="$(just _require-device)"
|
||||||
|
just install "$serial"
|
||||||
device_flag=(--device "$serial")
|
device_flag=(--device "$serial")
|
||||||
if [[ -n "{{avd}}" ]]; then
|
if [[ -n "{{avd}}" ]]; then
|
||||||
device_flag+=(--avd "{{avd}}")
|
device_flag+=(--avd "{{avd}}")
|
||||||
|
|||||||
Reference in new issue
Block a user