///|
pub fn scenario_metrics(matrix : ScenarioMatrix) -> ScenarioMetrics {
let stats = scenario_stats(matrix)
let mut covered_target_count = 0
let required_target_count = matrix.coverage.length()
let mut target_index = 0
while target_index < matrix.coverage.length() {
if matrix.coverage[target_index].complete {
covered_target_count += 1
}
target_index += 1
}
let mut total_duration_ms = 0
let mut min_duration_ms = 0
let mut max_duration_ms = 0
let mut observation_index = 0
while observation_index < matrix.observations.length() {
let duration = matrix.observations[observation_index].duration_ms
total_duration_ms += duration
if observation_index == 0 || duration < min_duration_ms {
min_duration_ms = duration
}
if duration > max_duration_ms {
max_duration_ms = duration
}
observation_index += 1
}
let coverage_percent = if stats.target_case_count == 0 {
0
} else {
stats.observed_case_count * 100 / stats.target_case_count
}
let pass_percent = if stats.observed_case_count == 0 {
0
} else {
stats.passed_case_count * 100 / stats.observed_case_count
}
let balance_score = metric_balance_score(matrix.coverage)
ScenarioMetrics::{
scenario_count: stats.scenario_count,
target_case_count: stats.target_case_count,
observed_case_count: stats.observed_case_count,
covered_target_count,
required_target_count,
passed_count: stats.passed_case_count,
failed_count: stats.failed_case_count,
skipped_count: stats.missing_case_count,
total_duration_ms,
min_duration_ms,
max_duration_ms,
coverage_percent,
pass_percent,
balance_score,
}
}
///|
pub fn ScenarioMetrics::ready(self : ScenarioMetrics) -> Bool {
self.coverage_percent == 100 &&
self.pass_percent == 100 &&
self.balance_score >= 80
}
///|
pub fn ScenarioMetrics::summary(self : ScenarioMetrics) -> String {
"cases=" +
self.target_case_count.to_string() +
", observed=" +
self.observed_case_count.to_string() +
", coverage=" +
self.coverage_percent.to_string() +
"%" +
", pass=" +
self.pass_percent.to_string() +
"%" +
", duration=" +
self.total_duration_ms.to_string() +
"ms" +
", balance=" +
self.balance_score.to_string() +
", ready=" +
bool_text(self.ready())
}
///|
pub fn ScenarioMetrics::to_markdown(self : ScenarioMetrics) -> String {
let mut out = "# Scenario Metrics\n\n"
out = out + "- Ready: " + bool_text(self.ready()) + "\n"
out = out + "- Scenarios: " + self.scenario_count.to_string() + "\n"
out = out + "- Target cases: " + self.target_case_count.to_string() + "\n"
out = out + "- Observed cases: " + self.observed_case_count.to_string() + "\n"
out = out + "- Target coverage: " + self.coverage_percent.to_string() + "% ("
out = out + self.covered_target_count.to_string() + "/"
out = out + self.required_target_count.to_string() + ")\n"
out = out + "- Passing observations: " + self.passed_count.to_string() + "\n"
out = out + "- Failed observations: " + self.failed_count.to_string() + "\n"
out = out + "- Skipped observations: " + self.skipped_count.to_string() + "\n"
out = out + "- Pass rate: " + self.pass_percent.to_string() + "%\n"
out = out + "- Duration: " + self.total_duration_ms.to_string() + " ms\n"
out = out + "- Min duration: " + self.min_duration_ms.to_string() + " ms\n"
out = out + "- Max duration: " + self.max_duration_ms.to_string() + " ms\n"
out + "- Target balance: " + self.balance_score.to_string() + "/100\n"
}
///|
pub fn ScenarioMetrics::to_json(self : ScenarioMetrics) -> String {
"{\"ready\":" +
bool_text(self.ready()) +
",\"scenarios\":" +
self.scenario_count.to_string() +
",\"cases\":" +
self.target_case_count.to_string() +
",\"observed\":" +
self.observed_case_count.to_string() +
",\"covered_targets\":" +
self.covered_target_count.to_string() +
",\"required_targets\":" +
self.required_target_count.to_string() +
",\"passed\":" +
self.passed_count.to_string() +
",\"failed\":" +
self.failed_count.to_string() +
",\"skipped\":" +
self.skipped_count.to_string() +
",\"total_duration_ms\":" +
self.total_duration_ms.to_string() +
",\"min_duration_ms\":" +
self.min_duration_ms.to_string() +
",\"max_duration_ms\":" +
self.max_duration_ms.to_string() +
",\"coverage_percent\":" +
self.coverage_percent.to_string() +
",\"pass_percent\":" +
self.pass_percent.to_string() +
",\"balance_score\":" +
self.balance_score.to_string() +
"}"
}
///|
pub fn metrics_target_gap(metrics : ScenarioMetrics) -> Int {
metrics.required_target_count - metrics.covered_target_count
}
///|
pub fn metrics_average_duration(metrics : ScenarioMetrics) -> Int {
if metrics.observed_case_count == 0 {
0
} else {
metrics.total_duration_ms / metrics.observed_case_count
}
}
///|
fn metric_balance_score(coverage : Array[TargetCoverage]) -> Int {
if coverage.length() == 0 {
return 0
}
let mut minimum = 100
let mut maximum = 0
let mut index = 0
while index < coverage.length() {
let item = coverage[index]
let percentage = if item.required_cases == 0 {
0
} else {
item.passed_cases * 100 / item.required_cases
}
if percentage < minimum {
minimum = percentage
}
if percentage > maximum {
maximum = percentage
}
index += 1
}
let spread = maximum - minimum
if spread >= 100 {
0
} else {
100 - spread
}
}