Shrink the shell-coverage allowlist 115 -> 106: cover scripts/lib
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run

Gives linux_configuration/scripts/lib a real test suite, which takes all
nine of its libraries off meta/shell-coverage-allowlist.txt at once —
coverage is per-directory, so this is the highest-ratio first batch.

139 assertions across four suites, all offline: the mtk toolkit is driven
through its existing MTK_ROOT_FIXTURE seam, and adb is shimmed onto a
narrowed PATH. Nothing reads the wall clock, this host's udev rules, or a
real device, because the suite runs in shell-tests.yml and again on every
pre-push via ci-mirror — a host-dependent assertion would be a flake that
blocks unrelated pushes.

The gate only checks that tests/run_all.sh EXISTS, so an empty one would
mark all nine libraries covered while testing nothing. Each of the four
load-bearing rules was therefore mutation-tested and the suite required to
go red: classify_device's && -> ||, discover_partitions' init_boot/boot
ordering, mtk_select_device's refusal to guess between two phones, and
is_hour_in_range's 10#$ octal guard.

That last one initially SURVIVED. Dropping 10#$ makes bash abort with
"value too great for base", a non-zero exit indistinguishable from an
honest out-of-range result, so the status-only assertion passed either
way. It now asserts stderr is empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-21 23:52:09 +02:00
parent 50854e7837
commit d42d8ff20d
9 changed files with 820 additions and 12 deletions

View File

@ -0,0 +1,20 @@
{
"title": "Shrink the shell-coverage allowlist: a real test suite for linux_configuration/scripts/lib",
"objective": "Start chipping at the 115-entry shell-coverage allowlist with the smallest concentrated batch, so the ratchet narrows rather than merely holding. One tests/run_all.sh covers all nine libraries in that directory at once, which is what makes this the highest-ratio first batch. The nine entries come off the allowlist as a result — the deliverable is the allowlist shrinking, not the suite existing.",
"acceptance_criteria": [
"run_all.sh runs every test_*.sh in the directory and exits 0, with all four suites reporting zero failures.",
"The suite is proven NON-VACUOUS by mutation: the coverage gate only checks that tests/run_all.sh exists, so an empty file would mark all nine libraries covered while testing nothing. Each of the four load-bearing rules must be mutated and the suite must go red for each.",
"meta/shell-coverage-allowlist.txt drops from 115 entries to 106, with zero entries remaining for linux_configuration/scripts/lib/ — the measured delta, not a rounded one.",
"No assertion depends on wall-clock time, this host's udev rules, or a real device: the suite runs in shell-tests.yml and again on every pre-push via ci-mirror, so a host-dependent assertion becomes a push-blocking flake.",
"Sourcing a library must not write outside the test tmpdir — android.sh calls ensure_dir on a $HOME path at source time.",
"shellcheck and shfmt pass with no suppression added."
],
"out_of_scope": [
"Wiring shell-coverage-ratchet into .pre-commit-config.yaml. That edit is denied as a sensitive file and belongs to the user; NEXT_SESSION_PROMPT.md carries the block.",
"The effecting half of these libraries: require_hosts_readable (exec sudo), check_adb_device/check_adb_root (die -> exit), collapse_mounts (umount -l), install_missing_pacman_packages, require_imagemagick, notify. Phase 1 measured this pattern already — decision logic reaches ~90%, effecting code reaches 0%.",
"mtk_udev_catchall_files, which globs /etc/udev/rules.d with no seam and would read this host's real rules.",
"The other 106 allowlist entries; the next concentrated batches are single_use/features/lib (45) and single_use/fixes/lib (22).",
"A percentage coverage bar. The bar remains presence, as settled in Phase 3."
],
"verifier": "bash linux_configuration/scripts/lib/tests/run_all.sh (exit 0; 139 assertions across 4 suites). Four mutations applied to throwaway copies, each required to turn the suite red: classify_device's `&&` -> `||` (would classify a stranger's MediaTek phone as the target device), discover_partitions' `init_boot boot` -> `boot init_boot` (would dump the wrong partition on Android 13+ hardware), mtk_select_device's `-gt 1` -> `-gt 99` (would silently pick one of two attached phones), and is_hour_in_range's `10#$` removal (the 08/09 octal trap). Allowlist delta measured with `grep -vc '^#\\|^$' meta/shell-coverage-allowlist.txt` before and after --seed."
}

View File

