mirror of
https://github.com/tw93/Mole.git
synced 2026-02-15 11:05:09 +00:00
Complete automated testing
This commit is contained in:
22
.github/workflows/tests.yml
vendored
Normal file
22
.github/workflows/tests.yml
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Mole Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: macos-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install bats-core
|
||||||
|
run: |
|
||||||
|
brew update
|
||||||
|
brew install bats-core
|
||||||
|
|
||||||
|
- name: Run test suite
|
||||||
|
run: tests/run.sh
|
||||||
71
tests/analyze.bats
Normal file
71
tests/analyze.bats
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-analyze-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
export TERM="dumb"
|
||||||
|
rm -rf "${HOME:?}"/*
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "scan_directories lists largest folders first" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/bin/analyze.sh"
|
||||||
|
|
||||||
|
root="$HOME/analyze-root"
|
||||||
|
mkdir -p "$root/Small" "$root/Large"
|
||||||
|
printf 'tiny' > "$root/Small/file.txt"
|
||||||
|
dd if=/dev/zero of="$root/Large/big.dat" bs=1024 count=200 >/dev/null 2>&1
|
||||||
|
|
||||||
|
output_file="$HOME/directories.txt"
|
||||||
|
scan_directories "$root" "$output_file" 1
|
||||||
|
|
||||||
|
head -n1 "$output_file"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Large"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "aggregate_by_directory sums child sizes per parent" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/bin/analyze.sh"
|
||||||
|
|
||||||
|
root="$HOME/group"
|
||||||
|
mkdir -p "$root/a" "$root/b"
|
||||||
|
|
||||||
|
input_file="$HOME/files.txt"
|
||||||
|
cat > "$input_file" <<LIST
|
||||||
|
1024|$root/a/file1
|
||||||
|
2048|$root/a/file2
|
||||||
|
512|$root/b/data.bin
|
||||||
|
LIST
|
||||||
|
|
||||||
|
output_file="$HOME/aggregated.txt"
|
||||||
|
aggregate_by_directory "$input_file" "$output_file"
|
||||||
|
|
||||||
|
cat "$output_file"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"3072|$HOME/group/a/"* ]]
|
||||||
|
[[ "$output" == *"512|$HOME/group/b/"* ]]
|
||||||
|
}
|
||||||
60
tests/clean.bats
Normal file
60
tests/clean.bats
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-clean-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
export TERM="xterm-256color"
|
||||||
|
rm -rf "${HOME:?}"/*
|
||||||
|
rm -rf "$HOME/Library" "$HOME/.config"
|
||||||
|
mkdir -p "$HOME/Library/Caches" "$HOME/.config/mole"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mo clean --dry-run skips system cleanup in non-interactive mode" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Dry Run Mode"* ]]
|
||||||
|
[[ "$output" != *"Deep system-level cleanup"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mo clean --dry-run reports user cache without deleting it" {
|
||||||
|
mkdir -p "$HOME/Library/Caches/TestApp"
|
||||||
|
echo "cache data" > "$HOME/Library/Caches/TestApp/cache.tmp"
|
||||||
|
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"User app cache"* ]]
|
||||||
|
[[ "$output" == *"Potential space"* ]]
|
||||||
|
[ -f "$HOME/Library/Caches/TestApp/cache.tmp" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mo clean honors whitelist entries" {
|
||||||
|
mkdir -p "$HOME/Library/Caches/WhitelistedApp"
|
||||||
|
echo "keep me" > "$HOME/Library/Caches/WhitelistedApp/data.tmp"
|
||||||
|
|
||||||
|
cat > "$HOME/.config/mole/whitelist" <<EOF
|
||||||
|
$HOME/Library/Caches/WhitelistedApp*
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" clean --dry-run
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Protected: 1"* ]]
|
||||||
|
[ -f "$HOME/Library/Caches/WhitelistedApp/data.tmp" ]
|
||||||
|
}
|
||||||
80
tests/cli.bats
Normal file
80
tests/cli.bats
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
rm -rf "$HOME/.config"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mole --help prints command overview" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"mo clean"* ]]
|
||||||
|
[[ "$output" == *"mo analyze"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mole --version reports script version" {
|
||||||
|
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"$expected_version"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mole unknown command returns error" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
[[ "$output" == *"Unknown command: unknown-command"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "clean.sh --help shows usage details" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/bin/clean.sh" --help
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Mole - Deeper system cleanup"* ]]
|
||||||
|
[[ "$output" == *"--dry-run"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "uninstall.sh --help highlights controls" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/bin/uninstall.sh" --help
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Usage: mole uninstall"* ]]
|
||||||
|
[[ "$output" == *"Keyboard Controls"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "analyze.sh --help outlines explorer features" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/bin/analyze.sh" --help
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Interactive disk space explorer"* ]]
|
||||||
|
[[ "$output" == *"mole analyze"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "touchid --help describes configuration options" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid --help
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Touch ID"* ]]
|
||||||
|
[[ "$output" == *"mo touchid enable"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "touchid status reports current configuration" {
|
||||||
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Touch ID"* ]]
|
||||||
|
}
|
||||||
140
tests/common.bats
Normal file
140
tests/common.bats
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
rm -rf "$HOME/.config"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown() {
|
||||||
|
unset MO_SPINNER_CHARS || true
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mo_spinner_chars returns default sequence when unset" {
|
||||||
|
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")"
|
||||||
|
[ "$result" = "|/-\\" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mo_spinner_chars respects MO_SPINNER_CHARS override" {
|
||||||
|
export MO_SPINNER_CHARS="abcd"
|
||||||
|
result="$(HOME="$HOME" MO_SPINNER_CHARS="$MO_SPINNER_CHARS" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; mo_spinner_chars")"
|
||||||
|
[ "$result" = "abcd" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect_architecture maps current CPU to friendly label" {
|
||||||
|
expected="Intel"
|
||||||
|
if [[ "$(uname -m)" == "arm64" ]]; then
|
||||||
|
expected="Apple Silicon"
|
||||||
|
fi
|
||||||
|
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; detect_architecture")"
|
||||||
|
[ "$result" = "$expected" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "get_free_space returns a non-empty value" {
|
||||||
|
result="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; get_free_space")"
|
||||||
|
[[ -n "$result" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "log_info prints message and appends to log file" {
|
||||||
|
local message="Informational message from test"
|
||||||
|
local stdout_output
|
||||||
|
stdout_output="$(HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_info '$message'")"
|
||||||
|
[[ "$stdout_output" == *"$message"* ]]
|
||||||
|
|
||||||
|
local log_file="$HOME/.config/mole/mole.log"
|
||||||
|
[[ -f "$log_file" ]]
|
||||||
|
grep -q "INFO: $message" "$log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "log_error writes to stderr and log file" {
|
||||||
|
local message="Something went wrong"
|
||||||
|
local stderr_file="$HOME/log_error_stderr.txt"
|
||||||
|
|
||||||
|
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/common.sh'; log_error '$message' 1>/dev/null 2>'$stderr_file'"
|
||||||
|
|
||||||
|
[[ -s "$stderr_file" ]]
|
||||||
|
grep -q "$message" "$stderr_file"
|
||||||
|
|
||||||
|
local log_file="$HOME/.config/mole/mole.log"
|
||||||
|
[[ -f "$log_file" ]]
|
||||||
|
grep -q "ERROR: $message" "$log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "bytes_to_human converts byte counts into readable units" {
|
||||||
|
output="$(HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
bytes_to_human 512
|
||||||
|
bytes_to_human 2048
|
||||||
|
bytes_to_human $((5 * 1024 * 1024))
|
||||||
|
bytes_to_human $((3 * 1024 * 1024 * 1024))
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
bytes_lines=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
bytes_lines+=("$line")
|
||||||
|
done <<< "$output"
|
||||||
|
|
||||||
|
[ "${bytes_lines[0]}" = "512B" ]
|
||||||
|
[ "${bytes_lines[1]}" = "2KB" ]
|
||||||
|
[ "${bytes_lines[2]}" = "5.0MB" ]
|
||||||
|
[ "${bytes_lines[3]}" = "3.00GB" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "create_temp_file and create_temp_dir are tracked and cleaned" {
|
||||||
|
HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
create_temp_file > "$HOME/temp_file_path.txt"
|
||||||
|
create_temp_dir > "$HOME/temp_dir_path.txt"
|
||||||
|
cleanup_temp_files
|
||||||
|
EOF
|
||||||
|
|
||||||
|
file_path="$(cat "$HOME/temp_file_path.txt")"
|
||||||
|
dir_path="$(cat "$HOME/temp_dir_path.txt")"
|
||||||
|
[ ! -e "$file_path" ]
|
||||||
|
[ ! -e "$dir_path" ]
|
||||||
|
rm -f "$HOME/temp_file_path.txt" "$HOME/temp_dir_path.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "parallel_execute runs worker across all items" {
|
||||||
|
output_file="$HOME/parallel_output.txt"
|
||||||
|
HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
worker() {
|
||||||
|
echo "$1" >> "$HOME/parallel_output.txt"
|
||||||
|
}
|
||||||
|
parallel_execute 2 worker "first" "second" "third"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sort "$output_file" > "$output_file.sorted"
|
||||||
|
results=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
results+=("$line")
|
||||||
|
done < "$output_file.sorted"
|
||||||
|
|
||||||
|
[ "${#results[@]}" -eq 3 ]
|
||||||
|
joined=" ${results[*]} "
|
||||||
|
[[ "$joined" == *" first "* ]]
|
||||||
|
[[ "$joined" == *" second "* ]]
|
||||||
|
[[ "$joined" == *" third "* ]]
|
||||||
|
rm -f "$output_file" "$output_file.sorted"
|
||||||
|
}
|
||||||
22
tests/helpers/uninstall_stubs.sh
Normal file
22
tests/helpers/uninstall_stubs.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# shellcheck disable=SC2329
|
||||||
|
# Helper stub definitions for uninstall tests
|
||||||
|
|
||||||
|
setup_uninstall_stubs() {
|
||||||
|
request_sudo_access() { return 0; }
|
||||||
|
start_inline_spinner() { :; }
|
||||||
|
stop_inline_spinner() { :; }
|
||||||
|
enter_alt_screen() { :; }
|
||||||
|
leave_alt_screen() { :; }
|
||||||
|
hide_cursor() { :; }
|
||||||
|
show_cursor() { :; }
|
||||||
|
remove_apps_from_dock() { :; }
|
||||||
|
|
||||||
|
pgrep() { return 1; }
|
||||||
|
pkill() { return 0; }
|
||||||
|
sudo() { return 0; }
|
||||||
|
|
||||||
|
export -f request_sudo_access start_inline_spinner stop_inline_spinner \
|
||||||
|
enter_alt_screen leave_alt_screen hide_cursor show_cursor \
|
||||||
|
remove_apps_from_dock pgrep pkill sudo || true
|
||||||
|
}
|
||||||
29
tests/run.sh
Executable file
29
tests/run.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
|
||||||
|
if ! command -v bats >/dev/null 2>&1; then
|
||||||
|
cat <<'EOF' >&2
|
||||||
|
bats is required to run Mole's test suite.
|
||||||
|
Install via Homebrew with 'brew install bats-core' or via npm with 'npm install -g bats'.
|
||||||
|
EOF
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
if [[ -z "${TERM:-}" ]]; then
|
||||||
|
export TERM="xterm-256color"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# -eq 0 ]]; then
|
||||||
|
set -- tests
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
bats -p "$@"
|
||||||
|
else
|
||||||
|
TERM="${TERM:-xterm-256color}" bats --tap "$@"
|
||||||
|
fi
|
||||||
104
tests/uninstall.bats
Normal file
104
tests/uninstall.bats
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-uninstall-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
export TERM="dumb"
|
||||||
|
rm -rf "${HOME:?}"/*
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
create_app_artifacts() {
|
||||||
|
mkdir -p "$HOME/Applications/TestApp.app"
|
||||||
|
mkdir -p "$HOME/Library/Application Support/TestApp"
|
||||||
|
mkdir -p "$HOME/Library/Caches/TestApp"
|
||||||
|
mkdir -p "$HOME/Library/Containers/com.example.TestApp"
|
||||||
|
mkdir -p "$HOME/Library/Preferences"
|
||||||
|
touch "$HOME/Library/Preferences/com.example.TestApp.plist"
|
||||||
|
mkdir -p "$HOME/Library/Preferences/ByHost"
|
||||||
|
touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist"
|
||||||
|
mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "find_app_files discovers user-level leftovers" {
|
||||||
|
create_app_artifacts
|
||||||
|
|
||||||
|
result="$(HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
find_app_files "com.example.TestApp" "TestApp"
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
[[ "$result" == *"Application Support/TestApp"* ]]
|
||||||
|
[[ "$result" == *"Caches/TestApp"* ]]
|
||||||
|
[[ "$result" == *"Preferences/com.example.TestApp.plist"* ]]
|
||||||
|
[[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]]
|
||||||
|
[[ "$result" == *"Containers/com.example.TestApp"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "calculate_total_size returns aggregate kilobytes" {
|
||||||
|
mkdir -p "$HOME/sized"
|
||||||
|
dd if=/dev/zero of="$HOME/sized/file1" bs=1024 count=1 >/dev/null 2>&1
|
||||||
|
dd if=/dev/zero of="$HOME/sized/file2" bs=1024 count=2 >/dev/null 2>&1
|
||||||
|
|
||||||
|
result="$(HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
files="$(printf '%s\n%s\n' "$HOME/sized/file1" "$HOME/sized/file2")"
|
||||||
|
calculate_total_size "$files"
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
# Result should be >=3 KB (some filesystems allocate slightly more)
|
||||||
|
[ "$result" -ge 3 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "batch_uninstall_applications removes selected app data" {
|
||||||
|
create_app_artifacts
|
||||||
|
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
source "$PROJECT_ROOT/lib/batch_uninstall.sh"
|
||||||
|
source "$PROJECT_ROOT/tests/helpers/uninstall_stubs.sh"
|
||||||
|
setup_uninstall_stubs
|
||||||
|
|
||||||
|
app_bundle="$HOME/Applications/TestApp.app"
|
||||||
|
mkdir -p "$app_bundle"
|
||||||
|
|
||||||
|
related="$(find_app_files "com.example.TestApp" "TestApp")"
|
||||||
|
encoded_related=$(printf '%s' "$related" | base64 | tr -d '\n')
|
||||||
|
|
||||||
|
selected_apps=()
|
||||||
|
selected_apps+=("0|$app_bundle|TestApp|com.example.TestApp|0|Never")
|
||||||
|
files_cleaned=0
|
||||||
|
total_items=0
|
||||||
|
total_size_cleaned=0
|
||||||
|
|
||||||
|
printf '\n' | batch_uninstall_applications >/dev/null
|
||||||
|
|
||||||
|
[[ ! -d "$app_bundle" ]] || exit 1
|
||||||
|
[[ ! -d "$HOME/Library/Application Support/TestApp" ]] || exit 1
|
||||||
|
[[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1
|
||||||
|
[[ ! -f "$HOME/Library/Preferences/com.example.TestApp.plist" ]] || exit 1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
111
tests/update_remove.bats
Normal file
111
tests/update_remove.bats
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-update-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
export TERM="dumb"
|
||||||
|
rm -rf "${HOME:?}"/*
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "update_via_homebrew reports already on latest version" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
MOLE_TEST_BREW_UPDATE_OUTPUT="Updated 0 formulae"
|
||||||
|
MOLE_TEST_BREW_UPGRADE_OUTPUT="Warning: mole 1.7.9 already installed"
|
||||||
|
MOLE_TEST_BREW_LIST_OUTPUT="mole 1.7.9"
|
||||||
|
start_inline_spinner() { :; }
|
||||||
|
stop_inline_spinner() { :; }
|
||||||
|
brew() {
|
||||||
|
case "$1" in
|
||||||
|
update) echo "$MOLE_TEST_BREW_UPDATE_OUTPUT";;
|
||||||
|
upgrade) echo "$MOLE_TEST_BREW_UPGRADE_OUTPUT";;
|
||||||
|
list) if [[ "$2" == "--versions" ]]; then echo "$MOLE_TEST_BREW_LIST_OUTPUT"; fi ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
export -f brew start_inline_spinner stop_inline_spinner
|
||||||
|
source "$PROJECT_ROOT/lib/common.sh"
|
||||||
|
update_via_homebrew "1.7.9"
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Already on latest version"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "update_mole skips download when already latest" {
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="$HOME/fake-bin:/usr/bin:/bin" TERM="dumb" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p "$HOME/fake-bin"
|
||||||
|
cat > "$HOME/fake-bin/curl" <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
out=""
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-o)
|
||||||
|
out="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ -n "$out" ]]; then
|
||||||
|
cat <<'INSTALLER' > "$out"
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
echo "Installer executed"
|
||||||
|
INSTALLER
|
||||||
|
else
|
||||||
|
echo 'VERSION="1.7.9"'
|
||||||
|
fi
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$HOME/fake-bin/curl"
|
||||||
|
cat > "$HOME/fake-bin/brew" <<'SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exit 1
|
||||||
|
SCRIPT
|
||||||
|
chmod +x "$HOME/fake-bin/brew"
|
||||||
|
|
||||||
|
"$PROJECT_ROOT/mole" update
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"Already on latest version"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "remove_mole deletes manual binaries and caches" {
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
touch "$HOME/.local/bin/mole"
|
||||||
|
touch "$HOME/.local/bin/mo"
|
||||||
|
mkdir -p "$HOME/.config/mole" "$HOME/.cache/mole"
|
||||||
|
|
||||||
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="/usr/bin:/bin" bash --noprofile --norc <<'EOF'
|
||||||
|
set -euo pipefail
|
||||||
|
start_inline_spinner() { :; }
|
||||||
|
stop_inline_spinner() { :; }
|
||||||
|
export -f start_inline_spinner stop_inline_spinner
|
||||||
|
printf '\n' | "$PROJECT_ROOT/mole" remove
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[ ! -f "$HOME/.local/bin/mole" ]
|
||||||
|
[ ! -f "$HOME/.local/bin/mo" ]
|
||||||
|
[ ! -d "$HOME/.config/mole" ]
|
||||||
|
[ ! -d "$HOME/.cache/mole" ]
|
||||||
|
}
|
||||||
99
tests/whitelist.bats
Normal file
99
tests/whitelist.bats
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
|
setup_file() {
|
||||||
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
|
export PROJECT_ROOT
|
||||||
|
|
||||||
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-whitelist-home.XXXXXX")"
|
||||||
|
export HOME
|
||||||
|
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown_file() {
|
||||||
|
rm -rf "$HOME"
|
||||||
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
|
export HOME="$ORIGINAL_HOME"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
rm -rf "$HOME/.config"
|
||||||
|
mkdir -p "$HOME"
|
||||||
|
WHITELIST_PATH="$HOME/.config/mole/whitelist"
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "patterns_equivalent treats paths with tilde expansion as equal" {
|
||||||
|
local status
|
||||||
|
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/test\""; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "patterns_equivalent distinguishes different paths" {
|
||||||
|
local status
|
||||||
|
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; patterns_equivalent '~/.cache/test' \"\$HOME/.cache/other\""; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "save_whitelist_patterns keeps unique entries and preserves header" {
|
||||||
|
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/foo\" \"\$HOME/.cache/foo\" \"\$HOME/.cache/bar\""
|
||||||
|
|
||||||
|
[[ -f "$WHITELIST_PATH" ]]
|
||||||
|
|
||||||
|
lines=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
lines+=("$line")
|
||||||
|
done < "$WHITELIST_PATH"
|
||||||
|
# Header is at least two lines (comments), plus two unique patterns
|
||||||
|
[ "${#lines[@]}" -ge 4 ]
|
||||||
|
# Ensure duplicate was not written twice
|
||||||
|
occurrences=$(grep -c "$HOME/.cache/foo" "$WHITELIST_PATH")
|
||||||
|
[ "$occurrences" -eq 1 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "load_whitelist falls back to defaults when config missing" {
|
||||||
|
rm -f "$WHITELIST_PATH"
|
||||||
|
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; rm -f \"\$HOME/.config/mole/whitelist\"; load_whitelist; printf '%s\n' \"\${CURRENT_WHITELIST_PATTERNS[@]}\"" > "$HOME/current_whitelist.txt"
|
||||||
|
HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; printf '%s\n' \"\${DEFAULT_WHITELIST_PATTERNS[@]}\"" > "$HOME/default_whitelist.txt"
|
||||||
|
|
||||||
|
current=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
current+=("$line")
|
||||||
|
done < "$HOME/current_whitelist.txt"
|
||||||
|
|
||||||
|
defaults=()
|
||||||
|
while IFS= read -r line; do
|
||||||
|
defaults+=("$line")
|
||||||
|
done < "$HOME/default_whitelist.txt"
|
||||||
|
|
||||||
|
[ "${#current[@]}" -eq "${#defaults[@]}" ]
|
||||||
|
[ "${current[0]}" = "${defaults[0]/\$HOME/$HOME}" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "is_whitelisted matches saved patterns exactly" {
|
||||||
|
local status
|
||||||
|
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/unique-pattern\""; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
|
if HOME="$HOME" bash --noprofile --norc -c "source '$PROJECT_ROOT/lib/whitelist_manager.sh'; save_whitelist_patterns \"\$HOME/.cache/unique-pattern\"; load_whitelist; is_whitelisted \"\$HOME/.cache/other-pattern\""; then
|
||||||
|
status=0
|
||||||
|
else
|
||||||
|
status=$?
|
||||||
|
fi
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user