mirror of
https://github.com/tw93/Mole.git
synced 2026-03-22 20:15:07 +00:00
Add dry-run support across destructive commands (#516)
* chore: update contributors [skip ci] * Add dry-run support across destructive commands Implement dry-run for uninstall, purge, installer, touchid, completion, and remove flows.\nGuard side effects in uninstall path (launchctl, defaults writes, kill/brew actions), update help/README, and add coverage in CLI/Bats tests.\n\nValidation: ./scripts/check.sh and ./scripts/test.sh (452 tests, 0 failures, 8 skipped). * test(purge): keep dev-compatible purge coverage --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Tw93 <hitw93@gmail.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 533 KiB After Width: | Height: | Size: 541 KiB |
@@ -68,6 +68,12 @@ mo clean --dry-run --debug # Detailed preview with risk levels and file info
|
|||||||
mo optimize --dry-run # Preview optimization actions
|
mo optimize --dry-run # Preview optimization actions
|
||||||
mo optimize --debug # Run with detailed operation logs
|
mo optimize --debug # Run with detailed operation logs
|
||||||
mo optimize --whitelist # Manage protected optimization rules
|
mo optimize --whitelist # Manage protected optimization rules
|
||||||
|
mo uninstall --dry-run # Preview app uninstall actions
|
||||||
|
mo purge --dry-run # Preview project artifact purge
|
||||||
|
mo installer --dry-run # Preview installer cleanup actions
|
||||||
|
mo touchid enable --dry-run # Preview Touch ID sudo config changes
|
||||||
|
mo completion --dry-run # Preview shell completion file updates
|
||||||
|
mo remove --dry-run # Preview Mole self-removal
|
||||||
mo purge --paths # Configure project scan directories
|
mo purge --paths # Configure project scan directories
|
||||||
mo analyze /Volumes # Analyze external drives only
|
mo analyze /Volumes # Analyze external drives only
|
||||||
```
|
```
|
||||||
@@ -75,7 +81,7 @@ mo analyze /Volumes # Analyze external drives only
|
|||||||
## Tips
|
## Tips
|
||||||
|
|
||||||
- Video tutorial: Watch the [Mole tutorial video](https://www.youtube.com/watch?v=UEe9-w4CcQ0), thanks to PAPAYA 電腦教室.
|
- Video tutorial: Watch the [Mole tutorial video](https://www.youtube.com/watch?v=UEe9-w4CcQ0), thanks to PAPAYA 電腦教室.
|
||||||
- Safety first: Deletions are permanent. Review carefully and preview with `mo clean --dry-run`. See [Security Audit](SECURITY_AUDIT.md).
|
- Safety first: Deletions are permanent. Review carefully with dry-run before applying changes. See [Security Audit](SECURITY_AUDIT.md).
|
||||||
- Debug and logs: Use `--debug` for detailed logs. Combine with `--dry-run` for a full preview. File operations are logged to `~/.config/mole/operations.log`. Disable with `MO_NO_OPLOG=1`.
|
- Debug and logs: Use `--debug` for detailed logs. Combine with `--dry-run` for a full preview. File operations are logged to `~/.config/mole/operations.log`. Disable with `MO_NO_OPLOG=1`.
|
||||||
- Navigation: Mole supports arrow keys and Vim bindings `h/j/k/l`.
|
- Navigation: Mole supports arrow keys and Vim bindings `h/j/k/l`.
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,33 @@ emit_fish_completions() {
|
|||||||
printf 'complete -c %s -n "not __fish_mole_no_subcommand" -a fish -d "generate fish completion" -n "__fish_see_subcommand_path completion"\n' "$cmd"
|
printf 'complete -c %s -n "not __fish_mole_no_subcommand" -a fish -d "generate fish completion" -n "__fish_see_subcommand_path completion"\n' "$cmd"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DRY_RUN_MODE=false
|
||||||
|
if [[ $# -gt 0 ]]; then
|
||||||
|
normalized_args=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
"--dry-run" | "-n")
|
||||||
|
DRY_RUN_MODE=true
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
normalized_args+=("$arg")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
if [[ ${#normalized_args[@]} -gt 0 ]]; then
|
||||||
|
set -- "${normalized_args[@]}"
|
||||||
|
else
|
||||||
|
set --
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# Auto-install mode when run without arguments
|
# Auto-install mode when run without arguments
|
||||||
if [[ $# -eq 0 ]]; then
|
if [[ $# -eq 0 ]]; then
|
||||||
|
if [[ "$DRY_RUN_MODE" == "true" ]]; then
|
||||||
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, shell config files will not be modified"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
# Detect current shell
|
# Detect current shell
|
||||||
current_shell="${SHELL##*/}"
|
current_shell="${SHELL##*/}"
|
||||||
if [[ -z "$current_shell" ]]; then
|
if [[ -z "$current_shell" ]]; then
|
||||||
@@ -73,16 +98,21 @@ if [[ $# -eq 0 ]]; then
|
|||||||
|
|
||||||
if [[ -z "$completion_name" ]]; then
|
if [[ -z "$completion_name" ]]; then
|
||||||
if [[ -f "$config_file" ]] && grep -Eq "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" 2> /dev/null; then
|
if [[ -f "$config_file" ]] && grep -Eq "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" 2> /dev/null; then
|
||||||
original_mode=""
|
if [[ "$DRY_RUN_MODE" == "true" ]]; then
|
||||||
original_mode="$(stat -f '%Mp%Lp' "$config_file" 2> /dev/null || true)"
|
echo -e "${GRAY}${ICON_REVIEW} [DRY RUN] Would remove stale completion entries from $config_file${NC}"
|
||||||
temp_file="$(mktemp)"
|
echo ""
|
||||||
grep -Ev "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" > "$temp_file" || true
|
else
|
||||||
mv "$temp_file" "$config_file"
|
original_mode=""
|
||||||
if [[ -n "$original_mode" ]]; then
|
original_mode="$(stat -f '%Mp%Lp' "$config_file" 2> /dev/null || true)"
|
||||||
chmod "$original_mode" "$config_file" 2> /dev/null || true
|
temp_file="$(mktemp)"
|
||||||
|
grep -Ev "(^# Mole shell completion$|(mole|mo)[[:space:]]+completion)" "$config_file" > "$temp_file" || true
|
||||||
|
mv "$temp_file" "$config_file"
|
||||||
|
if [[ -n "$original_mode" ]]; then
|
||||||
|
chmod "$original_mode" "$config_file" 2> /dev/null || true
|
||||||
|
fi
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed stale completion entries from $config_file"
|
||||||
|
echo ""
|
||||||
fi
|
fi
|
||||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} Removed stale completion entries from $config_file"
|
|
||||||
echo ""
|
|
||||||
fi
|
fi
|
||||||
log_error "mole not found in PATH, install Mole before enabling completion"
|
log_error "mole not found in PATH, install Mole before enabling completion"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -90,6 +120,12 @@ if [[ $# -eq 0 ]]; then
|
|||||||
|
|
||||||
# Check if already installed and normalize to latest line
|
# Check if already installed and normalize to latest line
|
||||||
if [[ -f "$config_file" ]] && grep -Eq "(mole|mo)[[:space:]]+completion" "$config_file" 2> /dev/null; then
|
if [[ -f "$config_file" ]] && grep -Eq "(mole|mo)[[:space:]]+completion" "$config_file" 2> /dev/null; then
|
||||||
|
if [[ "$DRY_RUN_MODE" == "true" ]]; then
|
||||||
|
echo -e "${GRAY}${ICON_REVIEW} [DRY RUN] Would normalize completion entry in $config_file${NC}"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
original_mode=""
|
original_mode=""
|
||||||
original_mode="$(stat -f '%Mp%Lp' "$config_file" 2> /dev/null || true)"
|
original_mode="$(stat -f '%Mp%Lp' "$config_file" 2> /dev/null || true)"
|
||||||
temp_file="$(mktemp)"
|
temp_file="$(mktemp)"
|
||||||
@@ -114,6 +150,11 @@ if [[ $# -eq 0 ]]; then
|
|||||||
echo -e "${GRAY}Will add to ${config_file}:${NC}"
|
echo -e "${GRAY}Will add to ${config_file}:${NC}"
|
||||||
echo " $completion_line"
|
echo " $completion_line"
|
||||||
echo ""
|
echo ""
|
||||||
|
if [[ "$DRY_RUN_MODE" == "true" ]]; then
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} Dry run complete, no changes made"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
echo -ne "${PURPLE}${ICON_ARROW}${NC} Enable completion for ${GREEN}${current_shell}${NC}? ${GRAY}Enter confirm / Q cancel${NC}: "
|
echo -ne "${PURPLE}${ICON_ARROW}${NC} Enable completion for ${GREEN}${current_shell}${NC}? ${GRAY}Enter confirm / Q cancel${NC}: "
|
||||||
IFS= read -r -s -n1 key || key=""
|
IFS= read -r -s -n1 key || key=""
|
||||||
drain_pending_input
|
drain_pending_input
|
||||||
@@ -227,6 +268,7 @@ Setup shell tab completion for mole and mo commands.
|
|||||||
|
|
||||||
Auto-install:
|
Auto-install:
|
||||||
mole completion # Auto-detect shell and install
|
mole completion # Auto-detect shell and install
|
||||||
|
mole completion --dry-run # Preview config changes without writing files
|
||||||
|
|
||||||
Manual install:
|
Manual install:
|
||||||
mole completion bash # Generate bash completion script
|
mole completion bash # Generate bash completion script
|
||||||
|
|||||||
@@ -650,13 +650,22 @@ perform_installers() {
|
|||||||
show_summary() {
|
show_summary() {
|
||||||
local summary_heading="Installers cleaned"
|
local summary_heading="Installers cleaned"
|
||||||
local -a summary_details=()
|
local -a summary_details=()
|
||||||
|
local dry_run_mode="${MOLE_DRY_RUN:-0}"
|
||||||
|
|
||||||
|
if [[ "$dry_run_mode" == "1" ]]; then
|
||||||
|
summary_heading="Dry run complete - no changes made"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $total_deleted -gt 0 ]]; then
|
if [[ $total_deleted -gt 0 ]]; then
|
||||||
local freed_mb
|
local freed_mb
|
||||||
freed_mb=$(echo "$total_size_freed_kb" | awk '{printf "%.2f", $1/1024}')
|
freed_mb=$(echo "$total_size_freed_kb" | awk '{printf "%.2f", $1/1024}')
|
||||||
|
|
||||||
summary_details+=("Removed ${GREEN}$total_deleted${NC} installers, freed ${GREEN}${freed_mb}MB${NC}")
|
if [[ "$dry_run_mode" == "1" ]]; then
|
||||||
summary_details+=("Your Mac is cleaner now!")
|
summary_details+=("Would remove ${GREEN}$total_deleted${NC} installers, free ${GREEN}${freed_mb}MB${NC}")
|
||||||
|
else
|
||||||
|
summary_details+=("Removed ${GREEN}$total_deleted${NC} installers, freed ${GREEN}${freed_mb}MB${NC}")
|
||||||
|
summary_details+=("Your Mac is cleaner now!")
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
summary_details+=("No installers were removed")
|
summary_details+=("No installers were removed")
|
||||||
fi
|
fi
|
||||||
@@ -675,6 +684,9 @@ main() {
|
|||||||
"--debug")
|
"--debug")
|
||||||
export MO_DEBUG=1
|
export MO_DEBUG=1
|
||||||
;;
|
;;
|
||||||
|
"--dry-run" | "-n")
|
||||||
|
export MOLE_DRY_RUN=1
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $arg"
|
echo "Unknown option: $arg"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -682,6 +694,11 @@ main() {
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No installer files will be removed"
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
hide_cursor
|
hide_cursor
|
||||||
perform_installers
|
perform_installers
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
|
|||||||
15
bin/purge.sh
15
bin/purge.sh
@@ -205,11 +205,18 @@ perform_purge() {
|
|||||||
rm -f "$stats_dir/purge_count"
|
rm -f "$stats_dir/purge_count"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
summary_heading="Dry run complete - no changes made"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ $total_size_cleaned -gt 0 ]]; then
|
if [[ $total_size_cleaned -gt 0 ]]; then
|
||||||
local freed_gb
|
local freed_gb
|
||||||
freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}')
|
freed_gb=$(echo "$total_size_cleaned" | awk '{printf "%.2f", $1/1024/1024}')
|
||||||
|
|
||||||
local summary_line="Space freed: ${GREEN}${freed_gb}GB${NC}"
|
local summary_line="Space freed: ${GREEN}${freed_gb}GB${NC}"
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
summary_line="Would free: ${GREEN}${freed_gb}GB${NC}"
|
||||||
|
fi
|
||||||
[[ $total_items_cleaned -gt 0 ]] && summary_line+=" | Items: $total_items_cleaned"
|
[[ $total_items_cleaned -gt 0 ]] && summary_line+=" | Items: $total_items_cleaned"
|
||||||
summary_line+=" | Free: $(get_free_space)"
|
summary_line+=" | Free: $(get_free_space)"
|
||||||
summary_details+=("$summary_line")
|
summary_details+=("$summary_line")
|
||||||
@@ -233,6 +240,7 @@ show_help() {
|
|||||||
echo ""
|
echo ""
|
||||||
echo -e "${YELLOW}Options:${NC}"
|
echo -e "${YELLOW}Options:${NC}"
|
||||||
echo " --paths Edit custom scan directories"
|
echo " --paths Edit custom scan directories"
|
||||||
|
echo " --dry-run Preview purge actions without making changes"
|
||||||
echo " --debug Enable debug logging"
|
echo " --debug Enable debug logging"
|
||||||
echo " --help Show this help message"
|
echo " --help Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
@@ -262,6 +270,9 @@ main() {
|
|||||||
"--debug")
|
"--debug")
|
||||||
export MO_DEBUG=1
|
export MO_DEBUG=1
|
||||||
;;
|
;;
|
||||||
|
"--dry-run" | "-n")
|
||||||
|
export MOLE_DRY_RUN=1
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Unknown option: $arg"
|
echo "Unknown option: $arg"
|
||||||
echo "Use 'mo purge --help' for usage information"
|
echo "Use 'mo purge --help' for usage information"
|
||||||
@@ -271,6 +282,10 @@ main() {
|
|||||||
done
|
done
|
||||||
|
|
||||||
start_purge
|
start_purge
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No project artifacts will be removed"
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
hide_cursor
|
hide_cursor
|
||||||
perform_purge
|
perform_purge
|
||||||
show_cursor
|
show_cursor
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ supports_touchid() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
touchid_dry_run_enabled() {
|
||||||
|
[[ "${MOLE_DRY_RUN:-0}" == "1" ]]
|
||||||
|
}
|
||||||
|
|
||||||
# Show current Touch ID status
|
# Show current Touch ID status
|
||||||
show_status() {
|
show_status() {
|
||||||
if is_touchid_configured; then
|
if is_touchid_configured; then
|
||||||
@@ -74,6 +78,16 @@ enable_touchid() {
|
|||||||
# Cleanup trap handled by global EXIT trap
|
# Cleanup trap handled by global EXIT trap
|
||||||
local temp_file=""
|
local temp_file=""
|
||||||
|
|
||||||
|
if touchid_dry_run_enabled; then
|
||||||
|
if is_touchid_configured; then
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS} Touch ID is already enabled, no changes needed${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS} [DRY RUN] Would enable Touch ID for sudo${NC}"
|
||||||
|
echo -e "${GRAY}${ICON_REVIEW} Target files: ${PAM_SUDO_FILE} and/or ${PAM_SUDO_LOCAL_FILE}${NC}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# First check if system supports Touch ID
|
# First check if system supports Touch ID
|
||||||
if ! supports_touchid; then
|
if ! supports_touchid; then
|
||||||
log_warning "This Mac may not support Touch ID"
|
log_warning "This Mac may not support Touch ID"
|
||||||
@@ -201,6 +215,16 @@ disable_touchid() {
|
|||||||
# Cleanup trap handled by global EXIT trap
|
# Cleanup trap handled by global EXIT trap
|
||||||
local temp_file=""
|
local temp_file=""
|
||||||
|
|
||||||
|
if touchid_dry_run_enabled; then
|
||||||
|
if ! is_touchid_configured; then
|
||||||
|
echo -e "${YELLOW}Touch ID is not currently enabled${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS} [DRY RUN] Would disable Touch ID for sudo${NC}"
|
||||||
|
echo -e "${GRAY}${ICON_REVIEW} Target files: ${PAM_SUDO_FILE} and/or ${PAM_SUDO_LOCAL_FILE}${NC}"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
if ! is_touchid_configured; then
|
if ! is_touchid_configured; then
|
||||||
echo -e "${YELLOW}Touch ID is not currently enabled${NC}"
|
echo -e "${YELLOW}Touch ID is not currently enabled${NC}"
|
||||||
return 0
|
return 0
|
||||||
@@ -303,12 +327,39 @@ show_menu() {
|
|||||||
|
|
||||||
# Main
|
# Main
|
||||||
main() {
|
main() {
|
||||||
local command="${1:-}"
|
local command=""
|
||||||
|
local arg
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
"--dry-run" | "-n")
|
||||||
|
export MOLE_DRY_RUN=1
|
||||||
|
;;
|
||||||
|
"--help" | "-h")
|
||||||
|
show_touchid_help
|
||||||
|
return 0
|
||||||
|
;;
|
||||||
|
enable | disable | status)
|
||||||
|
if [[ -z "$command" ]]; then
|
||||||
|
command="$arg"
|
||||||
|
else
|
||||||
|
log_error "Only one touchid command is supported per run"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log_error "Unknown command: $arg"
|
||||||
|
return 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if touchid_dry_run_enabled; then
|
||||||
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No sudo authentication files will be modified"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
case "$command" in
|
case "$command" in
|
||||||
"--help" | "-h")
|
|
||||||
show_touchid_help
|
|
||||||
;;
|
|
||||||
enable)
|
enable)
|
||||||
enable_touchid
|
enable_touchid
|
||||||
;;
|
;;
|
||||||
|
|||||||
@@ -822,10 +822,17 @@ main() {
|
|||||||
"--debug")
|
"--debug")
|
||||||
export MO_DEBUG=1
|
export MO_DEBUG=1
|
||||||
;;
|
;;
|
||||||
|
"--dry-run" | "-n")
|
||||||
|
export MOLE_DRY_RUN=1
|
||||||
|
;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
hide_cursor
|
hide_cursor
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, No app files or settings will be modified"
|
||||||
|
printf '\n'
|
||||||
|
fi
|
||||||
|
|
||||||
local first_scan=true
|
local first_scan=true
|
||||||
while true; do
|
while true; do
|
||||||
|
|||||||
@@ -1367,6 +1367,7 @@ clean_project_artifacts() {
|
|||||||
echo ""
|
echo ""
|
||||||
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
local stats_dir="${XDG_CACHE_HOME:-$HOME/.cache}/mole"
|
||||||
local cleaned_count=0
|
local cleaned_count=0
|
||||||
|
local dry_run_mode="${MOLE_DRY_RUN:-0}"
|
||||||
for idx in "${selected_indices[@]}"; do
|
for idx in "${selected_indices[@]}"; do
|
||||||
local item_path="${item_paths[idx]}"
|
local item_path="${item_paths[idx]}"
|
||||||
local artifact_type=$(basename "$item_path")
|
local artifact_type=$(basename "$item_path")
|
||||||
@@ -1388,7 +1389,7 @@ clean_project_artifacts() {
|
|||||||
fi
|
fi
|
||||||
if [[ -e "$item_path" ]]; then
|
if [[ -e "$item_path" ]]; then
|
||||||
safe_remove "$item_path" true
|
safe_remove "$item_path" true
|
||||||
if [[ ! -e "$item_path" ]]; then
|
if [[ "$dry_run_mode" == "1" || ! -e "$item_path" ]]; then
|
||||||
local current_total=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
local current_total=$(cat "$stats_dir/purge_stats" 2> /dev/null || echo "0")
|
||||||
echo "$((current_total + size_kb))" > "$stats_dir/purge_stats"
|
echo "$((current_total + size_kb))" > "$stats_dir/purge_stats"
|
||||||
cleaned_count=$((cleaned_count + 1))
|
cleaned_count=$((cleaned_count + 1))
|
||||||
@@ -1396,7 +1397,11 @@ clean_project_artifacts() {
|
|||||||
fi
|
fi
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
stop_inline_spinner
|
stop_inline_spinner
|
||||||
echo -e "${GREEN}${ICON_SUCCESS}${NC} $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}"
|
if [[ "$dry_run_mode" == "1" ]]; then
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} [DRY RUN] $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} $project_path, $artifact_type${NC}, ${GREEN}$size_human${NC}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
# Update count
|
# Update count
|
||||||
|
|||||||
@@ -1419,6 +1419,11 @@ force_kill_app() {
|
|||||||
local app_name="$1"
|
local app_name="$1"
|
||||||
local app_path="${2:-""}"
|
local app_path="${2:-""}"
|
||||||
|
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
debug_log "[DRY RUN] Would terminate running app: $app_name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Get the executable name from bundle if app_path is provided
|
# Get the executable name from bundle if app_path is provided
|
||||||
local exec_name=""
|
local exec_name=""
|
||||||
if [[ -n "$app_path" && -e "$app_path/Contents/Info.plist" ]]; then
|
if [[ -n "$app_path" && -e "$app_path/Contents/Info.plist" ]]; then
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ show_installer_help() {
|
|||||||
echo "Find and remove installer files (.dmg, .pkg, .iso, .xip, .zip)."
|
echo "Find and remove installer files (.dmg, .pkg, .iso, .xip, .zip)."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
|
echo " --dry-run Preview installer cleanup without making changes"
|
||||||
echo " --debug Show detailed operation logs"
|
echo " --debug Show detailed operation logs"
|
||||||
echo " -h, --help Show this help message"
|
echo " -h, --help Show this help message"
|
||||||
}
|
}
|
||||||
@@ -45,6 +46,7 @@ show_touchid_help() {
|
|||||||
echo " status Show current Touch ID status"
|
echo " status Show current Touch ID status"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
|
echo " --dry-run Preview Touch ID changes without modifying sudo config"
|
||||||
echo " -h, --help Show this help message"
|
echo " -h, --help Show this help message"
|
||||||
echo ""
|
echo ""
|
||||||
echo "If no command is provided, an interactive menu is shown."
|
echo "If no command is provided, an interactive menu is shown."
|
||||||
@@ -56,6 +58,7 @@ show_uninstall_help() {
|
|||||||
echo "Interactively remove applications and their leftover files."
|
echo "Interactively remove applications and their leftover files."
|
||||||
echo ""
|
echo ""
|
||||||
echo "Options:"
|
echo "Options:"
|
||||||
|
echo " --dry-run Preview app uninstallation without making changes"
|
||||||
echo " --debug Show detailed operation logs"
|
echo " --debug Show detailed operation logs"
|
||||||
echo " -h, --help Show this help message"
|
echo " -h, --help Show this help message"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|||||||
|
|
||||||
# Batch uninstall with a single confirmation.
|
# Batch uninstall with a single confirmation.
|
||||||
|
|
||||||
|
get_lsregister_path() {
|
||||||
|
echo "/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_uninstall_dry_run() {
|
||||||
|
[[ "${MOLE_DRY_RUN:-0}" == "1" ]]
|
||||||
|
}
|
||||||
|
|
||||||
# High-performance sensitive data detection (pure Bash, no subprocess)
|
# High-performance sensitive data detection (pure Bash, no subprocess)
|
||||||
# Faster than grep for batch operations, especially when processing many apps
|
# Faster than grep for batch operations, especially when processing many apps
|
||||||
has_sensitive_data() {
|
has_sensitive_data() {
|
||||||
@@ -77,6 +85,11 @@ stop_launch_services() {
|
|||||||
local bundle_id="$1"
|
local bundle_id="$1"
|
||||||
local has_system_files="${2:-false}"
|
local has_system_files="${2:-false}"
|
||||||
|
|
||||||
|
if is_uninstall_dry_run; then
|
||||||
|
debug_log "[DRY RUN] Would unload launch services for bundle: $bundle_id"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
[[ -z "$bundle_id" || "$bundle_id" == "unknown" ]] && return 0
|
||||||
|
|
||||||
# Validate bundle_id format: must be reverse-DNS style (e.g., com.example.app)
|
# Validate bundle_id format: must be reverse-DNS style (e.g., com.example.app)
|
||||||
@@ -152,6 +165,11 @@ remove_login_item() {
|
|||||||
local app_name="$1"
|
local app_name="$1"
|
||||||
local bundle_id="$2"
|
local bundle_id="$2"
|
||||||
|
|
||||||
|
if is_uninstall_dry_run; then
|
||||||
|
debug_log "[DRY RUN] Would remove login item: ${app_name:-$bundle_id}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Skip if no identifiers provided
|
# Skip if no identifiers provided
|
||||||
[[ -z "$app_name" && -z "$bundle_id" ]] && return 0
|
[[ -z "$app_name" && -z "$bundle_id" ]] && return 0
|
||||||
|
|
||||||
@@ -201,7 +219,12 @@ remove_file_list() {
|
|||||||
safe_remove_symlink "$file" "$use_sudo" && ((++count)) || true
|
safe_remove_symlink "$file" "$use_sudo" && ((++count)) || true
|
||||||
else
|
else
|
||||||
if [[ "$use_sudo" == "true" ]]; then
|
if [[ "$use_sudo" == "true" ]]; then
|
||||||
safe_sudo_remove "$file" && ((++count)) || true
|
if is_uninstall_dry_run; then
|
||||||
|
debug_log "[DRY RUN] Would sudo remove: $file"
|
||||||
|
((++count))
|
||||||
|
else
|
||||||
|
safe_sudo_remove "$file" && ((++count)) || true
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
safe_remove "$file" true && ((++count)) || true
|
safe_remove "$file" true && ((++count)) || true
|
||||||
fi
|
fi
|
||||||
@@ -437,7 +460,7 @@ batch_uninstall_applications() {
|
|||||||
export MOLE_UNINSTALL_MODE=1
|
export MOLE_UNINSTALL_MODE=1
|
||||||
|
|
||||||
# Request sudo if needed.
|
# Request sudo if needed.
|
||||||
if [[ ${#sudo_apps[@]} -gt 0 ]]; then
|
if [[ ${#sudo_apps[@]} -gt 0 && "${MOLE_DRY_RUN:-0}" != "1" ]]; then
|
||||||
if ! sudo -n true 2> /dev/null; then
|
if ! sudo -n true 2> /dev/null; then
|
||||||
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
|
if ! request_sudo_access "Admin required for system apps: ${sudo_apps[*]}"; then
|
||||||
echo ""
|
echo ""
|
||||||
@@ -547,12 +570,18 @@ batch_uninstall_applications() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
local ret=0
|
if is_uninstall_dry_run; then
|
||||||
safe_sudo_remove "$app_path" || ret=$?
|
if ! safe_remove "$app_path" true; then
|
||||||
if [[ $ret -ne 0 ]]; then
|
reason="dry-run path validation failed"
|
||||||
local diagnosis
|
fi
|
||||||
diagnosis=$(diagnose_removal_failure "$ret" "$app_name")
|
else
|
||||||
IFS='|' read -r reason suggestion <<< "$diagnosis"
|
local ret=0
|
||||||
|
safe_sudo_remove "$app_path" || ret=$?
|
||||||
|
if [[ $ret -ne 0 ]]; then
|
||||||
|
local diagnosis
|
||||||
|
diagnosis=$(diagnose_removal_failure "$ret" "$app_name")
|
||||||
|
IFS='|' read -r reason suggestion <<< "$diagnosis"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
@@ -583,10 +612,14 @@ batch_uninstall_applications() {
|
|||||||
remove_file_list "$system_all" "true" > /dev/null
|
remove_file_list "$system_all" "true" > /dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Clean up macOS defaults (preference domains).
|
# Defaults writes are side effects that should never run in dry-run mode.
|
||||||
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
|
if [[ -n "$bundle_id" && "$bundle_id" != "unknown" ]]; then
|
||||||
if defaults read "$bundle_id" &> /dev/null; then
|
if is_uninstall_dry_run; then
|
||||||
defaults delete "$bundle_id" 2> /dev/null || true
|
debug_log "[DRY RUN] Would clear defaults domain: $bundle_id"
|
||||||
|
else
|
||||||
|
if defaults read "$bundle_id" &> /dev/null; then
|
||||||
|
defaults delete "$bundle_id" 2> /dev/null || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ByHost preferences (machine-specific).
|
# ByHost preferences (machine-specific).
|
||||||
@@ -644,8 +677,15 @@ batch_uninstall_applications() {
|
|||||||
local success_text="app"
|
local success_text="app"
|
||||||
[[ $success_count -gt 1 ]] && success_text="apps"
|
[[ $success_count -gt 1 ]] && success_text="apps"
|
||||||
local success_line="Removed ${success_count} ${success_text}"
|
local success_line="Removed ${success_count} ${success_text}"
|
||||||
|
if is_uninstall_dry_run; then
|
||||||
|
success_line="Would remove ${success_count} ${success_text}"
|
||||||
|
fi
|
||||||
if [[ -n "$freed_display" ]]; then
|
if [[ -n "$freed_display" ]]; then
|
||||||
success_line+=", freed ${GREEN}${freed_display}${NC}"
|
if is_uninstall_dry_run; then
|
||||||
|
success_line+=", would free ${GREEN}${freed_display}${NC}"
|
||||||
|
else
|
||||||
|
success_line+=", freed ${GREEN}${freed_display}${NC}"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Format app list with max 3 per line.
|
# Format app list with max 3 per line.
|
||||||
@@ -730,24 +770,48 @@ batch_uninstall_applications() {
|
|||||||
if [[ "$summary_status" == "warn" ]]; then
|
if [[ "$summary_status" == "warn" ]]; then
|
||||||
title="Uninstall incomplete"
|
title="Uninstall incomplete"
|
||||||
fi
|
fi
|
||||||
|
if is_uninstall_dry_run; then
|
||||||
|
title="Uninstall dry run complete"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
print_summary_block "$title" "${summary_details[@]}"
|
print_summary_block "$title" "${summary_details[@]}"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
|
|
||||||
if [[ $success_count -gt 0 && ${#success_items[@]} -gt 0 ]]; then
|
# Auto-run brew autoremove if Homebrew casks were uninstalled
|
||||||
# Kick off LaunchServices rebuild in background immediately after summary.
|
if [[ $brew_apps_removed -gt 0 ]]; then
|
||||||
# The caller shows a 3s "Press Enter" prompt, giving the rebuild time to finish
|
if is_uninstall_dry_run; then
|
||||||
# before the user returns to the app list — fixes stale Spotlight entries (#490).
|
log_info "[DRY RUN] Would run brew autoremove"
|
||||||
(
|
else
|
||||||
refresh_launch_services_after_uninstall 2> /dev/null || true
|
# Show spinner while checking for orphaned dependencies
|
||||||
remove_apps_from_dock "${success_items[@]}" 2> /dev/null || true
|
if [[ -t 1 ]]; then
|
||||||
) > /dev/null 2>&1 &
|
start_inline_spinner "Checking brew dependencies..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local autoremove_output removed_count
|
||||||
|
autoremove_output=$(HOMEBREW_NO_ENV_HINTS=1 brew autoremove 2> /dev/null) || true
|
||||||
|
removed_count=$(printf '%s\n' "$autoremove_output" | grep -c "^Uninstalling" || true)
|
||||||
|
removed_count=${removed_count:-0}
|
||||||
|
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
stop_inline_spinner
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $removed_count -gt 0 ]]; then
|
||||||
|
echo -e "${GREEN}${ICON_SUCCESS}${NC} Cleaned $removed_count orphaned brew dependencies"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# brew autoremove can be slow — run in background so the prompt returns quickly.
|
# Clean up Dock entries for uninstalled apps.
|
||||||
if [[ $brew_apps_removed -gt 0 ]]; then
|
if [[ $success_count -gt 0 && ${#success_items[@]} -gt 0 ]]; then
|
||||||
(HOMEBREW_NO_ENV_HINTS=1 brew autoremove > /dev/null 2>&1 || true) &
|
if is_uninstall_dry_run; then
|
||||||
|
log_info "[DRY RUN] Would refresh LaunchServices and update Dock entries"
|
||||||
|
else
|
||||||
|
remove_apps_from_dock "${success_items[@]}" 2> /dev/null || true
|
||||||
|
refresh_launch_services_after_uninstall 2> /dev/null || true
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_cleanup_sudo_keepalive
|
_cleanup_sudo_keepalive
|
||||||
|
|||||||
@@ -168,6 +168,11 @@ brew_uninstall_cask() {
|
|||||||
local cask_name="$1"
|
local cask_name="$1"
|
||||||
local app_path="${2:-}"
|
local app_path="${2:-}"
|
||||||
|
|
||||||
|
if [[ "${MOLE_DRY_RUN:-0}" == "1" ]]; then
|
||||||
|
debug_log "[DRY RUN] Would brew uninstall --cask --zap $cask_name"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
is_homebrew_available || return 1
|
is_homebrew_available || return 1
|
||||||
[[ -z "$cask_name" ]] && return 1
|
[[ -z "$cask_name" ]] && return 1
|
||||||
|
|
||||||
|
|||||||
46
mole
46
mole
@@ -234,10 +234,16 @@ show_help() {
|
|||||||
|
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --dry-run" "$NC" "Preview optimization"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --dry-run" "$NC" "Preview optimization"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --whitelist" "$NC" "Manage protected items"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo optimize --whitelist" "$NC" "Manage protected items"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo uninstall --dry-run" "$NC" "Preview app uninstall"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo purge --dry-run" "$NC" "Preview project purge"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo installer --dry-run" "$NC" "Preview installer cleanup"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo touchid enable --dry-run" "$NC" "Preview Touch ID setup"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo completion --dry-run" "$NC" "Preview shell completion edits"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo purge --paths" "$NC" "Configure scan directories"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo purge --paths" "$NC" "Configure scan directories"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo analyze /Volumes" "$NC" "Analyze external drives only"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo analyze /Volumes" "$NC" "Analyze external drives only"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo update --force" "$NC" "Force reinstall latest stable version"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo update --force" "$NC" "Force reinstall latest stable version"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "mo update --nightly" "$NC" "Install latest unreleased main branch build"
|
printf " %s%-28s%s %s\n" "$GREEN" "mo update --nightly" "$NC" "Install latest unreleased main branch build"
|
||||||
|
printf " %s%-28s%s %s\n" "$GREEN" "mo remove --dry-run" "$NC" "Preview Mole removal"
|
||||||
echo
|
echo
|
||||||
printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC"
|
printf "%s%s%s\n" "$BLUE" "OPTIONS" "$NC"
|
||||||
printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs"
|
printf " %s%-28s%s %s\n" "$GREEN" "--debug" "$NC" "Show detailed operation logs"
|
||||||
@@ -462,6 +468,8 @@ update_mole() {
|
|||||||
|
|
||||||
# Remove flow (Homebrew + manual + config/cache).
|
# Remove flow (Homebrew + manual + config/cache).
|
||||||
remove_mole() {
|
remove_mole() {
|
||||||
|
local dry_run_mode="${1:-false}"
|
||||||
|
|
||||||
if [[ -t 1 ]]; then
|
if [[ -t 1 ]]; then
|
||||||
start_inline_spinner "Detecting Mole installations..."
|
start_inline_spinner "Detecting Mole installations..."
|
||||||
else
|
else
|
||||||
@@ -571,6 +579,31 @@ remove_mole() {
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
local has_error=false
|
local has_error=false
|
||||||
|
if [[ "$dry_run_mode" == "true" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}${ICON_DRY_RUN} DRY RUN MODE${NC}, no files will be removed"
|
||||||
|
|
||||||
|
if [[ "$is_homebrew" == "true" ]]; then
|
||||||
|
echo -e " ${GRAY}${ICON_LIST} Would run: brew uninstall --force mole${NC}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ${manual_count:-0} -gt 0 ]]; then
|
||||||
|
for install in "${manual_installs[@]}"; do
|
||||||
|
[[ -f "$install" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: ${install}${NC}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [[ ${alias_count:-0} -gt 0 ]]; then
|
||||||
|
for alias in "${alias_installs[@]}"; do
|
||||||
|
[[ -f "$alias" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: ${alias}${NC}"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
[[ -d "$HOME/.cache/mole" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: $HOME/.cache/mole${NC}"
|
||||||
|
[[ -d "$HOME/.config/mole" ]] && echo -e " ${GRAY}${ICON_LIST} Would remove: $HOME/.config/mole${NC}"
|
||||||
|
|
||||||
|
printf '\n%s\n\n' "${GREEN}${ICON_SUCCESS}${NC} Dry run complete, no changes made"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$is_homebrew" == "true" ]]; then
|
if [[ "$is_homebrew" == "true" ]]; then
|
||||||
if [[ -z "$brew_cmd" ]]; then
|
if [[ -z "$brew_cmd" ]]; then
|
||||||
log_error "Homebrew command not found. Please ensure Homebrew is installed and in your PATH."
|
log_error "Homebrew command not found. Please ensure Homebrew is installed and in your PATH."
|
||||||
@@ -859,7 +892,18 @@ main() {
|
|||||||
exit 0
|
exit 0
|
||||||
;;
|
;;
|
||||||
"remove")
|
"remove")
|
||||||
remove_mole
|
local dry_run_remove=false
|
||||||
|
for arg in "${args[@]:1}"; do
|
||||||
|
case "$arg" in
|
||||||
|
"--dry-run" | "-n") dry_run_remove=true ;;
|
||||||
|
*)
|
||||||
|
echo "Unknown remove option: $arg"
|
||||||
|
echo "Use 'mole remove [--dry-run]' for supported options."
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
remove_mole "$dry_run_remove"
|
||||||
;;
|
;;
|
||||||
"help" | "--help" | "-h")
|
"help" | "--help" | "-h")
|
||||||
show_help
|
show_help
|
||||||
|
|||||||
190
tests/cli.bats
190
tests/cli.bats
@@ -1,39 +1,39 @@
|
|||||||
#!/usr/bin/env bats
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
setup_file() {
|
setup_file() {
|
||||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
export PROJECT_ROOT
|
export PROJECT_ROOT
|
||||||
|
|
||||||
ORIGINAL_HOME="${HOME:-}"
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
export ORIGINAL_HOME
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")"
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-cli-home.XXXXXX")"
|
||||||
export HOME
|
export HOME
|
||||||
|
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown_file() {
|
teardown_file() {
|
||||||
rm -rf "$HOME"
|
rm -rf "$HOME"
|
||||||
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
export HOME="$ORIGINAL_HOME"
|
export HOME="$ORIGINAL_HOME"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
create_fake_utils() {
|
create_fake_utils() {
|
||||||
local dir="$1"
|
local dir="$1"
|
||||||
mkdir -p "$dir"
|
mkdir -p "$dir"
|
||||||
|
|
||||||
cat > "$dir/sudo" <<'SCRIPT'
|
cat >"$dir/sudo" <<'SCRIPT'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
if [[ "$1" == "-n" || "$1" == "-v" ]]; then
|
if [[ "$1" == "-n" || "$1" == "-v" ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
exec "$@"
|
exec "$@"
|
||||||
SCRIPT
|
SCRIPT
|
||||||
chmod +x "$dir/sudo"
|
chmod +x "$dir/sudo"
|
||||||
|
|
||||||
cat > "$dir/bioutil" <<'SCRIPT'
|
cat >"$dir/bioutil" <<'SCRIPT'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
if [[ "$1" == "-r" ]]; then
|
if [[ "$1" == "-r" ]]; then
|
||||||
echo "Touch ID: 1"
|
echo "Touch ID: 1"
|
||||||
@@ -41,138 +41,152 @@ if [[ "$1" == "-r" ]]; then
|
|||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
SCRIPT
|
SCRIPT
|
||||||
chmod +x "$dir/bioutil"
|
chmod +x "$dir/bioutil"
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
rm -rf "$HOME/.config"
|
rm -rf "$HOME/.config"
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mole --help prints command overview" {
|
@test "mole --help prints command overview" {
|
||||||
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"mo clean"* ]]
|
[[ "$output" == *"mo clean"* ]]
|
||||||
[[ "$output" == *"mo analyze"* ]]
|
[[ "$output" == *"mo analyze"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mole --version reports script version" {
|
@test "mole --version reports script version" {
|
||||||
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
|
expected_version="$(grep '^VERSION=' "$PROJECT_ROOT/mole" | head -1 | sed 's/VERSION=\"\(.*\)\"/\1/')"
|
||||||
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" --version
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"$expected_version"* ]]
|
[[ "$output" == *"$expected_version"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mole unknown command returns error" {
|
@test "mole unknown command returns error" {
|
||||||
run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" unknown-command
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
[[ "$output" == *"Unknown command: unknown-command"* ]]
|
[[ "$output" == *"Unknown command: unknown-command"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "touchid status reports current configuration" {
|
@test "touchid status reports current configuration" {
|
||||||
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" touchid status
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"Touch ID"* ]]
|
[[ "$output" == *"Touch ID"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo optimize command is recognized" {
|
@test "mo optimize command is recognized" {
|
||||||
run bash -c "grep -q '\"optimize\")' '$PROJECT_ROOT/mole'"
|
run bash -c "grep -q '\"optimize\")' '$PROJECT_ROOT/mole'"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo analyze binary is valid" {
|
@test "mo analyze binary is valid" {
|
||||||
if [[ -f "$PROJECT_ROOT/bin/analyze-go" ]]; then
|
if [[ -f "$PROJECT_ROOT/bin/analyze-go" ]]; then
|
||||||
[ -x "$PROJECT_ROOT/bin/analyze-go" ]
|
[ -x "$PROJECT_ROOT/bin/analyze-go" ]
|
||||||
run file "$PROJECT_ROOT/bin/analyze-go"
|
run file "$PROJECT_ROOT/bin/analyze-go"
|
||||||
[[ "$output" == *"Mach-O"* ]] || [[ "$output" == *"executable"* ]]
|
[[ "$output" == *"Mach-O"* ]] || [[ "$output" == *"executable"* ]]
|
||||||
else
|
else
|
||||||
skip "analyze-go binary not built"
|
skip "analyze-go binary not built"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo clean --debug creates debug log file" {
|
@test "mo clean --debug creates debug log file" {
|
||||||
mkdir -p "$HOME/.config/mole"
|
mkdir -p "$HOME/.config/mole"
|
||||||
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
|
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
MOLE_OUTPUT="$output"
|
MOLE_OUTPUT="$output"
|
||||||
|
|
||||||
DEBUG_LOG="$HOME/.config/mole/mole_debug_session.log"
|
DEBUG_LOG="$HOME/.config/mole/mole_debug_session.log"
|
||||||
[ -f "$DEBUG_LOG" ]
|
[ -f "$DEBUG_LOG" ]
|
||||||
|
|
||||||
run grep "Mole Debug Session" "$DEBUG_LOG"
|
run grep "Mole Debug Session" "$DEBUG_LOG"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
[[ "$MOLE_OUTPUT" =~ "Debug session log saved to" ]]
|
[[ "$MOLE_OUTPUT" =~ "Debug session log saved to" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo clean without debug does not show debug log path" {
|
@test "mo clean without debug does not show debug log path" {
|
||||||
mkdir -p "$HOME/.config/mole"
|
mkdir -p "$HOME/.config/mole"
|
||||||
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=0 "$PROJECT_ROOT/mole" clean --dry-run
|
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=0 "$PROJECT_ROOT/mole" clean --dry-run
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
[[ "$output" != *"Debug session log saved to"* ]]
|
[[ "$output" != *"Debug session log saved to"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo clean --debug logs system info" {
|
@test "mo clean --debug logs system info" {
|
||||||
mkdir -p "$HOME/.config/mole"
|
mkdir -p "$HOME/.config/mole"
|
||||||
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
|
run env HOME="$HOME" TERM="xterm-256color" MOLE_TEST_MODE=1 MO_DEBUG=1 "$PROJECT_ROOT/mole" clean --dry-run
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
DEBUG_LOG="$HOME/.config/mole/mole_debug_session.log"
|
DEBUG_LOG="$HOME/.config/mole/mole_debug_session.log"
|
||||||
|
|
||||||
run grep "User:" "$DEBUG_LOG"
|
run grep "User:" "$DEBUG_LOG"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
run grep "Architecture:" "$DEBUG_LOG"
|
run grep "Architecture:" "$DEBUG_LOG"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "touchid status reflects pam file contents" {
|
@test "touchid status reflects pam file contents" {
|
||||||
pam_file="$HOME/pam_test"
|
pam_file="$HOME/pam_test"
|
||||||
cat > "$pam_file" <<'EOF'
|
cat >"$pam_file" <<'EOF'
|
||||||
auth sufficient pam_opendirectory.so
|
auth sufficient pam_opendirectory.so
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
|
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"not configured"* ]]
|
[[ "$output" == *"not configured"* ]]
|
||||||
|
|
||||||
cat > "$pam_file" <<'EOF'
|
cat >"$pam_file" <<'EOF'
|
||||||
auth sufficient pam_tid.so
|
auth sufficient pam_tid.so
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
|
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" status
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"enabled"* ]]
|
[[ "$output" == *"enabled"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "enable_touchid inserts pam_tid line in pam file" {
|
@test "enable_touchid inserts pam_tid line in pam file" {
|
||||||
pam_file="$HOME/pam_enable"
|
pam_file="$HOME/pam_enable"
|
||||||
cat > "$pam_file" <<'EOF'
|
cat >"$pam_file" <<'EOF'
|
||||||
auth sufficient pam_opendirectory.so
|
auth sufficient pam_opendirectory.so
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
fake_bin="$HOME/fake-bin"
|
fake_bin="$HOME/fake-bin"
|
||||||
create_fake_utils "$fake_bin"
|
create_fake_utils "$fake_bin"
|
||||||
|
|
||||||
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable
|
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
grep -q "pam_tid.so" "$pam_file"
|
grep -q "pam_tid.so" "$pam_file"
|
||||||
[[ -f "${pam_file}.mole-backup" ]]
|
[[ -f "${pam_file}.mole-backup" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "disable_touchid removes pam_tid line" {
|
@test "disable_touchid removes pam_tid line" {
|
||||||
pam_file="$HOME/pam_disable"
|
pam_file="$HOME/pam_disable"
|
||||||
cat > "$pam_file" <<'EOF'
|
cat >"$pam_file" <<'EOF'
|
||||||
auth sufficient pam_tid.so
|
auth sufficient pam_tid.so
|
||||||
auth sufficient pam_opendirectory.so
|
auth sufficient pam_opendirectory.so
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
fake_bin="$HOME/fake-bin-disable"
|
fake_bin="$HOME/fake-bin-disable"
|
||||||
create_fake_utils "$fake_bin"
|
create_fake_utils "$fake_bin"
|
||||||
|
|
||||||
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" disable
|
run env PATH="$fake_bin:$PATH" MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" disable
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
run grep "pam_tid.so" "$pam_file"
|
run grep "pam_tid.so" "$pam_file"
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "touchid enable --dry-run does not modify pam file" {
|
||||||
|
pam_file="$HOME/pam_enable_dry_run"
|
||||||
|
cat >"$pam_file" <<'EOF'
|
||||||
|
auth sufficient pam_opendirectory.so
|
||||||
|
EOF
|
||||||
|
|
||||||
|
run env MOLE_PAM_SUDO_FILE="$pam_file" "$PROJECT_ROOT/bin/touchid.sh" enable --dry-run
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"DRY RUN MODE"* ]]
|
||||||
|
|
||||||
|
run grep "pam_tid.so" "$pam_file"
|
||||||
|
[ "$status" -ne 0 ]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,160 +1,165 @@
|
|||||||
#!/usr/bin/env bats
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
setup_file() {
|
setup_file() {
|
||||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
export PROJECT_ROOT
|
export PROJECT_ROOT
|
||||||
|
|
||||||
ORIGINAL_HOME="${HOME:-}"
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
export ORIGINAL_HOME
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
ORIGINAL_PATH="${PATH:-}"
|
ORIGINAL_PATH="${PATH:-}"
|
||||||
export ORIGINAL_PATH
|
export ORIGINAL_PATH
|
||||||
|
|
||||||
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-completion-home.XXXXXX")"
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-completion-home.XXXXXX")"
|
||||||
export HOME
|
export HOME
|
||||||
|
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
|
|
||||||
PATH="$PROJECT_ROOT:$PATH"
|
PATH="$PROJECT_ROOT:$PATH"
|
||||||
export PATH
|
export PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown_file() {
|
teardown_file() {
|
||||||
rm -rf "$HOME"
|
rm -rf "$HOME"
|
||||||
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
export HOME="$ORIGINAL_HOME"
|
export HOME="$ORIGINAL_HOME"
|
||||||
fi
|
fi
|
||||||
if [[ -n "${ORIGINAL_PATH:-}" ]]; then
|
if [[ -n "${ORIGINAL_PATH:-}" ]]; then
|
||||||
export PATH="$ORIGINAL_PATH"
|
export PATH="$ORIGINAL_PATH"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
rm -rf "$HOME/.config"
|
rm -rf "$HOME/.config"
|
||||||
rm -rf "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"
|
rm -rf "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile"
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion script exists and is executable" {
|
@test "completion script exists and is executable" {
|
||||||
[ -f "$PROJECT_ROOT/bin/completion.sh" ]
|
[ -f "$PROJECT_ROOT/bin/completion.sh" ]
|
||||||
[ -x "$PROJECT_ROOT/bin/completion.sh" ]
|
[ -x "$PROJECT_ROOT/bin/completion.sh" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion script has valid bash syntax" {
|
@test "completion script has valid bash syntax" {
|
||||||
run bash -n "$PROJECT_ROOT/bin/completion.sh"
|
run bash -n "$PROJECT_ROOT/bin/completion.sh"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion --help shows usage" {
|
@test "completion --help shows usage" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" --help
|
run "$PROJECT_ROOT/bin/completion.sh" --help
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
[[ "$output" == *"Usage: mole completion"* ]]
|
[[ "$output" == *"Usage: mole completion"* ]]
|
||||||
[[ "$output" == *"Auto-install"* ]]
|
[[ "$output" == *"Auto-install"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion bash generates valid bash script" {
|
@test "completion bash generates valid bash script" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" bash
|
run "$PROJECT_ROOT/bin/completion.sh" bash
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"_mole_completions"* ]]
|
[[ "$output" == *"_mole_completions"* ]]
|
||||||
[[ "$output" == *"complete -F _mole_completions mole mo"* ]]
|
[[ "$output" == *"complete -F _mole_completions mole mo"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion bash script includes all commands" {
|
@test "completion bash script includes all commands" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" bash
|
run "$PROJECT_ROOT/bin/completion.sh" bash
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"optimize"* ]]
|
[[ "$output" == *"optimize"* ]]
|
||||||
[[ "$output" == *"clean"* ]]
|
[[ "$output" == *"clean"* ]]
|
||||||
[[ "$output" == *"uninstall"* ]]
|
[[ "$output" == *"uninstall"* ]]
|
||||||
[[ "$output" == *"analyze"* ]]
|
[[ "$output" == *"analyze"* ]]
|
||||||
[[ "$output" == *"status"* ]]
|
[[ "$output" == *"status"* ]]
|
||||||
[[ "$output" == *"purge"* ]]
|
[[ "$output" == *"purge"* ]]
|
||||||
[[ "$output" == *"touchid"* ]]
|
[[ "$output" == *"touchid"* ]]
|
||||||
[[ "$output" == *"completion"* ]]
|
[[ "$output" == *"completion"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion bash script supports mo command" {
|
@test "completion bash script supports mo command" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" bash
|
run "$PROJECT_ROOT/bin/completion.sh" bash
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"complete -F _mole_completions mole mo"* ]]
|
[[ "$output" == *"complete -F _mole_completions mole mo"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion bash can be loaded in bash" {
|
@test "completion bash can be loaded in bash" {
|
||||||
run bash -c "eval \"\$(\"$PROJECT_ROOT/bin/completion.sh\" bash)\" && complete -p mole"
|
run bash -c "eval \"\$(\"$PROJECT_ROOT/bin/completion.sh\" bash)\" && complete -p mole"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"_mole_completions"* ]]
|
[[ "$output" == *"_mole_completions"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion zsh generates valid zsh script" {
|
@test "completion zsh generates valid zsh script" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" zsh
|
run "$PROJECT_ROOT/bin/completion.sh" zsh
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"#compdef mole mo"* ]]
|
[[ "$output" == *"#compdef mole mo"* ]]
|
||||||
[[ "$output" == *"_mole()"* ]]
|
[[ "$output" == *"_mole()"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion zsh includes command descriptions" {
|
@test "completion zsh includes command descriptions" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" zsh
|
run "$PROJECT_ROOT/bin/completion.sh" zsh
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"optimize:Check and maintain system"* ]]
|
[[ "$output" == *"optimize:Check and maintain system"* ]]
|
||||||
[[ "$output" == *"clean:Free up disk space"* ]]
|
[[ "$output" == *"clean:Free up disk space"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion fish generates valid fish script" {
|
@test "completion fish generates valid fish script" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" fish
|
run "$PROJECT_ROOT/bin/completion.sh" fish
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"complete -c mole"* ]]
|
[[ "$output" == *"complete -c mole"* ]]
|
||||||
[[ "$output" == *"complete -c mo"* ]]
|
[[ "$output" == *"complete -c mo"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion fish includes both mole and mo commands" {
|
@test "completion fish includes both mole and mo commands" {
|
||||||
output="$("$PROJECT_ROOT/bin/completion.sh" fish)"
|
output="$("$PROJECT_ROOT/bin/completion.sh" fish)"
|
||||||
mole_count=$(echo "$output" | grep -c "complete -c mole")
|
mole_count=$(echo "$output" | grep -c "complete -c mole")
|
||||||
mo_count=$(echo "$output" | grep -c "complete -c mo")
|
mo_count=$(echo "$output" | grep -c "complete -c mo")
|
||||||
|
|
||||||
[ "$mole_count" -gt 0 ]
|
[ "$mole_count" -gt 0 ]
|
||||||
[ "$mo_count" -gt 0 ]
|
[ "$mo_count" -gt 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion auto-install detects zsh" {
|
@test "completion auto-install detects zsh" {
|
||||||
# shellcheck disable=SC2030,SC2031
|
# shellcheck disable=SC2030,SC2031
|
||||||
export SHELL=/bin/zsh
|
export SHELL=/bin/zsh
|
||||||
|
|
||||||
# Simulate auto-install (no interaction)
|
# Simulate auto-install (no interaction)
|
||||||
run bash -c "echo 'y' | \"$PROJECT_ROOT/bin/completion.sh\""
|
run bash -c "echo 'y' | \"$PROJECT_ROOT/bin/completion.sh\""
|
||||||
|
|
||||||
if [[ "$output" == *"Already configured"* ]]; then
|
if [[ "$output" == *"Already configured"* ]]; then
|
||||||
skip "Already configured from previous test"
|
skip "Already configured from previous test"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -f "$HOME/.zshrc" ] || skip "Auto-install didn't create .zshrc"
|
[ -f "$HOME/.zshrc" ] || skip "Auto-install didn't create .zshrc"
|
||||||
|
|
||||||
run grep -E "mole[[:space:]]+completion" "$HOME/.zshrc"
|
run grep -E "mole[[:space:]]+completion" "$HOME/.zshrc"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion auto-install detects already installed" {
|
@test "completion auto-install detects already installed" {
|
||||||
# shellcheck disable=SC2031
|
mkdir -p "$HOME"
|
||||||
export SHELL=/bin/zsh
|
# shellcheck disable=SC2016
|
||||||
mkdir -p "$HOME"
|
echo 'eval "$(mole completion zsh)"' >"$HOME/.zshrc"
|
||||||
# shellcheck disable=SC2016
|
|
||||||
echo 'eval "$(mole completion zsh)"' > "$HOME/.zshrc"
|
|
||||||
|
|
||||||
run "$PROJECT_ROOT/bin/completion.sh"
|
run env SHELL=/bin/zsh "$PROJECT_ROOT/bin/completion.sh"
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"updated"* ]]
|
[[ "$output" == *"updated"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "completion --dry-run previews changes without writing config" {
|
||||||
|
run env SHELL=/bin/zsh "$PROJECT_ROOT/bin/completion.sh" --dry-run
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"DRY RUN MODE"* ]]
|
||||||
|
[ ! -f "$HOME/.zshrc" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion script handles invalid shell argument" {
|
@test "completion script handles invalid shell argument" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" invalid-shell
|
run "$PROJECT_ROOT/bin/completion.sh" invalid-shell
|
||||||
[ "$status" -ne 0 ]
|
[ "$status" -ne 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "completion subcommand supports bash/zsh/fish" {
|
@test "completion subcommand supports bash/zsh/fish" {
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" bash
|
run "$PROJECT_ROOT/bin/completion.sh" bash
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" zsh
|
run "$PROJECT_ROOT/bin/completion.sh" zsh
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
|
|
||||||
run "$PROJECT_ROOT/bin/completion.sh" fish
|
run "$PROJECT_ROOT/bin/completion.sh" fish
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +1,56 @@
|
|||||||
#!/usr/bin/env bats
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
setup_file() {
|
setup_file() {
|
||||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
export PROJECT_ROOT
|
export PROJECT_ROOT
|
||||||
|
|
||||||
ORIGINAL_HOME="${HOME:-}"
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
export ORIGINAL_HOME
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-installers-home.XXXXXX")"
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-installers-home.XXXXXX")"
|
||||||
export HOME
|
export HOME
|
||||||
|
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown_file() {
|
teardown_file() {
|
||||||
rm -rf "$HOME"
|
rm -rf "$HOME"
|
||||||
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
export HOME="$ORIGINAL_HOME"
|
export HOME="$ORIGINAL_HOME"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
export TERM="xterm-256color"
|
export TERM="xterm-256color"
|
||||||
export MO_DEBUG=0
|
export MO_DEBUG=0
|
||||||
|
|
||||||
# Create standard scan directories
|
# Create standard scan directories
|
||||||
mkdir -p "$HOME/Downloads"
|
mkdir -p "$HOME/Downloads"
|
||||||
mkdir -p "$HOME/Desktop"
|
mkdir -p "$HOME/Desktop"
|
||||||
mkdir -p "$HOME/Documents"
|
mkdir -p "$HOME/Documents"
|
||||||
mkdir -p "$HOME/Public"
|
mkdir -p "$HOME/Public"
|
||||||
mkdir -p "$HOME/Library/Downloads"
|
mkdir -p "$HOME/Library/Downloads"
|
||||||
|
|
||||||
# Clear previous test files
|
# Clear previous test files
|
||||||
rm -rf "${HOME:?}/Downloads"/*
|
rm -rf "${HOME:?}/Downloads"/*
|
||||||
rm -rf "${HOME:?}/Desktop"/*
|
rm -rf "${HOME:?}/Desktop"/*
|
||||||
rm -rf "${HOME:?}/Documents"/*
|
rm -rf "${HOME:?}/Documents"/*
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test arguments
|
# Test arguments
|
||||||
|
|
||||||
@test "installer.sh rejects unknown options" {
|
@test "installer.sh rejects unknown options" {
|
||||||
run "$PROJECT_ROOT/bin/installer.sh" --unknown-option
|
run "$PROJECT_ROOT/bin/installer.sh" --unknown-option
|
||||||
|
|
||||||
[ "$status" -eq 1 ]
|
[ "$status" -eq 1 ]
|
||||||
[[ "$output" == *"Unknown option"* ]]
|
[[ "$output" == *"Unknown option"* ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "installer.sh accepts --dry-run option" {
|
||||||
|
run env HOME="$HOME" TERM="xterm-256color" "$PROJECT_ROOT/bin/installer.sh" --dry-run
|
||||||
|
|
||||||
|
[[ "$status" -eq 0 || "$status" -eq 2 ]]
|
||||||
|
[[ "$output" == *"DRY RUN MODE"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test scan_installers_in_path function directly
|
# Test scan_installers_in_path function directly
|
||||||
@@ -53,187 +60,187 @@ setup() {
|
|||||||
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): finds .dmg files" {
|
@test "scan_installers_in_path (fallback find): finds .dmg files" {
|
||||||
touch "$HOME/Downloads/Chrome.dmg"
|
touch "$HOME/Downloads/Chrome.dmg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"Chrome.dmg"* ]]
|
[[ "$output" == *"Chrome.dmg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): finds multiple installer types" {
|
@test "scan_installers_in_path (fallback find): finds multiple installer types" {
|
||||||
touch "$HOME/Downloads/App1.dmg"
|
touch "$HOME/Downloads/App1.dmg"
|
||||||
touch "$HOME/Downloads/App2.pkg"
|
touch "$HOME/Downloads/App2.pkg"
|
||||||
touch "$HOME/Downloads/App3.iso"
|
touch "$HOME/Downloads/App3.iso"
|
||||||
touch "$HOME/Downloads/App.mpkg"
|
touch "$HOME/Downloads/App.mpkg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"App1.dmg"* ]]
|
[[ "$output" == *"App1.dmg"* ]]
|
||||||
[[ "$output" == *"App2.pkg"* ]]
|
[[ "$output" == *"App2.pkg"* ]]
|
||||||
[[ "$output" == *"App3.iso"* ]]
|
[[ "$output" == *"App3.iso"* ]]
|
||||||
[[ "$output" == *"App.mpkg"* ]]
|
[[ "$output" == *"App.mpkg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): respects max depth" {
|
@test "scan_installers_in_path (fallback find): respects max depth" {
|
||||||
mkdir -p "$HOME/Downloads/level1/level2/level3"
|
mkdir -p "$HOME/Downloads/level1/level2/level3"
|
||||||
touch "$HOME/Downloads/shallow.dmg"
|
touch "$HOME/Downloads/shallow.dmg"
|
||||||
touch "$HOME/Downloads/level1/mid.dmg"
|
touch "$HOME/Downloads/level1/mid.dmg"
|
||||||
touch "$HOME/Downloads/level1/level2/deep.dmg"
|
touch "$HOME/Downloads/level1/level2/deep.dmg"
|
||||||
touch "$HOME/Downloads/level1/level2/level3/too-deep.dmg"
|
touch "$HOME/Downloads/level1/level2/level3/too-deep.dmg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
# Default max depth is 2
|
# Default max depth is 2
|
||||||
[[ "$output" == *"shallow.dmg"* ]]
|
[[ "$output" == *"shallow.dmg"* ]]
|
||||||
[[ "$output" == *"mid.dmg"* ]]
|
[[ "$output" == *"mid.dmg"* ]]
|
||||||
[[ "$output" == *"deep.dmg"* ]]
|
[[ "$output" == *"deep.dmg"* ]]
|
||||||
[[ "$output" != *"too-deep.dmg"* ]]
|
[[ "$output" != *"too-deep.dmg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): honors MOLE_INSTALLER_SCAN_MAX_DEPTH" {
|
@test "scan_installers_in_path (fallback find): honors MOLE_INSTALLER_SCAN_MAX_DEPTH" {
|
||||||
mkdir -p "$HOME/Downloads/level1"
|
mkdir -p "$HOME/Downloads/level1"
|
||||||
touch "$HOME/Downloads/top.dmg"
|
touch "$HOME/Downloads/top.dmg"
|
||||||
touch "$HOME/Downloads/level1/nested.dmg"
|
touch "$HOME/Downloads/level1/nested.dmg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" MOLE_INSTALLER_SCAN_MAX_DEPTH=1 bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" MOLE_INSTALLER_SCAN_MAX_DEPTH=1 bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"top.dmg"* ]]
|
[[ "$output" == *"top.dmg"* ]]
|
||||||
[[ "$output" != *"nested.dmg"* ]]
|
[[ "$output" != *"nested.dmg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): handles non-existent directory" {
|
@test "scan_installers_in_path (fallback find): handles non-existent directory" {
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/NonExistent"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/NonExistent"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ -z "$output" ]]
|
[[ -z "$output" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): ignores non-installer files" {
|
@test "scan_installers_in_path (fallback find): ignores non-installer files" {
|
||||||
touch "$HOME/Downloads/document.pdf"
|
touch "$HOME/Downloads/document.pdf"
|
||||||
touch "$HOME/Downloads/image.jpg"
|
touch "$HOME/Downloads/image.jpg"
|
||||||
touch "$HOME/Downloads/archive.tar.gz"
|
touch "$HOME/Downloads/archive.tar.gz"
|
||||||
touch "$HOME/Downloads/Installer.dmg"
|
touch "$HOME/Downloads/Installer.dmg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" != *"document.pdf"* ]]
|
[[ "$output" != *"document.pdf"* ]]
|
||||||
[[ "$output" != *"image.jpg"* ]]
|
[[ "$output" != *"image.jpg"* ]]
|
||||||
[[ "$output" != *"archive.tar.gz"* ]]
|
[[ "$output" != *"archive.tar.gz"* ]]
|
||||||
[[ "$output" == *"Installer.dmg"* ]]
|
[[ "$output" == *"Installer.dmg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_all_installers: handles missing paths gracefully" {
|
@test "scan_all_installers: handles missing paths gracefully" {
|
||||||
# Don't create all scan directories, some may not exist
|
# Don't create all scan directories, some may not exist
|
||||||
# Only create Downloads, delete others if they exist
|
# Only create Downloads, delete others if they exist
|
||||||
rm -rf "$HOME/Desktop"
|
rm -rf "$HOME/Desktop"
|
||||||
rm -rf "$HOME/Documents"
|
rm -rf "$HOME/Documents"
|
||||||
rm -rf "$HOME/Public"
|
rm -rf "$HOME/Public"
|
||||||
rm -rf "$HOME/Public/Downloads"
|
rm -rf "$HOME/Public/Downloads"
|
||||||
rm -rf "$HOME/Library/Downloads"
|
rm -rf "$HOME/Library/Downloads"
|
||||||
mkdir -p "$HOME/Downloads"
|
mkdir -p "$HOME/Downloads"
|
||||||
|
|
||||||
# Add an installer to the one directory that exists
|
# Add an installer to the one directory that exists
|
||||||
touch "$HOME/Downloads/test.dmg"
|
touch "$HOME/Downloads/test.dmg"
|
||||||
|
|
||||||
run bash -euo pipefail -c '
|
run bash -euo pipefail -c '
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source "$1"
|
source "$1"
|
||||||
scan_all_installers
|
scan_all_installers
|
||||||
' bash "$PROJECT_ROOT/bin/installer.sh"
|
' bash "$PROJECT_ROOT/bin/installer.sh"
|
||||||
|
|
||||||
# Should succeed even with missing paths
|
# Should succeed even with missing paths
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
# Should still find the installer in the existing directory
|
# Should still find the installer in the existing directory
|
||||||
[[ "$output" == *"test.dmg"* ]]
|
[[ "$output" == *"test.dmg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test edge cases
|
# Test edge cases
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): handles filenames with spaces" {
|
@test "scan_installers_in_path (fallback find): handles filenames with spaces" {
|
||||||
touch "$HOME/Downloads/My App Installer.dmg"
|
touch "$HOME/Downloads/My App Installer.dmg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"My App Installer.dmg"* ]]
|
[[ "$output" == *"My App Installer.dmg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): handles filenames with special characters" {
|
@test "scan_installers_in_path (fallback find): handles filenames with special characters" {
|
||||||
touch "$HOME/Downloads/App-v1.2.3_beta.pkg"
|
touch "$HOME/Downloads/App-v1.2.3_beta.pkg"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"App-v1.2.3_beta.pkg"* ]]
|
[[ "$output" == *"App-v1.2.3_beta.pkg"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): returns empty for directory with no installers" {
|
@test "scan_installers_in_path (fallback find): returns empty for directory with no installers" {
|
||||||
# Create some non-installer files
|
# Create some non-installer files
|
||||||
touch "$HOME/Downloads/document.pdf"
|
touch "$HOME/Downloads/document.pdf"
|
||||||
touch "$HOME/Downloads/image.png"
|
touch "$HOME/Downloads/image.png"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ -z "$output" ]]
|
[[ -z "$output" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Symlink handling tests
|
# Symlink handling tests
|
||||||
|
|
||||||
@test "scan_installers_in_path (fallback find): skips symlinks to regular files" {
|
@test "scan_installers_in_path (fallback find): skips symlinks to regular files" {
|
||||||
touch "$HOME/Downloads/real.dmg"
|
touch "$HOME/Downloads/real.dmg"
|
||||||
ln -s "$HOME/Downloads/real.dmg" "$HOME/Downloads/symlink.dmg"
|
ln -s "$HOME/Downloads/real.dmg" "$HOME/Downloads/symlink.dmg"
|
||||||
ln -s /nonexistent "$HOME/Downloads/dangling.lnk"
|
ln -s /nonexistent "$HOME/Downloads/dangling.lnk"
|
||||||
|
|
||||||
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
run env PATH="/usr/bin:/bin" bash -euo pipefail -c "
|
||||||
export MOLE_TEST_MODE=1
|
export MOLE_TEST_MODE=1
|
||||||
source \"\$1\"
|
source \"\$1\"
|
||||||
scan_installers_in_path \"\$2\"
|
scan_installers_in_path \"\$2\"
|
||||||
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
" bash "$PROJECT_ROOT/bin/installer.sh" "$HOME/Downloads"
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"real.dmg"* ]]
|
[[ "$output" == *"real.dmg"* ]]
|
||||||
[[ "$output" != *"symlink.dmg"* ]]
|
[[ "$output" != *"symlink.dmg"* ]]
|
||||||
[[ "$output" != *"dangling.lnk"* ]]
|
[[ "$output" != *"dangling.lnk"* ]]
|
||||||
}
|
}
|
||||||
|
|||||||
489
tests/purge.bats
489
tests/purge.bats
@@ -1,35 +1,35 @@
|
|||||||
#!/usr/bin/env bats
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
setup_file() {
|
setup_file() {
|
||||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
export PROJECT_ROOT
|
export PROJECT_ROOT
|
||||||
|
|
||||||
ORIGINAL_HOME="${HOME:-}"
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
export ORIGINAL_HOME
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-purge-home.XXXXXX")"
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-purge-home.XXXXXX")"
|
||||||
export HOME
|
export HOME
|
||||||
|
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown_file() {
|
teardown_file() {
|
||||||
rm -rf "$HOME"
|
rm -rf "$HOME"
|
||||||
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
export HOME="$ORIGINAL_HOME"
|
export HOME="$ORIGINAL_HOME"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
mkdir -p "$HOME/www"
|
mkdir -p "$HOME/www"
|
||||||
mkdir -p "$HOME/dev"
|
mkdir -p "$HOME/dev"
|
||||||
mkdir -p "$HOME/.cache/mole"
|
mkdir -p "$HOME/.cache/mole"
|
||||||
|
|
||||||
rm -rf "${HOME:?}/www"/* "${HOME:?}/dev"/*
|
rm -rf "${HOME:?}/www"/* "${HOME:?}/dev"/*
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_safe_project_artifact: rejects shallow paths (protection against accidents)" {
|
@test "is_safe_project_artifact: rejects shallow paths (protection against accidents)" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_safe_project_artifact '$HOME/www/node_modules' '$HOME/www'; then
|
if is_safe_project_artifact '$HOME/www/node_modules' '$HOME/www'; then
|
||||||
echo 'UNSAFE'
|
echo 'UNSAFE'
|
||||||
@@ -37,11 +37,11 @@ setup() {
|
|||||||
echo 'SAFE'
|
echo 'SAFE'
|
||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
[[ "$result" == "SAFE" ]]
|
[[ "$result" == "SAFE" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_safe_project_artifact: allows proper project artifacts" {
|
@test "is_safe_project_artifact: allows proper project artifacts" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_safe_project_artifact '$HOME/www/myproject/node_modules' '$HOME/www'; then
|
if is_safe_project_artifact '$HOME/www/myproject/node_modules' '$HOME/www'; then
|
||||||
echo 'ALLOWED'
|
echo 'ALLOWED'
|
||||||
@@ -49,11 +49,11 @@ setup() {
|
|||||||
echo 'BLOCKED'
|
echo 'BLOCKED'
|
||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
[[ "$result" == "ALLOWED" ]]
|
[[ "$result" == "ALLOWED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_safe_project_artifact: rejects non-absolute paths" {
|
@test "is_safe_project_artifact: rejects non-absolute paths" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_safe_project_artifact 'relative/path/node_modules' '$HOME/www'; then
|
if is_safe_project_artifact 'relative/path/node_modules' '$HOME/www'; then
|
||||||
echo 'UNSAFE'
|
echo 'UNSAFE'
|
||||||
@@ -61,11 +61,11 @@ setup() {
|
|||||||
echo 'SAFE'
|
echo 'SAFE'
|
||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
[[ "$result" == "SAFE" ]]
|
[[ "$result" == "SAFE" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_safe_project_artifact: validates depth calculation" {
|
@test "is_safe_project_artifact: validates depth calculation" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_safe_project_artifact '$HOME/www/project/subdir/node_modules' '$HOME/www'; then
|
if is_safe_project_artifact '$HOME/www/project/subdir/node_modules' '$HOME/www'; then
|
||||||
echo 'ALLOWED'
|
echo 'ALLOWED'
|
||||||
@@ -73,14 +73,14 @@ setup() {
|
|||||||
echo 'BLOCKED'
|
echo 'BLOCKED'
|
||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
[[ "$result" == "ALLOWED" ]]
|
[[ "$result" == "ALLOWED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_safe_project_artifact: allows direct child when search path is project root" {
|
@test "is_safe_project_artifact: allows direct child when search path is project root" {
|
||||||
mkdir -p "$HOME/single-project/node_modules"
|
mkdir -p "$HOME/single-project/node_modules"
|
||||||
touch "$HOME/single-project/package.json"
|
touch "$HOME/single-project/package.json"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_safe_project_artifact '$HOME/single-project/node_modules' '$HOME/single-project'; then
|
if is_safe_project_artifact '$HOME/single-project/node_modules' '$HOME/single-project'; then
|
||||||
echo 'ALLOWED'
|
echo 'ALLOWED'
|
||||||
@@ -89,15 +89,15 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "ALLOWED" ]]
|
[[ "$result" == "ALLOWED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_safe_project_artifact: accepts physical path under symlinked search root" {
|
@test "is_safe_project_artifact: accepts physical path under symlinked search root" {
|
||||||
mkdir -p "$HOME/www/real/proj/node_modules"
|
mkdir -p "$HOME/www/real/proj/node_modules"
|
||||||
touch "$HOME/www/real/proj/package.json"
|
touch "$HOME/www/real/proj/package.json"
|
||||||
ln -s "$HOME/www/real" "$HOME/www/link"
|
ln -s "$HOME/www/real" "$HOME/www/link"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_safe_project_artifact '$HOME/www/real/proj/node_modules' '$HOME/www/link/proj'; then
|
if is_safe_project_artifact '$HOME/www/real/proj/node_modules' '$HOME/www/link/proj'; then
|
||||||
echo 'ALLOWED'
|
echo 'ALLOWED'
|
||||||
@@ -106,43 +106,43 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "ALLOWED" ]]
|
[[ "$result" == "ALLOWED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "filter_nested_artifacts: removes nested node_modules" {
|
@test "filter_nested_artifacts: removes nested node_modules" {
|
||||||
mkdir -p "$HOME/www/project/node_modules/package/node_modules"
|
mkdir -p "$HOME/www/project/node_modules/package/node_modules"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
printf '%s\n' '$HOME/www/project/node_modules' '$HOME/www/project/node_modules/package/node_modules' | \
|
printf '%s\n' '$HOME/www/project/node_modules' '$HOME/www/project/node_modules/package/node_modules' | \
|
||||||
filter_nested_artifacts | wc -l | tr -d ' '
|
filter_nested_artifacts | wc -l | tr -d ' '
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "1" ]]
|
[[ "$result" == "1" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "filter_nested_artifacts: keeps independent artifacts" {
|
@test "filter_nested_artifacts: keeps independent artifacts" {
|
||||||
mkdir -p "$HOME/www/project1/node_modules"
|
mkdir -p "$HOME/www/project1/node_modules"
|
||||||
mkdir -p "$HOME/www/project2/target"
|
mkdir -p "$HOME/www/project2/target"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
printf '%s\n' '$HOME/www/project1/node_modules' '$HOME/www/project2/target' | \
|
printf '%s\n' '$HOME/www/project1/node_modules' '$HOME/www/project2/target' | \
|
||||||
filter_nested_artifacts | wc -l | tr -d ' '
|
filter_nested_artifacts | wc -l | tr -d ' '
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "2" ]]
|
[[ "$result" == "2" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "filter_nested_artifacts: removes Xcode build subdirectories (Mac projects)" {
|
@test "filter_nested_artifacts: removes Xcode build subdirectories (Mac projects)" {
|
||||||
# Simulate Mac Xcode project with nested .build directories:
|
# Simulate Mac Xcode project with nested .build directories:
|
||||||
# ~/www/testapp/build
|
# ~/www/testapp/build
|
||||||
# ~/www/testapp/build/Framework.build
|
# ~/www/testapp/build/Framework.build
|
||||||
# ~/www/testapp/build/Package.build
|
# ~/www/testapp/build/Package.build
|
||||||
mkdir -p "$HOME/www/testapp/build/Framework.build"
|
mkdir -p "$HOME/www/testapp/build/Framework.build"
|
||||||
mkdir -p "$HOME/www/testapp/build/Package.build"
|
mkdir -p "$HOME/www/testapp/build/Package.build"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'$HOME/www/testapp/build' \
|
'$HOME/www/testapp/build' \
|
||||||
@@ -151,19 +151,19 @@ setup() {
|
|||||||
filter_nested_artifacts | wc -l | tr -d ' '
|
filter_nested_artifacts | wc -l | tr -d ' '
|
||||||
")
|
")
|
||||||
|
|
||||||
# Should only keep the top-level 'build' directory, filtering out nested .build dirs
|
# Should only keep the top-level 'build' directory, filtering out nested .build dirs
|
||||||
[[ "$result" == "1" ]]
|
[[ "$result" == "1" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vendor protection unit tests
|
# Vendor protection unit tests
|
||||||
@test "is_rails_project_root: detects valid Rails project" {
|
@test "is_rails_project_root: detects valid Rails project" {
|
||||||
mkdir -p "$HOME/www/test-rails/config"
|
mkdir -p "$HOME/www/test-rails/config"
|
||||||
mkdir -p "$HOME/www/test-rails/bin"
|
mkdir -p "$HOME/www/test-rails/bin"
|
||||||
touch "$HOME/www/test-rails/config/application.rb"
|
touch "$HOME/www/test-rails/config/application.rb"
|
||||||
touch "$HOME/www/test-rails/Gemfile"
|
touch "$HOME/www/test-rails/Gemfile"
|
||||||
touch "$HOME/www/test-rails/bin/rails"
|
touch "$HOME/www/test-rails/bin/rails"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_rails_project_root '$HOME/www/test-rails'; then
|
if is_rails_project_root '$HOME/www/test-rails'; then
|
||||||
echo 'YES'
|
echo 'YES'
|
||||||
@@ -172,14 +172,14 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "YES" ]]
|
[[ "$result" == "YES" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_rails_project_root: rejects non-Rails directory" {
|
@test "is_rails_project_root: rejects non-Rails directory" {
|
||||||
mkdir -p "$HOME/www/not-rails"
|
mkdir -p "$HOME/www/not-rails"
|
||||||
touch "$HOME/www/not-rails/package.json"
|
touch "$HOME/www/not-rails/package.json"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_rails_project_root '$HOME/www/not-rails'; then
|
if is_rails_project_root '$HOME/www/not-rails'; then
|
||||||
echo 'YES'
|
echo 'YES'
|
||||||
@@ -188,14 +188,14 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "NO" ]]
|
[[ "$result" == "NO" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_go_project_root: detects valid Go project" {
|
@test "is_go_project_root: detects valid Go project" {
|
||||||
mkdir -p "$HOME/www/test-go"
|
mkdir -p "$HOME/www/test-go"
|
||||||
touch "$HOME/www/test-go/go.mod"
|
touch "$HOME/www/test-go/go.mod"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_go_project_root '$HOME/www/test-go'; then
|
if is_go_project_root '$HOME/www/test-go'; then
|
||||||
echo 'YES'
|
echo 'YES'
|
||||||
@@ -204,14 +204,14 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "YES" ]]
|
[[ "$result" == "YES" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_php_project_root: detects valid PHP Composer project" {
|
@test "is_php_project_root: detects valid PHP Composer project" {
|
||||||
mkdir -p "$HOME/www/test-php"
|
mkdir -p "$HOME/www/test-php"
|
||||||
touch "$HOME/www/test-php/composer.json"
|
touch "$HOME/www/test-php/composer.json"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_php_project_root '$HOME/www/test-php'; then
|
if is_php_project_root '$HOME/www/test-php'; then
|
||||||
echo 'YES'
|
echo 'YES'
|
||||||
@@ -220,17 +220,17 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "YES" ]]
|
[[ "$result" == "YES" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_protected_vendor_dir: protects Rails vendor" {
|
@test "is_protected_vendor_dir: protects Rails vendor" {
|
||||||
mkdir -p "$HOME/www/rails-app/vendor"
|
mkdir -p "$HOME/www/rails-app/vendor"
|
||||||
mkdir -p "$HOME/www/rails-app/config"
|
mkdir -p "$HOME/www/rails-app/config"
|
||||||
touch "$HOME/www/rails-app/config/application.rb"
|
touch "$HOME/www/rails-app/config/application.rb"
|
||||||
touch "$HOME/www/rails-app/Gemfile"
|
touch "$HOME/www/rails-app/Gemfile"
|
||||||
touch "$HOME/www/rails-app/config/environment.rb"
|
touch "$HOME/www/rails-app/config/environment.rb"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_protected_vendor_dir '$HOME/www/rails-app/vendor'; then
|
if is_protected_vendor_dir '$HOME/www/rails-app/vendor'; then
|
||||||
echo 'PROTECTED'
|
echo 'PROTECTED'
|
||||||
@@ -239,14 +239,14 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "PROTECTED" ]]
|
[[ "$result" == "PROTECTED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_protected_vendor_dir: does not protect PHP vendor" {
|
@test "is_protected_vendor_dir: does not protect PHP vendor" {
|
||||||
mkdir -p "$HOME/www/php-app/vendor"
|
mkdir -p "$HOME/www/php-app/vendor"
|
||||||
touch "$HOME/www/php-app/composer.json"
|
touch "$HOME/www/php-app/composer.json"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_protected_vendor_dir '$HOME/www/php-app/vendor'; then
|
if is_protected_vendor_dir '$HOME/www/php-app/vendor'; then
|
||||||
echo 'PROTECTED'
|
echo 'PROTECTED'
|
||||||
@@ -255,11 +255,11 @@ setup() {
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "NOT_PROTECTED" ]]
|
[[ "$result" == "NOT_PROTECTED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_project_container detects project indicators" {
|
@test "is_project_container detects project indicators" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/clean/project.sh"
|
source "$PROJECT_ROOT/lib/clean/project.sh"
|
||||||
mkdir -p "$HOME/Workspace2/project"
|
mkdir -p "$HOME/Workspace2/project"
|
||||||
@@ -269,12 +269,12 @@ if is_project_container "$HOME/Workspace2" 2; then
|
|||||||
fi
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"yes"* ]]
|
[[ "$output" == *"yes"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "discover_project_dirs includes detected containers" {
|
@test "discover_project_dirs includes detected containers" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/clean/project.sh"
|
source "$PROJECT_ROOT/lib/clean/project.sh"
|
||||||
mkdir -p "$HOME/CustomProjects/app"
|
mkdir -p "$HOME/CustomProjects/app"
|
||||||
@@ -282,22 +282,22 @@ touch "$HOME/CustomProjects/app/go.mod"
|
|||||||
discover_project_dirs | grep -q "$HOME/CustomProjects"
|
discover_project_dirs | grep -q "$HOME/CustomProjects"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "save_discovered_paths writes config with tilde" {
|
@test "save_discovered_paths writes config with tilde" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/clean/project.sh"
|
source "$PROJECT_ROOT/lib/clean/project.sh"
|
||||||
save_discovered_paths "$HOME/Projects"
|
save_discovered_paths "$HOME/Projects"
|
||||||
grep -q "^~/" "$HOME/.config/mole/purge_paths"
|
grep -q "^~/" "$HOME/.config/mole/purge_paths"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "select_purge_categories returns failure on empty input" {
|
@test "select_purge_categories returns failure on empty input" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/clean/project.sh"
|
source "$PROJECT_ROOT/lib/clean/project.sh"
|
||||||
if select_purge_categories; then
|
if select_purge_categories; then
|
||||||
@@ -305,7 +305,7 @@ if select_purge_categories; then
|
|||||||
fi
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "select_purge_categories restores caller EXIT/INT/TERM traps" {
|
@test "select_purge_categories restores caller EXIT/INT/TERM traps" {
|
||||||
@@ -369,10 +369,10 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
@test "is_protected_vendor_dir: protects Go vendor" {
|
@test "is_protected_vendor_dir: protects Go vendor" {
|
||||||
mkdir -p "$HOME/www/go-app/vendor"
|
mkdir -p "$HOME/www/go-app/vendor"
|
||||||
touch "$HOME/www/go-app/go.mod"
|
touch "$HOME/www/go-app/go.mod"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_protected_vendor_dir '$HOME/www/go-app/vendor'; then
|
if is_protected_vendor_dir '$HOME/www/go-app/vendor'; then
|
||||||
echo 'PROTECTED'
|
echo 'PROTECTED'
|
||||||
@@ -381,13 +381,13 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "PROTECTED" ]]
|
[[ "$result" == "PROTECTED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_protected_vendor_dir: protects unknown vendor (conservative)" {
|
@test "is_protected_vendor_dir: protects unknown vendor (conservative)" {
|
||||||
mkdir -p "$HOME/www/unknown-app/vendor"
|
mkdir -p "$HOME/www/unknown-app/vendor"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_protected_vendor_dir '$HOME/www/unknown-app/vendor'; then
|
if is_protected_vendor_dir '$HOME/www/unknown-app/vendor'; then
|
||||||
echo 'PROTECTED'
|
echo 'PROTECTED'
|
||||||
@@ -396,14 +396,14 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "PROTECTED" ]]
|
[[ "$result" == "PROTECTED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_protected_purge_artifact: handles vendor directories correctly" {
|
@test "is_protected_purge_artifact: handles vendor directories correctly" {
|
||||||
mkdir -p "$HOME/www/php-app/vendor"
|
mkdir -p "$HOME/www/php-app/vendor"
|
||||||
touch "$HOME/www/php-app/composer.json"
|
touch "$HOME/www/php-app/composer.json"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_protected_purge_artifact '$HOME/www/php-app/vendor'; then
|
if is_protected_purge_artifact '$HOME/www/php-app/vendor'; then
|
||||||
echo 'PROTECTED'
|
echo 'PROTECTED'
|
||||||
@@ -412,14 +412,14 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
# PHP vendor should not be protected
|
# PHP vendor should not be protected
|
||||||
[[ "$result" == "NOT_PROTECTED" ]]
|
[[ "$result" == "NOT_PROTECTED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_protected_purge_artifact: returns false for non-vendor artifacts" {
|
@test "is_protected_purge_artifact: returns false for non-vendor artifacts" {
|
||||||
mkdir -p "$HOME/www/app/node_modules"
|
mkdir -p "$HOME/www/app/node_modules"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_protected_purge_artifact '$HOME/www/app/node_modules'; then
|
if is_protected_purge_artifact '$HOME/www/app/node_modules'; then
|
||||||
echo 'PROTECTED'
|
echo 'PROTECTED'
|
||||||
@@ -428,23 +428,23 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
# node_modules is not in the protected list
|
# node_modules is not in the protected list
|
||||||
[[ "$result" == "NOT_PROTECTED" ]]
|
[[ "$result" == "NOT_PROTECTED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Integration tests
|
# Integration tests
|
||||||
@test "scan_purge_targets: skips Rails vendor directory" {
|
@test "scan_purge_targets: skips Rails vendor directory" {
|
||||||
mkdir -p "$HOME/www/rails-app/vendor/javascript"
|
mkdir -p "$HOME/www/rails-app/vendor/javascript"
|
||||||
mkdir -p "$HOME/www/rails-app/config"
|
mkdir -p "$HOME/www/rails-app/config"
|
||||||
touch "$HOME/www/rails-app/config/application.rb"
|
touch "$HOME/www/rails-app/config/application.rb"
|
||||||
touch "$HOME/www/rails-app/Gemfile"
|
touch "$HOME/www/rails-app/Gemfile"
|
||||||
mkdir -p "$HOME/www/rails-app/bin"
|
mkdir -p "$HOME/www/rails-app/bin"
|
||||||
touch "$HOME/www/rails-app/bin/rails"
|
touch "$HOME/www/rails-app/bin/rails"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
scan_purge_targets '$HOME/www' '$scan_output'
|
scan_purge_targets '$HOME/www' '$scan_output'
|
||||||
if grep -q '$HOME/www/rails-app/vendor' '$scan_output'; then
|
if grep -q '$HOME/www/rails-app/vendor' '$scan_output'; then
|
||||||
@@ -454,19 +454,19 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
[[ "$result" == "SKIPPED" ]]
|
[[ "$result" == "SKIPPED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_purge_targets: cleans PHP Composer vendor directory" {
|
@test "scan_purge_targets: cleans PHP Composer vendor directory" {
|
||||||
mkdir -p "$HOME/www/php-app/vendor"
|
mkdir -p "$HOME/www/php-app/vendor"
|
||||||
touch "$HOME/www/php-app/composer.json"
|
touch "$HOME/www/php-app/composer.json"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
scan_purge_targets '$HOME/www' '$scan_output'
|
scan_purge_targets '$HOME/www' '$scan_output'
|
||||||
if grep -q '$HOME/www/php-app/vendor' '$scan_output'; then
|
if grep -q '$HOME/www/php-app/vendor' '$scan_output'; then
|
||||||
@@ -476,20 +476,20 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
[[ "$result" == "FOUND" ]]
|
[[ "$result" == "FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_purge_targets: skips Go vendor directory" {
|
@test "scan_purge_targets: skips Go vendor directory" {
|
||||||
mkdir -p "$HOME/www/go-app/vendor"
|
mkdir -p "$HOME/www/go-app/vendor"
|
||||||
touch "$HOME/www/go-app/go.mod"
|
touch "$HOME/www/go-app/go.mod"
|
||||||
touch "$HOME/www/go-app/go.sum"
|
touch "$HOME/www/go-app/go.sum"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
scan_purge_targets '$HOME/www' '$scan_output'
|
scan_purge_targets '$HOME/www' '$scan_output'
|
||||||
if grep -q '$HOME/www/go-app/vendor' '$scan_output'; then
|
if grep -q '$HOME/www/go-app/vendor' '$scan_output'; then
|
||||||
@@ -499,19 +499,19 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
[[ "$result" == "SKIPPED" ]]
|
[[ "$result" == "SKIPPED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_purge_targets: skips unknown vendor directory" {
|
@test "scan_purge_targets: skips unknown vendor directory" {
|
||||||
# Create a vendor directory without any project file
|
# Create a vendor directory without any project file
|
||||||
mkdir -p "$HOME/www/unknown-app/vendor"
|
mkdir -p "$HOME/www/unknown-app/vendor"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
scan_purge_targets '$HOME/www' '$scan_output'
|
scan_purge_targets '$HOME/www' '$scan_output'
|
||||||
if grep -q '$HOME/www/unknown-app/vendor' '$scan_output'; then
|
if grep -q '$HOME/www/unknown-app/vendor' '$scan_output'; then
|
||||||
@@ -521,20 +521,20 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
# Unknown vendor should be protected (conservative approach)
|
# Unknown vendor should be protected (conservative approach)
|
||||||
[[ "$result" == "SKIPPED" ]]
|
[[ "$result" == "SKIPPED" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_purge_targets: finds direct-child artifacts in project root with find mode" {
|
@test "scan_purge_targets: finds direct-child artifacts in project root with find mode" {
|
||||||
mkdir -p "$HOME/single-project/node_modules"
|
mkdir -p "$HOME/single-project/node_modules"
|
||||||
touch "$HOME/single-project/package.json"
|
touch "$HOME/single-project/package.json"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
MO_USE_FIND=1 scan_purge_targets '$HOME/single-project' '$scan_output'
|
MO_USE_FIND=1 scan_purge_targets '$HOME/single-project' '$scan_output'
|
||||||
if grep -q '$HOME/single-project/node_modules' '$scan_output'; then
|
if grep -q '$HOME/single-project/node_modules' '$scan_output'; then
|
||||||
@@ -544,19 +544,19 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
[[ "$result" == "FOUND" ]]
|
[[ "$result" == "FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_purge_targets: supports trailing slash search path in find mode" {
|
@test "scan_purge_targets: supports trailing slash search path in find mode" {
|
||||||
mkdir -p "$HOME/single-project/node_modules"
|
mkdir -p "$HOME/single-project/node_modules"
|
||||||
touch "$HOME/single-project/package.json"
|
touch "$HOME/single-project/package.json"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
MO_USE_FIND=1 scan_purge_targets '$HOME/single-project/' '$scan_output'
|
MO_USE_FIND=1 scan_purge_targets '$HOME/single-project/' '$scan_output'
|
||||||
if grep -q '$HOME/single-project/node_modules' '$scan_output'; then
|
if grep -q '$HOME/single-project/node_modules' '$scan_output'; then
|
||||||
@@ -566,16 +566,16 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
[[ "$result" == "FOUND" ]]
|
[[ "$result" == "FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_recently_modified: detects recent projects" {
|
@test "is_recently_modified: detects recent projects" {
|
||||||
mkdir -p "$HOME/www/project/node_modules"
|
mkdir -p "$HOME/www/project/node_modules"
|
||||||
touch "$HOME/www/project/package.json"
|
touch "$HOME/www/project/package.json"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_recently_modified '$HOME/www/project/node_modules'; then
|
if is_recently_modified '$HOME/www/project/node_modules'; then
|
||||||
@@ -584,66 +584,66 @@ EOF
|
|||||||
echo 'OLD'
|
echo 'OLD'
|
||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
[[ "$result" == "RECENT" ]]
|
[[ "$result" == "RECENT" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_recently_modified: marks old projects correctly" {
|
@test "is_recently_modified: marks old projects correctly" {
|
||||||
mkdir -p "$HOME/www/old-project/node_modules"
|
mkdir -p "$HOME/www/old-project/node_modules"
|
||||||
mkdir -p "$HOME/www/old-project"
|
mkdir -p "$HOME/www/old-project"
|
||||||
|
|
||||||
bash -c "
|
bash -c "
|
||||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
is_recently_modified '$HOME/www/old-project/node_modules' || true
|
is_recently_modified '$HOME/www/old-project/node_modules' || true
|
||||||
"
|
"
|
||||||
local exit_code=$?
|
local exit_code=$?
|
||||||
[ "$exit_code" -eq 0 ] || [ "$exit_code" -eq 1 ]
|
[ "$exit_code" -eq 0 ] || [ "$exit_code" -eq 1 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "purge targets are configured correctly" {
|
@test "purge targets are configured correctly" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
echo \"\${PURGE_TARGETS[@]}\"
|
echo \"\${PURGE_TARGETS[@]}\"
|
||||||
")
|
")
|
||||||
[[ "$result" == *"node_modules"* ]]
|
[[ "$result" == *"node_modules"* ]]
|
||||||
[[ "$result" == *"target"* ]]
|
[[ "$result" == *"target"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "get_dir_size_kb: calculates directory size" {
|
@test "get_dir_size_kb: calculates directory size" {
|
||||||
mkdir -p "$HOME/www/test-project/node_modules"
|
mkdir -p "$HOME/www/test-project/node_modules"
|
||||||
dd if=/dev/zero of="$HOME/www/test-project/node_modules/file.bin" bs=1024 count=1024 2>/dev/null
|
dd if=/dev/zero of="$HOME/www/test-project/node_modules/file.bin" bs=1024 count=1024 2>/dev/null
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
get_dir_size_kb '$HOME/www/test-project/node_modules'
|
get_dir_size_kb '$HOME/www/test-project/node_modules'
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" -ge 1000 ]] && [[ "$result" -le 1100 ]]
|
[[ "$result" -ge 1000 ]] && [[ "$result" -le 1100 ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "get_dir_size_kb: handles non-existent paths gracefully" {
|
@test "get_dir_size_kb: handles non-existent paths gracefully" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
get_dir_size_kb '$HOME/www/non-existent'
|
get_dir_size_kb '$HOME/www/non-existent'
|
||||||
")
|
")
|
||||||
[[ "$result" == "0" ]]
|
[[ "$result" == "0" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "get_dir_size_kb: returns TIMEOUT when size calculation hangs" {
|
@test "get_dir_size_kb: returns TIMEOUT when size calculation hangs" {
|
||||||
mkdir -p "$HOME/www/stuck-project/node_modules"
|
mkdir -p "$HOME/www/stuck-project/node_modules"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
run_with_timeout() { return 124; }
|
run_with_timeout() { return 124; }
|
||||||
get_dir_size_kb '$HOME/www/stuck-project/node_modules'
|
get_dir_size_kb '$HOME/www/stuck-project/node_modules'
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "TIMEOUT" ]]
|
[[ "$result" == "TIMEOUT" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "clean_project_artifacts: restores caller INT/TERM traps" {
|
@test "clean_project_artifacts: restores caller INT/TERM traps" {
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
export HOME='$HOME'
|
export HOME='$HOME'
|
||||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||||
@@ -669,92 +669,108 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == *"PASS"* ]]
|
[[ "$result" == *"PASS"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "clean_project_artifacts: handles empty directory gracefully" {
|
@test "clean_project_artifacts: handles empty directory gracefully" {
|
||||||
run bash -c "
|
run bash -c "
|
||||||
export HOME='$HOME'
|
export HOME='$HOME'
|
||||||
source '$PROJECT_ROOT/lib/core/common.sh'
|
source '$PROJECT_ROOT/lib/core/common.sh'
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
clean_project_artifacts
|
clean_project_artifacts
|
||||||
" < /dev/null
|
" </dev/null
|
||||||
|
|
||||||
[[ "$status" -eq 0 ]] || [[ "$status" -eq 2 ]]
|
[[ "$status" -eq 0 ]] || [[ "$status" -eq 2 ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "clean_project_artifacts: scans and finds artifacts" {
|
@test "clean_project_artifacts: scans and finds artifacts" {
|
||||||
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
||||||
skip "gtimeout/timeout not available"
|
skip "gtimeout/timeout not available"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$HOME/www/test-project/node_modules/package1"
|
mkdir -p "$HOME/www/test-project/node_modules/package1"
|
||||||
echo "test data" > "$HOME/www/test-project/node_modules/package1/index.js"
|
echo "test data" >"$HOME/www/test-project/node_modules/package1/index.js"
|
||||||
|
|
||||||
mkdir -p "$HOME/www/test-project"
|
mkdir -p "$HOME/www/test-project"
|
||||||
|
|
||||||
timeout_cmd="timeout"
|
timeout_cmd="timeout"
|
||||||
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
||||||
|
|
||||||
run bash -c "
|
run bash -c "
|
||||||
export HOME='$HOME'
|
export HOME='$HOME'
|
||||||
$timeout_cmd 5 '$PROJECT_ROOT/bin/purge.sh' 2>&1 < /dev/null || true
|
$timeout_cmd 5 '$PROJECT_ROOT/bin/purge.sh' 2>&1 < /dev/null || true
|
||||||
"
|
"
|
||||||
|
|
||||||
[[ "$output" =~ "Scanning" ]] ||
|
[[ "$output" =~ "Scanning" ]] ||
|
||||||
[[ "$output" =~ "Purge complete" ]] ||
|
[[ "$output" =~ "Purge complete" ]] ||
|
||||||
[[ "$output" =~ "No old" ]] ||
|
[[ "$output" =~ "No old" ]] ||
|
||||||
[[ "$output" =~ "Great" ]]
|
[[ "$output" =~ "Great" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo purge: command exists and is executable" {
|
@test "mo purge: command exists and is executable" {
|
||||||
[ -x "$PROJECT_ROOT/mole" ]
|
[ -x "$PROJECT_ROOT/mole" ]
|
||||||
[ -f "$PROJECT_ROOT/bin/purge.sh" ]
|
[ -f "$PROJECT_ROOT/bin/purge.sh" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo purge: shows in help text" {
|
@test "mo purge: shows in help text" {
|
||||||
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
|
run env HOME="$HOME" "$PROJECT_ROOT/mole" --help
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"mo purge"* ]]
|
[[ "$output" == *"mo purge"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo purge: accepts --debug flag" {
|
@test "mo purge: accepts --debug flag" {
|
||||||
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
||||||
skip "gtimeout/timeout not available"
|
skip "gtimeout/timeout not available"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
timeout_cmd="timeout"
|
timeout_cmd="timeout"
|
||||||
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
||||||
|
|
||||||
run bash -c "
|
run bash -c "
|
||||||
export HOME='$HOME'
|
export HOME='$HOME'
|
||||||
$timeout_cmd 2 '$PROJECT_ROOT/mole' purge --debug < /dev/null 2>&1 || true
|
$timeout_cmd 2 '$PROJECT_ROOT/mole' purge --debug < /dev/null 2>&1 || true
|
||||||
"
|
"
|
||||||
true
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "mo purge: accepts --dry-run flag" {
|
||||||
|
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
||||||
|
skip "gtimeout/timeout not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
timeout_cmd="timeout"
|
||||||
|
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
||||||
|
|
||||||
|
run bash -c "
|
||||||
|
export HOME='$HOME'
|
||||||
|
$timeout_cmd 2 '$PROJECT_ROOT/mole' purge --dry-run < /dev/null 2>&1 || true
|
||||||
|
"
|
||||||
|
|
||||||
|
[[ "$output" == *"DRY RUN MODE"* ]] || [[ "$output" == *"Dry run complete"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "mo purge: creates cache directory for stats" {
|
@test "mo purge: creates cache directory for stats" {
|
||||||
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
if ! command -v gtimeout >/dev/null 2>&1 && ! command -v timeout >/dev/null 2>&1; then
|
||||||
skip "gtimeout/timeout not available"
|
skip "gtimeout/timeout not available"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
timeout_cmd="timeout"
|
timeout_cmd="timeout"
|
||||||
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
command -v timeout >/dev/null 2>&1 || timeout_cmd="gtimeout"
|
||||||
|
|
||||||
bash -c "
|
bash -c "
|
||||||
export HOME='$HOME'
|
export HOME='$HOME'
|
||||||
$timeout_cmd 2 '$PROJECT_ROOT/mole' purge < /dev/null 2>&1 || true
|
$timeout_cmd 2 '$PROJECT_ROOT/mole' purge < /dev/null 2>&1 || true
|
||||||
"
|
"
|
||||||
|
|
||||||
[ -d "$HOME/.cache/mole" ] || [ -d "${XDG_CACHE_HOME:-$HOME/.cache}/mole" ]
|
[ -d "$HOME/.cache/mole" ] || [ -d "${XDG_CACHE_HOME:-$HOME/.cache}/mole" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
# .NET bin directory detection tests
|
# .NET bin directory detection tests
|
||||||
@test "is_dotnet_bin_dir: finds .NET context in parent directory with Debug dir" {
|
@test "is_dotnet_bin_dir: finds .NET context in parent directory with Debug dir" {
|
||||||
mkdir -p "$HOME/www/dotnet-app/bin/Debug"
|
mkdir -p "$HOME/www/dotnet-app/bin/Debug"
|
||||||
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_dotnet_bin_dir '$HOME/www/dotnet-app/bin'; then
|
if is_dotnet_bin_dir '$HOME/www/dotnet-app/bin'; then
|
||||||
echo 'FOUND'
|
echo 'FOUND'
|
||||||
@@ -763,14 +779,14 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
[[ "$result" == "FOUND" ]]
|
[[ "$result" == "FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_dotnet_bin_dir: requires .csproj AND Debug/Release" {
|
@test "is_dotnet_bin_dir: requires .csproj AND Debug/Release" {
|
||||||
mkdir -p "$HOME/www/dotnet-app/bin"
|
mkdir -p "$HOME/www/dotnet-app/bin"
|
||||||
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_dotnet_bin_dir '$HOME/www/dotnet-app/bin'; then
|
if is_dotnet_bin_dir '$HOME/www/dotnet-app/bin'; then
|
||||||
echo 'FOUND'
|
echo 'FOUND'
|
||||||
@@ -779,15 +795,15 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
# Should not find it because Debug/Release directories don't exist
|
# Should not find it because Debug/Release directories don't exist
|
||||||
[[ "$result" == "NOT_FOUND" ]]
|
[[ "$result" == "NOT_FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "is_dotnet_bin_dir: rejects non-bin directories" {
|
@test "is_dotnet_bin_dir: rejects non-bin directories" {
|
||||||
mkdir -p "$HOME/www/dotnet-app/obj"
|
mkdir -p "$HOME/www/dotnet-app/obj"
|
||||||
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
if is_dotnet_bin_dir '$HOME/www/dotnet-app/obj'; then
|
if is_dotnet_bin_dir '$HOME/www/dotnet-app/obj'; then
|
||||||
echo 'FOUND'
|
echo 'FOUND'
|
||||||
@@ -795,19 +811,18 @@ EOF
|
|||||||
echo 'NOT_FOUND'
|
echo 'NOT_FOUND'
|
||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
[[ "$result" == "NOT_FOUND" ]]
|
[[ "$result" == "NOT_FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Integration test for bin scanning
|
# Integration test for bin scanning
|
||||||
@test "scan_purge_targets: includes .NET bin directories with Debug/Release" {
|
@test "scan_purge_targets: includes .NET bin directories with Debug/Release" {
|
||||||
mkdir -p "$HOME/www/dotnet-app/bin/Debug"
|
mkdir -p "$HOME/www/dotnet-app/bin/Debug"
|
||||||
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
touch "$HOME/www/dotnet-app/MyProject.csproj"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
scan_purge_targets '$HOME/www' '$scan_output'
|
scan_purge_targets '$HOME/www' '$scan_output'
|
||||||
if grep -q '$HOME/www/dotnet-app/bin' '$scan_output'; then
|
if grep -q '$HOME/www/dotnet-app/bin' '$scan_output'; then
|
||||||
@@ -817,19 +832,19 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
|
|
||||||
[[ "$result" == "FOUND" ]]
|
[[ "$result" == "FOUND" ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "scan_purge_targets: skips generic bin directories (non-.NET)" {
|
@test "scan_purge_targets: skips generic bin directories (non-.NET)" {
|
||||||
mkdir -p "$HOME/www/ruby-app/bin"
|
mkdir -p "$HOME/www/ruby-app/bin"
|
||||||
touch "$HOME/www/ruby-app/Gemfile"
|
touch "$HOME/www/ruby-app/Gemfile"
|
||||||
|
|
||||||
local scan_output
|
local scan_output
|
||||||
scan_output="$(mktemp)"
|
scan_output="$(mktemp)"
|
||||||
|
|
||||||
result=$(bash -c "
|
result=$(bash -c "
|
||||||
source '$PROJECT_ROOT/lib/clean/project.sh'
|
source '$PROJECT_ROOT/lib/clean/project.sh'
|
||||||
scan_purge_targets '$HOME/www' '$scan_output'
|
scan_purge_targets '$HOME/www' '$scan_output'
|
||||||
if grep -q '$HOME/www/ruby-app/bin' '$scan_output'; then
|
if grep -q '$HOME/www/ruby-app/bin' '$scan_output'; then
|
||||||
@@ -839,6 +854,6 @@ EOF
|
|||||||
fi
|
fi
|
||||||
")
|
")
|
||||||
|
|
||||||
rm -f "$scan_output"
|
rm -f "$scan_output"
|
||||||
[[ "$result" == "SKIPPED" ]]
|
[[ "$result" == "SKIPPED" ]]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +1,67 @@
|
|||||||
#!/usr/bin/env bats
|
#!/usr/bin/env bats
|
||||||
|
|
||||||
setup_file() {
|
setup_file() {
|
||||||
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
PROJECT_ROOT="$(cd "${BATS_TEST_DIRNAME}/.." && pwd)"
|
||||||
export PROJECT_ROOT
|
export PROJECT_ROOT
|
||||||
|
|
||||||
ORIGINAL_HOME="${BATS_TMPDIR:-}" # Use BATS_TMPDIR as original HOME if set by bats
|
ORIGINAL_HOME="${BATS_TMPDIR:-}" # Use BATS_TMPDIR as original HOME if set by bats
|
||||||
if [[ -z "$ORIGINAL_HOME" ]]; then
|
if [[ -z "$ORIGINAL_HOME" ]]; then
|
||||||
ORIGINAL_HOME="${HOME:-}"
|
ORIGINAL_HOME="${HOME:-}"
|
||||||
fi
|
fi
|
||||||
export ORIGINAL_HOME
|
export ORIGINAL_HOME
|
||||||
|
|
||||||
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-uninstall-home.XXXXXX")"
|
HOME="$(mktemp -d "${BATS_TEST_DIRNAME}/tmp-uninstall-home.XXXXXX")"
|
||||||
export HOME
|
export HOME
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown_file() {
|
teardown_file() {
|
||||||
rm -rf "$HOME"
|
rm -rf "$HOME"
|
||||||
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
if [[ -n "${ORIGINAL_HOME:-}" ]]; then
|
||||||
export HOME="$ORIGINAL_HOME"
|
export HOME="$ORIGINAL_HOME"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
export TERM="dumb"
|
export TERM="dumb"
|
||||||
rm -rf "${HOME:?}"/*
|
rm -rf "${HOME:?}"/*
|
||||||
mkdir -p "$HOME"
|
mkdir -p "$HOME"
|
||||||
}
|
}
|
||||||
|
|
||||||
create_app_artifacts() {
|
create_app_artifacts() {
|
||||||
mkdir -p "$HOME/Applications/TestApp.app"
|
mkdir -p "$HOME/Applications/TestApp.app"
|
||||||
mkdir -p "$HOME/Library/Application Support/TestApp"
|
mkdir -p "$HOME/Library/Application Support/TestApp"
|
||||||
mkdir -p "$HOME/Library/Caches/TestApp"
|
mkdir -p "$HOME/Library/Caches/TestApp"
|
||||||
mkdir -p "$HOME/Library/Containers/com.example.TestApp"
|
mkdir -p "$HOME/Library/Containers/com.example.TestApp"
|
||||||
mkdir -p "$HOME/Library/Preferences"
|
mkdir -p "$HOME/Library/Preferences"
|
||||||
touch "$HOME/Library/Preferences/com.example.TestApp.plist"
|
touch "$HOME/Library/Preferences/com.example.TestApp.plist"
|
||||||
mkdir -p "$HOME/Library/Preferences/ByHost"
|
mkdir -p "$HOME/Library/Preferences/ByHost"
|
||||||
touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist"
|
touch "$HOME/Library/Preferences/ByHost/com.example.TestApp.ABC123.plist"
|
||||||
mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState"
|
mkdir -p "$HOME/Library/Saved Application State/com.example.TestApp.savedState"
|
||||||
mkdir -p "$HOME/Library/LaunchAgents"
|
mkdir -p "$HOME/Library/LaunchAgents"
|
||||||
touch "$HOME/Library/LaunchAgents/com.example.TestApp.plist"
|
touch "$HOME/Library/LaunchAgents/com.example.TestApp.plist"
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "find_app_files discovers user-level leftovers" {
|
@test "find_app_files discovers user-level leftovers" {
|
||||||
create_app_artifacts
|
create_app_artifacts
|
||||||
|
|
||||||
result="$(
|
result="$(
|
||||||
HOME="$HOME" bash --noprofile --norc << 'EOF'
|
HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
find_app_files "com.example.TestApp" "TestApp"
|
find_app_files "com.example.TestApp" "TestApp"
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
||||||
[[ "$result" == *"Application Support/TestApp"* ]]
|
[[ "$result" == *"Application Support/TestApp"* ]]
|
||||||
[[ "$result" == *"Caches/TestApp"* ]]
|
[[ "$result" == *"Caches/TestApp"* ]]
|
||||||
[[ "$result" == *"Preferences/com.example.TestApp.plist"* ]]
|
[[ "$result" == *"Preferences/com.example.TestApp.plist"* ]]
|
||||||
[[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]]
|
[[ "$result" == *"Saved Application State/com.example.TestApp.savedState"* ]]
|
||||||
[[ "$result" == *"Containers/com.example.TestApp"* ]]
|
[[ "$result" == *"Containers/com.example.TestApp"* ]]
|
||||||
[[ "$result" == *"LaunchAgents/com.example.TestApp.plist"* ]]
|
[[ "$result" == *"LaunchAgents/com.example.TestApp.plist"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "get_diagnostic_report_paths_for_app avoids executable prefix collisions" {
|
@test "get_diagnostic_report_paths_for_app avoids executable prefix collisions" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
|
||||||
@@ -92,16 +92,16 @@ result=$(get_diagnostic_report_paths_for_app "$app_dir" "Foo" "$diag_dir")
|
|||||||
[[ "$result" != *"Foobar_2026-01-01-120001_host.ips"* ]] || exit 1
|
[[ "$result" != *"Foobar_2026-01-01-120001_host.ips"* ]] || exit 1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "calculate_total_size returns aggregate kilobytes" {
|
@test "calculate_total_size returns aggregate kilobytes" {
|
||||||
mkdir -p "$HOME/sized"
|
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/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
|
dd if=/dev/zero of="$HOME/sized/file2" bs=1024 count=2 >/dev/null 2>&1
|
||||||
|
|
||||||
result="$(
|
result="$(
|
||||||
HOME="$HOME" bash --noprofile --norc << 'EOF'
|
HOME="$HOME" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
files="$(printf '%s
|
files="$(printf '%s
|
||||||
@@ -109,15 +109,15 @@ files="$(printf '%s
|
|||||||
' "$HOME/sized/file1" "$HOME/sized/file2")"
|
' "$HOME/sized/file1" "$HOME/sized/file2")"
|
||||||
calculate_total_size "$files"
|
calculate_total_size "$files"
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
||||||
[ "$result" -ge 3 ]
|
[ "$result" -ge 3 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "batch_uninstall_applications removes selected app data" {
|
@test "batch_uninstall_applications removes selected app data" {
|
||||||
create_app_artifacts
|
create_app_artifacts
|
||||||
|
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -155,22 +155,22 @@ batch_uninstall_applications
|
|||||||
[[ ! -f "$HOME/Library/LaunchAgents/com.example.TestApp.plist" ]] || exit 1
|
[[ ! -f "$HOME/Library/LaunchAgents/com.example.TestApp.plist" ]] || exit 1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "batch_uninstall_applications preview shows full related file list" {
|
@test "batch_uninstall_applications preview shows full related file list" {
|
||||||
mkdir -p "$HOME/Applications/TestApp.app"
|
mkdir -p "$HOME/Applications/TestApp.app"
|
||||||
mkdir -p "$HOME/Library/Application Support/TestApp"
|
mkdir -p "$HOME/Library/Application Support/TestApp"
|
||||||
mkdir -p "$HOME/Library/Caches/TestApp"
|
mkdir -p "$HOME/Library/Caches/TestApp"
|
||||||
mkdir -p "$HOME/Library/Logs/TestApp"
|
mkdir -p "$HOME/Library/Logs/TestApp"
|
||||||
touch "$HOME/Library/Logs/TestApp/log1.log"
|
touch "$HOME/Library/Logs/TestApp/log1.log"
|
||||||
touch "$HOME/Library/Logs/TestApp/log2.log"
|
touch "$HOME/Library/Logs/TestApp/log2.log"
|
||||||
touch "$HOME/Library/Logs/TestApp/log3.log"
|
touch "$HOME/Library/Logs/TestApp/log3.log"
|
||||||
touch "$HOME/Library/Logs/TestApp/log4.log"
|
touch "$HOME/Library/Logs/TestApp/log4.log"
|
||||||
touch "$HOME/Library/Logs/TestApp/log5.log"
|
touch "$HOME/Library/Logs/TestApp/log5.log"
|
||||||
touch "$HOME/Library/Logs/TestApp/log6.log"
|
touch "$HOME/Library/Logs/TestApp/log6.log"
|
||||||
|
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -210,28 +210,27 @@ total_size_cleaned=0
|
|||||||
printf 'q' | batch_uninstall_applications
|
printf 'q' | batch_uninstall_applications
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[[ "$output" == *"~/Library/Logs/TestApp/log6.log"* ]]
|
[[ "$output" == *"~/Library/Logs/TestApp/log6.log"* ]]
|
||||||
[[ "$output" != *"more files"* ]]
|
[[ "$output" != *"more files"* ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "safe_remove can remove a simple directory" {
|
@test "safe_remove can remove a simple directory" {
|
||||||
mkdir -p "$HOME/test_dir"
|
mkdir -p "$HOME/test_dir"
|
||||||
touch "$HOME/test_dir/file.txt"
|
touch "$HOME/test_dir/file.txt"
|
||||||
|
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
|
|
||||||
safe_remove "$HOME/test_dir"
|
safe_remove "$HOME/test_dir"
|
||||||
[[ ! -d "$HOME/test_dir" ]] || exit 1
|
[[ ! -d "$HOME/test_dir" ]] || exit 1
|
||||||
EOF
|
EOF
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@test "decode_file_list validates base64 encoding" {
|
@test "decode_file_list validates base64 encoding" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -242,11 +241,11 @@ result=$(decode_file_list "$valid_data" "TestApp")
|
|||||||
[[ -n "$result" ]] || exit 1
|
[[ -n "$result" ]] || exit 1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "decode_file_list rejects invalid base64" {
|
@test "decode_file_list rejects invalid base64" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -258,11 +257,11 @@ else
|
|||||||
fi
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "decode_file_list handles empty input" {
|
@test "decode_file_list handles empty input" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -272,11 +271,11 @@ result=$(decode_file_list "$empty_data" "TestApp" 2>/dev/null) || true
|
|||||||
[[ -z "$result" ]]
|
[[ -z "$result" ]]
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "decode_file_list rejects non-absolute paths" {
|
@test "decode_file_list rejects non-absolute paths" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -289,11 +288,11 @@ else
|
|||||||
fi
|
fi
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "decode_file_list handles both BSD and GNU base64 formats" {
|
@test "decode_file_list handles both BSD and GNU base64 formats" {
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
source "$PROJECT_ROOT/lib/core/common.sh"
|
source "$PROJECT_ROOT/lib/core/common.sh"
|
||||||
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
source "$PROJECT_ROOT/lib/uninstall/batch.sh"
|
||||||
@@ -311,16 +310,16 @@ result=$(decode_file_list "$encoded_data" "TestApp")
|
|||||||
[[ -n "$result" ]] || exit 1
|
[[ -n "$result" ]] || exit 1
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "remove_mole deletes manual binaries and caches" {
|
@test "remove_mole deletes manual binaries and caches" {
|
||||||
mkdir -p "$HOME/.local/bin"
|
mkdir -p "$HOME/.local/bin"
|
||||||
touch "$HOME/.local/bin/mole"
|
touch "$HOME/.local/bin/mole"
|
||||||
touch "$HOME/.local/bin/mo"
|
touch "$HOME/.local/bin/mo"
|
||||||
mkdir -p "$HOME/.config/mole" "$HOME/.cache/mole"
|
mkdir -p "$HOME/.config/mole" "$HOME/.cache/mole"
|
||||||
|
|
||||||
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="/usr/bin:/bin" bash --noprofile --norc << 'EOF'
|
run env HOME="$HOME" PROJECT_ROOT="$PROJECT_ROOT" PATH="/usr/bin:/bin" bash --noprofile --norc <<'EOF'
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
start_inline_spinner() { :; }
|
start_inline_spinner() { :; }
|
||||||
stop_inline_spinner() { :; }
|
stop_inline_spinner() { :; }
|
||||||
@@ -355,9 +354,31 @@ export -f start_inline_spinner stop_inline_spinner rm sudo
|
|||||||
printf '\n' | "$PROJECT_ROOT/mole" remove
|
printf '\n' | "$PROJECT_ROOT/mole" remove
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
[ "$status" -eq 0 ]
|
[ "$status" -eq 0 ]
|
||||||
[ ! -f "$HOME/.local/bin/mole" ]
|
[ ! -f "$HOME/.local/bin/mole" ]
|
||||||
[ ! -f "$HOME/.local/bin/mo" ]
|
[ ! -f "$HOME/.local/bin/mo" ]
|
||||||
[ ! -d "$HOME/.config/mole" ]
|
[ ! -d "$HOME/.config/mole" ]
|
||||||
[ ! -d "$HOME/.cache/mole" ]
|
[ ! -d "$HOME/.cache/mole" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "remove_mole dry-run keeps 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 --dry-run
|
||||||
|
EOF
|
||||||
|
|
||||||
|
[ "$status" -eq 0 ]
|
||||||
|
[[ "$output" == *"DRY RUN MODE"* ]]
|
||||||
|
[ -f "$HOME/.local/bin/mole" ]
|
||||||
|
[ -f "$HOME/.local/bin/mo" ]
|
||||||
|
[ -d "$HOME/.config/mole" ]
|
||||||
|
[ -d "$HOME/.cache/mole" ]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user