@ -0,0 +1,72 @@
{
"intent": "Shrink the shell-coverage allowlist from 115 entries to 106 by giving linux_configuration/scripts/lib a real test suite, so the ratchet narrows instead of merely holding the line. Nine libraries lose their exemption at once, because coverage is per-directory.",
"scope": [
"New: linux_configuration/scripts/lib/tests/ — run_all.sh, mtk_harness.sh, and four test_*.sh suites",
"Modified: meta/shell-coverage-allowlist.txt — reseeded, 115 -> 106 entries, all nine linux_configuration/scripts/lib/ entries removed",
"No change to any library under test. This commit adds tests and removes exemptions; it does not touch the code being tested.",
"Non-goal: wiring shell-coverage-ratchet into .pre-commit-config.yaml — denied as a sensitive file, so it stays with the user (see NEXT_SESSION_PROMPT.md)",
"Non-goal: the effecting half of these libraries (sudo re-exec, umount -l, pacman, adb against real hardware) and mtk_udev_catchall_files, which has no seam"
],
"changes": [
"Covered the mtk toolkit through its existing MTK_ROOT_FIXTURE seam, which mtk_common.sh documents as the only boundary with real hardware — props.txt and by-name.txt fixtures make the whole suite offline and hermetic",
"Asserted the three safety rules that a plausible refactor would break: classify_device requires a MediaTek signal AND a vendor match (never either, so a third phone lands in UNKNOWN); discover_partitions prefers init_boot over boot; mtk_select_device refuses to guess between two attached devices",
"Shimmed adb onto a narrowed PATH for the device-selection suite, with the shim exiting 97 on any unexpected subcommand rather than returning an empty string that would read as 'no devices'",
"Kept every assertion independent of wall-clock time: get_hour and get_day_of_week are shimmed where a specific value is needed, and the real /proc/uptime readers are asserted on shape only. This suite runs in shell-tests.yml and again on every pre-push via ci-mirror, so a time-dependent assertion would be a push-blocking flake.",
"Redirected HOME to a tmpdir before sourcing, because android.sh runs ensure_dir on a $HOME path at source time — merely loading the library writes to ~/.cache otherwise",
"Named the deliberately-untested functions and the reason for each in mtk_harness.sh's header, since the gate checks only that run_all.sh exists and cannot tell a real suite from an empty one"
],
"verification": [
{
"command": "bash linux_configuration/scripts/lib/tests/run_all.sh",
"result": "pass",
"evidence": "exit 0; 139 assertions across 4 suites, 0 failed (common_datetime 55, mtk_partitions 36, mtk_device 26, mtk_classify 22)."
},
{
"command": "mutation: classify_device's `$mtk_match -eq 1 && $ulefone_match -eq 1` -> `||`, on a throwaway copy",
"result": "pass",
"evidence": "Suite exits 1. This is the regression that would classify a stranger's MediaTek phone as the target device; the ULEFONE and PIXEL cases still pass under the mutation, so only the third-phone case catches it."
},
{
"command": "mutation: discover_partitions' `for candidate in init_boot boot` -> `boot init_boot`",
"result": "pass",
"evidence": "Suite exits 1. Checking boot first picks the wrong ramdisk carrier on Android 13+ hardware, silently, because both partitions exist and both reads succeed."
},
{
"command": "mutation: mtk_select_device's `${#serials[@]} -gt 1` -> `-gt 99`",
"result": "pass",
"evidence": "Suite exits 1. A version that took serials[0] with two phones attached passes every other assertion in that file."
},
{
"command": "mutation: is_hour_in_range's `$((10#$current_hour))` -> `$((current_hour))`",
"result": "pass",
"evidence": "Suite exits 1 — but only after the assertion was strengthened. The first version checked exit status alone and the mutation SURVIVED: bash aborts with 'value too great for base (error token is \"09\")', a non-zero exit indistinguishable from an honest out-of-range result. The assertion now checks stderr is empty."
},
{
"command": "grep -vc '^#\\|^$' meta/shell-coverage-allowlist.txt (before, after)",
"result": "pass",
"evidence": "115 -> 106. `grep -c 'linux_configuration/scripts/lib/'` goes 9 -> 0, matching the predicted delta exactly."
},
{
"command": "bash meta/scripts/check_shell_coverage.sh linux_configuration/scripts/lib/mtk_classify.sh linux_configuration/scripts/lib/common_datetime.sh",
"result": "pass",
"evidence": "exit 0 — the nine libraries are now covered on their merits rather than exempt."
},
{
"command": "git ls-files -s linux_configuration/scripts/lib/tests/",
"result": "pass",
"evidence": "All six files staged 100755. Staged with `git add --chmod=+x`; a plain `git add` afterwards would reset the bit and the shebang hook rejects 100644."
}
],
"risks": [
"Coverage is presence, not a percentage. The gate cannot distinguish these 139 assertions from an empty run_all.sh, and the nine libraries are now permanently off the allowlist — if a later commit guts the suite, nothing detects it. The harness header naming what is and is not exercised is the only durable record.",
"The effecting halves of android.sh, common_packages.sh and common_services.sh remain at 0%, so the exemption has been traded for partial coverage rather than full coverage. This is Phase 1's measured pattern, not a regression, but the allowlist no longer records it.",
"The adb shim narrows PATH to a shim dir plus /usr/bin and /bin. If mtk_device.sh later reaches for an external not seeded there, the suite aborts under `set -e` with no stderr — the exact failure that cost twenty minutes in Phase 1.",
"mtk_check_venv is asserted only on its 'missing' branch, reached by pointing MTK_ROOT_CACHE at an empty tmpdir. The 'broken' branch (a venv that exists but fails `import usb`) is unexercised.",
"This suite now runs on every pre-push via ci-mirror, so it is on the critical path for pushing any change to this repo, not just shell changes."
],
"rollback": [
"git revert this commit. It restores the nine allowlist entries and deletes the tests directory; no library under test changes, so nothing executable is affected.",
"After rollback, confirm `bash meta/scripts/check_shell_coverage.sh --all` reports 115 exempt entries and exits 0.",
"To keep the suite but restore the exemptions, delete linux_configuration/scripts/lib/tests/run_all.sh only — is_covered() checks for that one file, so the other suites can stay in place while the directory reverts to exempt."
]
}

View File

