feat: add android-fs5 CLI toolset

- fs5 entrypoint with status, push, pull, app, shell, logs commands
- lib/adb.sh: ADB wrapper functions with idempotency and --force guards
- lib/output.sh: human + JSON output formatting via Python
- 33 bats tests (6 status, 9 transfer, 7 app, 5 shell, 6 logs) — all passing
- Zero shellcheck warnings
- .env-based config (device serial, log path)
- Spec-Driven Development artifacts in specs/001-android-fs5-management/

Device: exone GmbH FS5 (RD51QE202392, Android 9)
This commit is contained in:
Jane Alesi
2026-03-02 15:55:09 +01:00
commit e6e5f34455
15 changed files with 1713 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bats
# tests/test_transfer.bats — Tests for 'fs5 push/pull' commands (US2)
bats_require_minimum_version 1.5.0
SCRIPT="${BATS_TEST_DIRNAME}/../fs5"
DEVICE_PATH="/sdcard/fs5-test"
setup() {
export DEVICE_SERIAL="RD51QE202392"
export LOG_FILE="/tmp/fs5-test-$$.log"
# Create a temp local file for testing
TEST_FILE=$(mktemp /tmp/fs5-push-test-XXXXXX.txt)
echo "fs5-test-content-$$" > "${TEST_FILE}"
PULL_DEST="/tmp/fs5-pulled-$$.txt"
}
teardown() {
rm -f "${TEST_FILE}" "${PULL_DEST}" "${LOG_FILE}"
# Clean up device test file
adb -s "${DEVICE_SERIAL}" shell "rm -f '${DEVICE_PATH}/$(basename "${TEST_FILE}")'" 2>/dev/null || true
}
@test "push: transfers file to device successfully" {
run "${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")"
[ "${status}" -eq 0 ]
[[ "${output}" == *"Pushed"* ]]
}
@test "push: logs the operation to LOG_FILE" {
run "${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
[ "${status}" -eq 0 ]
[ -f "${LOG_FILE}" ]
grep -q "PUSH" "${LOG_FILE}"
}
@test "push: skips if file exists without --force" {
# First push
"${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
# Second push without --force should warn, not fail
run "${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")"
[ "${status}" -eq 0 ]
[[ "${output}" == *"--force"* ]] || [[ "${output}" == *"exists"* ]]
}
@test "push: overwrites with --force flag" {
"${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
run "${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
[ "${status}" -eq 0 ]
[[ "${output}" == *"Pushed"* ]]
}
@test "push: fails with exit code 1 when source file missing" {
run "${SCRIPT}" push "/nonexistent/file.txt" "${DEVICE_PATH}/file.txt"
[ "${status}" -eq 1 ]
[[ "${output}" == *"not found"* ]]
}
@test "pull: downloads file from device successfully" {
# Push first so we have something to pull
"${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
run "${SCRIPT}" pull "${DEVICE_PATH}/$(basename "${TEST_FILE}")" "${PULL_DEST}"
[ "${status}" -eq 0 ]
[[ "${output}" == *"Pulled"* ]]
[ -f "${PULL_DEST}" ]
}
@test "pull: pulled file matches original" {
"${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
"${SCRIPT}" pull "${DEVICE_PATH}/$(basename "${TEST_FILE}")" "${PULL_DEST}" --force
run diff "${TEST_FILE}" "${PULL_DEST}"
[ "${status}" -eq 0 ]
}
@test "pull: skips if local file exists without --force" {
"${SCRIPT}" push "${TEST_FILE}" "${DEVICE_PATH}/$(basename "${TEST_FILE}")" --force
touch "${PULL_DEST}"
run "${SCRIPT}" pull "${DEVICE_PATH}/$(basename "${TEST_FILE}")" "${PULL_DEST}"
[ "${status}" -eq 0 ]
[[ "${output}" == *"--force"* ]] || [[ "${output}" == *"exists"* ]]
}
@test "pull: fails with exit code 1 when device file missing" {
run "${SCRIPT}" pull "${DEVICE_PATH}/nonexistent-file-xyz.txt" "${PULL_DEST}"
[ "${status}" -eq 1 ]
[[ "${output}" == *"not found"* ]]
}