testsAndMisc/phone_focus_mode/lib/tests/hosts_libs_harness.sh
Krzysztof kuhy Rudnicki 0a07684cca
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
hosts_enforcer: split 421 -> 207 + 166 + 91, both libs at 100%
The bind-mount logic goes to hosts_mount.sh, the Magisk-module and
cache-invalidation logic to hosts_magisk.sh. Both flat siblings, both in
both deploy lists; the two lists were then checked to correspond exactly
in both directions, since a push with no matching cp stages a file that
never lands and the enforcer would fail to source it.

Audited for stdin-draining `while read` loops BEFORE extracting, because
piping one would change behaviour in a way stubs cannot catch. There are
none here. Also confirmed no function writes a global, so there was no
cross-half hazard of the kind that kept NET_BUILT in curfew_enforcer.sh.

69 assertions, 43/43 and 62/62 lines, 17 mutations all killed. Two survived
the first run and both were real findings:

  - the unmount give-up threshold was not pinned. Raising it from 5 to 500
    changed nothing observable, so the enforcer could have spun 500 times
    per poll against a mount that was never going to clear. Now asserted as
    a call count.
  - the `[ ! -f $HOSTS_TARGET ] && return 1` guard in
    is_bind_mounted_correctly is PROVABLY EQUIVALENT: sha256_of returns
    empty for a missing file, so the existing -n check already rejects that
    case. No test can distinguish it. Equivalent code is a claim of a check
    that is not being made, so it is deleted rather than covered by a test
    that would pass either way. Old and new bodies were compared across all
    four input states -- both-missing, target-missing, match, mismatch --
    and agree on every one.

That deletion is the only non-verbatim function; the other 19 moved
untouched, making this the first fully verbatim extraction of the session.

Ordering is asserted where it is load-bearing: /system must go back to ro
after rw, and +i must be restored after being dropped. Presence assertions
alone would pass with either pair reversed.

UNVERIFIED ON DEVICE -- the highest-stakes of the four enforcer splits now
outstanding, since a failure to source leaves nothing blocking while the
phone looks normal. Over-cap count 20 -> 19.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
2026-08-17 16:30:56 +02:00