@ -0,0 +1,119 @@
#!/usr/bin/env bash
# lib/tests/mtk_harness.sh — shared fixture for linux_configuration/scripts/lib.
#
# Sourced, not executed.
#
# WHAT IS EXERCISED
# The pure decision logic and everything reachable through the
# MTK_ROOT_FIXTURE seam that mtk_common.sh documents as its only boundary
# with real hardware: classify_device, discover_partitions,
# mtk_ramdisk_carrier, mtk_check_layout_expectation, mtk_assert_mediatek,
# mtk_sanitize, mtk_sane_ramdisk_size, mtk_select_device/require_authorized
# under a fixture, plus the string helpers in common_datetime.sh.
#
# WHAT IS DELIBERATELY NOT EXERCISED, AND WHY
# The coverage gate only checks that this directory HAS a run_all.sh, so it
# cannot tell a real suite from an empty one. Naming the gaps is therefore
# the only honest record of them:
# - require_hosts_readable / init_android_script — `exec sudo -E bash "$0"`
# would replace the test process with a sudo re-exec of the suite.
# - check_adb_device / check_adb_root — call die(), which exits 1 and
# would take the whole run down; both also need real hardware.
# - collapse_mounts — calls `umount -l` on a live path.
# - install_missing_pacman_packages / require_imagemagick / notify —
# effecting code (pacman, desktop notifications).
# - mtk_udev_catchall_files — globs /etc/udev/rules.d with no seam, so it
# reads this host's real rules; the result differs between here and CI.
# - mtk_sha256 / mtk_file_size — one-line wrappers over sha256sum/stat.
# This matches Phase 1's measured pattern: decision logic reaches ~90%, the
# effecting code reaches 0%.
#
# TIME AND HOST DEPENDENCE
# This suite runs in shell-tests.yml and again on every pre-push via
# ci-mirror, so a wall-clock assertion would pass locally and fail at some
# other hour on some other machine. get_hour and get_day_of_week are
# therefore shimmed where a specific value is needed, and the readers of
# /proc/uptime are asserted on SHAPE, never on value.
set -euo pipefail
PASS=0
FAIL=0
_t_ok() {
PASS=$((PASS + 1))
printf ' OK: %s\n' "$1"
}
_t_bad() {
FAIL=$((FAIL + 1))
printf ' FAIL: %s\n' "$1"
}
# Assert equality. Every check in this suite funnels through here or _t_rc so
# there is exactly one place that counts a result.
_t_is() {
if [[ $2 == "$1" ]]; then
_t_ok "$3"
else
_t_bad "$3 (want '${1}', got '${2}')"
fi
}
# Assert a command's exit status. Run under `if` so a non-zero return is data
# rather than a `set -e` abort.
_t_rc() {
local want="$1" what="$2"
shift 2
local got=0
"$@" >/dev/null 2>&1 || got=$?
_t_is "$want" "$got" "$what"
}
# Assert a substring, for messages whose exact wording is not the contract.
_t_in() {
if [[ $2 == *"$1"* ]]; then
_t_ok "$3"
else
_t_bad "$3 (want substring '${1}')"
fi
}
# ---------------------------------------------------------------------------
# Isolation
# ---------------------------------------------------------------------------
# android.sh runs `ensure_dir "$ANDROID_WORK_DIR"` at SOURCE time, and that
# path is under $HOME. Redirect HOME before sourcing anything or merely
# loading the library writes to the real ~/.cache. MTK_ROOT_CACHE moves the
# mtk toolkit's artifacts for the same reason.
_t_sandbox() {
T_TMP="$(mktemp -d)"
export T_TMP
HOME="$T_TMP/home"
mkdir -p "$HOME"
export HOME
export MTK_ROOT_CACHE="$T_TMP/cache"
}
_t_cleanup() {
[[ -n ${T_TMP:-} && -d ${T_TMP:-} ]] && rm -rf "$T_TMP"
return 0
}
# Build a device fixture. mtk_common.sh reads props.txt as `key=value` lines
# and by-name.txt as one partition per line; those two files ARE the seam.
# Usage: _t_fixture <name> <props-text> <by-name-text>
_t_fixture() {
local dir="$T_TMP/fixtures/$1"
mkdir -p "$dir"
printf '%s\n' "$2" >"$dir/props.txt"
printf '%s\n' "$3" >"$dir/by-name.txt"
printf '%s' "$dir"
}
# Report and set the process exit status. Called at the end of every suite.
_t_done() {
printf '\n %s: %d passed, %d failed\n' "$1" "$PASS" "$FAIL"
_t_cleanup
[[ $FAIL -eq 0 ]]
}

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Runs every linux_configuration/scripts/lib test.
#
# The glob is test_*.sh, NOT one prefix per subject: this directory holds
# suites for several libraries (mtk_*, common_datetime, ...), and a prefix
# glob silently skips every suite added after it — including in the CI
# discovery step in shell-tests.yml, which runs this file.
#
# This file's existence is what meta/scripts/check_shell_coverage.sh checks:
# is_covered() tests for tests/run_all.sh and nothing more, so an empty one
# here would mark all nine libraries in the parent directory covered while
# testing nothing. The suites it runs, and the gaps they leave, are named in
# mtk_harness.sh's header.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
rc=0
for t in "$HERE"/test_*.sh; do
printf '\n=== %s ===\n' "$(basename "$t")"
# Each suite exits non-zero on failure; keep going so one red suite does not
# hide the state of the others, then fail the run as a whole.
# Invoked directly, NOT as `bash "$t"`: under kcov the latter instruments
# bash itself and reports zero lines for every lib. A fresh process per
# suite also matters here, because mtk_common.sh declares readonly
# patterns and sourcing it twice in one shell aborts under set -e.
"$t" || rc=1
done
exit "$rc"

View File

