-
-
Save trikitrok/c490522de807b73d040df26b9d86b313 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| set -uo pipefail | |
| # ============================================================ | |
| # Architecture Mutation Testing Script | |
| # | |
| # Creates a counterexample for each test case in | |
| # ArchitectureRulesTests, verifies the test catches it, then | |
| # generates a Stryker-like HTML report. | |
| # ============================================================ | |
| PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" | |
| MAIN_SRC="$PROJECT_ROOT/src/main/java/com/cms_comunidades" | |
| REPORT_DIR="$PROJECT_ROOT/target/arch-mutation-report" | |
| REPORT_FILE="$REPORT_DIR/index.html" | |
| SUREFIRE_XML="$PROJECT_ROOT/target/surefire-reports/TEST-com.cms_comunidades.ArchitectureRulesTests.xml" | |
| ARCH_TESTS_FILE="$PROJECT_ROOT/src/test/java/com/cms_comunidades/ArchitectureRulesTests.java" | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| BOLD='\033[1m' | |
| NC='\033[0m' | |
| VERBOSE=true | |
| # ============================================================ | |
| # Help | |
| # ============================================================ | |
| print_help() { | |
| cat <<EOF | |
| Usage: ./scripts/arch-mutation-test.sh [OPTIONS] | |
| Architecture Mutation Testing for ArchitectureRulesTests. | |
| Requires src/main/java to have no uncommitted changes (safe recovery via | |
| git checkout -- src/main/java if the script is interrupted mid-run). | |
| For each @ArchTest method this script: | |
| 1. Creates a counterexample (code that violates the architectural rule) | |
| 2. Compiles and runs only ArchitectureRulesTests | |
| 3. Records which tests were killed (caught the violation) | |
| 4. Reverts all changes | |
| 5. Generates a Stryker-like HTML report | |
| Mutants: | |
| 1. domain_should_not_depend_on_infrastructure | |
| A domain class that directly imports an infrastructure class. | |
| 2. no_cyclic_dependencies_in_domain_packages | |
| Two classes creating a cycle between domain.claims and domain.commands. | |
| 3. no_cyclic_dependencies_in_infrastructure_packages | |
| Two classes creating a cycle between infrastructure.companies and infrastructure.logging. | |
| 4. no_cyclic_dependencies_between_top_level_packages | |
| A new top-level package (adapter) sits between domain and infrastructure, | |
| creating a 3-way cycle (domain → adapter → infrastructure → domain) | |
| without triggering the direct domain→infrastructure rule. | |
| 5. domain_should_not_depend_on_infrastructure (and top-level cycle) | |
| A domain class imports an infrastructure class that already depends | |
| on the domain, creating both a direct violation and a 2-way cycle | |
| between top-level packages (domain ↔ infrastructure). | |
| Options: | |
| -h, --help Show this help message and exit | |
| -q, --quiet Suppress Maven build output during each mutant run | |
| Report is generated at: target/arch-mutation-report/index.html | |
| EOF | |
| } | |
| # ============================================================ | |
| # Argument parsing | |
| # ============================================================ | |
| while [[ "$#" -gt 0 ]]; do | |
| case $1 in | |
| -h|--help) print_help; exit 0 ;; | |
| -q|--quiet) VERBOSE=false; shift ;; | |
| *) echo "Unknown argument: $1"; echo "Run with --help for usage."; exit 1 ;; | |
| esac | |
| done | |
| # ============================================================ | |
| # Cleanup — remove all created temp files on exit | |
| # ============================================================ | |
| TEMP_FILES=() | |
| CAPTURED_KEYS=() | |
| CAPTURED_CONTENTS=() | |
| CURRENT_MUTANT_ID="" | |
| cleanup() { | |
| log_step "\n🧹 Cleaning up mutants..." | |
| # Revert all changes in src/main/java | |
| git -C "$PROJECT_ROOT" checkout -- src/main/java | |
| git -C "$PROJECT_ROOT" clean -fd src/main/java | |
| # Selective cleanup for src/test/java (preserving changes in ArchitectureRulesTests.java) | |
| # 1. Revert tracked changes excluding the test file | |
| git -C "$PROJECT_ROOT" status --porcelain -- src/test/java | grep -v "src/test/java/com/cms_comunidades/ArchitectureRulesTests.java" | grep -v "^??" | cut -c4- | xargs -r git -C "$PROJECT_ROOT" checkout -- | |
| # 2. Remove untracked files excluding the test file | |
| git -C "$PROJECT_ROOT" status --porcelain -- src/test/java | grep -v "src/test/java/com/cms_comunidades/ArchitectureRulesTests.java" | grep "^??" | cut -c4- | xargs -r rm -rf | |
| TEMP_FILES=() | |
| } | |
| # ============================================================ | |
| # Utility functions | |
| # ============================================================ | |
| log() { echo -e "$*"; } | |
| log_step() { echo -e "${BOLD}$*${NC}"; } | |
| create_file() { | |
| local path="$1" | |
| local content="$2" | |
| mkdir -p "$(dirname "$path")" | |
| printf '%s\n' "$content" > "$path" | |
| TEMP_FILES+=("$path") | |
| local rel_path="${path#$MAIN_SRC/}" | |
| CAPTURED_KEYS+=("${CURRENT_MUTANT_ID}:${rel_path}") | |
| CAPTURED_CONTENTS+=("$content") | |
| } | |
| remove_mutant_files() { | |
| # Use the same logic as cleanup | |
| cleanup | |
| } | |
| run_arch_tests() { | |
| if [ "$VERBOSE" = true ]; then | |
| mvn -f "$PROJECT_ROOT/pom.xml" -Dtest=ArchitectureRulesTests clean test | |
| else | |
| mvn -f "$PROJECT_ROOT/pom.xml" -Dtest=ArchitectureRulesTests clean test -q >/dev/null 2>&1 | |
| fi | |
| } | |
| get_failed_tests() { | |
| if [ ! -f "$SUREFIRE_XML" ]; then | |
| echo "" | |
| return | |
| fi | |
| python3 - "$SUREFIRE_XML" <<'PYEOF' | |
| import sys | |
| import xml.etree.ElementTree as ET | |
| tree = ET.parse(sys.argv[1]) | |
| root = tree.getroot() | |
| for tc in root.findall('.//testcase'): | |
| if tc.find('failure') is not None or tc.find('error') is not None: | |
| print(tc.get('name')) | |
| PYEOF | |
| } | |
| # ============================================================ | |
| # Mutant file definitions | |
| # ============================================================ | |
| setup_mutant_1() { | |
| # A brand-new infrastructure helper plus a domain class that imports it. | |
| # The helper defines a static final String constant. | |
| # This mutant specifically targets Rule 5 (source-code import check) because | |
| # compile-time constants are often inlined, making them invisible to | |
| # bytecode-based checks (Rule 1). | |
| create_file "$MAIN_SRC/infrastructure/ArchMutant1InfraHelper.java" \ | |
| 'package com.cms_comunidades.infrastructure; | |
| // ARCH-MUTANT: brand-new infrastructure helper — no domain imports | |
| public class ArchMutant1InfraHelper { | |
| public static final String HELPER_NAME = "mutant-1-helper"; | |
| }' | |
| create_file "$MAIN_SRC/domain/ArchMutant1DomainImportsInfra.java" \ | |
| 'package com.cms_comunidades.domain; | |
| import com.cms_comunidades.infrastructure.ArchMutant1InfraHelper; | |
| // ARCH-MUTANT: domain class importing a pure infrastructure helper | |
| class ArchMutant1DomainImportsInfra { | |
| String use() { | |
| return ArchMutant1InfraHelper.HELPER_NAME; | |
| } | |
| }' | |
| } | |
| setup_mutant_2() { | |
| # Two classes that create a cycle between domain.claims ↔ domain.commands. | |
| create_file "$MAIN_SRC/domain/claims/ArchMutant2ClaimsImportsCommands.java" \ | |
| 'package com.cms_comunidades.domain.claims; | |
| import com.cms_comunidades.domain.commands.AllClaimCommandsRepository; | |
| // ARCH-MUTANT: claims importing commands (half of a cycle) | |
| class ArchMutant2ClaimsImportsCommands { | |
| void use(AllClaimCommandsRepository repo) {} | |
| }' | |
| create_file "$MAIN_SRC/domain/commands/ArchMutant2CommandsImportsClaims.java" \ | |
| 'package com.cms_comunidades.domain.commands; | |
| import com.cms_comunidades.domain.claims.ClaimsReader; | |
| // ARCH-MUTANT: commands importing claims (other half of the cycle) | |
| class ArchMutant2CommandsImportsClaims { | |
| void use(ClaimsReader reader) {} | |
| }' | |
| } | |
| setup_mutant_3() { | |
| # Two classes that create a cycle between infrastructure.companies ↔ infrastructure.logging. | |
| create_file "$MAIN_SRC/infrastructure/companies/ArchMutant3CompaniesImportsLogging.java" \ | |
| 'package com.cms_comunidades.infrastructure.companies; | |
| import com.cms_comunidades.infrastructure.logging.ApplicationLogger; | |
| // ARCH-MUTANT: companies importing logging (half of a cycle) | |
| class ArchMutant3CompaniesImportsLogging { | |
| void use(ApplicationLogger logger) {} | |
| }' | |
| create_file "$MAIN_SRC/infrastructure/logging/ArchMutant3LoggingImportsCompanies.java" \ | |
| 'package com.cms_comunidades.infrastructure.logging; | |
| import com.cms_comunidades.infrastructure.companies.ClaimDataChecker; | |
| // ARCH-MUTANT: logging importing companies (other half of the cycle) | |
| class ArchMutant3LoggingImportsCompanies { | |
| void use(ClaimDataChecker checker) {} | |
| }' | |
| } | |
| setup_mutant_4() { | |
| # Introduces a new sub-package `adapter.api` and a new infrastructure helper | |
| # to build a fully self-contained 3-way cycle: | |
| # domain.claims → adapter.api → infrastructure → domain.claims | |
| # | |
| # All three legs of the cycle are injected by this mutant; no production | |
| # class is relied upon to close the loop. | |
| # | |
| # With the 2-level slice pattern ("com.cms_comunidades.(*)..") | |
| # each top-level package forms a distinct slice, so the cycle is visible. | |
| # This intentionally avoids a direct domain→infrastructure import, so only | |
| # the cycle rule (test 4) is triggered, NOT the direct-dependency rule (test 1). | |
| create_file "$MAIN_SRC/infrastructure/ArchMutant4InfraImportsDomain.java" \ | |
| 'package com.cms_comunidades.infrastructure; | |
| import com.cms_comunidades.domain.claims.ClaimsReader; | |
| // ARCH-MUTANT: infrastructure class that closes the 3-way cycle by importing domain.claims | |
| public class ArchMutant4InfraImportsDomain { | |
| void use(ClaimsReader reader) {} | |
| }' | |
| create_file "$MAIN_SRC/adapter/api/ArchMutant4Adapter.java" \ | |
| 'package com.cms_comunidades.adapter.api; | |
| import com.cms_comunidades.infrastructure.ArchMutant4InfraImportsDomain; | |
| // ARCH-MUTANT: new sub-package that bridges domain.claims and infrastructure | |
| public class ArchMutant4Adapter { | |
| void use(ArchMutant4InfraImportsDomain infra) {} | |
| }' | |
| create_file "$MAIN_SRC/domain/claims/ArchMutant4DomainImportsAdapter.java" \ | |
| 'package com.cms_comunidades.domain.claims; | |
| import com.cms_comunidades.adapter.api.ArchMutant4Adapter; | |
| // ARCH-MUTANT: domain.claims imports the adapter package (starting the 3-way cycle) | |
| class ArchMutant4DomainImportsAdapter { | |
| void use(ArchMutant4Adapter adapter) {} | |
| }' | |
| } | |
| setup_mutant_5() { | |
| # Violates both: | |
| # 1. domain must not depend on infrastructure (direct dependency) | |
| # 2. no cyclic dependencies between top-level packages (domain ↔ infrastructure) | |
| # | |
| # We create a domain class that imports EBrokerStateChangeNotifier (infrastructure). | |
| # Since EBrokerStateChangeNotifier already imports domain classes (OpeningListener, etc.), | |
| # this completes a 2-way cycle between the 'domain' and 'infrastructure' top-level slices. | |
| create_file "$MAIN_SRC/domain/ArchMutant5Domain.java" \ | |
| 'package com.cms_comunidades.domain; | |
| import com.cms_comunidades.infrastructure.ebroker.EBrokerStateChangeNotifier; | |
| // ARCH-MUTANT: domain class importing an infrastructure class that depends back on domain | |
| class ArchMutant5Domain { | |
| void use(EBrokerStateChangeNotifier notifier) {} | |
| }' | |
| } | |
| # ============================================================ | |
| # Mutant registry | |
| # ============================================================ | |
| # Each entry: "id|target_test|setup_function|description|diff_snippet" | |
| # The diff_snippet is shown in the HTML report. | |
| MUTANT_IDS=("mutant-1" "mutant-2" "mutant-3" "mutant-4" "mutant-5") | |
| declare -A MUTANT_SETUP_FN | |
| MUTANT_SETUP_FN["mutant-1"]="setup_mutant_1" | |
| MUTANT_SETUP_FN["mutant-2"]="setup_mutant_2" | |
| MUTANT_SETUP_FN["mutant-3"]="setup_mutant_3" | |
| MUTANT_SETUP_FN["mutant-4"]="setup_mutant_4" | |
| MUTANT_SETUP_FN["mutant-5"]="setup_mutant_5" | |
| declare -A MUTANT_LABEL | |
| MUTANT_LABEL["mutant-1"]="Domain imports inlined constant from infrastructure" | |
| MUTANT_LABEL["mutant-2"]="Cycle between domain subpackages (claims ↔ commands)" | |
| MUTANT_LABEL["mutant-3"]="Cycle between infrastructure subpackages (companies ↔ logging)" | |
| MUTANT_LABEL["mutant-4"]="Top-level 3-way cycle (domain → adapter → infrastructure → domain)" | |
| MUTANT_LABEL["mutant-5"]="Top-level 2-way cycle (domain ↔ infrastructure)" | |
| declare -A MUTANT_FILES_ADDED | |
| MUTANT_FILES_ADDED["mutant-1"]="infrastructure/ArchMutant1InfraHelper.java domain/ArchMutant1DomainImportsInfra.java" | |
| MUTANT_FILES_ADDED["mutant-2"]="domain/claims/ArchMutant2ClaimsImportsCommands.java domain/commands/ArchMutant2CommandsImportsClaims.java" | |
| MUTANT_FILES_ADDED["mutant-3"]="infrastructure/companies/ArchMutant3CompaniesImportsLogging.java infrastructure/logging/ArchMutant3LoggingImportsCompanies.java" | |
| MUTANT_FILES_ADDED["mutant-4"]="infrastructure/ArchMutant4InfraImportsDomain.java adapter/api/ArchMutant4Adapter.java domain/claims/ArchMutant4DomainImportsAdapter.java" | |
| MUTANT_FILES_ADDED["mutant-5"]="domain/ArchMutant5Domain.java" | |
| declare -A MUTANT_RATIONALE | |
| MUTANT_RATIONALE["mutant-1"]="A domain class imports a <code>static final String</code> constant from <code>ArchMutant1InfraHelper</code>. In Java, these constants are often inlined at compile-time. As a result, ArchUnit's bytecode analysis (Rule 1) cannot 'see' the dependency. This mutant verifies that the source-code-based check (Rule 5) correctly identifies the forbidden import." | |
| MUTANT_RATIONALE["mutant-2"]="Two temporary classes mutually import each other across the <code>domain.claims</code> and <code>domain.commands</code> packages. This creates a circular dependency within the domain layer that should be caught by the domain-specific slice cycle detector (Rule 2)." | |
| MUTANT_RATIONALE["mutant-3"]="Two temporary classes mutually import each other across the <code>infrastructure.companies</code> and <code>infrastructure.logging</code> packages. This creates a circular dependency within the infrastructure layer that should be caught by the infrastructure-specific slice cycle detector (Rule 3)." | |
| MUTANT_RATIONALE["mutant-4"]="Three self-contained temporary classes build a fully injected 3-way cycle: <code>domain.claims</code> imports a new <code>adapter.api</code> class, which imports a new <code>infrastructure</code> class, which imports back into <code>domain.claims</code>. No production class is relied upon to close the loop. This intentionally avoids a direct <code>domain→infrastructure</code> import, so only Rule 4 (top-level cycle) is triggered — verifying that it catches indirect violations that the direct-dependency rules cannot see." | |
| MUTANT_RATIONALE["mutant-5"]="A domain class directly imports an infrastructure class (<code>EBrokerStateChangeNotifier</code>). Since the infrastructure class already depends back on the domain, this creates a direct 2-way cycle between top-level packages. It should be caught by Rule 1 (dependency), Rule 5 (source import), and Rule 4 (top-level cycle)." | |
| # Dynamically get architecture tests from the java file | |
| ALL_TESTS=($(grep -E "@(Arch)?Test" -A 1 "$ARCH_TESTS_FILE" | grep "void" | sed -E 's/.*void ([a-zA-Z0-9_]+)\(.*/\1/')) | |
| # ============================================================ | |
| # Result storage | |
| # ============================================================ | |
| declare -A RESULT_STATUS # "killed" | "survived" | "compile_error" | |
| declare -A RESULT_FAILED_TESTS # newline-separated list of failed test names | |
| # ============================================================ | |
| # Precondition checks | |
| # ============================================================ | |
| log_step "\n🔍 Checking preconditions..." | |
| if ! command -v python3 &>/dev/null; then | |
| log "${RED}❌ python3 is required to parse Surefire XML reports but was not found.${NC}" | |
| exit 1 | |
| fi | |
| if ! command -v bc &>/dev/null; then | |
| log "${RED}❌ bc is required to render the SVG score circle in the HTML report but was not found.${NC}" | |
| exit 1 | |
| fi | |
| # Check for uncommitted changes or untracked files (excluding the ArchitectureRulesTests.java) | |
| CHANGES=$(git -C "$PROJECT_ROOT" status --porcelain -- src/main/java src/test/java | grep -v "src/test/java/com/cms_comunidades/ArchitectureRulesTests.java" || true) | |
| if [ -n "$CHANGES" ]; then | |
| log "${RED}❌ Project has uncommitted changes or untracked files in src/main/java or src/test/java.${NC}" | |
| log " (Allowed exception: src/test/java/com/cms_comunidades/ArchitectureRulesTests.java)" | |
| log "\nChanges detected:\n$CHANGES" | |
| log "\n${YELLOW}The script requires a clean baseline to safely use 'git' for mutant cleanup.${NC}" | |
| exit 1 | |
| fi | |
| trap cleanup EXIT | |
| # ============================================================ | |
| # Baseline — architecture tests must pass before mutating | |
| # ============================================================ | |
| log_step "\n🏁 Running baseline architecture tests..." | |
| if ! run_arch_tests; then | |
| log "${RED}❌ Architecture tests are already failing — skipping mutation testing.${NC}" | |
| log " Fix the existing violations first, then re-run the script." | |
| exit 1 | |
| fi | |
| log "${GREEN}✅ Baseline passed — proceeding with mutation testing.${NC}" | |
| # ============================================================ | |
| # Run each mutant | |
| # ============================================================ | |
| log_step "\n🧬 Running mutants..." | |
| TOTAL=0 | |
| KILLED=0 | |
| for id in "${MUTANT_IDS[@]}"; do | |
| TOTAL=$((TOTAL + 1)) | |
| label="${MUTANT_LABEL[$id]}" | |
| setup_fn="${MUTANT_SETUP_FN[$id]}" | |
| log "\n ${BOLD}[${id}]${NC} $label" | |
| log -n " Running... " | |
| CURRENT_MUTANT_ID="$id" | |
| "$setup_fn" | |
| failed_tests="" | |
| if run_arch_tests; then | |
| # All tests passed → mutant survived (bad) | |
| log "${RED}SURVIVED${NC}" | |
| RESULT_STATUS["$id"]="survived" | |
| RESULT_FAILED_TESTS["$id"]="" | |
| else | |
| # At least one test failed → mutant killed | |
| failed_tests="$(get_failed_tests || true)" | |
| log "${GREEN}KILLED${NC}" | |
| RESULT_STATUS["$id"]="killed" | |
| KILLED=$((KILLED + 1)) | |
| RESULT_FAILED_TESTS["$id"]="$failed_tests" | |
| fi | |
| if [ "$VERBOSE" = true ] && [ -n "$failed_tests" ]; then | |
| log " Failed tests:" | |
| while IFS= read -r t; do | |
| [ -n "$t" ] && log " • $t" | |
| done <<< "$failed_tests" | |
| fi | |
| remove_mutant_files | |
| done | |
| # ============================================================ | |
| # Summary | |
| # ============================================================ | |
| SURVIVED=$((TOTAL - KILLED)) | |
| if [ "$TOTAL" -gt 0 ]; then | |
| SCORE=$(( (KILLED * 100) / TOTAL )) | |
| else | |
| SCORE=0 | |
| fi | |
| log "\n${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| log "${BOLD}Results: $KILLED/$TOTAL killed — Mutation Score: ${SCORE}%${NC}" | |
| log "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| for id in "${MUTANT_IDS[@]}"; do | |
| status="${RESULT_STATUS[$id]}" | |
| label="${MUTANT_LABEL[$id]}" | |
| if [ "$status" = "killed" ]; then | |
| log " ${GREEN}✔ KILLED${NC} [$id] $label" | |
| else | |
| log " ${RED}✘ SURVIVED${NC} [$id] $label" | |
| fi | |
| done | |
| # ============================================================ | |
| # HTML Report generation | |
| # ============================================================ | |
| log_step "\n📄 Generating HTML report..." | |
| mkdir -p "$REPORT_DIR" | |
| if [ "$SCORE" -ge 80 ]; then SCORE_COLOR="#22c55e"; SCORE_LABEL="Good" | |
| elif [ "$SCORE" -ge 60 ]; then SCORE_COLOR="#eab308"; SCORE_LABEL="Warning" | |
| else SCORE_COLOR="#ef4444"; SCORE_LABEL="Danger" | |
| fi | |
| GENERATED_AT="$(date '+%Y-%m-%d %H:%M:%S')" | |
| # Build mutant rows HTML | |
| MUTANT_ROWS_HTML="" | |
| ALL_MODALS_HTML="" | |
| for id in "${MUTANT_IDS[@]}"; do | |
| status="${RESULT_STATUS[$id]}" | |
| label="${MUTANT_LABEL[$id]}" | |
| rationale="${MUTANT_RATIONALE[$id]}" | |
| failed="${RESULT_FAILED_TESTS[$id]}" | |
| if [ "$status" = "killed" ]; then | |
| badge_class="badge-killed" | |
| badge_text="Killed" | |
| row_class="row-killed" | |
| else | |
| badge_class="badge-survived" | |
| badge_text="Survived" | |
| row_class="row-survived" | |
| fi | |
| # Build "killed by" list | |
| killed_by_html="" | |
| if [ -n "$failed" ]; then | |
| killed_by_html="<span class=\"killed-by-label\">Killed by:</span>" | |
| while IFS= read -r t; do | |
| if [ -n "$t" ]; then | |
| killed_by_html="${killed_by_html} <code class=\"killing-test\">${t}</code>" | |
| fi | |
| done <<< "$failed" | |
| else | |
| killed_by_html="<span class=\"survived-label\">Mutant survived (no tests failed)</span>" | |
| fi | |
| # Build files list with clickable code viewers | |
| files_html="" | |
| file_idx=0 | |
| for i in "${!CAPTURED_KEYS[@]}"; do | |
| cap_key="${CAPTURED_KEYS[$i]}" | |
| if [[ "$cap_key" == "${id}:"* ]]; then | |
| rel_file="${cap_key#${id}:}" | |
| raw_content="${CAPTURED_CONTENTS[$i]}" | |
| esc="${raw_content//&/&}" | |
| esc="${esc//</<}" | |
| esc="${esc//>/>}" | |
| modal_id="${id}-file-${file_idx}" | |
| files_html="${files_html}<button class=\"file-badge\" onclick=\"openModal('${modal_id}')\">+ ${rel_file}</button>" | |
| ALL_MODALS_HTML="${ALL_MODALS_HTML} | |
| <div class=\"modal-overlay\" id=\"${modal_id}\" onclick=\"closeModalOnOverlay(event,'${modal_id}')\"> | |
| <div class=\"modal-box\"> | |
| <div class=\"modal-header\"> | |
| <span class=\"modal-filename\">${rel_file}</span> | |
| <button class=\"modal-close\" onclick=\"closeModal('${modal_id}')\">✕</button> | |
| </div> | |
| <pre class=\"code-block\"><code>${esc}</code></pre> | |
| </div> | |
| </div>" | |
| file_idx=$((file_idx + 1)) | |
| fi | |
| done | |
| MUTANT_ROWS_HTML="${MUTANT_ROWS_HTML} | |
| <div class=\"mutant-card ${row_class}\" id=\"${id}\"> | |
| <div class=\"mutant-header\"> | |
| <span class=\"badge ${badge_class}\">${badge_text}</span> | |
| <span class=\"mutant-id\">${id}</span> | |
| <span class=\"mutant-label\">${label}</span> | |
| <button class=\"toggle-btn\" onclick=\"toggle('${id}-detail')\">▼ Details</button> | |
| </div> | |
| <div class=\"mutant-target\"> | |
| ${killed_by_html} | |
| </div> | |
| <div class=\"mutant-detail\" id=\"${id}-detail\" style=\"display:none\"> | |
| <div class=\"detail-section\"> | |
| <h4>What this mutant does</h4> | |
| <p>${rationale}</p> | |
| </div> | |
| <div class=\"detail-section\"> | |
| <h4>Files added</h4> | |
| <div class=\"files-list\">${files_html}</div> | |
| </div> | |
| </div> | |
| </div>" | |
| done | |
| # Build all-tests legend | |
| TESTS_LEGEND_HTML="" | |
| for t in "${ALL_TESTS[@]}"; do | |
| TESTS_LEGEND_HTML="${TESTS_LEGEND_HTML}<li><code>${t}</code></li>" | |
| done | |
| cat > "$REPORT_FILE" <<HTML | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Architecture Mutation Test Report</title> | |
| <style> | |
| *, *::before, *::after { box-sizing: border-box; } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | |
| margin: 0; | |
| background: #0f172a; | |
| color: #e2e8f0; | |
| min-height: 100vh; | |
| } | |
| /* ── Header ── */ | |
| header { | |
| background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); | |
| border-bottom: 1px solid #334155; | |
| padding: 0 32px; | |
| } | |
| .header-top { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 20px 0 12px; | |
| } | |
| .logo { | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| letter-spacing: -0.5px; | |
| color: #f8fafc; | |
| } | |
| .logo span { color: ${SCORE_COLOR}; } | |
| .header-subtitle { | |
| font-size: 0.8rem; | |
| color: #94a3b8; | |
| margin-left: auto; | |
| } | |
| /* ── Score bar ── */ | |
| .score-bar { | |
| display: flex; | |
| align-items: center; | |
| gap: 24px; | |
| padding: 16px 0 20px; | |
| border-top: 1px solid #1e293b; | |
| } | |
| .score-circle { | |
| position: relative; | |
| width: 80px; | |
| height: 80px; | |
| flex-shrink: 0; | |
| } | |
| .score-circle svg { transform: rotate(-90deg); } | |
| .score-circle .score-text { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.1rem; | |
| font-weight: 700; | |
| color: ${SCORE_COLOR}; | |
| } | |
| .score-circle .score-text small { | |
| font-size: 0.55rem; | |
| font-weight: 400; | |
| color: #94a3b8; | |
| margin-top: 1px; | |
| } | |
| .score-stats { | |
| display: flex; | |
| gap: 32px; | |
| } | |
| .stat { text-align: center; } | |
| .stat .val { | |
| font-size: 2rem; | |
| font-weight: 700; | |
| line-height: 1; | |
| } | |
| .stat .lbl { | |
| font-size: 0.7rem; | |
| color: #94a3b8; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-top: 4px; | |
| } | |
| .val-killed { color: #22c55e; } | |
| .val-survived { color: #ef4444; } | |
| .val-score { color: ${SCORE_COLOR}; } | |
| /* ── Progress bar ── */ | |
| .progress-track { | |
| flex: 1; | |
| background: #1e293b; | |
| border-radius: 6px; | |
| height: 12px; | |
| overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| border-radius: 6px; | |
| background: ${SCORE_COLOR}; | |
| width: ${SCORE}%; | |
| transition: width 0.6s ease; | |
| } | |
| /* ── Main content ── */ | |
| main { | |
| max-width: 960px; | |
| margin: 32px auto; | |
| padding: 0 16px; | |
| } | |
| /* ── Section header ── */ | |
| .section-header { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.08em; | |
| color: #64748b; | |
| margin-bottom: 12px; | |
| padding-bottom: 8px; | |
| border-bottom: 1px solid #1e293b; | |
| } | |
| /* ── Mutant cards ── */ | |
| .mutant-card { | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| overflow: hidden; | |
| } | |
| .row-killed { border-left: 4px solid #22c55e; } | |
| .row-survived { border-left: 4px solid #ef4444; } | |
| .mutant-header { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 12px 16px; | |
| } | |
| .badge { | |
| padding: 3px 10px; | |
| border-radius: 20px; | |
| font-size: 0.7rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| flex-shrink: 0; | |
| } | |
| .badge-killed { background: #166534; color: #86efac; } | |
| .badge-survived { background: #7f1d1d; color: #fca5a5; } | |
| .mutant-id { | |
| font-size: 0.75rem; | |
| font-family: monospace; | |
| color: #64748b; | |
| flex-shrink: 0; | |
| } | |
| .mutant-label { | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| color: #cbd5e1; | |
| flex: 1; | |
| } | |
| .toggle-btn { | |
| background: none; | |
| border: 1px solid #334155; | |
| border-radius: 4px; | |
| color: #94a3b8; | |
| cursor: pointer; | |
| font-size: 0.75rem; | |
| padding: 4px 10px; | |
| white-space: nowrap; | |
| } | |
| .toggle-btn:hover { background: #334155; color: #e2e8f0; } | |
| .mutant-target { | |
| padding: 0 16px 12px; | |
| font-size: 0.8rem; | |
| color: #94a3b8; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| flex-wrap: wrap; | |
| } | |
| .killed-by-label { | |
| font-weight: 600; | |
| color: #94a3b8; | |
| margin-right: 4px; | |
| } | |
| .killing-test { | |
| background: #0f172a; | |
| padding: 2px 6px; | |
| border-radius: 3px; | |
| font-size: 0.78rem; | |
| color: #93c5fd; | |
| } | |
| .survived-label { | |
| color: #ef4444; | |
| font-weight: 600; | |
| } | |
| /* ── Detail panel ── */ | |
| .mutant-detail { | |
| border-top: 1px solid #334155; | |
| padding: 16px; | |
| display: flex; | |
| gap: 24px; | |
| } | |
| .detail-section { flex: 1; } | |
| .detail-section h4 { | |
| margin: 0 0 8px; | |
| font-size: 0.75rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: #64748b; | |
| } | |
| .detail-section p { | |
| margin: 0; | |
| font-size: 0.85rem; | |
| color: #94a3b8; | |
| line-height: 1.6; | |
| } | |
| .files-list { display: flex; flex-direction: column; gap: 6px; } | |
| .file-badge { | |
| display: inline-block; | |
| background: #0f172a; | |
| border: 1px solid #22c55e44; | |
| color: #86efac; | |
| font-size: 0.78rem; | |
| font-family: monospace; | |
| padding: 4px 10px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| text-align: left; | |
| } | |
| .file-badge:hover { background: #1e3a2e; border-color: #22c55e88; } | |
| /* ── Code modal ── */ | |
| .modal-overlay { | |
| display: none; | |
| position: fixed; | |
| inset: 0; | |
| background: rgba(0,0,0,0.75); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .modal-overlay.open { display: flex; } | |
| .modal-box { | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| border-radius: 8px; | |
| max-width: 820px; | |
| width: 92%; | |
| max-height: 80vh; | |
| overflow: hidden; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .modal-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 12px 16px; | |
| border-bottom: 1px solid #334155; | |
| flex-shrink: 0; | |
| } | |
| .modal-filename { | |
| font-family: monospace; | |
| font-size: 0.85rem; | |
| color: #86efac; | |
| } | |
| .modal-close { | |
| background: none; | |
| border: none; | |
| color: #94a3b8; | |
| cursor: pointer; | |
| font-size: 1rem; | |
| padding: 4px 8px; | |
| line-height: 1; | |
| } | |
| .modal-close:hover { color: #e2e8f0; } | |
| .code-block { | |
| margin: 0; | |
| padding: 16px; | |
| overflow: auto; | |
| font-size: 0.82rem; | |
| line-height: 1.6; | |
| color: #e2e8f0; | |
| background: #0f172a; | |
| flex: 1; | |
| } | |
| .code-block code { font-family: monospace; } | |
| /* ── Legend ── */ | |
| .legend-box { | |
| background: #1e293b; | |
| border: 1px solid #334155; | |
| border-radius: 8px; | |
| padding: 16px 20px; | |
| margin-bottom: 24px; | |
| } | |
| .legend-box h3 { | |
| margin: 0 0 10px; | |
| font-size: 0.85rem; | |
| color: #94a3b8; | |
| } | |
| .legend-box ul { | |
| margin: 0; | |
| padding: 0 0 0 18px; | |
| font-size: 0.82rem; | |
| color: #64748b; | |
| line-height: 1.8; | |
| } | |
| .legend-box code { | |
| background: #0f172a; | |
| padding: 1px 5px; | |
| border-radius: 3px; | |
| color: #93c5fd; | |
| font-size: 0.78rem; | |
| } | |
| /* ── Footer ── */ | |
| footer { | |
| text-align: center; | |
| color: #334155; | |
| font-size: 0.75rem; | |
| padding: 32px 16px; | |
| } | |
| /* ── Responsive ── */ | |
| @media (max-width: 600px) { | |
| .score-bar { flex-wrap: wrap; } | |
| .mutant-detail { flex-direction: column; } | |
| .score-stats { gap: 16px; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <header> | |
| <div class="header-top"> | |
| <div class="logo">Arch<span>Mutant</span></div> | |
| <div class="header-subtitle">Architecture Mutation Testing Report · ${GENERATED_AT}</div> | |
| </div> | |
| <div class="score-bar"> | |
| <div class="score-circle"> | |
| <svg width="80" height="80" viewBox="0 0 80 80"> | |
| <circle cx="40" cy="40" r="34" fill="none" stroke="#1e293b" stroke-width="8"/> | |
| <circle cx="40" cy="40" r="34" fill="none" stroke="${SCORE_COLOR}" stroke-width="8" | |
| stroke-dasharray="$(echo "scale=2; 3.14159 * 2 * 34 * ${SCORE} / 100" | bc) 999" | |
| stroke-linecap="round"/> | |
| </svg> | |
| <div class="score-text">${SCORE}%<small>score</small></div> | |
| </div> | |
| <div class="score-stats"> | |
| <div class="stat"><div class="val val-killed">${KILLED}</div><div class="lbl">Killed</div></div> | |
| <div class="stat"><div class="val val-survived">${SURVIVED}</div><div class="lbl">Survived</div></div> | |
| <div class="stat"><div class="val">${TOTAL}</div><div class="lbl">Total</div></div> | |
| </div> | |
| <div class="progress-track"> | |
| <div class="progress-fill"></div> | |
| </div> | |
| </div> | |
| </header> | |
| <main> | |
| <div class="legend-box"> | |
| <h3>Architecture rules under test (ArchitectureRulesTests.java)</h3> | |
| <ul> | |
| ${TESTS_LEGEND_HTML} | |
| </ul> | |
| </div> | |
| <div class="section-header">Mutants (${TOTAL} total)</div> | |
| ${MUTANT_ROWS_HTML} | |
| </main> | |
| <footer> | |
| Generated by <strong>arch-mutation-test.sh</strong> · Repository: cms-comunidades/claims-integration | |
| </footer> | |
| <script> | |
| function toggle(id) { | |
| var el = document.getElementById(id); | |
| var btn = el.previousElementSibling.querySelector('.toggle-btn'); | |
| if (el.style.display === 'none') { | |
| el.style.display = 'flex'; | |
| btn.textContent = '▲ Details'; | |
| } else { | |
| el.style.display = 'none'; | |
| btn.textContent = '▼ Details'; | |
| } | |
| } | |
| function openModal(id) { | |
| document.getElementById(id).classList.add('open'); | |
| } | |
| function closeModal(id) { | |
| document.getElementById(id).classList.remove('open'); | |
| } | |
| function closeModalOnOverlay(event, id) { | |
| if (event.target === document.getElementById(id)) closeModal(id); | |
| } | |
| document.addEventListener('keydown', function(e) { | |
| if (e.key === 'Escape') { | |
| document.querySelectorAll('.modal-overlay.open').forEach(function(el) { | |
| el.classList.remove('open'); | |
| }); | |
| } | |
| }); | |
| </script> | |
| ${ALL_MODALS_HTML} | |
| </body> | |
| </html> | |
| HTML | |
| log "${GREEN}✅ Report generated: ${REPORT_FILE}${NC}" | |
| # ============================================================ | |
| # Exit code: 0 if all mutants killed, 1 if any survived | |
| # ============================================================ | |
| if [ "$SURVIVED" -gt 0 ]; then | |
| log "\n${RED}⚠️ ${SURVIVED} mutant(s) survived — the corresponding architecture tests may not be effective.${NC}" | |
| exit 1 | |
| fi | |
| log "\n${GREEN}🎉 All mutants killed! Architecture tests are effective.${NC}" | |
| exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment