///|
/// Deterministic expectations captured from one behavior tree asset run.
pub(all) struct RegressionBaseline {
name : String
expected_status : BtStatus
expected_digest : String
expected_ticks : Int
minimum_coverage : Int
max_ticks : Int
} derive(Debug, Eq)
///|
pub fn regression_baseline(
name : String,
expected_status : BtStatus,
expected_digest : String,
expected_ticks : Int,
minimum_coverage : Int,
max_ticks? : Int,
) -> RegressionBaseline {
{
name,
expected_status,
expected_digest,
expected_ticks,
minimum_coverage,
max_ticks: max_ticks.unwrap_or(32),
}
}
///|
pub fn RegressionBaseline::summary(self : RegressionBaseline) -> String {
self.name +
": status=" +
self.expected_status.to_text() +
", ticks=" +
self.expected_ticks.to_string() +
", coverage>=" +
self.minimum_coverage.to_string() +
"%, digest=" +
self.expected_digest
}
///|
/// Result of checking an asset against a deterministic baseline.
pub(all) struct BaselineReport {
name : String
expected_status : BtStatus
actual_status : BtStatus
expected_digest : String
actual_digest : String
expected_ticks : Int
actual_ticks : Int
minimum_coverage : Int
actual_coverage : Int
status_matches : Bool
digest_matches : Bool
ticks_match : Bool
coverage_meets : Bool
} derive(Debug, Eq)
///|
pub fn BaselineReport::ok(self : BaselineReport) -> Bool {
self.status_matches &&
self.digest_matches &&
self.ticks_match &&
self.coverage_meets
}
///|
pub fn BaselineReport::failed_checks(self : BaselineReport) -> Int {
let mut failed = 0
if !self.status_matches {
failed = failed + 1
}
if !self.digest_matches {
failed = failed + 1
}
if !self.ticks_match {
failed = failed + 1
}
if !self.coverage_meets {
failed = failed + 1
}
failed
}
///|
pub fn BaselineReport::summary(self : BaselineReport) -> String {
self.name +
": ok=" +
(if self.ok() { "true" } else { "false" }) +
", failed_checks=" +
self.failed_checks().to_string() +
", status=" +
self.actual_status.to_text() +
", ticks=" +
self.actual_ticks.to_string() +
", coverage=" +
self.actual_coverage.to_string() +
"%, digest=" +
self.actual_digest
}
///|
pub fn BaselineReport::lines(self : BaselineReport) -> Array[String] {
[
self.summary(),
"status: expected=" +
self.expected_status.to_text() +
", actual=" +
self.actual_status.to_text() +
", match=" +
bool_text(self.status_matches),
"digest: expected=" +
self.expected_digest +
", actual=" +
self.actual_digest +
", match=" +
bool_text(self.digest_matches),
"ticks: expected=" +
self.expected_ticks.to_string() +
", actual=" +
self.actual_ticks.to_string() +
", match=" +
bool_text(self.ticks_match),
"coverage: minimum=" +
self.minimum_coverage.to_string() +
"%, actual=" +
self.actual_coverage.to_string() +
"%, pass=" +
bool_text(self.coverage_meets),
]
}
///|
pub fn BaselineReport::markdown(self : BaselineReport) -> String {
let lines = Array::new()
lines.push("# MoonBTKit Regression: " + self.name)
lines.push("")
let details = self.lines()
let mut i = 0
while i < details.length() {
lines.push("- " + details[i])
i = i + 1
}
join_strings(lines, "\n")
}
///|
/// Capture a baseline from a successful deterministic DSL execution.
pub fn capture_baseline(
name : String,
source : String,
max_ticks? : Int,
) -> Result[RegressionBaseline, BtError] {
let limit = max_ticks.unwrap_or(32)
match run_baseline_source(source, limit) {
Ok(run) =>
Ok({
name,
expected_status: run.status,
expected_digest: run.digest,
expected_ticks: run.ticks,
minimum_coverage: run.coverage,
max_ticks: limit,
})
Err(err) => Err(err)
}
}
///|
/// Verify a DSL asset against status, digest, tick, and coverage expectations.
pub fn verify_baseline(
source : String,
baseline : RegressionBaseline,
) -> Result[BaselineReport, BtError] {
match run_baseline_source(source, baseline.max_ticks) {
Ok(run) =>
Ok({
name: baseline.name,
expected_status: baseline.expected_status,
actual_status: run.status,
expected_digest: baseline.expected_digest,
actual_digest: run.digest,
expected_ticks: baseline.expected_ticks,
actual_ticks: run.ticks,
minimum_coverage: baseline.minimum_coverage,
actual_coverage: run.coverage,
status_matches: baseline.expected_status == run.status,
digest_matches: baseline.expected_digest == run.digest,
ticks_match: baseline.expected_ticks == run.ticks,
coverage_meets: run.coverage >= baseline.minimum_coverage,
})
Err(err) => Err(err)
}
}
///|
priv struct BaselineRun {
status : BtStatus
digest : String
ticks : Int
coverage : Int
}
///|
fn run_baseline_source(
source : String,
max_ticks : Int,
) -> Result[BaselineRun, BtError] {
match parse_dsl(source) {
Ok(doc) => {
let engine = new_engine(doc.tree, blackboard=doc.blackboard)
match engine.run_until_done(max_ticks~) {
Ok(result) => {
let coverage = coverage_from_trace(doc.tree, engine.trace)
Ok({
status: result.status,
digest: engine.trace_digest(),
ticks: result.tick,
coverage: coverage.percent(),
})
}
Err(err) => Err(err)
}
}
Err(err) => Err(err)
}
}
///|
fn bool_text(value : Bool) -> String {
if value {
"true"
} else {
"false"
}
}