///|
pub(all) enum QualityGateStatus {
GatePassed
GateFailed
GateNoSignal
} derive(Debug, Eq, ToJson)
///|
pub(all) struct QualityGate {
min_score_percent : Int
max_survived : Int?
max_compile_error : Int?
max_timeout : Int?
max_skipped : Int?
} derive(Debug, Eq, ToJson)
///|
pub(all) struct QualityGateViolation {
metric : String
actual : Int
limit : Int
message : String
} derive(Debug, Eq, ToJson)
///|
pub(all) struct QualityGateReport {
status : QualityGateStatus
score_percent : Int
scored_mutants : Int
total_mutants : Int
killed : Int
survived : Int
compile_error : Int
timeout : Int
skipped : Int
equivalent : Int
violations : Array[QualityGateViolation]
message : String
} derive(Debug, Eq, ToJson)
///|
pub fn QualityGate::default() -> QualityGate {
{
min_score_percent: 80,
max_survived: None,
max_compile_error: Some(0),
max_timeout: Some(0),
max_skipped: None,
}
}
///|
pub fn QualityGate::strict(min_score_percent? : Int = 90) -> QualityGate {
{
min_score_percent,
max_survived: Some(0),
max_compile_error: Some(0),
max_timeout: Some(0),
max_skipped: Some(0),
}
}
///|
pub fn quality_gate_status_label(status : QualityGateStatus) -> String {
match status {
GatePassed => "passed"
GateFailed => "failed"
GateNoSignal => "no-signal"
}
}
///|
pub fn evaluate_quality_gate(
report : ProjectRunReport,
gate : QualityGate,
) -> QualityGateReport {
let violations : Array[QualityGateViolation] = []
let scored_mutants = report.killed + report.survived
if scored_mutants > 0 && report.score_percent < gate.min_score_percent {
violations.push({
metric: "score_percent",
actual: report.score_percent,
limit: gate.min_score_percent,
message: "mutation score is below the required threshold",
})
}
add_limit_violation(
violations,
metric="survived",
actual=report.survived,
limit=gate.max_survived,
message="too many mutants survived",
)
add_limit_violation(
violations,
metric="compile_error",
actual=report.compile_error,
limit=gate.max_compile_error,
message="too many mutants failed during check/build",
)
add_limit_violation(
violations,
metric="timeout",
actual=report.timeout,
limit=gate.max_timeout,
message="too many mutants timed out",
)
add_limit_violation(
violations,
metric="skipped",
actual=report.skipped,
limit=gate.max_skipped,
message="too many mutants were skipped",
)
let status = if !violations.is_empty() {
GateFailed
} else if scored_mutants == 0 {
GateNoSignal
} else {
GatePassed
}
{
status,
score_percent: report.score_percent,
scored_mutants,
total_mutants: report.mutation_count,
killed: report.killed,
survived: report.survived,
compile_error: report.compile_error,
timeout: report.timeout,
skipped: report.skipped,
equivalent: report.equivalent,
violations,
message: quality_gate_message(status, violations.length(), scored_mutants),
}
}
///|
pub fn evaluate_quality_gate_after_baseline(
report : ProjectRunReport,
gate : QualityGate,
baseline : BaselineReport,
) -> QualityGateReport {
if baseline.can_run_mutants {
evaluate_quality_gate(report, gate)
} else {
let violation = {
metric: "baseline",
actual: 0,
limit: 1,
message: "baseline did not pass: \{baseline.detail}",
}
{
status: GateFailed,
score_percent: report.score_percent,
scored_mutants: report.killed + report.survived,
total_mutants: report.mutation_count,
killed: report.killed,
survived: report.survived,
compile_error: report.compile_error,
timeout: report.timeout,
skipped: report.skipped,
equivalent: report.equivalent,
violations: [violation],
message: "baseline must pass before mutation quality can be trusted",
}
}
}
///|
pub fn format_quality_gate(gate : QualityGate) -> String {
[
"Quality gate",
"min-score-percent: \{gate.min_score_percent}",
"max-survived: \{format_gate_limit(gate.max_survived)}",
"max-compile-error: \{format_gate_limit(gate.max_compile_error)}",
"max-timeout: \{format_gate_limit(gate.max_timeout)}",
"max-skipped: \{format_gate_limit(gate.max_skipped)}",
].join("\n")
}
///|
pub fn format_quality_gate_report(report : QualityGateReport) -> String {
let lines : Array[String] = [
"Quality gate report",
"status: \{quality_gate_status_label(report.status)}",
"score: \{report.score_percent}%",
"scored-mutants: \{report.scored_mutants}",
"total-mutants: \{report.total_mutants}",
"killed: \{report.killed}",
"survived: \{report.survived}",
"compile-error: \{report.compile_error}",
"timeout: \{report.timeout}",
"skipped: \{report.skipped}",
"equivalent: \{report.equivalent}",
"message: \{report.message}",
]
if report.violations.is_empty() {
lines.push("violations: none")
} else {
lines.push("violations:")
for violation in report.violations {
lines.push(
"- \{violation.metric}: actual=\{violation.actual}, " +
"limit=\{violation.limit}, \{violation.message}",
)
}
}
lines.join("\n")
}
///|
fn add_limit_violation(
violations : Array[QualityGateViolation],
metric~ : String,
actual~ : Int,
limit~ : Int?,
message~ : String,
) -> Unit {
match limit {
Some(max) =>
if actual > max {
violations.push({ metric, actual, limit: max, message })
}
None => ()
}
}
///|
fn quality_gate_message(
status : QualityGateStatus,
violation_count : Int,
scored_mutants : Int,
) -> String {
match status {
GatePassed => "quality gate passed"
GateFailed => "\{violation_count} quality gate violation(s)"
GateNoSignal =>
if scored_mutants == 0 {
"no killed or survived mutants were available for scoring"
} else {
"quality gate could not produce a signal"
}
}
}
///|
fn format_gate_limit(limit : Int?) -> String {
match limit {
Some(value) => value.to_string()
None => "unlimited"
}
}