@ -0,0 +1,160 @@
#!/usr/bin/env bash
# Covers the pure helpers in common_datetime.sh: validate_resolution,
# generate_output_filename, has_cmd, ask_yes_no, mount_layers_count, the
# zero-fork time readers, and the two predicates built on them.
#
# NO ASSERTION HERE DEPENDS ON THE WALL CLOCK. This suite runs in
# shell-tests.yml and again on every pre-push via ci-mirror, so
# `is_hour_in_range 9 17` would pass during the working day and fail at
# night — a flake that blocks a push for reasons unrelated to the change.
# The predicates are tested by shimming get_hour/get_day_of_week to fixed
# values; the real readers are asserted on SHAPE only.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../common_datetime.sh
source "$HERE/../common_datetime.sh"
# --- validate_resolution ---------------------------------------------------
_t_rc 0 "a WIDTHxHEIGHT resolution is valid" validate_resolution "1920x1080"
_t_rc 0 "a single-digit resolution is valid" validate_resolution "1x1"
_t_rc 1 "a capital X is not the separator" validate_resolution "1920X1080"
_t_rc 1 "a missing dimension is invalid" validate_resolution "1920x"
_t_rc 1 "a bare number is not a resolution" validate_resolution "1920"
_t_rc 1 "trailing text is rejected" validate_resolution "1920x1080p"
_t_rc 1 "an empty string is not a resolution" validate_resolution ""
# Anchoring matters: an unanchored regex would accept this and the caller
# would go on to pass it to a scaler.
_t_rc 1 "an embedded match is rejected, not extracted" validate_resolution "a1920x1080b"
# --- generate_output_filename ----------------------------------------------
_t_is "./photo_resized.jpg" "$(generate_output_filename "photo.jpg" "_resized")" \
"suffix goes before the extension, not after"
_t_is "/tmp/photo_small.png" "$(generate_output_filename "/tmp/photo.png" "_small")" \
"the directory is preserved"
# A file with no extension: the caller's fallback is used, defaulting to jpg.
_t_is "./photo_x.jpg" "$(generate_output_filename "photo" "_x")" \
"an extensionless input defaults to jpg"
_t_is "./photo_x.png" "$(generate_output_filename "photo" "_x" "png")" \
"an explicit fallback extension is honoured"
# Only the LAST dot separates the extension.
_t_is "./archive.tar_x.gz" "$(generate_output_filename "archive.tar.gz" "_x")" \
"only the final extension is split off"
_t_is "/a/b/c_out.jpg" "$(generate_output_filename "/a/b/c.jpg" "_out")" \
"a nested path is preserved"
# --- has_cmd ---------------------------------------------------------------
_t_rc 0 "has_cmd finds a command that exists" has_cmd bash
_t_rc 1 "has_cmd rejects a command that does not" has_cmd definitely-not-a-real-binary-xyz
# --- ask_yes_no ------------------------------------------------------------
# Reads stdin, so it is drivable without a tty.
_t_yn() {
printf '%s\n' "$1" | ask_yes_no "prompt?" >/dev/null 2>&1
}
_t_rc 0 "'y' is yes" _t_yn "y"
_t_rc 0 "'Y' is yes" _t_yn "Y"
_t_rc 0 "'yes' is yes" _t_yn "yes"
_t_rc 1 "'n' is no" _t_yn "n"
# Default-no is the safety property: an empty answer must never mean yes.
_t_rc 1 "an empty answer defaults to no" _t_yn ""
_t_rc 1 "unrecognised input is no, not a reprompt" _t_yn "maybe"
# --- mount_layers_count ----------------------------------------------------
# A path with no bind mount over it has zero layers. `/` always appears in
# mountinfo, so it is the one value that is stable on any host.
_t_is "0" "$(mount_layers_count "/definitely/not/a/mount/point")" \
"an unmounted path counts zero layers"
_t_rc 0 "a mounted path counts without failing" mount_layers_count "/"
root_layers="$(mount_layers_count "/")"
_t_rc 0 "the root mount count is numeric" grep -qE '^[0-9]+$' <<<"$root_layers"
# --- time readers: shape, never value --------------------------------------
_t_rc 0 "get_timestamp returns digits" grep -qE '^[0-9]+$' <<<"$(get_timestamp)"
_t_rc 0 "get_date is YYYY-MM-DD" grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' <<<"$(get_date)"
_t_rc 0 "get_time is HH:MM:SS" grep -qE '^[0-9]{2}:[0-9]{2}:[0-9]{2}$' <<<"$(get_time)"
_t_rc 0 "get_datetime joins both" \
grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$' <<<"$(get_datetime)"
_t_rc 0 "get_day_of_week is 1-7" grep -qE '^[1-7]$' <<<"$(get_day_of_week)"
_t_rc 0 "get_day_name is alphabetic" grep -qE '^[A-Za-z]+$' <<<"$(get_day_name)"
_t_rc 0 "get_hour is 00-23" grep -qE '^([01][0-9]|2[0-3])$' <<<"$(get_hour)"
_t_rc 0 "get_minute is 00-59" grep -qE '^[0-5][0-9]$' <<<"$(get_minute)"
_t_rc 0 "get_second is 00-60" grep -qE '^([0-5][0-9]|60)$' <<<"$(get_second)"
_t_rc 0 "get_uptime_seconds is a whole number" grep -qE '^[0-9]+$' <<<"$(get_uptime_seconds)"
_t_rc 0 "get_boot_datetime is a datetime" \
grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}$' <<<"$(get_boot_datetime)"
_t_rc 0 "get_boot_date is a date" grep -qE '^[0-9]{4}-[0-9]{2}-[0-9]{2}$' <<<"$(get_boot_date)"
_t_rc 0 "get_boot_hour is 00-23" grep -qE '^([01][0-9]|2[0-3])$' <<<"$(get_boot_hour)"
# Boot time is in the past, and by the same margin /proc/uptime reports. This
# is a relation between two readings, so it holds at any wall-clock time.
now_ts="$(get_timestamp)"
up_s="$(get_uptime_seconds)"
_t_rc 0 "boot precedes now by the uptime" test "$((now_ts - up_s))" -le "$now_ts"
# --- predicates, with the clock shimmed ------------------------------------
# is_hour_in_range is half-open: start inclusive, end exclusive. Overriding
# get_hour is what makes these assertions true at 3am and at 3pm alike.
get_hour() { printf '%s' "$T_FAKE_HOUR"; }
T_FAKE_HOUR=06
_t_rc 0 "06 is inside 5..8" is_hour_in_range 5 8
T_FAKE_HOUR=05
_t_rc 0 "the start hour is inside the range" is_hour_in_range 5 8
T_FAKE_HOUR=08
_t_rc 1 "the end hour is outside the range" is_hour_in_range 5 8
T_FAKE_HOUR=04
_t_rc 1 "an hour before the range is outside" is_hour_in_range 5 8
T_FAKE_HOUR=23
_t_rc 1 "an hour after the range is outside" is_hour_in_range 5 8
# A leading zero must not be read as octal — 08 and 09 are the classic trap,
# and the implementation uses 10#$ precisely to avoid it.
#
# Assert on STDERR, not only on the status. Dropping the 10# prefix makes
# bash abort with "value too great for base", which is a non-zero exit that
# an exit-status-only check reads as an ordinary "hour is out of range" —
# so the mutation survives. Mutation-tested: removing 10#$ fails this.
_t_hour_err() {
T_FAKE_HOUR="$1"
# Capture stderr ONLY: stdout is discarded first, then stderr is pointed at
# it. Braces make the order explicit rather than relying on a bare
# `2>&1 >/dev/null`, which reads backwards.
{ is_hour_in_range "$2" "$3" >/dev/null; } 2>&1 || true
}
for _t_h in 08 09; do
_t_is "" "$(_t_hour_err "$_t_h" 9 17)" "${_t_h} raises no arithmetic error"
done
T_FAKE_HOUR=09
_t_rc 0 "09 is parsed as decimal nine, not invalid octal" is_hour_in_range 9 17
T_FAKE_HOUR=08
_t_rc 1 "08 is decimal eight, so it is below the 9..17 range" is_hour_in_range 9 17
T_FAKE_HOUR=00
_t_rc 0 "midnight is parsed as zero" is_hour_in_range 0 1
_t_is "" "$(_t_hour_err 00 0 1)" "00 raises no arithmetic error"
# is_day_of_week takes a list and matches any of them.
get_day_of_week() { printf '%s' "$T_FAKE_DOW"; }
T_FAKE_DOW=1
_t_rc 0 "Monday matches a list containing it" is_day_of_week 1 5 6 7
T_FAKE_DOW=3
_t_rc 1 "Wednesday does not match that list" is_day_of_week 1 5 6 7
T_FAKE_DOW=7
_t_rc 0 "Sunday matches at the end of the list" is_day_of_week 1 5 6 7
T_FAKE_DOW=4
_t_rc 0 "a single-element list matches" is_day_of_week 4
T_FAKE_DOW=4
_t_rc 1 "an empty list matches nothing" is_day_of_week
_t_done "test_common_datetime.sh"

