- Mark all 33 tasks complete in specs/001 tasks.md (85/85 tests passing) - Add NEXT_STEPS.md: spec 002 completion, shellcheck, README ui docs - Add docs/learnings/2026-03-02: BATS fixtures, UIAutomator XML parsing, KDE Connect graceful degradation, scrcpy headless, JSON output pattern
147 lines
4.4 KiB
Markdown
147 lines
4.4 KiB
Markdown
---
|
|
description: >-
|
|
Learnings from building android-fs5: a full ADB/scrcpy/UIAutomator CLI
|
|
toolset with BATS TDD, covering test fixture patterns, KDE Connect SMS
|
|
integration, and UIAutomator XML parsing in pure Bash.
|
|
tags:
|
|
- android
|
|
- adb
|
|
- bats
|
|
- tdd
|
|
- shell
|
|
last_updated: '2026-03-02'
|
|
---
|
|
|
|
# Android FS5 CLI — BATS TDD Learnings
|
|
|
|
## Context
|
|
|
|
Built `android-fs5` from scratch today: a Bash CLI (`fs5`) for managing a Samsung
|
|
Galaxy FS5 device via ADB, scrcpy, KDE Connect, and UIAutomator. 85 BATS tests,
|
|
all passing. Two specs completed in one session.
|
|
|
|
---
|
|
|
|
## Key Learnings
|
|
|
|
### 1. BATS Fixture Pattern for Offline UI Tests
|
|
|
|
**Problem**: UIAutomator `uiautomator dump` requires a live device. Tests would be
|
|
flaky/slow without a fixture.
|
|
|
|
**Solution**: Capture a real `ui_dump.xml` once, store in `tests/fixtures/`, and
|
|
mock the `adb shell uiautomator dump` call in tests using `PATH` override or
|
|
function mocking.
|
|
|
|
```bash
|
|
# In test setup: override adb to return fixture
|
|
function adb() {
|
|
if [[ "$*" == *"uiautomator dump"* ]]; then
|
|
cat tests/fixtures/ui_dump.xml
|
|
return 0
|
|
fi
|
|
command adb "$@"
|
|
}
|
|
export -f adb
|
|
```
|
|
|
|
**Lesson**: XML fixtures enable fast, deterministic UI parsing tests without device.
|
|
|
|
### 2. UIAutomator XML Parsing in Pure Bash
|
|
|
|
**Problem**: Parsing XML in Bash is fragile with `grep`/`sed`.
|
|
|
|
**Solution**: Use `grep -oP` with Perl regex for attribute extraction. Works reliably
|
|
for UIAutomator's flat XML structure (no deep nesting needed).
|
|
|
|
```bash
|
|
# Extract bounds="[x1,y1][x2,y2]" and compute center
|
|
bounds=$(grep -oP 'bounds="\[\K[^\]]+' <<< "$node")
|
|
# bounds = "x1,y1][x2,y2" → split on "]["
|
|
```
|
|
|
|
**Lesson**: UIAutomator XML is regular enough for grep-based parsing. No need for
|
|
`xmllint` dependency.
|
|
|
|
### 3. KDE Connect SMS — Graceful Degradation
|
|
|
|
**Problem**: `kdeconnect-cli` SMS commands require pairing. Tests must not fail
|
|
when device is unpaired.
|
|
|
|
**Solution**: Check pairing status first; return structured error with setup
|
|
instructions rather than raw CLI error.
|
|
|
|
```bash
|
|
if ! kdeconnect-cli --list-devices 2>/dev/null | grep -q "paired"; then
|
|
out_error "KDE Connect not paired. Run: kdeconnect-cli --pair -d <device-id>"
|
|
exit 2
|
|
fi
|
|
```
|
|
|
|
**Lesson**: Exit code 2 = "device/dependency not available" (distinct from exit 1 =
|
|
"usage error"). Consistent exit codes make tests reliable.
|
|
|
|
### 4. BATS `bats_require_minimum_version` — Don't Run as Bash
|
|
|
|
**Problem**: Running `.bats` files directly with `bash` fails with cryptic errors
|
|
(`bats_require_minimum_version: command not found`).
|
|
|
|
**Solution**: Always run via `bats tests/` or `bats tests/test_foo.bats`. The BATS
|
|
runtime provides the `@test`, `run`, `assert_*` functions.
|
|
|
|
**Lesson**: Add to project README: "Run tests with `bats tests/`, not `bash tests/`."
|
|
|
|
### 5. Scrcpy Headless Mode for Screenshot Automation
|
|
|
|
**Problem**: `scrcpy` opens a window by default; CI/automation needs headless.
|
|
|
|
**Solution**: `scrcpy --no-display --screenshot-file=path.png` captures a screenshot
|
|
without opening a window. Combine with `--serial` for multi-device.
|
|
|
|
```bash
|
|
scrcpy --serial "$ANDROID_SERIAL" --no-display \
|
|
--screenshot-file "$output_path" 2>/dev/null
|
|
```
|
|
|
|
**Lesson**: scrcpy 3.x has `--no-display` + `--screenshot-file` as first-class flags.
|
|
No need for `adb exec-out screencap` workarounds.
|
|
|
|
### 6. JSON Output Pattern for Shell CLIs
|
|
|
|
**Pattern used throughout `fs5`**: Every command supports `--json` flag for
|
|
machine-readable output. Implemented via `lib/output.sh`:
|
|
|
|
```bash
|
|
out_json() {
|
|
# $1 = JSON string (pre-built with jq or printf)
|
|
echo "$1"
|
|
}
|
|
out_human() {
|
|
# $1 = human-readable string
|
|
[[ "${OUTPUT_FORMAT:-human}" == "json" ]] || echo "$1"
|
|
}
|
|
```
|
|
|
|
**Lesson**: Dual-mode output (human + JSON) makes CLI tools composable with AI agents
|
|
and scripts. Use `jq -n` for safe JSON construction.
|
|
|
|
---
|
|
|
|
## Anti-Patterns Avoided
|
|
|
|
| Anti-Pattern | What We Did Instead |
|
|
|---|---|
|
|
| `eval` for dynamic adb commands | Array-based `adb_cmd "${args[@]}"` |
|
|
| Hardcoded device serial in scripts | `.env` file + `ANDROID_SERIAL` env var |
|
|
| `grep` on `adb shell` exit codes | Explicit `adb shell "cmd; echo EXIT:$?"` pattern |
|
|
| Monolithic `fs5` script | Modular `lib/adb.sh`, `lib/output.sh`, `lib/input.sh` |
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [BATS Core Docs](https://bats-core.readthedocs.io/)
|
|
- [scrcpy README](https://github.com/Genymobile/scrcpy)
|
|
- [UIAutomator Docs](https://developer.android.com/training/testing/other-components/ui-automator)
|
|
- [KDE Connect CLI](https://userbase.kde.org/KDEConnect)
|