Skip to content

Instantly share code, notes, and snippets.

@trikitrok
Created June 28, 2026 00:18
Show Gist options
  • Select an option

  • Save trikitrok/c490522de807b73d040df26b9d86b313 to your computer and use it in GitHub Desktop.

Select an option

Save trikitrok/c490522de807b73d040df26b9d86b313 to your computer and use it in GitHub Desktop.
#!/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&#10;domain/ArchMutant1DomainImportsInfra.java"
MUTANT_FILES_ADDED["mutant-2"]="domain/claims/ArchMutant2ClaimsImportsCommands.java&#10;domain/commands/ArchMutant2CommandsImportsClaims.java"
MUTANT_FILES_ADDED["mutant-3"]="infrastructure/companies/ArchMutant3CompaniesImportsLogging.java&#10;infrastructure/logging/ArchMutant3LoggingImportsCompanies.java"
MUTANT_FILES_ADDED["mutant-4"]="infrastructure/ArchMutant4InfraImportsDomain.java&#10;adapter/api/ArchMutant4Adapter.java&#10;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//&/&amp;}"
esc="${esc//</&lt;}"
esc="${esc//>/&gt;}"
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