View File

@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Covers classify_device, mtk_assert_mediatek, mtk_explain_classification
# and mtk_sanitize, all through the MTK_ROOT_FIXTURE seam.
#
# The load-bearing assertion here is the THIRD PHONE case. classify_device's
# contract is that ULEFONE requires a MediaTek signal AND a vendor/model
# match — both, never either — so an unrelated MediaTek handset must land in
# UNKNOWN rather than being treated as the Ulefone. Every caller refuses to
# act on UNKNOWN, and that is what stops this toolkit flashing a stranger's
# phone. A rule rewritten to `||` would still pass the ULEFONE and PIXEL
# cases below; only this one catches it.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../mtk_common.sh
source "$HERE/../mtk_common.sh"
# --- classify_device -------------------------------------------------------
MTK_ROOT_FIXTURE="$(_t_fixture ulefone \
'ro.product.manufacturer=Ulefone
ro.product.model=Armor 26 Ultra
ro.board.platform=mt6989
ro.hardware=mt6989
ro.product.device=Armor26Ultra' 'boot_a')"
export MTK_ROOT_FIXTURE
classify_device
_t_is "ULEFONE" "$MTK_DEVICE_CLASS" "MediaTek platform + Ulefone vendor -> ULEFONE"
_t_in "MediaTek platform" "$MTK_CLASS_REASON" "ULEFONE reason names the platform signal"
MTK_ROOT_FIXTURE="$(_t_fixture pixel \
'ro.product.manufacturer=Google
ro.product.model=Pixel 6a
ro.board.platform=gs101
ro.hardware=oriole
ro.product.device=bluejay' 'boot_a')"
classify_device
_t_is "PIXEL" "$MTK_DEVICE_CLASS" "Google + Pixel identity -> PIXEL"
# A MediaTek phone that is NOT the Ulefone. The single most important case:
# "not a Pixel, so presumably the Ulefone" is exactly the reasoning the
# implementation refuses, and it is what would target the wrong hardware.
MTK_ROOT_FIXTURE="$(_t_fixture third_party \
'ro.product.manufacturer=Xiaomi
ro.product.model=Redmi Note 12
ro.board.platform=mt6833
ro.hardware=mt6833
ro.product.device=tapas' 'boot_a')"
classify_device
_t_is "UNKNOWN" "$MTK_DEVICE_CLASS" "MediaTek but wrong vendor -> UNKNOWN, not ULEFONE"
# Ulefone identity with no MediaTek signal is equally not enough.
MTK_ROOT_FIXTURE="$(_t_fixture ulefone_no_mtk \
'ro.product.manufacturer=Ulefone
ro.product.model=Armor 26 Ultra
ro.board.platform=sm8650
ro.hardware=qcom
ro.product.device=Armor26Ultra' 'boot_a')"
classify_device
_t_is "UNKNOWN" "$MTK_DEVICE_CLASS" "Ulefone identity without MediaTek -> UNKNOWN"
# An unauthorized device answers every getprop with an empty string. That must
# not resolve to a class.
MTK_ROOT_FIXTURE="$(_t_fixture empty '' 'boot_a')"
classify_device
_t_is "UNKNOWN" "$MTK_DEVICE_CLASS" "no properties at all -> UNKNOWN"
_t_in "no rule matched" "$MTK_CLASS_REASON" "UNKNOWN reason explains which reads were empty"
# --- mtk_assert_mediatek ---------------------------------------------------
# A positive assertion, deliberately not "is not a Pixel". It gates the only
# script that touches partitions, so both directions are asserted.
MTK_ROOT_FIXTURE="$(_t_fixture assert_mtk \
'ro.board.platform=mt6989
ro.hardware=mt6989' 'boot_a')"
classify_device
_t_rc 0 "mtk_assert_mediatek accepts an mt#### platform" mtk_assert_mediatek
MTK_ROOT_FIXTURE="$(_t_fixture assert_qcom \
'ro.board.platform=sm8650
ro.hardware=qcom' 'boot_a')"
classify_device
_t_rc 1 "mtk_assert_mediatek rejects a non-MediaTek platform" mtk_assert_mediatek
# hardware alone is enough — some devices report the SoC there and leave
# ro.board.platform unset.
MTK_ROOT_FIXTURE="$(_t_fixture assert_hw_only 'ro.hardware=mt8183' 'boot_a')"
classify_device
_t_rc 0 "mtk_assert_mediatek accepts a MediaTek ro.hardware alone" mtk_assert_mediatek
# --- mtk_explain_classification --------------------------------------------
explain_out="$(mtk_explain_classification 2>&1)"
_t_in "Verdict:" "$explain_out" "explain output states a verdict"
_t_in "mt8183" "$explain_out" "explain output echoes the property it read"
# --- mtk_sanitize ----------------------------------------------------------
# adb shell returns CRLF, which silently breaks string comparisons — the
# reason this function exists at all.
_t_is "mt6989" "$(mtk_sanitize "$(printf 'mt6989\r')")" "sanitize strips a trailing CR"
_t_is "Armor 26 Ultra" "$(mtk_sanitize 'Armor 26 Ultra')" "sanitize keeps spaces and digits"
# tr -cd DELETES disallowed bytes rather than truncating at the first one, so
# the alphanumerics inside a substitution survive as inert text. What matters
# is that $ ( ) ` ; & | cannot come back out.
# The metacharacters are assembled from \-escapes rather than written inside
# single quotes, so that "these must not expand" is stated in the code rather
# than left to the reader (and to shellcheck) to infer.
hostile="rm -rf \$(whoami)\`id\`; reboot & echo|x"
sanitized="$(mtk_sanitize "$hostile")"
_t_is "rm -rf whoamiid reboot echox" "$sanitized" "sanitize deletes shell metacharacters"
for meta in '$' '(' ')' '`' ';' '&' '|'; do
_t_is "" "${sanitized//[^"$meta"]/}" "sanitize leaves no '${meta}' behind"
done
_t_done "test_mtk_classify.sh"

