mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 13:16:47 +00:00
Automated test optimization increased to 132
This commit is contained in:
169
tests/regression_bugs.bats
Normal file
169
tests/regression_bugs.bats
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env bats
|
||||
# Regression tests for previously fixed bugs
|
||||
# Ensures历史bug不再复现
|
||||
|
||||
setup() {
|
||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||
export PROJECT_ROOT
|
||||
export HOME="$BATS_TEST_TMPDIR/home"
|
||||
mkdir -p "$HOME/.config/mole"
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# 退出问题回归测试 (bb21bb1, 4b6c436, d75c34d)
|
||||
# =================================================================
|
||||
|
||||
@test "find with non-existent directory doesn't cause script exit (pipefail bug)" {
|
||||
# 这个模式曾导致 lib/clean/user.sh 在 pipefail 模式下意外退出
|
||||
result=$(bash -c '
|
||||
set -euo pipefail
|
||||
find /non/existent/dir -name "*.cache" 2>/dev/null || true
|
||||
echo "survived"
|
||||
')
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
@test "browser directory check pattern is safe when directories don't exist" {
|
||||
# 修复模式:先检查目录是否存在
|
||||
result=$(bash -c '
|
||||
set -euo pipefail
|
||||
search_dirs=()
|
||||
[[ -d "/non/existent/chrome" ]] && search_dirs+=("/non/existent/chrome")
|
||||
[[ -d "/tmp" ]] && search_dirs+=("/tmp")
|
||||
|
||||
if [[ ${#search_dirs[@]} -gt 0 ]]; then
|
||||
find "${search_dirs[@]}" -maxdepth 1 -type f 2>/dev/null || true
|
||||
fi
|
||||
echo "survived"
|
||||
')
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
@test "empty array doesn't cause unbound variable error" {
|
||||
result=$(bash -c '
|
||||
set -euo pipefail
|
||||
search_dirs=()
|
||||
|
||||
# 这不应该执行且不应该报错
|
||||
if [[ ${#search_dirs[@]} -gt 0 ]]; then
|
||||
echo "should not reach here"
|
||||
fi
|
||||
echo "survived"
|
||||
')
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
# ===============================================================
|
||||
# 更新检查回归测试 (260254f, b61b3f7, 2a64cae, 7a9c946)
|
||||
# ===============================================================
|
||||
|
||||
@test "version comparison works correctly" {
|
||||
result=$(bash -c '
|
||||
v1="1.11.8"
|
||||
v2="1.11.9"
|
||||
if [[ "$(printf "%s\n" "$v1" "$v2" | sort -V | head -1)" == "$v1" && "$v1" != "$v2" ]]; then
|
||||
echo "update_needed"
|
||||
fi
|
||||
')
|
||||
[[ "$result" == "update_needed" ]]
|
||||
}
|
||||
|
||||
@test "version comparison with same versions" {
|
||||
result=$(bash -c '
|
||||
v1="1.11.8"
|
||||
v2="1.11.8"
|
||||
if [[ "$(printf "%s\n" "$v1" "$v2" | sort -V | head -1)" == "$v1" && "$v1" != "$v2" ]]; then
|
||||
echo "update_needed"
|
||||
else
|
||||
echo "up_to_date"
|
||||
fi
|
||||
')
|
||||
[[ "$result" == "up_to_date" ]]
|
||||
}
|
||||
|
||||
@test "version prefix v/V is stripped correctly" {
|
||||
result=$(bash -c '
|
||||
version="v1.11.9"
|
||||
clean=${version#v}
|
||||
clean=${clean#V}
|
||||
echo "$clean"
|
||||
')
|
||||
[[ "$result" == "1.11.9" ]]
|
||||
}
|
||||
|
||||
@test "network timeout prevents hanging (simulated)" {
|
||||
# curl 超时参数应该生效
|
||||
# shellcheck disable=SC2016
|
||||
result=$(timeout 5 bash -c '
|
||||
result=$(curl -fsSL --connect-timeout 1 --max-time 2 "http://192.0.2.1:12345/test" 2>/dev/null || echo "failed")
|
||||
if [[ "$result" == "failed" ]]; then
|
||||
echo "timeout_works"
|
||||
fi
|
||||
')
|
||||
[[ "$result" == "timeout_works" ]]
|
||||
}
|
||||
|
||||
@test "empty version string is handled gracefully" {
|
||||
result=$(bash -c '
|
||||
latest=""
|
||||
if [[ -z "$latest" ]]; then
|
||||
echo "handled"
|
||||
fi
|
||||
')
|
||||
[[ "$result" == "handled" ]]
|
||||
}
|
||||
|
||||
# ===============================================================
|
||||
# Pipefail 模式安全模式测试
|
||||
# ===============================================================
|
||||
|
||||
@test "grep with no match doesn't cause exit in pipefail mode" {
|
||||
result=$(bash -c '
|
||||
set -euo pipefail
|
||||
echo "test" | grep "nonexistent" || true
|
||||
echo "survived"
|
||||
')
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
@test "command substitution failure is handled with || true" {
|
||||
result=$(bash -c '
|
||||
set -euo pipefail
|
||||
output=$(false) || true
|
||||
echo "survived"
|
||||
')
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
@test "arithmetic on zero doesn't cause exit" {
|
||||
result=$(bash -c '
|
||||
set -euo pipefail
|
||||
count=0
|
||||
((count++)) || true
|
||||
echo "$count"
|
||||
')
|
||||
[[ "$result" == "1" ]]
|
||||
}
|
||||
|
||||
# ===============================================================
|
||||
# 实际场景回归测试
|
||||
# ===============================================================
|
||||
|
||||
@test "safe_remove pattern doesn't fail on non-existent path" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||
safe_remove '$HOME/non/existent/path' true > /dev/null 2>&1 || true
|
||||
echo 'survived'
|
||||
")
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
@test "module loading doesn't fail" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||
echo 'loaded'
|
||||
")
|
||||
[[ "$result" == "loaded" ]]
|
||||
}
|
||||
230
tests/timeout_tests.bats
Normal file
230
tests/timeout_tests.bats
Normal file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env bats
|
||||
# Timeout functionality tests
|
||||
# Tests for lib/core/timeout.sh
|
||||
|
||||
setup() {
|
||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||
export PROJECT_ROOT
|
||||
export MO_DEBUG=0 # Disable debug output for cleaner tests
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Basic Timeout Functionality
|
||||
# =================================================================
|
||||
|
||||
@test "run_with_timeout: command completes before timeout" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 echo 'success'
|
||||
")
|
||||
[[ "$result" == "success" ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: zero timeout runs command normally" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 0 echo 'no_timeout'
|
||||
")
|
||||
[[ "$result" == "no_timeout" ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: invalid timeout runs command normally" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout invalid echo 'no_timeout'
|
||||
")
|
||||
[[ "$result" == "no_timeout" ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: negative timeout runs command normally" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout -5 echo 'no_timeout'
|
||||
")
|
||||
[[ "$result" == "no_timeout" ]]
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Exit Code Handling
|
||||
# =================================================================
|
||||
|
||||
@test "run_with_timeout: preserves command exit code on success" {
|
||||
bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 true
|
||||
"
|
||||
exit_code=$?
|
||||
[[ $exit_code -eq 0 ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: preserves command exit code on failure" {
|
||||
set +e
|
||||
bash -c "
|
||||
set +e # Don't exit on error
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 false
|
||||
exit \$?
|
||||
"
|
||||
exit_code=$?
|
||||
set -e
|
||||
[[ $exit_code -eq 1 ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: returns 124 on timeout (if using gtimeout)" {
|
||||
# This test only passes if gtimeout/timeout is available
|
||||
# Skip if using shell fallback (can't guarantee exit code 124 in all cases)
|
||||
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
||||
skip "gtimeout/timeout not available"
|
||||
fi
|
||||
|
||||
set +e
|
||||
bash -c "
|
||||
set +e
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 1 sleep 10
|
||||
exit \$?
|
||||
"
|
||||
exit_code=$?
|
||||
set -e
|
||||
[[ $exit_code -eq 124 ]]
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Timeout Behavior
|
||||
# =================================================================
|
||||
|
||||
@test "run_with_timeout: kills long-running command" {
|
||||
# Command should be killed after 2 seconds
|
||||
start_time=$(date +%s)
|
||||
set +e
|
||||
bash -c "
|
||||
set +e
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 2 sleep 30
|
||||
" >/dev/null 2>&1
|
||||
set -e
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
# Should complete in ~2 seconds, not 30
|
||||
# Allow some margin (up to 5 seconds for slow systems)
|
||||
[[ $duration -lt 10 ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: handles fast-completing commands" {
|
||||
# Fast command should complete immediately
|
||||
start_time=$(date +%s)
|
||||
bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 10 echo 'fast'
|
||||
" >/dev/null 2>&1
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
|
||||
# Should complete in ~0 seconds
|
||||
[[ $duration -lt 3 ]]
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Pipefail Compatibility
|
||||
# =================================================================
|
||||
|
||||
@test "run_with_timeout: works in pipefail mode" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 echo 'pipefail_test'
|
||||
")
|
||||
[[ "$result" == "pipefail_test" ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: doesn't cause unintended exits" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 true || true
|
||||
echo 'survived'
|
||||
")
|
||||
[[ "$result" == "survived" ]]
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Command Arguments
|
||||
# =================================================================
|
||||
|
||||
@test "run_with_timeout: handles commands with arguments" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 echo 'arg1' 'arg2' 'arg3'
|
||||
")
|
||||
[[ "$result" == "arg1 arg2 arg3" ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: handles commands with spaces in arguments" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 echo 'hello world'
|
||||
")
|
||||
[[ "$result" == "hello world" ]]
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Debug Logging
|
||||
# =================================================================
|
||||
|
||||
@test "run_with_timeout: debug logging when MO_DEBUG=1" {
|
||||
output=$(bash -c "
|
||||
set -euo pipefail
|
||||
export MO_DEBUG=1
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
run_with_timeout 5 echo 'test' 2>&1
|
||||
")
|
||||
# Should contain debug output
|
||||
[[ "$output" =~ TIMEOUT ]]
|
||||
}
|
||||
|
||||
@test "run_with_timeout: no debug logging when MO_DEBUG=0" {
|
||||
# When MO_DEBUG=0, no debug messages should appear during function execution
|
||||
# (Initialization messages may appear if module is loaded for first time)
|
||||
output=$(bash -c "
|
||||
set -euo pipefail
|
||||
export MO_DEBUG=0
|
||||
unset MO_TIMEOUT_INITIALIZED # Force re-initialization
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
# Capture only the function call output, not initialization
|
||||
run_with_timeout 5 echo 'test'
|
||||
" 2>/dev/null) # Discard stderr (initialization messages)
|
||||
# Should only have command output
|
||||
[[ "$output" == "test" ]]
|
||||
}
|
||||
|
||||
# =================================================================
|
||||
# Module Loading
|
||||
# =================================================================
|
||||
|
||||
@test "timeout.sh: prevents multiple sourcing" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
echo 'loaded'
|
||||
")
|
||||
[[ "$result" == "loaded" ]]
|
||||
}
|
||||
|
||||
@test "timeout.sh: sets MOLE_TIMEOUT_LOADED flag" {
|
||||
result=$(bash -c "
|
||||
set -euo pipefail
|
||||
source '$PROJECT_ROOT/lib/core/timeout.sh'
|
||||
echo \"\$MOLE_TIMEOUT_LOADED\"
|
||||
")
|
||||
[[ "$result" == "1" ]]
|
||||
}
|
||||
@@ -4,7 +4,10 @@ setup_file() {
|
||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||
export PROJECT_ROOT
|
||||
|
||||
ORIGINAL_HOME="${HOME:-}"
|
||||
ORIGINAL_HOME="${BATS_TMPDIR:-}" # Use BATS_TMPDIR as original HOME if set by bats
|
||||
if [[ -z "$ORIGINAL_HOME" ]]; then
|
||||
ORIGINAL_HOME="${HOME:-}"
|
||||
fi
|
||||
export ORIGINAL_HOME
|
||||
|
||||
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-uninstall-home.XXXXXX")"
|
||||
@@ -63,7 +66,9 @@ EOF
|
||||
HOME="$HOME" bash --noprofile --norc << 'EOF'
|
||||
set -euo pipefail
|
||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||
files="$(printf '%s\n%s\n' "$HOME/sized/file1" "$HOME/sized/file2")"
|
||||
files="$(printf '%s
|
||||
%s
|
||||
' "$HOME/sized/file1" "$HOME/sized/file2")"
|
||||
calculate_total_size "$files"
|
||||
EOF
|
||||
)"
|
||||
@@ -91,10 +96,10 @@ show_cursor() { :; }
|
||||
remove_apps_from_dock() { :; }
|
||||
pgrep() { return 1; }
|
||||
pkill() { return 0; }
|
||||
sudo() { return 0; }
|
||||
sudo() { return 0; } # Mock sudo command
|
||||
|
||||
app_bundle="$HOME/Applications/TestApp.app"
|
||||
mkdir -p "$app_bundle"
|
||||
mkdir -p "$app_bundle" # Ensure this is created in the temp HOME
|
||||
|
||||
related="$(find_app_files "com.example.TestApp" "TestApp")"
|
||||
encoded_related=$(printf '%s' "$related" | base64 | tr -d '\n')
|
||||
@@ -105,8 +110,10 @@ files_cleaned=0
|
||||
total_items=0
|
||||
total_size_cleaned=0
|
||||
|
||||
printf '\n' | batch_uninstall_applications >/dev/null
|
||||
# Use the actual bash function directly, don't pipe printf as that complicates stdin
|
||||
batch_uninstall_applications
|
||||
|
||||
# Verify cleanup
|
||||
[[ ! -d "$app_bundle" ]] || exit 1
|
||||
[[ ! -d "$HOME/Library/Application Support/TestApp" ]] || exit 1
|
||||
[[ ! -d "$HOME/Library/Caches/TestApp" ]] || exit 1
|
||||
@@ -116,6 +123,21 @@ EOF
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
@test "safe_remove can remove a simple directory" {
|
||||
mkdir -p "$HOME/test_dir"
|
||||
touch "$HOME/test_dir/file.txt"
|
||||
|
||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||
set -euo pipefail
|
||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||
|
||||
safe_remove "$HOME/test_dir"
|
||||
[[ ! -d "$HOME/test_dir" ]] || exit 1
|
||||
EOF
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
|
||||
|
||||
@test "decode_file_list validates base64 encoding" {
|
||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
||||
set -euo pipefail
|
||||
@@ -123,7 +145,8 @@ source "$PROJECT_ROOT/lib/core/common.sh"
|
||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||
|
||||
# Valid base64 encoded path list
|
||||
valid_data=$(printf '/path/one\n/path/two' | base64)
|
||||
valid_data=$(printf '/path/one
|
||||
/path/two' | base64)
|
||||
result=$(decode_file_list "$valid_data" "TestApp")
|
||||
[[ -n "$result" ]] || exit 1
|
||||
EOF
|
||||
@@ -184,4 +207,4 @@ fi
|
||||
EOF
|
||||
|
||||
[ "$status" -eq 0 ]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user