#!/usr/bin/env bash # Test Category Runner # Sprint: CI/CD Enhancement - Script Consolidation # # Purpose: Run tests for a specific category across all test projects # Usage: ./run-test-category.sh [options] # # Options: # --fail-on-empty Fail if no tests are found for the category # --collect-coverage Collect code coverage data # --verbose Show detailed output # # Exit Codes: # 0 - Success (all tests passed or no tests found) # 1 - One or more tests failed # 2 - Invalid usage set -euo pipefail # Source shared libraries if available SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" if [[ -f "$REPO_ROOT/devops/scripts/lib/logging.sh" ]]; then source "$REPO_ROOT/devops/scripts/lib/logging.sh" else # Minimal logging fallback log_info() { echo "[INFO] $*"; } log_error() { echo "[ERROR] $*" >&2; } log_debug() { [[ -n "${DEBUG:-}" ]] && echo "[DEBUG] $*"; } log_step() { echo "==> $*"; } fi if [[ -f "$REPO_ROOT/devops/scripts/lib/exit-codes.sh" ]]; then source "$REPO_ROOT/devops/scripts/lib/exit-codes.sh" fi # ============================================================================= # Constants # ============================================================================= readonly FIND_PATTERN='\( -name "*.Tests.csproj" -o -name "*UnitTests.csproj" -o -name "*SmokeTests.csproj" -o -name "*FixtureTests.csproj" -o -name "*IntegrationTests.csproj" \)' readonly EXCLUDE_PATHS='! -path "*/node_modules/*" ! -path "*/.git/*" ! -path "*/bin/*" ! -path "*/obj/*"' readonly EXCLUDE_FILES='! -name "StellaOps.TestKit.csproj" ! -name "*Testing.csproj"' # ============================================================================= # Functions # ============================================================================= usage() { cat < [options] Run tests for a specific test category across all test projects. Arguments: category Test category (Unit, Architecture, Contract, Integration, Security, Golden, Performance, Benchmark, AirGap, Chaos, Determinism, Resilience, Observability) Options: --fail-on-empty Exit with error if no tests found for the category --collect-coverage Collect XPlat Code Coverage data --verbose Show detailed test output --results-dir DIR Custom results directory (default: ./TestResults/) --help Show this help message Environment Variables: DOTNET_VERSION .NET SDK version (default: uses installed version) TZ Timezone (should be UTC for determinism) Examples: $(basename "$0") Unit $(basename "$0") Integration --collect-coverage $(basename "$0") Performance --results-dir ./perf-results EOF } find_test_projects() { local search_dir="${1:-src}" # Use eval to properly expand the find pattern eval "find '$search_dir' $FIND_PATTERN -type f $EXCLUDE_PATHS $EXCLUDE_FILES" | sort } sanitize_project_name() { local proj="$1" # Replace slashes with underscores, remove .csproj extension echo "$proj" | sed 's|/|_|g' | sed 's|\.csproj$||' } run_tests() { local category="$1" local results_dir="$2" local collect_coverage="$3" local verbose="$4" local fail_on_empty="$5" local passed=0 local failed=0 local skipped=0 local no_tests=0 mkdir -p "$results_dir" local projects projects=$(find_test_projects "$REPO_ROOT/src") if [[ -z "$projects" ]]; then log_error "No test projects found" return 1 fi local project_count project_count=$(echo "$projects" | grep -c '.csproj' || echo "0") log_info "Found $project_count test projects" local category_lower category_lower=$(echo "$category" | tr '[:upper:]' '[:lower:]') while IFS= read -r proj; do [[ -z "$proj" ]] && continue local proj_name proj_name=$(sanitize_project_name "$proj") local trx_name="${proj_name}-${category_lower}.trx" # GitHub Actions grouping if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::group::Testing $proj ($category)" else log_step "Testing $proj ($category)" fi # Build dotnet test command local cmd="dotnet test \"$proj\"" cmd+=" --filter \"Category=$category\"" cmd+=" --configuration Release" cmd+=" --logger \"trx;LogFileName=$trx_name\"" cmd+=" --results-directory \"$results_dir\"" if [[ "$collect_coverage" == "true" ]]; then cmd+=" --collect:\"XPlat Code Coverage\"" fi if [[ "$verbose" == "true" ]]; then cmd+=" --verbosity normal" else cmd+=" --verbosity minimal" fi # Execute tests local exit_code=0 eval "$cmd" 2>&1 || exit_code=$? if [[ $exit_code -eq 0 ]]; then # Check if TRX was created (tests actually ran) if [[ -f "$results_dir/$trx_name" ]]; then ((passed++)) log_info "PASS: $proj" else ((no_tests++)) log_debug "SKIP: $proj (no $category tests)" fi else # Check if failure was due to no tests matching the filter if [[ -f "$results_dir/$trx_name" ]]; then ((failed++)) log_error "FAIL: $proj" else ((no_tests++)) log_debug "SKIP: $proj (no $category tests or build error)" fi fi # Close GitHub Actions group if [[ -n "${GITHUB_ACTIONS:-}" ]]; then echo "::endgroup::" fi done <<< "$projects" # Generate summary log_info "" log_info "==========================================" log_info "$category Test Summary" log_info "==========================================" log_info "Passed: $passed" log_info "Failed: $failed" log_info "No Tests: $no_tests" log_info "Total: $project_count" log_info "==========================================" # GitHub Actions summary if [[ -n "${GITHUB_ACTIONS:-}" ]]; then { echo "## $category Test Summary" echo "" echo "| Metric | Count |" echo "|--------|-------|" echo "| Passed | $passed |" echo "| Failed | $failed |" echo "| No Tests | $no_tests |" echo "| Total Projects | $project_count |" } >> "$GITHUB_STEP_SUMMARY" fi # Determine exit code if [[ $failed -gt 0 ]]; then return 1 fi if [[ "$fail_on_empty" == "true" ]] && [[ $passed -eq 0 ]]; then log_error "No tests found for category: $category" return 1 fi return 0 } # ============================================================================= # Main # ============================================================================= main() { local category="" local results_dir="" local collect_coverage="false" local verbose="false" local fail_on_empty="false" # Parse arguments while [[ $# -gt 0 ]]; do case "$1" in --help|-h) usage exit 0 ;; --fail-on-empty) fail_on_empty="true" shift ;; --collect-coverage) collect_coverage="true" shift ;; --verbose|-v) verbose="true" shift ;; --results-dir) results_dir="$2" shift 2 ;; -*) log_error "Unknown option: $1" usage exit 2 ;; *) if [[ -z "$category" ]]; then category="$1" else log_error "Unexpected argument: $1" usage exit 2 fi shift ;; esac done # Validate category if [[ -z "$category" ]]; then log_error "Category is required" usage exit 2 fi # Validate category name local valid_categories="Unit Architecture Contract Integration Security Golden Performance Benchmark AirGap Chaos Determinism Resilience Observability" if ! echo "$valid_categories" | grep -qw "$category"; then log_error "Invalid category: $category" log_error "Valid categories: $valid_categories" exit 2 fi # Set default results directory if [[ -z "$results_dir" ]]; then results_dir="./TestResults/$category" fi log_info "Running $category tests..." log_info "Results directory: $results_dir" run_tests "$category" "$results_dir" "$collect_coverage" "$verbose" "$fail_on_empty" } main "$@"