///|
pub fn build_scenario_matrix(
manifest : ScenarioManifest,
observations : Array[ScenarioObservation],
required_targets : Array[ScenarioTarget],
) -> ScenarioMatrix {
let specs = manifest.specs
let issues : Array[ScenarioIssue] = []
let coverage : Array[TargetCoverage] = []
let mut required_count = 0
let mut completed_count = 0
let mut passed_count = 0
let mut failed_count = 0
let mut skipped_count = 0
let mut issue_index = 0
while issue_index < manifest.issues.length() {
issues.push(manifest.issues[issue_index])
issue_index += 1
}
validate_duplicate_specs(specs, issues)
validate_duplicate_observations(observations, issues)
let mut spec_index = 0
while spec_index < specs.length() {
let spec = specs[spec_index]
let mut target_index = 0
while target_index < spec.targets.length() {
let target = spec.targets[target_index]
if spec.required {
required_count += 1
}
match observation_for(observations, spec.id, target) {
None => {
skipped_count += 1
issues.push(
scenario_issue(
"observation-missing",
if spec.required {
Error
} else {
Notice
},
spec.id,
target.name(),
"no run observation was recorded for this scenario target",
"Run the declared command and record its output and evidence digest.",
),
)
}
Some(observation) => {
completed_count += if spec.required { 1 } else { 0 }
if observation_matches(spec, observation) {
passed_count += 1
} else {
failed_count += 1
let observation_issues = validate_observation(spec, observation)
let mut observation_issue_index = 0
while observation_issue_index < observation_issues.length() {
issues.push(observation_issues[observation_issue_index])
observation_issue_index += 1
}
}
}
}
target_index += 1
}
spec_index += 1
}
let mut target_index = 0
while target_index < required_targets.length() {
let target = required_targets[target_index]
coverage.push(compute_coverage(specs, observations, target))
if !coverage[coverage.length() - 1].complete {
issues.push(
scenario_issue(
"target-coverage-incomplete",
Error,
"",
target.name(),
"required target coverage is incomplete",
"Record every required scenario for this target before publishing the matrix.",
),
)
}
target_index += 1
}
ScenarioMatrix::{
name: manifest.name,
version: manifest.version,
specs,
observations,
coverage,
issues,
required_count,
completed_count,
passed_count,
failed_count,
skipped_count,
}
}
///|
pub fn observation_for(
observations : Array[ScenarioObservation],
id : String,
target : ScenarioTarget,
) -> ScenarioObservation? {
let mut index = 0
while index < observations.length() {
if observations[index].id == id && observations[index].target == target {
return Some(observations[index])
}
index += 1
}
None
}
///|
fn validate_duplicate_specs(
specs : Array[ScenarioSpec],
issues : Array[ScenarioIssue],
) -> Unit {
let mut left = 0
while left < specs.length() {
let mut right = left + 1
while right < specs.length() {
if specs[left].id == specs[right].id {
issues.push(
scenario_issue(
"scenario-id-duplicate",
Error,
specs[left].id,
"",
"scenario id is declared more than once",
"Give each scenario one stable id so observations can be joined safely.",
),
)
}
right += 1
}
left += 1
}
}
///|
fn validate_duplicate_observations(
observations : Array[ScenarioObservation],
issues : Array[ScenarioIssue],
) -> Unit {
let mut left = 0
while left < observations.length() {
let mut right = left + 1
while right < observations.length() {
let first = observations[left]
let second = observations[right]
if first.id == second.id && first.target == second.target {
issues.push(
scenario_issue(
"observation-duplicate",
Error,
first.id,
first.target.name(),
"more than one observation matches the same scenario target",
"Keep one deterministic observation row per id and target.",
),
)
}
right += 1
}
left += 1
}
}
///|
fn observation_matches(
spec : ScenarioSpec,
observation : ScenarioObservation,
) -> Bool {
let status_ok = match spec.expected {
Success => observation.status == Passed
Failure => observation.status == Failed
}
let output_ok = spec.expected_output.length() == 0 ||
contains(observation.stdout, spec.expected_output)
let duration_ok = observation.duration_ms >= 0 &&
observation.duration_ms <= spec.timeout_ms
let evidence_ok = observation.evidence_digest.length() > 0
status_ok && output_ok && duration_ok && evidence_ok
}
///|
fn compute_coverage(
specs : Array[ScenarioSpec],
observations : Array[ScenarioObservation],
target : ScenarioTarget,
) -> TargetCoverage {
let mut required_cases = 0
let mut observed_cases = 0
let mut passed_cases = 0
let mut spec_index = 0
while spec_index < specs.length() {
let spec = specs[spec_index]
if spec.required && spec.has_target(target) {
required_cases += 1
match observation_for(observations, spec.id, target) {
None => ()
Some(observation) => {
observed_cases += 1
if observation_matches(spec, observation) {
passed_cases += 1
}
}
}
}
spec_index += 1
}
TargetCoverage::{
target,
required_cases,
observed_cases,
passed_cases,
complete: required_cases > 0 && required_cases == passed_cases,
}
}
///|
pub fn validate_observation(
spec : ScenarioSpec,
observation : ScenarioObservation,
) -> Array[ScenarioIssue] {
let issues : Array[ScenarioIssue] = []
if !contains_target(spec.targets, observation.target) {
issues.push(
scenario_issue(
"observation-target-unexpected",
Error,
spec.id,
observation.target.name(),
"observation target is not declared by the scenario",
"Add the target to the scenario or remove the observation.",
),
)
}
match spec.expected {
Success =>
if observation.status != Passed {
issues.push(
scenario_issue(
"observation-status-mismatch",
Error,
spec.id,
observation.target.name(),
"scenario expected success but observation did not pass",
"Inspect stderr and record a passing run for a success scenario.",
),
)
}
Failure =>
if observation.status != Failed {
issues.push(
scenario_issue(
"observation-status-mismatch",
Error,
spec.id,
observation.target.name(),
"scenario expected failure but observation did not fail",
"Use a deterministic failing fixture when documenting failure behavior.",
),
)
}
}
if spec.expected_output.length() > 0 &&
!contains(observation.stdout, spec.expected_output) {
issues.push(
scenario_issue(
"observation-output-mismatch",
Error,
spec.id,
observation.target.name(),
"observed stdout does not contain the declared output fragment",
"Record the stable output fragment that proves the scenario behavior.",
),
)
}
if observation.duration_ms < 0 || observation.duration_ms > spec.timeout_ms {
issues.push(
scenario_issue(
"observation-timeout",
Error,
spec.id,
observation.target.name(),
"observed duration is outside the declared timeout",
"Use a bounded run and record duration in milliseconds.",
),
)
}
if !digest_is_well_formed(observation.evidence_digest) {
issues.push(
scenario_issue(
"observation-evidence-missing",
Error,
spec.id,
observation.target.name(),
"observation has no deterministic evidence digest",
"Hash the stable stdout/stderr evidence with scenario_evidence_digest.",
),
)
}
issues
}
///|
pub fn scenario_matrix_summary(matrix : ScenarioMatrix) -> String {
"matrix=" +
matrix.name +
", version=" +
matrix.version +
", passed=" +
matrix.passed_count.to_string() +
", failed=" +
matrix.failed_count.to_string() +
", skipped=" +
matrix.skipped_count.to_string() +
", required=" +
matrix.required_count.to_string() +
", complete=" +
bool_text(matrix.passes())
}
///|
pub fn ScenarioMatrix::issue_count(
self : ScenarioMatrix,
severity : IssueSeverity,
) -> Int {
let mut count = 0
let mut index = 0
while index < self.issues.length() {
if self.issues[index].severity == severity {
count += 1
}
index += 1
}
count
}
///|
pub fn ScenarioMatrix::required_targets_complete(self : ScenarioMatrix) -> Bool {
let mut index = 0
while index < self.coverage.length() {
if !self.coverage[index].complete {
return false
}
index += 1
}
true
}