///|
pub(all) struct BtAssertion {
name : String
ok : Bool
detail : String
} derive(Debug, Eq)
///|
pub fn BtAssertion::to_line(self : BtAssertion) -> String {
(if self.ok { "ok " } else { "fail " }) + self.name + ": " + self.detail
}
///|
pub(all) struct AssertionReport {
name : String
assertions : Array[BtAssertion]
} derive(Debug, Eq)
///|
pub fn AssertionReport::ok(self : AssertionReport) -> Bool {
let mut i = 0
while i < self.assertions.length() {
if !self.assertions[i].ok {
return false
}
i = i + 1
}
true
}
///|
pub fn AssertionReport::failed_count(self : AssertionReport) -> Int {
let mut count = 0
let mut i = 0
while i < self.assertions.length() {
if !self.assertions[i].ok {
count = count + 1
}
i = i + 1
}
count
}
///|
pub fn AssertionReport::summary(self : AssertionReport) -> String {
self.name +
": assertions=" +
self.assertions.length().to_string() +
", failed=" +
self.failed_count().to_string()
}
///|
pub fn AssertionReport::lines(self : AssertionReport) -> Array[String] {
let lines = Array::new()
lines.push(self.summary())
let mut i = 0
while i < self.assertions.length() {
lines.push(self.assertions[i].to_line())
i = i + 1
}
lines
}
///|
pub fn new_assertion_report(name : String) -> AssertionReport {
{ name, assertions: Array::new() }
}
///|
pub fn AssertionReport::add(self : AssertionReport, assertion : BtAssertion) -> Unit {
self.assertions.push(assertion)
}
///|
pub fn assert_true(name : String, condition : Bool, detail : String) -> BtAssertion {
{ name, ok: condition, detail }
}
///|
pub fn assert_tree_valid(tree : BehaviorTree) -> BtAssertion {
let report = tree.validate()
{ name: "tree valid", ok: report.ok, detail: report.message() }
}
///|
pub fn assert_node_count(tree : BehaviorTree, expected : Int) -> BtAssertion {
let actual = tree.node_count()
{
name: "node count",
ok: actual == expected,
detail: "expected " + expected.to_string() + ", got " + actual.to_string(),
}
}
///|
pub fn assert_has_node(tree : BehaviorTree, id : String) -> BtAssertion {
{
name: "has node " + id,
ok: tree.has_node(id),
detail: if tree.has_node(id) { "present" } else { "missing" },
}
}
///|
pub fn assert_status(actual : BtStatus, expected : BtStatus) -> BtAssertion {
{
name: "status",
ok: actual == expected,
detail: "expected " + expected.to_text() + ", got " + actual.to_text(),
}
}
///|
pub fn assert_board_value(
board : Array[(String, BtValue)],
key : String,
value : BtValue,
) -> BtAssertion {
{
name: "board " + key,
ok: board_contains(board, key, value),
detail: key + "=" + value.to_text(),
}
}
///|
pub fn assert_trace_has(
events : Array[TickEvent],
node_id : String,
status : BtStatus,
) -> BtAssertion {
{
name: "trace " + node_id,
ok: trace_contains(events, node_id, status),
detail: "expected " + node_id + " -> " + status.to_text(),
}
}
///|
pub fn assert_lint_ok(tree : BehaviorTree) -> BtAssertion {
let lint = lint_tree(tree)
{ name: "lint", ok: lint.ok, detail: lint.summary() }
}
///|
pub fn combine_assertion_reports(
name : String,
reports : Array[AssertionReport],
) -> AssertionReport {
let out = new_assertion_report(name)
let mut r = 0
while r < reports.length() {
let mut i = 0
while i < reports[r].assertions.length() {
out.add(reports[r].assertions[i])
i = i + 1
}
r = r + 1
}
out
}
///|
pub(all) struct CaseExpectation {
final_status : BtStatus
required_board : Array[(String, BtValue)]
max_ticks : Int
} derive(Debug, Eq)
///|
pub(all) struct CaseSpec {
name : String
dsl : String
expectation : CaseExpectation
} derive(Debug, Eq)
///|
pub(all) struct CaseReport {
name : String
parsed : Bool
run : RunSummary
assertions : AssertionReport
} derive(Debug, Eq)
///|
pub fn CaseReport::ok(self : CaseReport) -> Bool {
self.parsed && self.run.ok && self.assertions.ok()
}
///|
pub fn CaseReport::to_line(self : CaseReport) -> String {
self.name +
": parsed=" +
(if self.parsed { "true" } else { "false" }) +
", run_ok=" +
(if self.run.ok { "true" } else { "false" }) +
", assertion_failed=" +
self.assertions.failed_count().to_string()
}
///|
pub fn case_from_fixture(spec : FixtureSpec) -> CaseSpec {
{
name: spec.name,
dsl: spec.dsl,
expectation: {
final_status: spec.expected,
required_board: [],
max_ticks: spec.max_ticks,
},
}
}
///|
pub fn fixture_cases() -> Array[CaseSpec] {
let specs = fixture_catalog()
let cases = Array::new()
let mut i = 0
while i < specs.length() {
cases.push(case_from_fixture(specs[i]))
i = i + 1
}
cases
}
///|
pub fn run_case(spec : CaseSpec) -> CaseReport {
match parse_dsl(spec.dsl) {
Ok(doc) => {
let engine = new_engine(doc.tree, blackboard=doc.blackboard)
match engine.run_until_done(max_ticks=spec.expectation.max_ticks) {
Ok(result) => {
let assertions = new_assertion_report(spec.name)
assertions.add(assert_tree_valid(doc.tree))
assertions.add(assert_status(result.status, spec.expectation.final_status))
let mut i = 0
while i < spec.expectation.required_board.length() {
assertions.add(
assert_board_value(
result.blackboard,
spec.expectation.required_board[i].0,
spec.expectation.required_board[i].1,
),
)
i = i + 1
}
{
name: spec.name,
parsed: true,
run: {
name: spec.name,
final_status: result.status,
ticks: result.tick,
trace_events: engine.trace.length(),
digest: engine.trace_digest(),
ok: result.status == spec.expectation.final_status,
detail: "case executed",
},
assertions,
}
}
Err(err) => {
let assertions = new_assertion_report(spec.name)
assertions.add(assert_true("run", false, err.message()))
{
name: spec.name,
parsed: true,
run: {
name: spec.name,
final_status: Failure,
ticks: engine.tick_count,
trace_events: engine.trace.length(),
digest: engine.trace_digest(),
ok: false,
detail: err.message(),
},
assertions,
}
}
}
}
Err(err) => {
let assertions = new_assertion_report(spec.name)
assertions.add(assert_true("parse", false, err.message()))
{
name: spec.name,
parsed: false,
run: {
name: spec.name,
final_status: Failure,
ticks: 0,
trace_events: 0,
digest: "parse-error",
ok: false,
detail: err.message(),
},
assertions,
}
}
}
}
///|
pub fn run_cases(cases : Array[CaseSpec]) -> Array[CaseReport] {
let reports = Array::new()
let mut i = 0
while i < cases.length() {
reports.push(run_case(cases[i]))
i = i + 1
}
reports
}
///|
pub fn all_cases_ok(reports : Array[CaseReport]) -> Bool {
let mut i = 0
while i < reports.length() {
if !reports[i].ok() {
return false
}
i = i + 1
}
true
}
///|
pub fn case_report_lines(reports : Array[CaseReport]) -> Array[String] {
let lines = Array::new()
let mut failed = 0
let mut i = 0
while i < reports.length() {
if !reports[i].ok() {
failed = failed + 1
}
lines.push(reports[i].to_line())
i = i + 1
}
lines.push(
"cases=" +
reports.length().to_string() +
", failed=" +
failed.to_string(),
)
lines
}
///|
pub fn replace_blackboard_line(
source : String,
key : String,
value : BtValue,
) -> String {
let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
let out = Array::new()
let mut replaced = false
let mut i = 0
while i < lines.length() {
let line = lines[i].to_owned()
let tokens = split_ascii_words(trim_ascii(strip_comment(line)))
if tokens.length() == 3 && tokens[0] == "blackboard" && tokens[1] == key {
out.push("blackboard " + key + " " + value.to_text())
replaced = true
} else {
out.push(line)
}
i = i + 1
}
if !replaced {
out.push("blackboard " + key + " " + value.to_text())
}
join_strings(out, "\n")
}
///|
pub fn boolean_variants(
base_name : String,
source : String,
key : String,
expected_true : BtStatus,
expected_false : BtStatus,
max_ticks : Int,
) -> Array[CaseSpec] {
[
{
name: base_name + "_true",
dsl: replace_blackboard_line(source, key, BoolValue(true)),
expectation: { final_status: expected_true, required_board: [], max_ticks },
},
{
name: base_name + "_false",
dsl: replace_blackboard_line(source, key, BoolValue(false)),
expectation: { final_status: expected_false, required_board: [], max_ticks },
},
]
}