1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 18:34:46 +00:00

Automated test optimization increased to 132

This commit is contained in:
Tw93
2025-12-08 15:33:23 +08:00
parent e7fd73302d
commit 479cba6ca3
6 changed files with 632 additions and 62 deletions

63
.github/workflows/quality.yml vendored Normal file
View File

@@ -0,0 +1,63 @@
name: Quality
on:
push:
branches: [main]
pull_request:
permissions:
contents: write
jobs:
shell-quality:
name: Code Quality
runs-on: macos-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache Homebrew
uses: actions/cache@v4
with:
path: |
~/Library/Caches/Homebrew
/usr/local/Cellar/shfmt
/usr/local/Cellar/shellcheck
key: ${{ runner.os }}-brew-quality-${{ hashFiles('**/Brewfile') }}
restore-keys: |
${{ runner.os }}-brew-quality-
- name: Install tools
run: brew install shfmt shellcheck
- name: Format check
run: |
echo "Checking shell script formatting..."
./scripts/format.sh
if [[ -n $(git status --porcelain) ]]; then
echo "Code formatting issues found:"
git diff
exit 1
fi
echo "✓ All scripts properly formatted"
- name: ShellCheck
run: |
echo "Running ShellCheck on all shell scripts..."
shellcheck mole
shellcheck bin/*.sh
find lib -name "*.sh" -exec shellcheck {} +
echo "✓ ShellCheck passed"
- name: Syntax check
run: |
echo "Checking Bash syntax..."
bash -n mole
for script in bin/*.sh; do
bash -n "$script"
done
find lib -name "*.sh" | while read -r script; do
bash -n "$script"
done
echo "✓ All scripts have valid syntax"

View File

@@ -1,55 +0,0 @@
name: Shell Script Quality Checks
on:
push:
branches: [main]
pull_request:
permissions:
contents: write
jobs:
shell-quality-checks:
runs-on: macos-latest
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24.6"
cache: true
- name: Cache Homebrew packages
uses: actions/cache@v4
with:
path: |
~/Library/Caches/Homebrew
/usr/local/Cellar/bats-core
/usr/local/Cellar/shfmt
/usr/local/Cellar/shellcheck
key: ${{ runner.os }}-brew-v1
restore-keys: |
${{ runner.os }}-brew-
- name: Install shell linting and testing tools
run: brew install bats-core shfmt shellcheck
- name: Auto-format shell scripts with shfmt
run: ./scripts/format.sh
- name: Run shellcheck linter and bats tests
run: |
./scripts/check.sh
echo "✓ All quality checks passed"
- name: Run all bats tests with summary
run: |
echo "Running all test suites..."
bats tests/*.bats --formatter tap
echo ""
echo "Test summary:"
echo " Total test files: $(ls tests/*.bats | wc -l | tr -d ' ')"
echo " Total tests: $(grep -c "^@test" tests/*.bats | awk -F: '{sum+=$2} END {print sum}')"

140
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,140 @@
name: Tests
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
jobs:
unit-tests:
name: Unit Tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install bats
run: brew install bats-core
- name: Run all test suites
run: |
echo "Running all test suites..."
bats tests/*.bats --formatter tap
echo ""
echo "Test summary:"
echo " Total test files: $(ls tests/*.bats | wc -l | tr -d ' ')"
echo " Total tests: $(grep -c "^@test" tests/*.bats | awk -F: '{sum+=$2} END {print sum}')"
echo "✓ All tests passed"
go-tests:
name: Go Tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Build Go binaries
run: |
echo "Building Go binaries..."
go build ./...
echo "✓ Build successful"
- name: Run go vet
run: |
echo "Running go vet..."
go vet ./cmd/...
echo "✓ Vet passed"
- name: Check formatting
run: |
echo "Checking Go formatting..."
if [ -n "$(gofmt -l ./cmd)" ]; then
echo "Go code is not formatted:"
gofmt -d ./cmd
exit 1
fi
echo "✓ Go code properly formatted"
integration-tests:
name: Integration Tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: brew install coreutils
- name: Test module loading
run: |
echo "Testing module loading..."
bash -c 'source lib/core/common.sh && echo "✓ Modules loaded successfully"'
- name: Test clean --dry-run
run: |
echo "Testing clean --dry-run..."
./bin/clean.sh --dry-run
echo "✓ Clean dry-run completed"
- name: Test installation
run: |
echo "Testing installation script..."
./install.sh --prefix /tmp/mole-test
test -f /tmp/mole-test/mole
echo "✓ Installation successful"
compatibility:
name: macOS Compatibility
strategy:
matrix:
os: [macos-13, macos-14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Test on ${{ matrix.os }}
run: |
echo "Testing on ${{ matrix.os }}..."
bash -n mole
source lib/core/common.sh
echo "✓ Successfully loaded on ${{ matrix.os }}"
security:
name: Security Checks
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Check for unsafe rm usage
run: |
echo "Checking for unsafe rm patterns..."
if grep -r "rm -rf" --include="*.sh" lib/ | grep -v "safe_remove\|validate_path\|# "; then
echo "✗ Unsafe rm -rf usage found"
exit 1
fi
echo "✓ No unsafe rm usage found"
- name: Verify app protection
run: |
echo "Verifying critical file protection..."
bash -c '
source lib/core/common.sh
if should_protect_from_uninstall "com.apple.Safari"; then
echo "✓ Safari is protected"
else
echo "✗ Safari protection failed"
exit 1
fi
'
- name: Check for secrets
run: |
echo "Checking for hardcoded secrets..."
if grep -r "password\|secret\|api_key" --include="*.sh" . | grep -v "# \|test"; then
echo "✗ Potential secrets found"
exit 1
fi
echo "✓ No secrets found"

169
tests/regression_bugs.bats Normal file
View 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
View 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" ]]
}

View File

@@ -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 ]
}
}