mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 20:07:04 +01:00
block_compulsive_opening: test the allow/block decision
21 assertions for cco_state.sh, the lib that decides whether a launch is
permitted. Coverage measured: 62/66 lines = 93.94%. The only uncovered lines
are kill_app's pkill/pkill -9 pair, which must never run in a test on a
machine whose apps are live. cco_wrapper/cco_install/cco_report stay at 0%:
every function in them moves system binaries, writes /etc/pacman.d/hooks or
kills processes.
The assertion that matters most is the one in the negative direction: a
PREVIOUS hour's stamp must NOT keep blocking. If that decision inverted, a
legitimately-closed app could never be reopened, and the failure would look
like the tool working. Both directions are pinned, plus the empty-stamp case
and stale suspend-marker cleanup.
Also generalises run_all.sh from test_leechblock_*.sh to test_*.sh. This
directory now holds suites for more than one entry script, and a prefix glob
silently skips every suite added after it — including inside the CI discovery
step added in fbec4656, which runs this file.
The harness asserts its own fixture is complete rather than letting a typo in
a global surface as a confusing unbound-variable from inside the code under
test. That check is also what makes the fixture's globals legible to
shellcheck, which cannot see them being read across the source boundary — a
real check that happens to remove SC2034, not a suppression.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F3PzGc75gBcdxP3PkHXGcV
This commit is contained in:
parent
f5edebae4a
commit
72e1b6a8b1
@ -1,20 +1,21 @@
|
||||
{
|
||||
"title": "Split block_compulsive_opening.sh under the 250-line cap",
|
||||
"objective": "Bring linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh (705 lines) under the repo-wide 250-line file cap by moving its state, wrapper, install and reporting concerns into four sourced libs, preserving behaviour exactly and keeping the deployed /usr/local/bin entry path intact so already-installed app wrappers and the pacman hook continue to work.",
|
||||
"objective": "Bring block_compulsive_opening.sh (705 lines) under the repo-wide 250-line cap by moving its 26 functions into four sourced libs, preserving behaviour exactly, and keeping the deployed /usr/local/bin copy working — it is executed by this machine's beeper/signal/discord launcher wrappers and by a live pacman PostTransaction hook.",
|
||||
"acceptance_criteria": [
|
||||
"The entry script and all four lib/cco_*.sh files are each at most 250 lines.",
|
||||
"A line-multiset check against the pre-split file reports zero missing functional lines.",
|
||||
"All three heredoc bodies -- the generated app wrapper, the generated pacman hook, and the help text -- are byte-identical to the pre-split versions, since they define deployed artifacts that verify_shell_split.sh cannot inspect.",
|
||||
"status output is byte-identical to the still-monolithic deployed copy, and both exit 0.",
|
||||
"The script runs correctly in BOTH layouts: lib/ beside the script (repo) and libs flat beside the script (deployed), and fails closed with exit 1 when a lib is missing.",
|
||||
"install_all deploys the libs and uninstall_all removes them, in this same commit, so the deployed copy is never split-without-libs.",
|
||||
"shellcheck passes without -x and shfmt -w is a no-op on all five files, with no suppression added."
|
||||
"`help`, `status` and an unknown-command run are byte-identical to the pre-split baselines with unchanged exit codes (0, 0, 1).",
|
||||
"verify_shell_split.sh shows no -/+ pair except install_all and uninstall_all, the two functions deliberately changed to deploy and remove the libs.",
|
||||
"A line-multiset check accounts for every code line of the 705-line original.",
|
||||
"The FLAT deploy layout (entry + libs side by side, no lib/ subdir) runs: proven in a scratch directory, since /usr/local/bin cannot be written from a test.",
|
||||
"A missing lib fails closed with an actionable message rather than loading partially.",
|
||||
"shellcheck passes without -x and shfmt -w is a no-op on every file, with no suppression added.",
|
||||
"The new lib/tests suite passes and per-lib coverage is measured and recorded as a real number."
|
||||
],
|
||||
"out_of_scope": [
|
||||
"Shell test coverage for the new libs -- no suite exists for this script; the split is verified behaviourally and textually.",
|
||||
"Changing the deployed entry path or converting it to a symlink -- installed app wrappers hard-code /usr/local/bin/block-compulsive-opening.sh.",
|
||||
"Deploying the libs under /usr/local/share/digital_wellbeing/lib, which install_leechblock_payload mirrors with rm -rf.",
|
||||
"Any change to the app timeout policy, the hour-bucket semantics, or the managed app list."
|
||||
"Executing install_all / uninstall_all / install_wrapper against the real system: they move /usr/bin binaries and write /etc/pacman.d/hooks.",
|
||||
"Executing kill_app or launch_with_timer: they pkill running applications.",
|
||||
"Re-deploying to /usr/local/bin. That is a separate, user-visible step; this commit leaves the working deployed monolith in place.",
|
||||
"The pre-existing `APPS[$app]: unbound variable` message on an unknown app, confirmed identical pre-split."
|
||||
],
|
||||
"verifier": "bash meta/scripts/check_file_length.sh --all for the cap; a python3 line-multiset diff and per-heredoc body diffs against HEAD for content equivalence; status output compared against the pre-split deployed copy; a flat-layout run plus a deleted-lib run for the two _CCO_LIB_DIR branches and the fail-closed guard; shellcheck, shfmt idempotence and bash -n on all five files; pre-commit run --files on the staged set."
|
||||
"verifier": "bash meta/scripts/check_file_length.sh --all; bash meta/scripts/verify_shell_split.sh HEAD <old> <old> <libs>; diff of help/status/unknown against captured baselines; a flat-layout simulation in a scratch dir including the missing-lib case; shellcheck + shfmt idempotence; lib/tests/run_all.sh and shell_coverage.sh."
|
||||
}
|
||||
|
||||
@ -1,64 +1,73 @@
|
||||
{
|
||||
"intent": "Bring block_compulsive_opening.sh (705 lines) under the 250-line cap by moving its state, wrapper, install and reporting logic into four sourced libs, without changing behaviour and without breaking the already-installed app wrappers that hard-code the deployed script's path.",
|
||||
"intent": "Bring block_compulsive_opening.sh (705 lines) under the 250-line cap by moving its 26 functions into four sourced libs, without breaking the deployed copy that this machine's beeper/signal/discord launchers and a live pacman PostTransaction hook execute on every use.",
|
||||
"scope": [
|
||||
"linux_configuration/scripts/periodic_background/digital_wellbeing/block_compulsive_opening.sh: 705 -> 182 lines",
|
||||
"New: lib/cco_install.sh (238), lib/cco_report.sh (102), lib/cco_state.sh (149), lib/cco_wrapper.sh (120)",
|
||||
"install_all deploys the libs in the same commit, so there is no window where the deployed copy is split but its libs are missing",
|
||||
"Non-goal: shell coverage for the new libs; no test suite exists for this script"
|
||||
"block_compulsive_opening.sh: 705 -> 182 lines",
|
||||
"New: lib/cco_state.sh (147), lib/cco_wrapper.sh (119), lib/cco_install.sh (238), lib/cco_report.sh (101)",
|
||||
"New: lib/tests/cco_harness.sh + lib/tests/test_cco_state.sh (21 assertions)",
|
||||
"Non-goal: executing install/uninstall/kill paths under test — they move system binaries and pkill running apps"
|
||||
],
|
||||
"changes": [
|
||||
"Split into four libs by concern: cco_state.sh (hour-bucket state files), cco_wrapper.sh (the launch gate), cco_install.sh (install/uninstall, the generated app wrapper and the pacman hook), cco_report.sh (status and help).",
|
||||
"The entry script keeps its exact deployed path, /usr/local/bin/block-compulsive-opening.sh, and stays a real file there rather than a symlink. Already-installed app wrappers on disk hard-code that path, and the pacman hook at 96-restore-pkg-wrappers.hook invokes it; a symlink would also break dirname \"${BASH_SOURCE[0]}\" resolution.",
|
||||
"_CCO_LIB_DIR probes for lib/ beside the script (repo tree) and falls back to the script's own directory (the flat /usr/local/bin deploy layout). Both branches are exercised below.",
|
||||
"install_all copies the four libs flat into /usr/local/bin alongside the entry script, and uninstall_all removes them so no orphans are stranded. The copy runs even when the script is already at install_path, so a lib changed upstream still reaches the deployed copy that `rewrap-quiet` executes.",
|
||||
"A guard before the source lines fails closed with an explicit message when any lib is unreadable, rather than dying on a bare `source` error.",
|
||||
"Ran shfmt -w on the entry script, which removed a block of ~26 consecutive blank lines. No associative-array keys were affected: unlike install_leechblock.sh, this file's hyphenated keys (\"signal-desktop\") were already quoted, so the shfmt arithmetic-parsing bug does not apply here. Verified by grep after the run."
|
||||
"Moved all 26 functions into four libs by responsibility: state/timing (cco_state), the launch path (cco_wrapper), install/uninstall + pacman hook (cco_install), status/reset/usage (cco_report). Extracted with meta/scripts/extract_shell_functions.py, which walks brace-by-brace rather than slicing line ranges.",
|
||||
"DEPLOY SHAPE (user's decision): the libs are deployed FLAT beside the entry script in /usr/local/bin, not in a lib/ subdirectory. This matches the existing precedent — /usr/local/bin/pacman_lock_lib.sh and heavy_job_lock.sh are already deployed and sourced exactly that way. The entry script probes for lib/ first and falls back to its own directory, so the repo layout and the deployed layout both work from one source of truth.",
|
||||
"JUSTIFIED BODY CHANGE 1 -- install_all now copies the four libs to /usr/local/bin. Without this the deployed entry script would source libs that are not there. It runs even when the script is already at $install_path, because `rewrap-quiet` (the pacman hook) executes the DEPLOYED copy and would otherwise keep sourcing stale libs after an upstream change.",
|
||||
"JUSTIFIED BODY CHANGE 2 -- uninstall_all removes those four libs, so an uninstall does not strand orphans in /usr/local/bin.",
|
||||
"The entry script fails CLOSED on a missing lib, with an actionable message naming the re-install command. The pacman wrapper's fail-open pattern is deliberately NOT copied here: a half-loaded blocker that silently stops blocking is the exact failure this tool exists to prevent, and unlike pacman you do not need this script in order to repair it."
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "bash meta/scripts/check_file_length.sh --all",
|
||||
"result": "pass",
|
||||
"evidence": "Over-cap list went from 4 files to 3; block_compulsive_opening.sh no longer listed. All five files 182/238/149/120/102 lines, under 250."
|
||||
"evidence": "Over-cap list went from 4 files to 3. block_compulsive_opening.sh is 182 lines; the largest new lib is cco_install.sh at 238."
|
||||
},
|
||||
{
|
||||
"command": "python3 line-multiset check of entry+4 libs against HEAD's block_compulsive_opening.sh",
|
||||
"command": "diff of `help`, `status` and an unknown command against pre-split baselines",
|
||||
"result": "pass",
|
||||
"evidence": "MISSING functional lines: 0. Every non-comment, non-blank line from the pre-split file is accounted for across the five files."
|
||||
"evidence": "All three byte-identical, exit codes 0/0/1 unchanged. Re-verified after shfmt. One apparent diff was traced to $0 echoing the invocation path when run from a different cwd, not to a code change."
|
||||
},
|
||||
{
|
||||
"command": "Heredoc body extraction and diff for all three heredocs",
|
||||
"command": "bash meta/scripts/verify_shell_split.sh HEAD <old> <old> <4 libs>",
|
||||
"result": "pass",
|
||||
"evidence": "This is the check verify_shell_split.sh cannot perform, because it cannot see inside a heredoc body. WRAPPER_EOF (5 lines, the generated app wrapper), HOOK_EOF (14 lines, the generated pacman hook) and EOF (26 lines, the help text) are each byte-identical between HEAD and the split. The two deployed artifacts this script generates are therefore provably unchanged."
|
||||
"evidence": "Exactly two -/+ pairs, install_all and uninstall_all — the two functions deliberately changed above. The other 24 functions moved byte-identically."
|
||||
},
|
||||
{
|
||||
"command": "bash block_compulsive_opening.sh status vs /usr/local/bin/block-compulsive-opening.sh status",
|
||||
"command": "python3 line-multiset check, original vs entry+libs (comments excluded)",
|
||||
"result": "pass",
|
||||
"evidence": "Byte-identical output, both exit 0. The deployed copy is still the pre-split monolith and sha256-identical to HEAD, so this compares the split against the real pre-split behaviour rather than against itself. `help` differs only in the $0 invocation path, as expected."
|
||||
"evidence": "0 code lines from the 705-line original missing from the split."
|
||||
},
|
||||
{
|
||||
"command": "Flat-layout test: entry script + libs copied into one directory (the deployed shape)",
|
||||
"command": "flat-layout simulation in a scratch dir (entry + 4 libs, no lib/ subdir)",
|
||||
"result": "pass",
|
||||
"evidence": "status runs correctly with exit 0, confirming the _CCO_LIB_DIR fallback branch works in the /usr/local/bin layout, not just the repo tree."
|
||||
"evidence": "This is the shape install_all writes to /usr/local/bin. `help` and `status` both exit 0 from the flat layout. Removing one lib produces the fail-closed error naming the missing file and the re-install command, rather than a partial load."
|
||||
},
|
||||
{
|
||||
"command": "Missing-lib guard: delete cco_report.sh from the flat layout and re-run",
|
||||
"command": "wrapper path against an unknown app, with HOME/XDG_STATE_HOME pointed at a tmpdir",
|
||||
"result": "pass",
|
||||
"evidence": "Fails closed with 'Error: missing library cco_report.sh in <dir>' and exit code 1 (measured directly, not through a pipeline). A partially-deployed install cannot look successful to the pacman hook."
|
||||
"evidence": "Rejects cleanly. The `APPS[$app]: unbound variable` message it emits was confirmed PRE-EXISTING by running the identical command against the pre-split original from git — byte-identical output, so not a regression introduced here. The real ~/.local/state/compulsive-block was confirmed untouched afterwards."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck (no -x) + shfmt idempotence + bash -n on all five files",
|
||||
"command": "lib/tests/run_all.sh",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck rc=0 with no suppressions; shfmt -w is a no-op on all five, so the pre-commit auto-format-and-re-stage cannot alter them; all five parse."
|
||||
"evidence": "21 assertions for cco_state, 0 failures (50 across the whole directory including the leechblock suites). Covers the allow/block decision in both directions, including the case that matters most: a PREVIOUS hour's stamp must not keep blocking, or an app could never be reopened."
|
||||
},
|
||||
{
|
||||
"command": "bash meta/scripts/shell_coverage.sh <run_all.sh> cco_state.sh 0",
|
||||
"result": "measured, below 100%",
|
||||
"evidence": "MEASURED: cco_state.sh 62/66 lines = 93.94%. The only four uncovered lines are kill_app's pkill/pkill -9 pair, which must never execute in a test on this machine. cco_wrapper/cco_install/cco_report are 0%: every function in them moves system binaries, writes /etc/pacman.d/hooks or kills processes."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck (no -x) + shfmt -w idempotence on all 8 files",
|
||||
"result": "pass",
|
||||
"evidence": "shellcheck rc=0, no suppressions added. shfmt is a no-op on every file. The hyphenated associative-array keys (signal-desktop, and the APPS/REAL_BINARIES/APP_TIMEOUT_MINUTES/PROCESS_MATCH maps) were already quoted and the corruption canary is clean."
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"The deployed copy at /usr/local/bin/block-compulsive-opening.sh is still the pre-split monolith and keeps working untouched; nothing breaks until someone runs `sudo block-compulsive-opening.sh install`, which now deploys the libs too. Verified that the pacman hook's `rewrap-quiet` path does not call install_all, so the hook cannot trigger a partial deploy.",
|
||||
"The libs deploy flat into /usr/local/bin rather than into /usr/local/share/digital_wellbeing/lib. That is deliberate: install_leechblock_payload does an `rm -rf` mirror of the latter, which would delete these libs on the next pacman-wrapper install.",
|
||||
"No test suite exists for this script, so coverage is 0% and the verification above is behavioural and textual rather than test-driven."
|
||||
"The deployed /usr/local/bin copy is NOT updated by this commit. Until `sudo block_compulsive_opening.sh install` is re-run, the deployed 705-line monolith keeps running — which is safe, because it is self-contained and unchanged. The dangerous ordering would be deploying the new entry script without its libs; the fail-closed guard turns that into a loud error rather than a silently disabled blocker.",
|
||||
"install_all is not executed by any test (it moves /usr/bin binaries), so its new lib-copy block is verified by the flat-layout simulation rather than by running it. The simulation reproduces exactly what it writes.",
|
||||
"A pre-existing quirk was confirmed rather than fixed: `wrapper <unknown-app>` prints a raw `APPS[$app]: unbound variable` before its clean error. Identical pre-split; out of scope for a behaviour-preserving split."
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; block_compulsive_opening.sh returns to a single 705-line file and lib/cco_*.sh disappear.",
|
||||
"If the split had already been deployed, re-run `sudo block-compulsive-opening.sh install` from the reverted tree to restore the monolith and remove the deployed libs.",
|
||||
"Validate with: block-compulsive-opening.sh status (expect exit 0 and the per-app status table)."
|
||||
"git revert the commit; block_compulsive_opening.sh returns to a single 705-line file and lib/cco_*.sh disappear.",
|
||||
"The deployed copy needs no rollback — this commit never touched it.",
|
||||
"If the split HAD been deployed before rolling back, re-run `sudo block_compulsive_opening.sh install` from the reverted tree and then confirm `ls /usr/local/bin/cco_*.sh` returns nothing and a wrapped app still launches."
|
||||
]
|
||||
}
|
||||
|
||||
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
# lib/tests/cco_harness.sh — shared fixture for the block_compulsive_opening.sh split.
|
||||
#
|
||||
# Sourced, not executed. Scope: the hour-key/state logic that decides whether an
|
||||
# app launch is allowed. Everything that moves system binaries (install_wrapper,
|
||||
# uninstall_wrapper, install_all) or kills processes (kill_app, launch_with_timer)
|
||||
# is NOT executed here — this machine's beeper/signal/discord wrappers and a live
|
||||
# pacman hook depend on those paths, and a test that ran them would rewrite them.
|
||||
#
|
||||
# STATE_DIR and LOG_FILE were already variables in the pre-split script, so the
|
||||
# tests point them at a tmpdir and nothing touches ~/.local/state.
|
||||
set -euo pipefail
|
||||
|
||||
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"
|
||||
local got="$2"
|
||||
local what="$3"
|
||||
if [[ $got == "$want" ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want '${want}', got '${got}')"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_has() {
|
||||
local haystack="$1"
|
||||
local needle="$2"
|
||||
local what="$3"
|
||||
if [[ $haystack == *"$needle"* ]]; then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (want a substring '${needle}')"
|
||||
fi
|
||||
}
|
||||
|
||||
# These helpers return 0/1 to mean allowed/blocked, and which way they fail is
|
||||
# the whole point — assert on the status explicitly rather than letting `set -e`
|
||||
# end the run. Called via `if` so a non-zero return does not abort the suite.
|
||||
_t_true() {
|
||||
local what="$2"
|
||||
if ("$1"); then
|
||||
_t_pass "$what"
|
||||
else
|
||||
_t_fail "$what (expected success, got non-zero)"
|
||||
fi
|
||||
}
|
||||
|
||||
_t_false() {
|
||||
local what="$2"
|
||||
if ("$1"); then
|
||||
_t_fail "$what (expected non-zero, got success)"
|
||||
else
|
||||
_t_pass "$what"
|
||||
fi
|
||||
}
|
||||
|
||||
CCO_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
export CCO_LIB_DIR
|
||||
|
||||
# Stand in for the globals the entry script defines before sourcing the libs.
|
||||
_t_cco_setup() {
|
||||
TEST_TMPDIR="$(mktemp -d)"
|
||||
export TEST_TMPDIR
|
||||
|
||||
STATE_DIR="$TEST_TMPDIR/state"
|
||||
LOG_FILE="$STATE_DIR/compulsive-block.log"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
AUTO_CLOSE_TIMEOUT_MINUTES=10
|
||||
AUTO_CLOSE_WARNING_MINUTES=2
|
||||
|
||||
declare -gA APP_TIMEOUT_MINUTES=(["beeper"]=20 ["signal-desktop"]=20)
|
||||
declare -gA APPS=(
|
||||
["beeper"]="$TEST_TMPDIR/bin/beeper"
|
||||
["signal-desktop"]="$TEST_TMPDIR/bin/signal-desktop"
|
||||
["discord"]="$TEST_TMPDIR/bin/discord"
|
||||
)
|
||||
declare -gA REAL_BINARIES=(
|
||||
["beeper"]="$TEST_TMPDIR/real/beepertexts"
|
||||
["signal-desktop"]="$TEST_TMPDIR/real/signal-desktop"
|
||||
["discord"]="$TEST_TMPDIR/bin/discord.orig"
|
||||
)
|
||||
declare -gA PROCESS_MATCH=(["discord"]="$TEST_TMPDIR/home/.config/discord/")
|
||||
|
||||
mkdir -p "$TEST_TMPDIR/bin" "$TEST_TMPDIR/real"
|
||||
|
||||
# notify() is defined in the entry script, which the tests never source.
|
||||
notify() { printf 'NOTIFY: %s | %s\n' "$1" "$2" >>"$TEST_TMPDIR/notifications"; }
|
||||
: >"$TEST_TMPDIR/notifications"
|
||||
|
||||
_t_cco_assert_fixture_complete
|
||||
}
|
||||
|
||||
# Every global above is read by the sourced lib, never by this file, so the
|
||||
# linter reports each one as unused (SC2034) and notify() as uncalled
|
||||
# (SC2329) — it cannot see across the `source` boundary the tests set up.
|
||||
# Referencing them here is not linter appeasement: it is a real fixture check.
|
||||
# A typo in one of these names would otherwise surface as a confusing unbound
|
||||
# variable from deep inside the code under test, or worse, as a silently
|
||||
# skipped assertion.
|
||||
_t_cco_assert_fixture_complete() {
|
||||
local missing=""
|
||||
[[ -n $LOG_FILE ]] || missing+=" LOG_FILE"
|
||||
[[ -n $AUTO_CLOSE_TIMEOUT_MINUTES ]] || missing+=" AUTO_CLOSE_TIMEOUT_MINUTES"
|
||||
[[ -n $AUTO_CLOSE_WARNING_MINUTES ]] || missing+=" AUTO_CLOSE_WARNING_MINUTES"
|
||||
[[ ${#APPS[@]} -eq 3 ]] || missing+=" APPS"
|
||||
[[ ${#REAL_BINARIES[@]} -eq 3 ]] || missing+=" REAL_BINARIES"
|
||||
[[ ${#APP_TIMEOUT_MINUTES[@]} -eq 2 ]] || missing+=" APP_TIMEOUT_MINUTES"
|
||||
[[ ${#PROCESS_MATCH[@]} -eq 1 ]] || missing+=" PROCESS_MATCH"
|
||||
|
||||
# Call notify() for real rather than probing it with `type -t`: block_app
|
||||
# routes every blocked launch through it, so a fixture whose notify is
|
||||
# broken would turn the notification assertions into silent no-ops. The
|
||||
# probe file is truncated afterwards so this call is not mistaken for one
|
||||
# made by the code under test.
|
||||
notify "fixture" "self-check"
|
||||
[[ -s "$TEST_TMPDIR/notifications" ]] || missing+=" notify"
|
||||
: >"$TEST_TMPDIR/notifications"
|
||||
|
||||
if [[ -n $missing ]]; then
|
||||
printf 'harness fixture incomplete:%s\n' "$missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
_t_cco_teardown() {
|
||||
[[ -n ${TEST_TMPDIR:-} && -d ${TEST_TMPDIR} ]] && rm -rf "$TEST_TMPDIR"
|
||||
}
|
||||
|
||||
_t_report() {
|
||||
printf '\n%s: %d passed, %d failed\n' "${1:-tests}" "$PASS" "$FAIL"
|
||||
[[ $FAIL -eq 0 ]]
|
||||
}
|
||||
@ -1,12 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs every leechblock lib test. Used directly as the coverage subject:
|
||||
# Runs every digital_wellbeing lib test. Used directly as the coverage subject:
|
||||
# meta/scripts/shell_coverage.sh wants the test script itself, not `bash <it>`.
|
||||
#
|
||||
# The glob is test_*.sh, NOT one prefix per split: this directory holds suites
|
||||
# for several entry scripts (leechblock, block_compulsive_opening, ...), and a
|
||||
# prefix glob silently skips every suite added after it — including in the CI
|
||||
# discovery step, which runs this file.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
rc=0
|
||||
for t in "$HERE"/test_leechblock_*.sh; do
|
||||
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.
|
||||
|
||||
@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env bash
|
||||
# Covers lib/cco_state.sh — the logic that decides whether a launch is allowed.
|
||||
#
|
||||
# This is the whole point of the tool: was_opened_this_hour deciding wrong in
|
||||
# either direction is the failure that matters. Too permissive and the blocker
|
||||
# does nothing; too strict and a legitimately-closed app can never be reopened.
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=cco_harness.sh
|
||||
. "$HERE/cco_harness.sh"
|
||||
# shellcheck source=../cco_state.sh
|
||||
. "$CCO_LIB_DIR/cco_state.sh"
|
||||
|
||||
_t_cco_setup
|
||||
trap _t_cco_teardown EXIT
|
||||
|
||||
printf 'get_hour_key: a stable YYYY-MM-DD-HH stamp\n'
|
||||
hour="$(get_hour_key)"
|
||||
if [[ $hour =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{2}$ ]]; then
|
||||
_t_pass "the hour key is YYYY-MM-DD-HH"
|
||||
else
|
||||
_t_fail "the hour key is YYYY-MM-DD-HH (got '$hour')"
|
||||
fi
|
||||
_t_eq "$hour" "$(get_hour_key)" "two calls in the same hour agree"
|
||||
|
||||
printf '\nget_state_file: one file per app, under STATE_DIR\n'
|
||||
_t_eq "$STATE_DIR/discord.lastopen" "$(get_state_file discord)" "the state path is derived from the app name"
|
||||
_t_eq "$STATE_DIR/discord.running" "$(get_running_file discord)" "the running-state path is separate from the lastopen path"
|
||||
|
||||
printf '\nwas_opened_this_hour: the core allow/block decision\n'
|
||||
rm -f "$STATE_DIR/discord.lastopen"
|
||||
if was_opened_this_hour discord; then
|
||||
_t_fail "a fresh app is not yet opened this hour"
|
||||
else
|
||||
_t_pass "a fresh app is not yet opened this hour"
|
||||
fi
|
||||
|
||||
record_opening discord >/dev/null 2>&1
|
||||
if was_opened_this_hour discord; then
|
||||
_t_pass "after record_opening the app counts as opened"
|
||||
else
|
||||
_t_fail "after record_opening the app counts as opened"
|
||||
fi
|
||||
|
||||
printf '\nwas_opened_this_hour: a stale hour must NOT keep blocking\n'
|
||||
# The bug that would matter most: if a previous hour's stamp still blocked, an
|
||||
# app could never be reopened. Write a deliberately old stamp.
|
||||
echo "2000-01-01-00" >"$STATE_DIR/discord.lastopen"
|
||||
if was_opened_this_hour discord; then
|
||||
_t_fail "a previous hour's stamp must not block the current hour"
|
||||
else
|
||||
_t_pass "a previous hour's stamp does not block the current hour"
|
||||
fi
|
||||
|
||||
printf '\nwas_opened_this_hour: an unreadable/empty stamp allows the launch\n'
|
||||
: >"$STATE_DIR/discord.lastopen"
|
||||
if was_opened_this_hour discord; then
|
||||
_t_fail "an empty state file must not block"
|
||||
else
|
||||
_t_pass "an empty state file does not block"
|
||||
fi
|
||||
|
||||
printf '\nrecord_opening: writes exactly the current hour\n'
|
||||
rm -f "$STATE_DIR/beeper.lastopen"
|
||||
record_opening beeper >/dev/null 2>&1
|
||||
_t_eq "$(get_hour_key)" "$(cat "$STATE_DIR/beeper.lastopen")" "the stamp written is the current hour key"
|
||||
|
||||
printf '\nblock_app: notifies the user\n'
|
||||
: >"$TEST_TMPDIR/notifications"
|
||||
block_app discord >/dev/null 2>&1
|
||||
_t_has "$(cat "$TEST_TMPDIR/notifications")" "Blocked" "blocking sends a desktop notification"
|
||||
|
||||
printf '\nis_autoclose_suspended: only todays marker counts\n'
|
||||
rm -f "$STATE_DIR/discord.suspend-autoclose"
|
||||
if is_autoclose_suspended discord; then
|
||||
_t_fail "no marker means not suspended"
|
||||
else
|
||||
_t_pass "no marker means not suspended"
|
||||
fi
|
||||
|
||||
printf '%(%Y-%m-%d)T' -1 >"$STATE_DIR/discord.suspend-autoclose"
|
||||
if is_autoclose_suspended discord; then
|
||||
_t_pass "todays marker suspends auto-close"
|
||||
else
|
||||
_t_fail "todays marker suspends auto-close"
|
||||
fi
|
||||
|
||||
echo "1999-12-31" >"$STATE_DIR/discord.suspend-autoclose"
|
||||
if is_autoclose_suspended discord; then
|
||||
_t_fail "a stale marker must not suspend auto-close"
|
||||
else
|
||||
_t_pass "a stale marker does not suspend auto-close"
|
||||
fi
|
||||
if [[ -f "$STATE_DIR/discord.suspend-autoclose" ]]; then
|
||||
_t_fail "a stale marker is cleaned up on detection"
|
||||
else
|
||||
_t_pass "a stale marker is cleaned up on detection"
|
||||
fi
|
||||
|
||||
printf '\nget_real_binary: only reports a binary once the wrapper is installed\n'
|
||||
rm -f "$TEST_TMPDIR/bin/beeper.orig"
|
||||
if get_real_binary beeper >/dev/null 2>&1; then
|
||||
_t_fail "an unwrapped app reports no real binary"
|
||||
else
|
||||
_t_pass "an unwrapped app reports no real binary"
|
||||
fi
|
||||
|
||||
: >"$TEST_TMPDIR/bin/beeper.orig"
|
||||
_t_eq "$TEST_TMPDIR/real/beepertexts" "$(get_real_binary beeper)" "a wrapped app resolves to its real binary"
|
||||
|
||||
printf '\nlog_message: appends to the log and echoes to stderr\n'
|
||||
: >"$LOG_FILE"
|
||||
log_message "hello from the test" 2>/dev/null
|
||||
_t_has "$(cat "$LOG_FILE")" "hello from the test" "the message reaches the log file"
|
||||
|
||||
printf '\nensure_state_dir: creates STATE_DIR when it is missing\n'
|
||||
rm -rf "$STATE_DIR"
|
||||
ensure_state_dir
|
||||
if [[ -d $STATE_DIR ]]; then
|
||||
_t_pass "a missing state directory is created"
|
||||
else
|
||||
_t_fail "a missing state directory is created"
|
||||
fi
|
||||
# Idempotent: the real script calls it on every wrapper launch.
|
||||
ensure_state_dir
|
||||
_t_pass "calling it again on an existing directory is harmless"
|
||||
|
||||
printf '\ncleanup_stale_running_state: clears state when nothing is running\n'
|
||||
running="$(get_running_file discord)"
|
||||
echo "999999 123" >"$running"
|
||||
# PROCESS_MATCH points at a path no process can match, so this is the
|
||||
# "app already exited" case.
|
||||
cleanup_stale_running_state discord 2>/dev/null
|
||||
if [[ -f $running ]]; then
|
||||
_t_fail "a running-state file for a dead process is removed"
|
||||
else
|
||||
_t_pass "a running-state file for a dead process is removed"
|
||||
fi
|
||||
|
||||
printf '\ncleanup_stale_running_state: no running file is a no-op\n'
|
||||
# The early return. It must not fall through to the pgrep path, which on a
|
||||
# machine where the app IS running would otherwise be consulted for nothing.
|
||||
rm -f "$running"
|
||||
if (cleanup_stale_running_state discord); then
|
||||
_t_pass "a missing running-state file returns success without probing"
|
||||
else
|
||||
_t_fail "a missing running-state file returns success without probing"
|
||||
fi
|
||||
|
||||
_t_report "test_cco_state"
|
||||
Loading…
Reference in New Issue
Block a user