View File

@ -0,0 +1,152 @@
#!/usr/bin/env bash
# Covers mtk_list_devices, mtk_device_state, mtk_select_device and
# mtk_require_authorized.
#
# The load-bearing case is REFUSING TO GUESS. With two phones attached and no
# --serial, mtk_select_device must fail rather than pick one: a wrong choice
# here points a partition-flashing toolkit at the wrong device. A version
# that took serials[0] would pass every other assertion in this file.
#
# adb is shimmed onto PATH rather than mocked in-process, because
# mtk_list_devices calls the binary directly. _t_adb_says writes the shim's
# canned `adb devices` output; nothing here talks to real hardware, and the
# shim refuses any subcommand the tests do not expect.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../mtk_common.sh
source "$HERE/../mtk_common.sh"
# --- adb shim --------------------------------------------------------------
# Narrow PATH to a shim dir. Every external these functions reach for must be
# seeded here: a missing one aborts the suite under `set -e` with no stderr.
_t_shim_dir="$T_TMP/bin"
mkdir -p "$_t_shim_dir"
cat >"$_t_shim_dir/adb" <<'SHIM'
#!/usr/bin/env bash
# Test double. Prints the canned `adb devices` table and nothing else; any
# other subcommand is a bug in the test, so say so loudly rather than
# returning an empty string that would read as "no devices".
if [[ ${1:-} == "devices" ]]; then
cat "${T_ADB_DEVICES:?}"
exit 0
fi
printf 'adb shim: unexpected invocation: %s\n' "$*" >&2
exit 97
SHIM
chmod +x "$_t_shim_dir/adb"
for _t_ext in awk mapfile cat printf; do
_t_bin="$(command -v "$_t_ext" 2>/dev/null || true)"
[[ -n $_t_bin ]] && ln -sf "$_t_bin" "$_t_shim_dir/$_t_ext"
done
PATH="$_t_shim_dir:/usr/bin:/bin"
export PATH
T_ADB_DEVICES="$T_TMP/adb_devices.txt"
export T_ADB_DEVICES
_t_adb_says() {
printf 'List of devices attached\n%s\n' "$1" >"$T_ADB_DEVICES"
}
# --- mtk_list_devices ------------------------------------------------------
_t_adb_says 'ABC123 device'
_t_is "ABC123 device" "$(mtk_list_devices)" "one attached device is listed with its state"
_t_adb_says 'ABC123 device
DEF456 unauthorized'
_t_is "2" "$(mtk_list_devices | wc -l)" "both attached devices are listed"
# A blank table is what an empty `adb devices` looks like.
_t_adb_says ''
_t_is "" "$(mtk_list_devices)" "no attached devices lists nothing"
# --- mtk_device_state ------------------------------------------------------
_t_adb_says 'ABC123 device
DEF456 unauthorized
GHI789 offline'
_t_is "device" "$(mtk_device_state ABC123)" "an authorized device reports 'device'"
_t_is "unauthorized" "$(mtk_device_state DEF456)" "an unauthorized device reports so"
_t_is "offline" "$(mtk_device_state GHI789)" "an offline device reports so"
_t_is "absent" "$(mtk_device_state NOPE000)" "an unattached serial reports 'absent'"
# --- mtk_select_device -----------------------------------------------------
_t_adb_says 'ABC123 device'
unset MTK_SERIAL
_t_rc 0 "a single attached device is selected" mtk_select_device
unset MTK_SERIAL
mtk_select_device
_t_is "ABC123" "$MTK_SERIAL" "the selected serial is the attached one"
# Two devices, no request: must refuse.
_t_adb_says 'ABC123 device
DEF456 device'
unset MTK_SERIAL
_t_rc 1 "two devices with no --serial refuses to guess" mtk_select_device
unset MTK_SERIAL
refuse_out="$(mtk_select_device 2>&1 || true)"
_t_in "refusing to guess" "$refuse_out" "the refusal says why"
_t_in "--serial" "$refuse_out" "the refusal names the way out"
# Two devices WITH a request: the named one is honoured.
unset MTK_SERIAL
mtk_select_device DEF456
_t_is "DEF456" "$MTK_SERIAL" "an explicit serial disambiguates"
# A requested serial that is not attached must fail, not fall back.
unset MTK_SERIAL
_t_rc 1 "a requested serial that is absent fails" mtk_select_device ZZZ999
unset MTK_SERIAL
_t_in "is not attached" "$(mtk_select_device ZZZ999 2>&1 || true)" \
"the absent-serial error names the problem"
# No devices at all.
_t_adb_says ''
unset MTK_SERIAL
_t_rc 1 "no attached devices fails" mtk_select_device
unset MTK_SERIAL
_t_in "No device visible" "$(mtk_select_device 2>&1 || true)" "the no-device error is actionable"
# --- mtk_require_authorized ------------------------------------------------
_t_adb_says 'ABC123 device
DEF456 unauthorized
GHI789 offline'
MTK_SERIAL=ABC123
_t_rc 0 "an authorized device passes the gate" mtk_require_authorized
# The case this function exists for: an unauthorized device answers every
# getprop with an empty string, which reads exactly like "no properties".
MTK_SERIAL=DEF456
_t_rc 1 "an unauthorized device is caught up front" mtk_require_authorized
_t_in "UNAUTHORIZED" "$(mtk_require_authorized 2>&1 || true)" \
"the unauthorized error blames the prompt, not the device"
MTK_SERIAL=GHI789
_t_rc 1 "an offline device is caught" mtk_require_authorized
_t_in "OFFLINE" "$(mtk_require_authorized 2>&1 || true)" "the offline error is actionable"
MTK_SERIAL=NOPE000
_t_rc 1 "an absent device is caught" mtk_require_authorized
# Under a fixture both selection and authorization are bypassed entirely,
# which is what keeps the rest of this directory's suites offline.
MTK_ROOT_FIXTURE="$(_t_fixture selectable '' 'boot')"
export MTK_ROOT_FIXTURE
unset MTK_SERIAL
_t_rc 0 "a fixture selects without adb" mtk_select_device
unset MTK_SERIAL
mtk_select_device
_t_is "fixture" "$MTK_SERIAL" "the fixture serial is a placeholder"
_t_rc 0 "a fixture is always authorized" mtk_require_authorized
_t_done "test_mtk_device.sh"

