Something went wrong. Try again.
extentions for bash language
Something went wrong. Try again.
22 kB · 824 lines
Shell
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824#!/bin/bash
# Testing Module# Provides comprehensive testing framework for Bash scripts
module_version testing 1.0.0
# Test framework globalsdeclare -a TEST_RESULTS=()declare -a TEST_NAMES=()declare -i TESTS_PASSED=0declare -i TESTS_FAILED=0declare -i TESTS_TOTAL=0declare -i TESTS_SKIPPED=0declare -i TESTS_ERRORS=0declare -i CURRENT_TEST_INDEX=0declare -i VERBOSE_TESTS=0declare -i STOP_ON_FAILURE=0declare -i TEST_TIMEOUT=30
# Test state (for compatibility)declare -g TEST_CURRENT_SUITE=""declare -g TEST_CURRENT_TEST=""
# Test result codesreadonly TEST_PASS=0readonly TEST_FAIL=1readonly TEST_SKIP=2readonly TEST_ERROR=3
# Colors for test outputdeclare -A TEST_COLORS=( ["pass"]="\033[32m" # Green ["fail"]="\033[31m" # Red ["skip"]="\033[33m" # Yellow ["error"]="\033[35m" # Magenta ["info"]="\033[36m" # Cyan ["reset"]="\033[0m" # Reset ["bold"]="\033[1m" # Bold)
# Initialize TEST_COLORS if not already defined (for compatibility)if [[ ${#TEST_COLORS[@]} -eq 0 ]]; then declare -A TEST_COLORS=( ["pass"]="\033[32m" # Green ["fail"]="\033[31m" # Red ["error"]="\033[35m" # Magenta ["info"]="\033[36m" # Cyan ["reset"]="\033[0m" # Reset )fi
# Initialize test frameworkinit_tests() { local verbose="${1:-0}" local stop_on_fail="${2:-0}" local timeout="${3:-30}" TESTS_PASSED=0 TESTS_FAILED=0 TESTS_ERRORS=0 TESTS_TOTAL=0 TESTS_SKIPPED=0 CURRENT_TEST_INDEX=0 TEST_RESULTS=() TEST_NAMES=() VERBOSE_TESTS=$verbose STOP_ON_FAILURE=$stop_on_fail TEST_TIMEOUT=$timeout if [[ $VERBOSE_TESTS -eq 1 ]]; then echo -e "${TEST_COLORS[info]}${TEST_COLORS[bold]}Initializing test framework${TEST_COLORS[reset]}" echo "Verbose: $VERBOSE_TESTS" echo "Stop on failure: $STOP_ON_FAILURE" echo "Test timeout: ${TEST_TIMEOUT}s" echo fi}
# Test suite (for compatibility)describe() { TEST_CURRENT_SUITE="$1" echo printc "info" "bold" "Test Suite: $TEST_CURRENT_SUITE" echo "$(repeat_str "=" 50)"}
# Test case (for compatibility)it() { TEST_CURRENT_TEST="$1" ((TESTS_TOTAL++)) echo -n " • $TEST_CURRENT_TEST ... "}
# Test assertion functionsassert_true() { local condition="$1" local message="${2:-Expected true}" if eval "$condition"; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_false() { local condition="$1" local message="${2:-Expected false}" if ! eval "$condition"; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_equals() { local expected="$1" local actual="$2" local message="${3:-Expected values to be equal}" if [[ "$expected" == "$actual" ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_not_equals() { local not_expected="$1" local actual="$2" local message="${3:-Expected values to be different}" if [[ "$not_expected" != "$actual" ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_contains() { local haystack="$1" local needle="$2" local message="${3:-Expected string to contain substring}" if [[ "$haystack" == *"$needle"* ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_not_contains() { local haystack="$1" local needle="$2" local message="${3:-Expected string not to contain substring}" if [[ "$haystack" != *"$needle"* ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_match() { local string="$1" local pattern="$2" local message="${3:-Expected string to match pattern}" if [[ "$string" =~ $pattern ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_matches() { assert_match "$@"}
assert_file_exists() { local file="$1" local message="${2:-Expected file to exist}" if [[ -f "$file" ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_file_not_exists() { local file="$1" local message="${2:-Expected file not to exist}" if [[ ! -f "$file" ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_dir_exists() { local dir="$1" local message="${2:-Expected directory to exist}" if [[ -d "$dir" ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_dir_not_exists() { local dir="$1" local message="${2:-Expected directory not to exist}" if [[ ! -d "$dir" ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_command_success() { local command="$1" local message="${2:-Expected command to succeed}" if eval "$command" >/dev/null 2>&1; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_command_failure() { local command="$1" local message="${2:-Expected command to fail}" if ! eval "$command" >/dev/null 2>&1; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
assert_exit_code() { local expected="$1" local command="$2" local message="${3:-Expected specific exit code}" eval "$command" >/dev/null 2>&1 local actual=$? if [[ $actual -eq $expected ]]; then test_pass return $TEST_PASS else test_fail "$message" return $TEST_FAIL fi}
# Test result handlerstest_pass() { if [[ -n "$TEST_CURRENT_TEST" ]]; then echo -e "${TEST_COLORS[pass]}PASS${TEST_COLORS[reset]}" fi ((TESTS_PASSED++)) TEST_RESULTS+=("$TEST_CURRENT_SUITE:$TEST_CURRENT_TEST:PASS")}
test_fail() { local message="$1" if [[ -n "$TEST_CURRENT_TEST" ]]; then echo -e "${TEST_COLORS[fail]}FAIL${TEST_COLORS[reset]}" echo " $message" fi ((TESTS_FAILED++)) TEST_RESULTS+=("$TEST_CURRENT_SUITE:$TEST_CURRENT_TEST:FAIL:$message")}
test_error() { local message="$1" if [[ -n "$TEST_CURRENT_TEST" ]]; then echo -e "${TEST_COLORS[error]}ERROR${TEST_COLORS[reset]}" echo " $message" fi ((TESTS_ERRORS++)) TEST_RESULTS+=("$TEST_CURRENT_SUITE:$TEST_CURRENT_TEST:ERROR:$message")}
# Test runner functionsrun_test() { local test_name="$1" local test_function="$2" local timeout="${3:-$TEST_TIMEOUT}" ((TESTS_TOTAL++)) CURRENT_TEST_INDEX=$TESTS_TOTAL TEST_CURRENT_TEST="$test_name" local start_time=$(date +%s) local result=$TEST_PASS local output="" local error_output="" if [[ $VERBOSE_TESTS -eq 1 ]]; then echo -e "${TEST_COLORS[info]}Running test $CURRENT_TEST_INDEX/$TESTS_TOTAL: $test_name${TEST_COLORS[reset]}" fi # Run test with timeout if command -v timeout >/dev/null 2>&1; then output=$(timeout "$timeout" bash -c "$test_function" 2>&1) result=$? if [[ $result -eq 124 ]]; then result=$TEST_ERROR error_output="Test timed out after ${timeout}s" fi else output=$(eval "$test_function" 2>&1) result=$? fi local end_time=$(date +%s) local duration=$((end_time - start_time)) # Store test result TEST_RESULTS[$result]=$((TEST_RESULTS[$result] + 1)) TEST_NAMES[$TESTS_TOTAL]="$test_name" # Update counters case $result in $TEST_PASS) ((TESTS_PASSED++)) if [[ $VERBOSE_TESTS -eq 1 ]]; then echo -e "${TEST_COLORS[pass]}✓ PASS${TEST_COLORS[reset]} ($duration)s" fi ;; $TEST_FAIL) ((TESTS_FAILED++)) echo -e "${TEST_COLORS[fail]}✗ FAIL${TEST_COLORS[reset]} $test_name ($duration)s" if [[ -n "$output" ]]; then echo -e "${TEST_COLORS[fail]}$output${TEST_COLORS[reset]}" fi ;; $TEST_SKIP) ((TESTS_SKIPPED++)) echo -e "${TEST_COLORS[skip]}- SKIP${TEST_COLORS[reset]} $test_name ($duration)s" ;; $TEST_ERROR) ((TESTS_ERRORS++)) echo -e "${TEST_COLORS[error]}✗ ERROR${TEST_COLORS[reset]} $test_name ($duration)s" if [[ -n "$error_output" ]]; then echo -e "${TEST_COLORS[error]}$error_output${TEST_COLORS[reset]}" fi if [[ -n "$output" ]]; then echo -e "${TEST_COLORS[error]}$output${TEST_COLORS[reset]}" fi ;; esac # Stop on failure if enabled if [[ $result -ne $TEST_PASS && $result -ne $TEST_SKIP && $STOP_ON_FAILURE -eq 1 ]]; then echo -e "${TEST_COLORS[fail]}Stopping test execution due to failure${TEST_COLORS[reset]}" return $result fi return $result}
# Test suite functionstest_suite() { local suite_name="$1" shift local -a test_functions=("$@") echo -e "${TEST_COLORS[bold]}${TEST_COLORS[info]}Test Suite: $suite_name${TEST_COLORS[reset]}" echo local suite_start_time=$(date +%s) local suite_result=$TEST_PASS for test_func in "${test_functions[@]}"; do if declare -f "$test_func" >/dev/null; then run_test "$test_func" "$test_func" local result=$? if [[ $result -ne $TEST_PASS && $result -ne $TEST_SKIP ]]; then suite_result=$result fi else echo -e "${TEST_COLORS[fail]}Test function not found: $test_func${TEST_COLORS[reset]}" ((TESTS_FAILED++)) suite_result=$TEST_FAIL fi done local suite_end_time=$(date +%s) local suite_duration=$((suite_end_time - suite_start_time)) echo echo -e "${TEST_COLORS[bold]}Suite Results: $suite_name${TEST_COLORS[reset]}" echo "Duration: ${suite_duration}s" echo "Passed: $TESTS_PASSED" echo "Failed: $TESTS_FAILED" echo "Errors: $TESTS_ERRORS" echo "Skipped: $TESTS_SKIPPED" echo "Total: $TESTS_TOTAL" echo return $suite_result}
# Test helper functionsskip_test() { local reason="${1:-No reason provided}" echo "Skipping test: $reason" return $TEST_SKIP}
setup_test() { local test_name="$1" local setup_function="$2" if [[ -n "$setup_function" ]] && declare -f "$setup_function" >/dev/null; then if [[ $VERBOSE_TESTS -eq 1 ]]; then echo "Setting up test: $test_name" fi eval "$setup_function" fi}
teardown_test() { local test_name="$1" local teardown_function="$2" if [[ -n "$teardown_function" ]] && declare -f "$teardown_function" >/dev/null; then if [[ $VERBOSE_TESTS -eq 1 ]]; then echo "Tearing down test: $test_name" fi eval "$teardown_function" fi}
# Before/after hooks (for compatibility)before_each() { local setup_function="$1" if declare -f "$setup_function" > /dev/null; then "$setup_function" fi}
after_each() { local cleanup_function="$1" if declare -f "$cleanup_function" > /dev/null; then "$cleanup_function" fi}
# Test reporting functionsprint_test_summary() { echo -e "${TEST_COLORS[bold]}${TEST_COLORS[info]}Test Summary${TEST_COLORS[reset]}" echo "================================" echo "Total tests: $TESTS_TOTAL" echo -e "Passed: ${TEST_COLORS[pass]}$TESTS_PASSED${TEST_COLORS[reset]}" echo -e "Failed: ${TEST_COLORS[fail]}$TESTS_FAILED${TEST_COLORS[reset]}" echo -e "Errors: ${TEST_COLORS[error]}$TESTS_ERRORS${TEST_COLORS[reset]}" echo -e "Skipped: ${TEST_COLORS[skip]}$TESTS_SKIPPED${TEST_COLORS[reset]}" echo if [[ $TESTS_FAILED -eq 0 && $TESTS_ERRORS -eq 0 ]]; then echo -e "${TEST_COLORS[pass]}${TEST_COLORS[bold]}All tests passed!${TEST_COLORS[reset]}" return $TEST_PASS else echo -e "${TEST_COLORS[fail]}${TEST_COLORS[bold]}Some tests failed!${TEST_COLORS[reset]}" return $TEST_FAIL fi}
print_test_details() { echo -e "${TEST_COLORS[bold]}${TEST_COLORS[info]}Test Details${TEST_COLORS[reset]}" echo "==================" for ((i=1; i<=TESTS_TOTAL; i++)); do local test_name="${TEST_NAMES[$i]}" local status="Unknown" # Determine test status (simplified) if [[ $i -le $TESTS_PASSED ]]; then status="PASS" echo -e "${TEST_COLORS[pass]}$i. $test_name: $status${TEST_COLORS[reset]}" elif [[ $i -le $((TESTS_PASSED + TESTS_FAILED + TESTS_ERRORS)) ]]; then status="FAIL/ERROR" echo -e "${TEST_COLORS[fail]}$i. $test_name: $status${TEST_COLORS[reset]}" else status="SKIP" echo -e "${TEST_COLORS[skip]}$i. $test_name: $status${TEST_COLORS[reset]}" fi done}
# Test file generationgenerate_test_file() { local content="$1" local file_path="$2" if [[ -z "$file_path" ]]; then file_path="/tmp/test_file_$(date +%s).txt" fi echo "$content" > "$file_path" echo "$file_path"}
# Test directory generationgenerate_test_dir() { local dir_path="${1:-/tmp/test_dir_$(date +%s)}" mkdir -p "$dir_path" echo "$dir_path"}
# Mock functions for testingmock_function() { local original_func="$1" local mock_func="$2" local mock_return="${3:-0}" # Save original function if it exists if declare -f "$original_func" >/dev/null; then eval "original_${original_func}() { $(declare -f "$original_func" | tail -n +2); }" fi # Create mock function eval "$original_func() { $mock_func; return $mock_return; }"}
unmock_function() { local original_func="$1" # Restore original function if it was saved if declare -f "original_${original_func}" >/dev/null; then eval "$original_func() { $(declare -f "original_${original_func}" | tail -n +2); }" unset -f "original_${original_func}" else unset -f "$original_func" fi}
# Mock functions (for compatibility)mock() { local function_name="$1" local return_value="${2:-0}" local output="${3:-}" # Save original function if declare -f "$function_name" > /dev/null; then eval "ORIGINAL_$function_name() { $(declare -f "$function_name" | tail -n +2); }" fi # Create mock eval "$function_name() { echo '$output'; return $return_value; }"}
unmock() { local function_name="$1" # Restore original function if exists if declare -f "ORIGINAL_$function_name" > /dev/null; then eval "$function_name() { $(declare -f "ORIGINAL_$function_name" | tail -n +2); }" unset -f "ORIGINAL_$function_name" else unset -f "$function_name" fi}
# Performance testingbenchmark_function() { local func_name="$1" local iterations="${2:-100}" local -n result_ref="$3" if [[ ! -f "$func_name" ]] && ! declare -f "$func_name"; then error "Function not found: $func_name" return ${EXIT_FAILURE:-1} fi local start_time=$(date +%s%N) for ((i=0; i<iterations; i++)); do if declare -f "$func_name" >/dev/null; then "$func_name" >/dev/null else bash "$func_name" >/dev/null fi done local end_time=$(date +%s%N) local duration=$(((end_time - start_time) / 1000000)) # Convert to milliseconds result_ref["iterations"]=$iterations result_ref["duration_ms"]=$duration result_ref["avg_ms"]=$((duration / iterations)) echo "Benchmark results for $func_name:" echo "Iterations: $iterations" echo "Total duration: ${duration}ms" echo "Average per iteration: ${result_ref[avg_ms]}ms"}
# Test coverage (basic)test_coverage() { local script_file="$1" local -n result_ref="$2" if [[ ! -f "$script_file" ]]; then error "Script file not found: $script_file" return ${EXIT_FAILURE:-1} fi # Extract function names local -a functions mapfile -t functions < <(grep -n "^function \|^ *[a-zA-Z_][a-zA-Z0-9_]*() {" "$script_file" | grep -v "function test_" | head -10) result_ref["total_functions"]=${#functions[@]} result_ref["functions_tested"]=0 # This would require more sophisticated analysis echo "Coverage analysis for $script_file:" echo "Total functions found: ${result_ref[total_functions]}" echo "Functions tested: ${result_ref[functions_tested]}" echo "Coverage: $((result_ref[functions_tested] * 100 / result_ref[total_functions]))%"}
# Test configurationconfigure_tests() { local config_file="$1" if [[ -f "$config_file" ]]; then source "$config_file" if [[ $VERBOSE_TESTS -eq 1 ]]; then echo "Loaded test configuration from: $config_file" fi else if [[ $VERBOSE_TESTS -eq 1 ]]; then echo "Test configuration file not found: $config_file" fi fi}
# Test runner (for compatibility)run_test_file() { local test_file="$1" if [[ ! -f "$test_file" ]]; then echo "Test file not found: $test_file" return 1 fi echo "Running tests from: $test_file" echo "$(repeat_str "-" 60)" # Reset counters TEST_TOTAL=0 TEST_PASSED=0 TEST_FAILED=0 TEST_ERRORS=0 TEST_SKIPPED=0 TEST_RESULTS=() # Source test file in subshell ( source "$test_file" # Print summary echo printc "info" "bold" "Test Summary:" echo " Total: $TEST_TOTAL" echo -e " Passed: ${TEST_COLORS[pass]}$TEST_PASSED${TEST_COLORS[reset]}" echo -e " Failed: ${TEST_COLORS[fail]}$TEST_FAILED${TEST_COLORS[reset]}" echo -e " Errors: ${TEST_COLORS[error]}$TEST_ERRORS${TEST_COLORS[reset]}" echo -e " Skipped: ${TEST_COLORS[skip]}$TESTS_SKIPPED${TEST_COLORS[reset]}" # Return appropriate exit code if [[ $TEST_FAILED -gt 0 ]] || [[ $TEST_ERRORS -gt 0 ]]; then exit 1 else exit 0 fi )}
# Run all test files (for compatibility)run_all_tests() { local test_dir="${1:-tests}" if [[ ! -d "$test_dir" ]]; then echo "Test directory not found: $test_dir" return 1 fi local total_passed=0 local total_failed=0 local total_files=0 echo "Running all tests in: $test_dir" echo "$(repeat_str "=" 80)" for test_file in "$test_dir"/test_*.sh; do if [[ -f "$test_file" ]]; then ((total_files++)) echo if run_test_file "$test_file"; then ((total_passed++)) else ((total_failed++)) fi fi done echo echo "$(repeat_str "=" 80)" printc "info" "bold" "Overall Summary:" echo " Test files: $total_files" echo " Passed: $total_passed" echo " Failed: $total_failed" if [[ $total_failed -eq 0 ]]; then echo -e "${TEST_COLORS[pass]}All tests passed!${TEST_COLORS[reset]}" return 0 else echo -e "${TEST_COLORS[fail]}Some tests failed!${TEST_COLORS[reset]}" return 1 fi}
# Generate test report (for compatibility)generate_report() { local output_file="${1:-test_report.txt}" { echo "Test Report - $(date)" echo "$(repeat_str "=" 50)" echo echo "Test Summary:" echo " Total Tests: $TEST_TOTAL" echo " Passed: $TEST_PASSED" echo " Failed: $TEST_FAILED" echo " Errors: $TEST_ERRORS" echo " Skipped: $TESTS_SKIPPED" echo if [[ ${#TEST_RESULTS[@]} -gt 0 ]]; then echo "Detailed Results:" echo "$(repeat_str "-" 50)" for result in "${TEST_RESULTS[@]}"; do IFS=':' read -r suite test status message <<< "$result" echo "$suite :: $test :: $status" if [[ -n "$message" ]]; then echo " └─ $message" fi done fi } > "$output_file" echo "Test report generated: $output_file"}
# Helper function for colored output (for compatibility)printc() { local color="$1" local style="$2" local text="$3" echo -e "${TEST_COLORS[$color]}$text${TEST_COLORS[reset]}"}
# Export module functionsexport_module testing \ init_tests \ describe it \ assert_true assert_false assert_equals assert_not_equals assert_contains assert_not_contains assert_match assert_matches \ assert_file_exists assert_file_not_exists assert_dir_exists assert_dir_not_exists \ assert_command_success assert_command_failure assert_exit_code \ test_pass test_fail test_error \ run_test test_suite \ skip_test setup_test teardown_test before_each after_each \ print_test_summary print_test_details \ generate_test_file generate_test_dir \ mock_function unmock_function mock unmock \ benchmark_function test_coverage \ configure_tests \ run_test_file run_all_tests generate_report printc