--- 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 " 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)