moon_mutest
", "Mutation Testing Report
", "\{escape_html(mutation_risk_summary(report))}
", "///|
pub(all) struct ProjectMutantResult {
mutation : ProjectMutation
outcome : MutantOutcome
detail : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct FileRunReport {
file : String
total : Int
killed : Int
survived : Int
compile_error : Int
timeout : Int
skipped : Int
equivalent : Int
score_percent : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ProjectRunReport {
file_count : Int
mutation_count : Int
killed : Int
survived : Int
compile_error : Int
timeout : Int
skipped : Int
equivalent : Int
score_percent : Int
files : Array[FileRunReport]
results : Array[ProjectMutantResult]
} derive(Debug, Eq, ToJson)
///|
pub fn project_result(
mutation : ProjectMutation,
outcome : MutantOutcome,
detail? : String = "",
) -> ProjectMutantResult {
{ mutation, outcome, detail }
}
///|
pub fn classify_project_sequence(
mutation : ProjectMutation,
commands : ArrayView[CommandResult],
) -> ProjectMutantResult {
let classified = classify_sequence(mutation.candidate, commands)
project_result(mutation, classified.outcome, detail=classified.detail)
}
///|
pub fn summarize_project_run(
plan : ProjectMutationPlan,
results : ArrayView[ProjectMutantResult],
) -> ProjectRunReport {
let files : Array[FileRunReport] = []
for file in plan.files {
files.push(summarize_file_run(file.file, results))
}
let totals = summarize_project_totals(results)
{
file_count: plan.file_count,
mutation_count: plan.mutation_count,
killed: totals.killed,
survived: totals.survived,
compile_error: totals.compile_error,
timeout: totals.timeout,
skipped: totals.skipped,
equivalent: totals.equivalent,
score_percent: totals.score_percent,
files,
results: results.to_owned(),
}
}
///|
pub fn mutation_risk_level(report : ProjectRunReport) -> String {
if report.survived > 0 || report.score_percent < 50 {
"high"
} else if report.compile_error > 0 ||
report.timeout > 0 ||
report.skipped > 0 ||
report.score_percent < 80 {
"medium"
} else {
"low"
}
}
///|
pub fn file_risk_level(file : FileRunReport) -> String {
let scored = file.killed + file.survived
if file.survived > 0 || (scored > 0 && file.score_percent < 50) {
"high"
} else if file.compile_error > 0 ||
file.timeout > 0 ||
file.skipped > 0 ||
(scored > 0 && file.score_percent < 80) {
"medium"
} else {
"low"
}
}
///|
pub fn risk_ranked_files(report : ProjectRunReport) -> Array[FileRunReport] {
let files = [ for file in report.files => file ]
for i in 0.. file_risk_score(files[i]) {
let tmp = files[i]
files[i] = files[j]
files[j] = tmp
}
}
}
files
}
///|
pub fn mutation_risk_summary(report : ProjectRunReport) -> String {
match mutation_risk_level(report) {
"high" =>
"high: survived mutants or very low score indicate weak assertions"
"medium" =>
"medium: review compile errors, timeouts, skipped mutants, or score gaps"
_ =>
"low: selected mutants were killed and the run produced a useful signal"
}
}
///|
fn file_risk_score(file : FileRunReport) -> Int {
let scored = file.killed + file.survived
let score_gap = if scored == 0 { 0 } else { 100 - file.score_percent }
file.survived * 10000 +
score_gap * 100 +
file.compile_error * 50 +
file.timeout * 50 +
file.skipped * 25
}
///|
pub fn diagnose_mutant_result(result : ProjectMutantResult) -> String {
let candidate = result.mutation.candidate
match candidate.rule.label {
"eq-to-ne" | "ne-to-eq" =>
"add assertions for both equal and non-equal cases around this branch"
"and-to-or" | "or-to-and" =>
"add tests where each side of the boolean expression independently changes the result"
"true-to-false" | "false-to-true" =>
"assert the observable behavior of this boolean decision, not only that the path runs"
"add-to-sub" | "sub-to-add" | "mul-to-div" | "div-to-mul" =>
"assert exact numeric results and include boundary values for this calculation"
"lt-to-le" | "le-to-lt" | "gt-to-ge" | "ge-to-gt" =>
"add boundary tests for values exactly at and just outside this comparison limit"
"zero-to-one" | "one-to-zero" | "minus-one-to-zero" =>
"add tests for numeric sentinel and boundary values near this literal"
_ =>
"add a focused assertion that would fail if this replacement changed behavior"
}
}
///|
pub fn format_survived_diagnostics(report : ProjectRunReport) -> String {
let lines : Array[String] = ["Survived mutant diagnostics"]
for result in report.results {
if result.outcome == Survived {
let mutation = result.mutation
let candidate = mutation.candidate
lines.push(
"- #\{mutation.global_id} \{mutation.file}:\{candidate.span.line} " +
"\{candidate.rule.label} `\{candidate.original}` -> " +
"`\{candidate.replacement}`: \{diagnose_mutant_result(result)}",
)
}
}
if lines.length() == 1 {
lines.push("none")
}
lines.join("\n")
}
///|
pub fn format_html_run_report(report : ProjectRunReport) -> String {
let lines : Array[String] = [
"",
"",
"",
"",
"",
"moon_mutest report ",
html_styles(),
"",
"",
"",
"",
"moon_mutest
",
"Mutation Testing Report
",
"\{escape_html(mutation_risk_summary(report))}
",
" ",
"",
html_metric_card(
"Score",
"\{report.score_percent}%",
mutation_risk_level(report),
),
html_metric_card("Killed", report.killed.to_string(), "low"),
html_metric_card(
"Survived",
report.survived.to_string(),
if report.survived > 0 {
"high"
} else {
"low"
},
),
html_metric_card("Files", report.file_count.to_string(), "neutral"),
" ",
"",
"Overall Signal
\{mutation_risk_level(report)}",
"",
"",
"",
html_pair_row("Mutations", report.mutation_count.to_string()),
html_pair_row("Executed results", report.results.length().to_string()),
html_pair_row("Compile errors", report.compile_error.to_string()),
html_pair_row("Timeouts", report.timeout.to_string()),
html_pair_row("Skipped", report.skipped.to_string()),
html_pair_row("Equivalent", report.equivalent.to_string()),
"",
"
",
" ",
]
append_html_file_ranking(lines, report)
append_html_survived_diagnostics(lines, report)
append_html_mutant_results(lines, report)
lines.push(" ")
lines.push("")
lines.push("")
lines.join("\n")
}
///|
fn summarize_file_run(
file : String,
results : ArrayView[ProjectMutantResult],
) -> FileRunReport {
let file_results = [
for result in results if result.mutation.file == file => result
]
let totals = summarize_project_totals(file_results)
{
file,
total: file_results.length(),
killed: totals.killed,
survived: totals.survived,
compile_error: totals.compile_error,
timeout: totals.timeout,
skipped: totals.skipped,
equivalent: totals.equivalent,
score_percent: totals.score_percent,
}
}
///|
fn summarize_project_totals(
results : ArrayView[ProjectMutantResult],
) -> MutationRunReport {
let mut killed = 0
let mut survived = 0
let mut compile_error = 0
let mut timeout = 0
let mut skipped = 0
let mut equivalent = 0
for result in results {
match result.outcome {
Killed => killed += 1
Survived => survived += 1
CompileError => compile_error += 1
Timeout => timeout += 1
Skipped => skipped += 1
Equivalent => equivalent += 1
}
}
let scored = killed + survived
{
file: "",
total: results.length(),
killed,
survived,
compile_error,
timeout,
skipped,
equivalent,
score_percent: if scored == 0 {
0
} else {
killed * 100 / scored
},
}
}
///|
pub fn format_markdown_project_plan(plan : ProjectMutationPlan) -> String {
let lines : Array[String] = [
"# Mutation Plan",
"",
"| Metric | Value |",
"| --- | ---: |",
"| Files | \{plan.file_count} |",
"| Mutations | \{plan.mutation_count} |",
"",
"## Files",
"",
"| File | Candidates | ID Range |",
"| --- | ---: | --- |",
]
for file in plan.files {
lines.push(
"| \{file.file} | \{file.summary.candidate_count} | " +
"\{file.start_id}..\{file.end_id} |",
)
}
lines.push("")
lines.push("## Mutations")
lines.push("")
lines.push("| ID | File | Line | Rule | Change |")
lines.push("| ---: | --- | ---: | --- | --- |")
for mutation in plan.mutations {
lines.push(
"| \{mutation.global_id} | \{mutation.file} | " +
"\{mutation.candidate.span.line} | \{mutation.candidate.rule.label} | " +
"`\{mutation.candidate.original}` -> `\{mutation.candidate.replacement}` |",
)
}
lines.join("\n")
}
///|
fn append_html_file_ranking(
lines : Array[String],
report : ProjectRunReport,
) -> Unit {
lines.push("")
lines.push("File Risk Ranking
")
lines.push("")
lines.push(
"# File Risk Total Killed Survived Score ",
)
lines.push("")
for index, file in risk_ranked_files(report) {
let risk = file_risk_level(file)
lines.push(
"\{index + 1} \{escape_html(file.file)} " +
"\{risk} " +
"\{file.total} \{file.killed} \{file.survived} " +
"\{file.score_percent}% ",
)
}
if report.files.is_empty() {
lines.push("No files were reported. ")
}
lines.push("")
lines.push("
")
lines.push(" ")
}
///|
fn append_html_survived_diagnostics(
lines : Array[String],
report : ProjectRunReport,
) -> Unit {
lines.push("")
lines.push("Survived Diagnostics
")
if report.survived == 0 {
lines.push(
"No survived mutants in the selected run.
",
)
} else {
lines.push("")
for result in report.results {
if result.outcome == Survived {
let mutation = result.mutation
let candidate = mutation.candidate
lines.push(
"- #\{mutation.global_id} " +
"
\{escape_html(mutation.file)}:\{candidate.span.line} " +
"\{escape_html(candidate.rule.label)} " +
"\{escape_html(candidate.original)} -> " +
"\{escape_html(candidate.replacement)}
" +
"\{escape_html(diagnose_mutant_result(result))} ",
)
}
}
lines.push("
")
}
lines.push(" ")
}
///|
fn append_html_mutant_results(
lines : Array[String],
report : ProjectRunReport,
) -> Unit {
lines.push("")
lines.push("Mutant Results
")
lines.push("")
lines.push(
"ID Outcome File Line Rule Change Detail ",
)
lines.push("")
for result in report.results {
let mutation = result.mutation
let candidate = mutation.candidate
lines.push(
"\{mutation.global_id} " +
"\{escape_html(outcome_label(result.outcome))} " +
"\{escape_html(mutation.file)} " +
"\{candidate.span.line} " +
"\{escape_html(candidate.rule.label)} " +
"\{escape_html(candidate.original)} -> \{escape_html(candidate.replacement)} " +
"\{escape_html(result.detail)} ",
)
}
if report.results.is_empty() {
lines.push("No mutants were executed. ")
}
lines.push("")
lines.push("
")
lines.push(" ")
}
///|
fn html_metric_card(label : String, value : String, risk : String) -> String {
"\{escape_html(label)}\{escape_html(value)}\{escape_html(risk)} "
}
///|
fn html_pair_row(label : String, value : String) -> String {
"\{escape_html(label)} \{escape_html(value)} "
}
///|
fn html_percent(value : Int) -> Int {
if value < 0 {
0
} else if value > 100 {
100
} else {
value
}
}
///|
fn outcome_html_class(outcome : MutantOutcome) -> String {
match outcome {
Killed => "low"
Survived => "high"
CompileError | Timeout | Skipped => "medium"
Equivalent => "neutral"
}
}
///|
fn escape_html(text : String) -> String {
text
.replace(old="&", new="&")
.replace(old="<", new="<")
.replace(old=">", new=">")
.replace(old="\"", new=""")
}
///|
fn html_styles() -> String {
(
#|
)
}
///|
pub fn format_markdown_run_report(report : ProjectRunReport) -> String {
let lines : Array[String] = [
"# Mutation Testing Report",
"",
"| Metric | Value |",
"| --- | ---: |",
"| Files | \{report.file_count} |",
"| Mutations | \{report.mutation_count} |",
"| Killed | \{report.killed} |",
"| Survived | \{report.survived} |",
"| Compile errors | \{report.compile_error} |",
"| Timeouts | \{report.timeout} |",
"| Skipped | \{report.skipped} |",
"| Equivalent | \{report.equivalent} |",
"| Score | \{report.score_percent}% |",
"| Risk | \{mutation_risk_level(report)} |",
"",
"## File Summary",
"",
"| File | Total | Killed | Survived | Score |",
"| --- | ---: | ---: | ---: | ---: |",
]
for file in report.files {
lines.push(
"| \{file.file} | \{file.total} | \{file.killed} | " +
"\{file.survived} | \{file.score_percent}% |",
)
}
lines.push("")
lines.push("## Mutant Results")
lines.push("")
lines.push("| ID | Outcome | File | Line | Rule | Detail |")
lines.push("| ---: | --- | --- | ---: | --- | --- |")
for result in report.results {
let mutation = result.mutation
lines.push(
"| \{mutation.global_id} | \{outcome_label(result.outcome)} | " +
"\{mutation.file} | \{mutation.candidate.span.line} | " +
"\{mutation.candidate.rule.label} | \{escape_markdown_cell(result.detail)} |",
)
}
lines.push("")
lines.push("## Survived Diagnostics")
lines.push("")
for result in report.results {
if result.outcome == Survived {
let mutation = result.mutation
let candidate = mutation.candidate
lines.push(
"- `#\{mutation.global_id}` \{mutation.file}:\{candidate.span.line} " +
"\{candidate.rule.label}: \{escape_markdown_cell(diagnose_mutant_result(result))}",
)
}
}
if report.survived == 0 {
lines.push("- none")
}
lines.join("\n")
}
///|
fn escape_markdown_cell(text : String) -> String {
if text == "" {
""
} else {
text.replace(old="|", new="\\|").replace(old="\n", new=" ")
}
}