View File

@ -0,0 +1,142 @@
#!/usr/bin/env bash
# Covers discover_partitions, mtk_ramdisk_carrier,
# mtk_check_layout_expectation, mtk_sane_ramdisk_size and mtk_check_venv.
#
# The load-bearing case is INIT_BOOT WINS OVER BOOT. A device launched on
# Android 13+ carries the ramdisk in init_boot even though boot also exists,
# so a loop that checked boot first would pick the wrong partition on exactly
# the hardware this toolkit targets — and would do it silently, because both
# names are present and both reads succeed.
#
# The second is that discover_partitions consults the FILESYSTEM, never the
# API level. mtk_check_layout_expectation may disagree and warn, but it must
# not change the answer.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=mtk_harness.sh
source "$HERE/mtk_harness.sh"
_t_sandbox
# shellcheck source=../mtk_common.sh
source "$HERE/../mtk_common.sh"
# --- discover_partitions ---------------------------------------------------
# A/B device with init_boot: the ramdisk lives in init_boot, not boot.
MTK_ROOT_FIXTURE="$(_t_fixture ab_initboot 'ro.boot.slot_suffix=_a' \
'boot_a
boot_b
init_boot_a
init_boot_b
vbmeta_a
vbmeta_b
userdata')"
export MTK_ROOT_FIXTURE
_t_rc 0 "discover_partitions succeeds when a ramdisk carrier exists" discover_partitions
discover_partitions
_t_is "A/B" "$MTK_AB_SCHEME" "an _a suffix in the listing means A/B"
_t_is "_a" "$MTK_SLOT_SUFFIX" "slot suffix comes from ro.boot.slot_suffix"
_t_is "init_boot_a" "$MTK_RAMDISK_PART" "init_boot wins over boot when both exist"
_t_is "vbmeta_a" "$MTK_VBMETA_PART" "vbmeta picks up the active slot suffix"
_t_is "init_boot" "$(mtk_ramdisk_carrier)" "carrier strips the slot suffix"
_t_is "0" "$MTK_SLOT_ASSUMED" "a reported slot suffix is not an assumption"
# A-only device with no init_boot: must fall through to boot.
MTK_ROOT_FIXTURE="$(_t_fixture aonly_boot '' \
'boot
recovery
vbmeta
userdata')"
discover_partitions
_t_is "A-only" "$MTK_AB_SCHEME" "no _a suffix means A-only"
_t_is "" "$MTK_SLOT_SUFFIX" "A-only has no slot suffix"
_t_is "boot" "$MTK_RAMDISK_PART" "falls through to boot when init_boot is absent"
_t_is "boot" "$(mtk_ramdisk_carrier)" "carrier of an unsuffixed boot is boot"
# A/B device whose slot_suffix is unreadable. The code falls back to _a
# because a dump has to start somewhere, but must FLAG that it guessed.
MTK_ROOT_FIXTURE="$(_t_fixture ab_no_suffix '' \
'boot_a
boot_b
vbmeta_a
vbmeta_b')"
discover_partitions
_t_is "_a" "$MTK_SLOT_SUFFIX" "A/B with no reported suffix falls back to _a"
_t_is "1" "$MTK_SLOT_ASSUMED" "the _a fallback is recorded as an assumption"
_t_is "boot_a" "$MTK_RAMDISK_PART" "assumed suffix still resolves a carrier"
# An empty listing is what an unauthorized or absent device produces. It must
# fail rather than reporting a plausible-looking empty layout.
MTK_ROOT_FIXTURE="$(_t_fixture no_partitions '' '')"
_t_rc 1 "discover_partitions fails on an empty by-name listing" discover_partitions
discover_partitions || true
_t_is "unknown" "$MTK_AB_SCHEME" "an empty listing yields an unknown scheme"
_t_is "" "$MTK_RAMDISK_PART" "an empty listing names no ramdisk partition"
# A listing with neither boot nor init_boot: non-empty, but no carrier.
MTK_ROOT_FIXTURE="$(_t_fixture no_carrier '' \
'system
vendor
userdata')"
_t_rc 1 "discover_partitions fails when no carrier is present" discover_partitions
# --- mtk_check_layout_expectation ------------------------------------------
# first_api_level >= 33 predicts init_boot. Agreement is quiet.
MTK_ROOT_FIXTURE="$(_t_fixture api34_initboot \
'ro.boot.slot_suffix=_a
ro.product.first_api_level=34' \
'boot_a
init_boot_a')"
discover_partitions
_t_rc 0 "api 34 + init_boot carrier agrees" mtk_check_layout_expectation
# Disagreement warns and returns 1 — but the device still wins.
MTK_ROOT_FIXTURE="$(_t_fixture api34_boot_only \
'ro.boot.slot_suffix=_a
ro.product.first_api_level=34' \
'boot_a
vbmeta_a')"
discover_partitions
_t_rc 1 "api 34 + boot carrier disagrees and says so" mtk_check_layout_expectation
_t_is "boot_a" "$MTK_RAMDISK_PART" "a disagreeing expectation does NOT change the answer"
warn_out="$(mtk_check_layout_expectation 2>&1 || true)"
_t_in "Trusting the device" "$warn_out" "the warning states the filesystem wins"
# Below 33 predicts boot.
MTK_ROOT_FIXTURE="$(_t_fixture api30_boot 'ro.product.first_api_level=30' 'boot')"
discover_partitions
_t_rc 0 "api 30 + boot carrier agrees" mtk_check_layout_expectation
# An unreadable or non-numeric api level is not an error — it is simply no
# cross-check. This must not be mistaken for a disagreement.
MTK_ROOT_FIXTURE="$(_t_fixture api_missing '' 'boot')"
discover_partitions
_t_rc 0 "an absent api level skips the cross-check" mtk_check_layout_expectation
# --- mtk_sane_ramdisk_size -------------------------------------------------
# Both an empty read and a whole-disk grab look like success to mtkclient's
# exit code, which is the entire reason this band exists.
_t_rc 1 "zero bytes is implausible" mtk_sane_ramdisk_size 0
_t_in "implausibly small" "$(mtk_sane_ramdisk_size 0 || true)" "small failure says which way"
_t_rc 1 "a truncated read is implausible" mtk_sane_ramdisk_size 1024
_t_rc 0 "8 MiB is a plausible ramdisk" mtk_sane_ramdisk_size $((8 * 1024 * 1024))
_t_rc 0 "the lower bound itself is accepted" mtk_sane_ramdisk_size $((1024 * 1024))
_t_rc 0 "the upper bound itself is accepted" mtk_sane_ramdisk_size $((128 * 1024 * 1024))
_t_rc 1 "a whole-disk grab is implausible" mtk_sane_ramdisk_size $((4 * 1024 * 1024 * 1024))
_t_in "implausibly large" "$(mtk_sane_ramdisk_size $((4 * 1024 * 1024 * 1024)) || true)" \
"large failure says which way"
# `stat` failing yields an empty string, not a number.
_t_rc 1 "a non-numeric size is rejected" mtk_sane_ramdisk_size "not-a-number"
_t_rc 1 "an empty size is rejected" mtk_sane_ramdisk_size ""
# --- mtk_check_venv --------------------------------------------------------
# MTK_ROOT_CACHE points at an empty tmpdir, so this takes the deterministic
# "missing" branch on any host, CI included.
_t_rc 1 "an absent venv is reported as missing" mtk_check_venv
_t_in "missing:" "$(mtk_check_venv || true)" "the missing branch names the interpreter path"
_t_done "test_mtk_partitions.sh"

