From 6d46ae71fd5ff1702416beada21c3694af321715 Mon Sep 17 00:00:00 2001 From: Xiaoming-AMD Date: Wed, 12 Nov 2025 19:32:41 -0600 Subject: [PATCH] feat(runner): add patch execution system with comprehensive test suite Add execute_patches.sh utility for conditional patch script execution with support for success (exit 0), skip (exit 2), and failure handling. Changes: - Add runner/helpers/execute_patches.sh with conditional patch execution * Supports multiple patch scripts with exit code handling * Implements skip logic for patches that don't need to run * Forces common.sh re-sourcing in subshells to avoid function inheritance issues * Provides fallback logging when common.sh is not available - Add LOG_ERROR_RANK0 function to runner/lib/common.sh * Mirrors LOG_INFO_RANK0 and LOG_SUCCESS_RANK0 pattern * Ensures rank 0-only error logging for distributed environments - Add comprehensive test suite for execute_patches.sh (36 tests) * Test no patches, single/multiple patches, skip logic * Test error handling: missing patches, unreadable files * Test failure scenarios and exit code handling * Validates stop-on-first-failure behavior - Update run_all_tests.sh to include execute_patches tests * Now runs 4 test suites with 121 total tests * All tests passing Technical fixes: - Fix subshell function inheritance issue by unsetting __PRIMUS_COMMON_SOURCED before re-sourcing common.sh in execute_patches.sh - Environment variables are inherited by subshells but functions are not, requiring explicit re-sourcing to ensure function availability --- runner/helpers/execute_patches.sh | 105 +++++ runner/lib/common.sh | 6 + tests/runner/helpers/test_execute_patches.sh | 385 +++++++++++++++++++ tests/runner/run_all_tests.sh | 1 + 4 files changed, 497 insertions(+) create mode 100755 runner/helpers/execute_patches.sh create mode 100755 tests/runner/helpers/test_execute_patches.sh diff --git a/runner/helpers/execute_patches.sh b/runner/helpers/execute_patches.sh new file mode 100755 index 00000000..c995b4c1 --- /dev/null +++ b/runner/helpers/execute_patches.sh @@ -0,0 +1,105 @@ +#!/bin/bash +############################################################################### +# Copyright (c) 2025, Advanced Micro Devices, Inc. All rights reserved. +# +# See LICENSE for license information. +############################################################################### +# +# Execute patch scripts +# Usage: execute_patches [patch_script2] ... +# +# Exit codes from patch scripts: +# 0 - Success, continue to next patch +# 2 - Skip this patch (not an error) +# other - Failure, stop execution +# +# Example patch script with conditional skip: +# #!/bin/bash +# # Check if patch is needed +# if [[ -f /tmp/already_patched ]]; then +# echo "Patch already applied, skipping" +# exit 2 # Skip this patch +# fi +# +# # Apply patch +# echo "Applying patch..." +# # ... patch logic ... +# exit 0 # Success +# + +# Requires common.sh to be sourced +if [[ -z "${__PRIMUS_COMMON_SOURCED:-}" ]]; then + # Fallback logging if common.sh not loaded + LOG_INFO_RANK0() { + if [ "${NODE_RANK:-0}" -eq 0 ]; then + echo "[INFO] $*" >&2 + fi + } + LOG_ERROR_RANK0() { + if [ "${NODE_RANK:-0}" -eq 0 ]; then + echo "[ERROR] $*" >&2 + fi + } + LOG_SUCCESS_RANK0() { + if [ "${NODE_RANK:-0}" -eq 0 ]; then + echo "[SUCCESS] $*" + fi + } +fi + +# Execute multiple patch scripts +# Args: +# $@: Patch script paths +execute_patches() { + if [[ $# -eq 0 ]]; then + LOG_INFO_RANK0 "[Execute Patches] No patch scripts specified" + return 0 + fi + + local patch_scripts=("$@") + + LOG_INFO_RANK0 "[Execute Patches] Detected patch scripts: ${patch_scripts[*]}" + + for patch in "${patch_scripts[@]}"; do + if [[ ! -f "$patch" ]]; then + LOG_ERROR_RANK0 "[Execute Patches] Patch script not found: $patch" + return 1 + fi + + if [[ ! -r "$patch" ]]; then + LOG_ERROR_RANK0 "[Execute Patches] Patch script not readable: $patch" + return 1 + fi + + LOG_INFO_RANK0 "[Execute Patches] Running patch: bash $patch" + + bash "$patch" + local exit_code=$? + + if [[ $exit_code -eq 0 ]]; then + LOG_INFO_RANK0 "[Execute Patches] Patch completed successfully: $patch" + elif [[ $exit_code -eq 2 ]]; then + LOG_INFO_RANK0 "[Execute Patches] Patch skipped (exit code 2): $patch" + else + LOG_ERROR_RANK0 "[Execute Patches] Patch script failed: $patch (exit code: $exit_code)" + return 1 + fi + done + + LOG_SUCCESS_RANK0 "[Execute Patches] All patch scripts executed successfully" + return 0 +} + +# If called directly (not sourced), execute the function +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # Always source common.sh when called directly, as functions are not inherited by subshells + # Unset the guard variable to force re-sourcing in this new shell instance + unset __PRIMUS_COMMON_SOURCED + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + # shellcheck disable=SC1091 + source "$SCRIPT_DIR/../lib/common.sh" || { + echo "[ERROR] Failed to load common library" >&2 + exit 1 + } + execute_patches "$@" +fi diff --git a/runner/lib/common.sh b/runner/lib/common.sh index 1c450cdd..bd1bf63d 100644 --- a/runner/lib/common.sh +++ b/runner/lib/common.sh @@ -142,6 +142,12 @@ LOG_SUCCESS_RANK0() { fi } +LOG_ERROR_RANK0() { + if [[ "${NODE_RANK:-0}" == "0" ]]; then + LOG_ERROR "$@" + fi +} + # --------------------------------------------------------------------------- # Simple Print Functions (without timestamps and prefixes) # --------------------------------------------------------------------------- diff --git a/tests/runner/helpers/test_execute_patches.sh b/tests/runner/helpers/test_execute_patches.sh new file mode 100755 index 00000000..013655b4 --- /dev/null +++ b/tests/runner/helpers/test_execute_patches.sh @@ -0,0 +1,385 @@ +#!/bin/bash +############################################################################### +# Copyright (c) 2025, Advanced Micro Devices, Inc. All rights reserved. +# +# See LICENSE for license information. +############################################################################### + +# Unit tests for execute_patches.sh + +# Get project root +PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +RUNNER_DIR="$PROJECT_ROOT/runner" + +# Test counters +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Color codes +COLOR_GREEN='\033[0;32m' +COLOR_RED='\033[0;31m' +COLOR_YELLOW='\033[1;33m' +COLOR_RESET='\033[0m' + +# Print section header +print_section() { + echo "" + echo -e "${COLOR_YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLOR_RESET}" + echo -e "${COLOR_YELLOW}$1${COLOR_RESET}" + echo -e "${COLOR_YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${COLOR_RESET}" +} + +# Assert helper +assert_contains() { + TESTS_RUN=$((TESTS_RUN + 1)) + local output="$1" + local expected="$2" + local description="$3" + + if echo "$output" | grep -q "$expected"; then + echo -e "${COLOR_GREEN}✓${COLOR_RESET} $description" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo -e "${COLOR_RED}✗${COLOR_RESET} $description" + echo " Expected to contain: $expected" + echo " Actual output (first 10 lines):" + echo "$output" | head -10 | sed 's/^/ /' + TESTS_FAILED=$((TESTS_FAILED + 1)) + return 1 + fi +} + +assert_not_contains() { + TESTS_RUN=$((TESTS_RUN + 1)) + local output="$1" + local unexpected="$2" + local description="$3" + + if ! echo "$output" | grep -q "$unexpected"; then + echo -e "${COLOR_GREEN}✓${COLOR_RESET} $description" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo -e "${COLOR_RED}✗${COLOR_RESET} $description" + echo " Should not contain: $unexpected" + echo " Actual output (first 10 lines):" + echo "$output" | head -10 | sed 's/^/ /' + TESTS_FAILED=$((TESTS_FAILED + 1)) + return 1 + fi +} + +assert_exit_code() { + TESTS_RUN=$((TESTS_RUN + 1)) + local actual_code="$1" + local expected_code="$2" + local description="$3" + + if [[ "$actual_code" -eq "$expected_code" ]]; then + echo -e "${COLOR_GREEN}✓${COLOR_RESET} $description" + TESTS_PASSED=$((TESTS_PASSED + 1)) + return 0 + else + echo -e "${COLOR_RED}✗${COLOR_RESET} $description" + echo " Expected exit code: $expected_code" + echo " Actual exit code: $actual_code" + TESTS_FAILED=$((TESTS_FAILED + 1)) + return 1 + fi +} + +# ============================================================================ +# Test 1: No patch scripts (empty call) +# ============================================================================ +test_no_patches() { + print_section "Test 1: No Patch Scripts" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 0 "Exit code is 0 for no patches" + assert_contains "$output" "No patch scripts specified" "No patches message displayed" +} + +# ============================================================================ +# Test 2: Single successful patch +# ============================================================================ +test_single_success() { + print_section "Test 2: Single Successful Patch" + + # Create test patch + local test_patch="/tmp/test_patch_success_$$.sh" + cat > "$test_patch" << 'EOF' +#!/bin/bash +echo "Patch executing successfully" +exit 0 +EOF + chmod +x "$test_patch" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$test_patch" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 0 "Exit code is 0 for successful patch" + assert_contains "$output" "Detected patch scripts" "Detected message shown" + assert_contains "$output" "Running patch: bash $test_patch" "Running message shown" + assert_contains "$output" "Patch completed successfully" "Success message shown" + assert_contains "$output" "All patch scripts executed successfully" "All patches success message shown" + + rm -f "$test_patch" +} + +# ============================================================================ +# Test 3: Patch with exit code 2 (skip) +# ============================================================================ +test_patch_skip() { + print_section "Test 3: Patch Skip (Exit Code 2)" + + # Create test patch that skips + local test_patch="/tmp/test_patch_skip_$$.sh" + cat > "$test_patch" << 'EOF' +#!/bin/bash +echo "Conditions not met, skipping" +exit 2 +EOF + chmod +x "$test_patch" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$test_patch" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 0 "Exit code is 0 when patch skips" + assert_contains "$output" "Patch skipped (exit code 2)" "Skip message shown" + assert_contains "$output" "All patch scripts executed successfully" "All patches success message shown" + + rm -f "$test_patch" +} + +# ============================================================================ +# Test 4: Patch failure (exit code 1) +# ============================================================================ +test_patch_failure() { + print_section "Test 4: Patch Failure" + + # Create test patch that fails + local test_patch="/tmp/test_patch_fail_$$.sh" + cat > "$test_patch" << 'EOF' +#!/bin/bash +echo "Patch failed" +exit 1 +EOF + chmod +x "$test_patch" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$test_patch" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 1 "Exit code is 1 for failed patch" + assert_contains "$output" "Patch script failed" "Failure message shown" + assert_contains "$output" "exit code: 1" "Exit code displayed" + assert_not_contains "$output" "All patch scripts executed successfully" "Success message not shown" + + rm -f "$test_patch" +} + +# ============================================================================ +# Test 5: Multiple patches (success + skip + success) +# ============================================================================ +test_multiple_patches() { + print_section "Test 5: Multiple Patches (Success + Skip + Success)" + + # Create test patches + local patch1="/tmp/test_patch_1_$$.sh" + local patch2="/tmp/test_patch_2_$$.sh" + local patch3="/tmp/test_patch_3_$$.sh" + + cat > "$patch1" << 'EOF' +#!/bin/bash +echo "Patch 1 executing" +exit 0 +EOF + chmod +x "$patch1" + + cat > "$patch2" << 'EOF' +#!/bin/bash +echo "Patch 2 skipping" +exit 2 +EOF + chmod +x "$patch2" + + cat > "$patch3" << 'EOF' +#!/bin/bash +echo "Patch 3 executing" +exit 0 +EOF + chmod +x "$patch3" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$patch1" "$patch2" "$patch3" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 0 "Exit code is 0 for mixed patches" + assert_contains "$output" "Patch 1 executing" "Patch 1 executed" + assert_contains "$output" "Patch completed successfully: $patch1" "Patch 1 success" + assert_contains "$output" "Patch 2 skipping" "Patch 2 skipped" + assert_contains "$output" "Patch skipped (exit code 2): $patch2" "Patch 2 skip message" + assert_contains "$output" "Patch 3 executing" "Patch 3 executed" + assert_contains "$output" "Patch completed successfully: $patch3" "Patch 3 success" + assert_contains "$output" "All patch scripts executed successfully" "All patches success message" + + rm -f "$patch1" "$patch2" "$patch3" +} + +# ============================================================================ +# Test 6: Patch not found +# ============================================================================ +test_patch_not_found() { + print_section "Test 6: Patch Not Found" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "/tmp/nonexistent_patch_$$.sh" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 1 "Exit code is 1 for missing patch" + assert_contains "$output" "Patch script not found" "Not found error shown" + assert_not_contains "$output" "All patch scripts executed successfully" "Success message not shown" +} + +# ============================================================================ +# Test 7: Patch not readable (no permission) +# ============================================================================ +test_patch_not_readable() { + print_section "Test 7: Patch Not Readable" + + # Create test patch without read permission + local test_patch="/tmp/test_patch_noread_$$.sh" + cat > "$test_patch" << 'EOF' +#!/bin/bash +echo "Should not run" +exit 0 +EOF + chmod 000 "$test_patch" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$test_patch" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 1 "Exit code is 1 for unreadable patch" + assert_contains "$output" "Patch script not readable" "Not readable error shown" + assert_not_contains "$output" "All patch scripts executed successfully" "Success message not shown" + + chmod 644 "$test_patch" + rm -f "$test_patch" +} + +# ============================================================================ +# Test 8: Stop on first failure +# ============================================================================ +test_stop_on_failure() { + print_section "Test 8: Stop on First Failure" + + # Create test patches + local patch1="/tmp/test_patch_ok_$$.sh" + local patch2="/tmp/test_patch_fail_$$.sh" + local patch3="/tmp/test_patch_never_run_$$.sh" + + cat > "$patch1" << 'EOF' +#!/bin/bash +echo "Patch 1 OK" +exit 0 +EOF + chmod +x "$patch1" + + cat > "$patch2" << 'EOF' +#!/bin/bash +echo "Patch 2 FAIL" +exit 1 +EOF + chmod +x "$patch2" + + cat > "$patch3" << 'EOF' +#!/bin/bash +echo "Patch 3 should not run" +exit 0 +EOF + chmod +x "$patch3" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$patch1" "$patch2" "$patch3" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 1 "Exit code is 1 when patch fails" + assert_contains "$output" "Patch 1 OK" "Patch 1 executed" + assert_contains "$output" "Patch 2 FAIL" "Patch 2 executed" + assert_not_contains "$output" "Patch 3 should not run" "Patch 3 not executed" + assert_contains "$output" "Patch script failed" "Failure message shown" + + rm -f "$patch1" "$patch2" "$patch3" +} + +# ============================================================================ +# Test 9: Patch with custom exit code (not 0, 1, or 2) +# ============================================================================ +test_custom_exit_code() { + print_section "Test 9: Patch with Custom Exit Code" + + # Create test patch with exit code 5 + local test_patch="/tmp/test_patch_custom_$$.sh" + cat > "$test_patch" << 'EOF' +#!/bin/bash +echo "Patch with custom exit code" +exit 5 +EOF + chmod +x "$test_patch" + + local output + output=$(bash "$RUNNER_DIR/helpers/execute_patches.sh" "$test_patch" 2>&1) + local exit_code=$? + + assert_exit_code "$exit_code" 1 "Exit code is 1 for custom exit code" + assert_contains "$output" "Patch script failed" "Failure message shown" + assert_contains "$output" "exit code: 5" "Custom exit code displayed" + + rm -f "$test_patch" +} + +# ============================================================================ +# Run all tests +# ============================================================================ +echo "Starting execute_patches.sh unit tests..." +echo "Project root: $PROJECT_ROOT" + +test_no_patches +test_single_success +test_patch_skip +test_patch_failure +test_multiple_patches +test_patch_not_found +test_patch_not_readable +test_stop_on_failure +test_custom_exit_code + +# Print summary +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Test Summary:" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Total: $TESTS_RUN" +echo -e " Passed: ${COLOR_GREEN}$TESTS_PASSED${COLOR_RESET}" +if [[ $TESTS_FAILED -gt 0 ]]; then + echo -e " Failed: ${COLOR_RED}$TESTS_FAILED${COLOR_RESET}" +else + echo " Failed: 0" +fi +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +if [[ $TESTS_FAILED -eq 0 ]]; then + echo -e "${COLOR_GREEN}✓ All tests passed!${COLOR_RESET}" + exit 0 +else + echo -e "${COLOR_RED}✗ Some tests failed${COLOR_RESET}" + exit 1 +fi diff --git a/tests/runner/run_all_tests.sh b/tests/runner/run_all_tests.sh index 3de69c85..f208affa 100755 --- a/tests/runner/run_all_tests.sh +++ b/tests/runner/run_all_tests.sh @@ -33,6 +33,7 @@ TEST_SCRIPTS=( "$SCRIPT_DIR/lib/test_common.sh" "$SCRIPT_DIR/lib/test_validation.sh" "$SCRIPT_DIR/lib/test_config.sh" + "$SCRIPT_DIR/helpers/test_execute_patches.sh" ) # Run each test suite