#!/bin/bash # Test runner for Mole. # Runs unit, Go, and integration tests. # Exits non-zero on failures. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$PROJECT_ROOT" # Never allow the scripted test run to trigger real sudo or Touch ID prompts. export MOLE_TEST_NO_AUTH=1 # shellcheck source=lib/core/file_ops.sh source "$PROJECT_ROOT/lib/core/file_ops.sh" echo "===============================" echo "Mole Test Runner" echo "===============================" echo "" FAILED=0 report_unit_result() { if [[ $1 -eq 0 ]]; then printf "${GREEN}${ICON_SUCCESS} Unit tests passed${NC}\n" else printf "${RED}${ICON_ERROR} Unit tests failed${NC}\n" ((FAILED++)) fi } echo "1. Linting test scripts..." if command -v shellcheck > /dev/null 2>&1; then TEST_FILES=() while IFS= read -r file; do TEST_FILES+=("$file") done < <(find tests -type f \( -name '*.bats' -o -name '*.sh' \) | sort) if [[ ${#TEST_FILES[@]} -gt 0 ]]; then if shellcheck --rcfile "$PROJECT_ROOT/.shellcheckrc" "${TEST_FILES[@]}"; then printf "${GREEN}${ICON_SUCCESS} Test script lint passed${NC}\n" else printf "${RED}${ICON_ERROR} Test script lint failed${NC}\n" ((FAILED++)) fi else printf "${YELLOW}${ICON_WARNING} No test scripts found, skipping${NC}\n" fi else printf "${YELLOW}${ICON_WARNING} shellcheck not installed, skipping${NC}\n" fi echo "" echo "2. Running unit tests..." if command -v bats > /dev/null 2>&1 && [ -d "tests" ]; then if [[ -z "${TERM:-}" ]]; then export TERM="xterm-256color" fi if [[ $# -eq 0 ]]; then fd_available=0 zip_available=0 zip_list_available=0 if command -v fd > /dev/null 2>&1; then fd_available=1 fi if command -v zip > /dev/null 2>&1; then zip_available=1 fi if command -v zipinfo > /dev/null 2>&1 || command -v unzip > /dev/null 2>&1; then zip_list_available=1 fi TEST_FILES=() while IFS= read -r file; do case "$file" in tests/installer_fd.bats) if [[ $fd_available -eq 1 ]]; then TEST_FILES+=("$file") fi ;; tests/installer_zip.bats) if [[ $zip_available -eq 1 && $zip_list_available -eq 1 ]]; then TEST_FILES+=("$file") fi ;; *) TEST_FILES+=("$file") ;; esac done < <(find tests -type f -name '*.bats' | sort) if [[ ${#TEST_FILES[@]} -gt 0 ]]; then set -- "${TEST_FILES[@]}" else set -- tests fi fi use_color=false if [[ -t 1 && "${TERM:-}" != "dumb" ]]; then use_color=true fi # Enable parallel execution across test files when GNU parallel is available. # Cap at 6 jobs to balance speed vs. system load during CI. bats_opts=() if command -v parallel > /dev/null 2>&1 && bats --help 2>&1 | grep -q -- "--jobs"; then _ncpu="$(sysctl -n hw.logicalcpu 2> /dev/null || nproc 2> /dev/null || echo 4)" _jobs="$((_ncpu > 6 ? 6 : (_ncpu < 2 ? 2 : _ncpu)))" # --no-parallelize-within-files ensures each test file's tests run # sequentially (they share a $HOME set by setup_file and are not safe # to run concurrently). Parallelism is only across files. bats_opts+=("--jobs" "$_jobs" "--no-parallelize-within-files") unset _ncpu _jobs fi # core_performance.bats has wall-clock timing assertions that are skewed by # CPU contention from parallel test workers. When parallel mode is active, # split it out to run sequentially after the parallel batch completes. _perf_files=() if [[ ${#bats_opts[@]} -gt 0 ]]; then _all=("$@") _rest=() if [[ ${#_all[@]} -eq 1 && -d "${_all[0]}" ]]; then while IFS= read -r _f; do case "$_f" in *core_performance.bats) _perf_files+=("$_f") ;; *) _rest+=("$_f") ;; esac done < <(find "${_all[0]}" -type f -name '*.bats' | sort) else for _f in "${_all[@]}"; do case "$_f" in *core_performance.bats) _perf_files+=("$_f") ;; *) _rest+=("$_f") ;; esac done fi if [[ ${#_rest[@]} -gt 0 ]]; then set -- "${_rest[@]}" else set -- fi unset _all _rest _f fi # Accumulate pass/fail across all bats invocations. _unit_rc=0 # Main run (parallel when bats_opts has --jobs, skipped if no files remain). if [[ $# -gt 0 ]]; then if bats --help 2>&1 | grep -q -- "--formatter"; then formatter="${BATS_FORMATTER:-pretty}" if [[ "$formatter" == "tap" ]]; then if $use_color; then esc=$'\033' bats ${bats_opts[@]+"${bats_opts[@]}"} --formatter tap "$@" | sed -e "s/^ok /${esc}[32mok ${esc}[0m /" \ -e "s/^not ok /${esc}[31mnot ok ${esc}[0m /" || _unit_rc=1 else bats ${bats_opts[@]+"${bats_opts[@]}"} --formatter tap "$@" || _unit_rc=1 fi else # Pretty format for local development bats ${bats_opts[@]+"${bats_opts[@]}"} --formatter "$formatter" "$@" || _unit_rc=1 fi else if $use_color; then esc=$'\033' bats ${bats_opts[@]+"${bats_opts[@]}"} --tap "$@" | sed -e "s/^ok /${esc}[32mok ${esc}[0m /" \ -e "s/^not ok /${esc}[31mnot ok ${esc}[0m /" || _unit_rc=1 else bats ${bats_opts[@]+"${bats_opts[@]}"} --tap "$@" || _unit_rc=1 fi fi fi # Post-run: timing-sensitive perf tests run after parallel workers have # finished so CPU contention does not skew wall-clock assertions. for _pf in ${_perf_files[@]+"${_perf_files[@]}"}; do bats "$_pf" || _unit_rc=1 done unset _perf_files _pf report_unit_result "$_unit_rc" else printf "${YELLOW}${ICON_WARNING} bats not installed or no tests found, skipping${NC}\n" fi echo "" echo "3. Running Go tests..." if command -v go > /dev/null 2>&1; then GO_TEST_CACHE="${MOLE_GO_TEST_CACHE:-/tmp/mole-go-build-cache}" mkdir -p "$GO_TEST_CACHE" if GOCACHE="$GO_TEST_CACHE" go build ./... > /dev/null 2>&1 && GOCACHE="$GO_TEST_CACHE" go vet ./cmd/... > /dev/null 2>&1 && GOCACHE="$GO_TEST_CACHE" go test ./cmd/... > /dev/null 2>&1; then printf "${GREEN}${ICON_SUCCESS} Go tests passed${NC}\n" else printf "${RED}${ICON_ERROR} Go tests failed${NC}\n" ((FAILED++)) fi else printf "${YELLOW}${ICON_WARNING} Go not installed, skipping Go tests${NC}\n" fi echo "" echo "4. Testing module loading..." if bash -c 'source lib/core/common.sh && echo "OK"' > /dev/null 2>&1; then printf "${GREEN}${ICON_SUCCESS} Module loading passed${NC}\n" else printf "${RED}${ICON_ERROR} Module loading failed${NC}\n" ((FAILED++)) fi echo "" echo "5. Running integration tests..." # Quick syntax check for main scripts if bash -n mole && bash -n bin/clean.sh && bash -n bin/optimize.sh; then printf "${GREEN}${ICON_SUCCESS} Integration tests passed${NC}\n" else printf "${RED}${ICON_ERROR} Integration tests failed${NC}\n" ((FAILED++)) fi echo "" echo "6. Testing installation..." # Installation script is macOS-specific; skip this test on non-macOS platforms if [[ "$(uname -s)" != "Darwin" ]]; then printf "${YELLOW}${ICON_WARNING} Installation test skipped (non-macOS)${NC}\n" else # Skip if Homebrew mole is installed (install.sh will refuse to overwrite) install_test_home="" if command -v brew > /dev/null 2>&1 && brew list mole &> /dev/null; then printf "${GREEN}${ICON_SUCCESS} Installation test skipped, Homebrew${NC}\n" else install_test_home="$(mktemp -d /tmp/mole-test-home.XXXXXX 2> /dev/null || true)" if [[ -z "$install_test_home" ]]; then install_test_home="/tmp/mole-test-home" mkdir -p "$install_test_home" fi fi if [[ -z "$install_test_home" ]]; then : elif HOME="$install_test_home" \ XDG_CONFIG_HOME="$install_test_home/.config" \ XDG_CACHE_HOME="$install_test_home/.cache" \ MO_NO_OPLOG=1 \ ./install.sh --prefix /tmp/mole-test > /dev/null 2>&1; then if [[ -f "/tmp/mole-test/mole" ]]; then printf "${GREEN}${ICON_SUCCESS} Installation test passed${NC}\n" else printf "${RED}${ICON_ERROR} Installation test failed${NC}\n" ((FAILED++)) fi else printf "${RED}${ICON_ERROR} Installation test failed${NC}\n" ((FAILED++)) fi MO_NO_OPLOG=1 safe_remove "/tmp/mole-test" true || true if [[ -n "$install_test_home" ]]; then MO_NO_OPLOG=1 safe_remove "$install_test_home" true || true fi fi echo "" echo "===============================" if [[ $FAILED -eq 0 ]]; then printf "${GREEN}${ICON_SUCCESS} All tests passed!${NC}\n" exit 0 fi printf "${RED}${ICON_ERROR} $FAILED tests failed!${NC}\n" exit 1