View File

@ -1,8 +1,10 @@
# Shell libraries that predate the coverage ratchet.
#
# Each line is a file exempt from check_shell_coverage.sh until it is
# next modified. Modifying an exempt file requires giving its lib/
# directory a tests/run_all.sh suite and deleting the line.
# Each line is a file exempt from check_shell_coverage.sh. The exemption
# is static: editing one of these files is fine and does not require
# tests. An entry goes away when its lib/ directory gains a
# tests/run_all.sh, at which point every file in that directory is
# enforced.
#
# This list only ever shrinks. Never add a path to it by hand -- a new
# library must ship with its tests.
@ -10,15 +12,6 @@
# Regenerate (only ever to REMOVE now-covered entries):
# bash meta/scripts/check_shell_coverage.sh --seed
linux_configuration/.githooks/lib/common.sh
linux_configuration/scripts/lib/android.sh
linux_configuration/scripts/lib/common_datetime.sh
linux_configuration/scripts/lib/common_packages.sh
linux_configuration/scripts/lib/common_services.sh
linux_configuration/scripts/lib/common.sh
linux_configuration/scripts/lib/mtk_classify.sh
linux_configuration/scripts/lib/mtk_common.sh
linux_configuration/scripts/lib/mtk_device.sh
linux_configuration/scripts/lib/mtk_partitions.sh
linux_configuration/scripts/meta/lib/shell_check_discover.sh
linux_configuration/scripts/meta/lib/shell_check_install.sh
linux_configuration/scripts/meta/lib/shell_check_lint.sh