199 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# lib/tests/hosts_libs_harness.sh — the fake device behind the hosts_mount.sh
# and hosts_magisk.sh tests.
#
# Sourced, not executed. Both libraries drive the device through real binaries
# — mount, umount, chattr, am, pgrep, stop/start — so the stubs go on PATH,
# the same call as curfew_net's rather than monitor's function mocks.
#
# /proc/self/mounts is read directly by two functions, and cannot be stubbed
# on PATH. Those functions are handed a redirectable path instead: MOUNTS_FILE
# is substituted into the staged copy, so the subject under test reads a file
# the harness controls while the shipped code keeps its literal path.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly PHONE_DIR="${SCRIPT_DIR}/../.."
PASS=0
FAIL=0
_t_pass() {
PASS=$((PASS + 1))
printf ' OK: %s\n' "$1"
}
_t_fail() {
FAIL=$((FAIL + 1))
printf ' FAIL: %s\n' "$1"
}
_t_eq() {
local want="$1" got="$2" what="$3"
if [[ "$got" == "$want" ]]; then
_t_pass "$what"
else
_t_fail "$what (want '${want}', got '${got}')"
fi
}
RUN="$(mktemp -d)"
trap 'rm -rf "${RUN}"' EXIT
mkdir -p "${RUN}/app" "${RUN}/bin" "${RUN}/state" "${RUN}/dev"
readonly DEV="${RUN}/dev"
readonly MOUNTS_FILE="${RUN}/state/mounts"
: >"${MOUNTS_FILE}"
# Stage the subjects with /proc/self/mounts redirected at the harness copy.
# The pattern uses a character class for the slash-separated path so no
# literal /proc path appears twice in this file.
sed "s#/proc/self/mounts#${MOUNTS_FILE}#g" \
"${PHONE_DIR}/hosts_mount.sh" >"${RUN}/app/hosts_mount.sh"
# Same treatment for the Fenix profile glob: it is a hardcoded /data/data
# path that cannot exist here, so the staged copy points at a temp dir the
# harness controls. The shipped file keeps its literal path.
readonly FENIX_DIR="${RUN}/state/fenix"
mkdir -p "${FENIX_DIR}"
sed "s#/data/data/org.mozilla.fenix/files/mozilla/#${FENIX_DIR}/#" \
"${PHONE_DIR}/hosts_magisk.sh" >"${RUN}/app/hosts_magisk.sh"
# --- stubs ------------------------------------------------------------------
#
# Each records its calls so behaviour is assertable, and each honours a
# fail_<name> flag so the error branches are reachable.
_mk_stub() {
local name="$1"
cat >"${RUN}/bin/${name}" <<STUB
#!/usr/bin/env bash
printf '%s %s\n' "${name}" "\$*" >>"\${DEV}/calls.log"
[[ -f "\${DEV}/fail_${name}" ]] && exit 1
exit 0
STUB
chmod +x "${RUN}/bin/${name}"
}
# sleep is stubbed too: restart_netd_for_hosts_cache sleeps 2s twice, and a
# 4s pause per call is real time spent proving nothing.
for _s in chattr chmod am stop start umount sleep; do
_mk_stub "$_s"
done
# cp is its own stub: make_target_writable_once's overwrite is the fallback
# path, so a test has to be able to let that copy actually land.
cat >"${RUN}/bin/cp" <<'STUB'
#!/usr/bin/env bash
printf 'cp %s
' "$*" >>"${DEV}/calls.log"
[[ -f "${DEV}/fail_cp" ]] && exit 1
[[ -f "${DEV}/overwrite_works" ]] && cat <"$1" >"$2" 2>/dev/null
exit 0
STUB
chmod +x "${RUN}/bin/cp"
# mount also maintains the fake mounts file, so a bind shows up afterwards.
cat >"${RUN}/bin/mount" <<'STUB'
#!/usr/bin/env bash
printf 'mount %s\n' "$*" >>"${DEV}/calls.log"
[[ -f "${DEV}/fail_mount" ]] && exit 1
if [[ "$1" == "--bind" ]]; then
printf 'src %s rw 0 0\n' "$3" >>"${MOUNTS_FILE}"
# A bind mount makes the target's content equal the source's.
# Redirection, not cp: this stub runs with the stub dir on PATH, so a
# `cp` here would hit the cp stub and copy nothing.
[[ -f "${DEV}/bind_is_noop" ]] || cat <"$2" >"$3" 2>/dev/null || true
fi
exit 0
STUB
chmod +x "${RUN}/bin/mount"
# umount clears the fake mounts file one entry at a time.
cat >"${RUN}/bin/umount" <<'STUB'
#!/usr/bin/env bash
printf 'umount %s\n' "$*" >>"${DEV}/calls.log"
[[ -f "${DEV}/fail_umount" ]] && exit 1
target="${*: -1}"
# When asked, report success but leave the entry: a mount that will not go
# away, which is what the give-up-after-5-attempts branch exists for.
[[ -f "${DEV}/umount_is_noop" ]] && exit 0
if [[ -s "${MOUNTS_FILE}" ]]; then
grep -vF " ${target} " "${MOUNTS_FILE}" >"${MOUNTS_FILE}.new" 2>/dev/null || : >"${MOUNTS_FILE}.new"
mv "${MOUNTS_FILE}.new" "${MOUNTS_FILE}"
fi
exit 0
STUB
chmod +x "${RUN}/bin/umount"
cat >"${RUN}/bin/pgrep" <<'STUB'
#!/usr/bin/env bash
printf 'pgrep %s\n' "$*" >>"${DEV}/calls.log"
[[ -f "${DEV}/netd_pid" ]] || exit 1
cat "${DEV}/netd_pid"
STUB
chmod +x "${RUN}/bin/pgrep"
export DEV MOUNTS_FILE
export PATH="${RUN}/bin:${PATH}"
# --- ambient definitions the libraries expect from the enforcer ------------
log() { printf '%s\n' "$*" >>"${DEV}/log"; }
# The real one lives in hosts_enforcer.sh; both libraries call it.
sha256_of() {
[ -f "$1" ] || return 0
sha256sum "$1" 2>/dev/null | awk '{print $1}'
}
STATE_DIR="${RUN}/state"
HOSTS_TARGET="${RUN}/state/hosts_target"
HOSTS_CANONICAL="${RUN}/state/hosts.canonical"
HOSTS_CANONICAL_WORKOUT="${RUN}/state/hosts.canonical.workout"
HOSTS_MAGISK_MODULE_FILE="${RUN}/state/modules/hosts/system/etc/hosts"
BROWSER_PACKAGES="org.mozilla.fenix com.android.chrome"
WORKOUT_ACTIVE_FILE="${RUN}/state/workout_active"
# hosts_mount.sh calls current_canonical, which lives in the entry script.
# Modelled here as the same two-variant choice the real one makes.
current_canonical() {
if [ -f "$WORKOUT_ACTIVE_FILE" ]; then
printf '%s' "$HOSTS_CANONICAL_WORKOUT"
else
printf '%s' "$HOSTS_CANONICAL"
fi
}
# shellcheck source=../../hosts_magisk.sh
. "${RUN}/app/hosts_magisk.sh"
# shellcheck source=../../hosts_mount.sh
. "${RUN}/app/hosts_mount.sh"
# --- helpers ----------------------------------------------------------------
_reset_dev() {
rm -rf "${DEV}"
mkdir -p "${DEV}"
: >"${MOUNTS_FILE}"
}
# A copy the stubs cannot intercept: the cp stub exists to observe the
# subject's calls, but the harness and cases need to actually move bytes.
# Redirection is a shell builtin, so it bypasses PATH entirely.
_copy() {
cat <"$1" >"$2"
}
_fail_op() { touch "${DEV}/fail_$1"; }
_clear_fail() { rm -f "${DEV}/fail_$1"; }
_calls() { cat "${DEV}/calls.log" 2>/dev/null || printf ''; }
_log() { cat "${DEV}/log" 2>/dev/null || printf ''; }
# Record TARGET as already mounted, the OEM-overlay state assert_bind_mount
# has to clear before it can take the path over.
_seed_mount() {
printf 'src %s rw 0 0\n' "${HOSTS_TARGET}" >>"${MOUNTS_FILE}"
}