Files
android-fs5/docs/learnings/2026-03-02-android-fs5-bats-tdd.md
T
Jane Alesi ad6cbf5fa4 docs: end-of-day — tasks complete, learnings, next steps
- 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
2026-03-02 18:33:04 +01:00

4.4 KiB

description, tags, last_updated
description tags last_updated
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.
android
adb
bats
tdd
shell
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.

# 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).

# 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.

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.

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:

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