///|
/// One named behavior tree asset and its acceptance expectation.
pub(all) struct AssetSpec {
name : String
source : String
expected_status : BtStatus
max_ticks : Int
} derive(Debug, Eq)
///|
pub fn asset_spec(
name : String,
source : String,
expected_status? : BtStatus,
max_ticks? : Int,
) -> AssetSpec {
{
name,
source,
expected_status: expected_status.unwrap_or(Success),
max_ticks: max_ticks.unwrap_or(32),
}
}
///|
/// Compile and execution evidence for one catalog asset.
pub(all) struct AssetReport {
name : String
parsed : Bool
lint_ok : Bool
run_ok : Bool
expected_status : BtStatus
actual_status : BtStatus
node_count : Int
blackboard_entries : Int
digest : String
coverage : Int
canonical_dsl : String
issues : Array[String]
} derive(Debug, Eq)
///|
pub fn AssetReport::ok(self : AssetReport) -> Bool {
self.parsed && self.lint_ok && self.run_ok && self.issues.length() == 0
}
///|
pub fn AssetReport::summary(self : AssetReport) -> String {
self.name +
": ok=" +
(if self.ok() { "true" } else { "false" }) +
", parsed=" +
bool_text(self.parsed) +
", lint=" +
bool_text(self.lint_ok) +
", run=" +
bool_text(self.run_ok) +
", nodes=" +
self.node_count.to_string() +
", coverage=" +
self.coverage.to_string() +
"%, digest=" +
self.digest
}
///|
pub fn AssetReport::lines(self : AssetReport) -> Array[String] {
let lines = Array::new()
lines.push(self.summary())
let mut i = 0
while i < self.issues.length() {
lines.push("issue: " + self.issues[i])
i = i + 1
}
lines
}
///|
/// Batch report that keeps valid and invalid assets in one deterministic order.
pub(all) struct AssetCatalogReport {
name : String
assets : Array[AssetReport]
duplicate_names : Array[String]
} derive(Debug, Eq)
///|
pub fn AssetCatalogReport::passed(self : AssetCatalogReport) -> Int {
let mut count = 0
let mut i = 0
while i < self.assets.length() {
if self.assets[i].ok() {
count = count + 1
}
i = i + 1
}
count
}
///|
pub fn AssetCatalogReport::failed(self : AssetCatalogReport) -> Int {
self.assets.length() - self.passed()
}
///|
pub fn AssetCatalogReport::ok(self : AssetCatalogReport) -> Bool {
self.failed() == 0 && self.duplicate_names.length() == 0
}
///|
pub fn AssetCatalogReport::summary(self : AssetCatalogReport) -> String {
self.name +
": ok=" +
bool_text(self.ok()) +
", assets=" +
self.assets.length().to_string() +
", passed=" +
self.passed().to_string() +
", failed=" +
self.failed().to_string() +
", duplicate_names=" +
self.duplicate_names.length().to_string()
}
///|
pub fn AssetCatalogReport::lines(self : AssetCatalogReport) -> Array[String] {
let lines = Array::new()
lines.push(self.summary())
if self.duplicate_names.length() > 0 {
lines.push("duplicates: " + join_strings(self.duplicate_names, ", "))
}
let mut i = 0
while i < self.assets.length() {
let details = self.assets[i].lines()
let mut j = 0
while j < details.length() {
lines.push(details[j])
j = j + 1
}
i = i + 1
}
lines
}
///|
pub fn AssetCatalogReport::markdown(self : AssetCatalogReport) -> String {
let lines = Array::new()
lines.push("# MoonBTKit Asset Catalog: " + self.name)
lines.push("")
lines.push("- " + self.summary())
if self.duplicate_names.length() > 0 {
lines.push("- duplicate names: " + join_strings(self.duplicate_names, ", "))
}
lines.push("")
lines.push("## Assets")
lines.push("")
let mut i = 0
while i < self.assets.length() {
lines.push("### " + self.assets[i].name)
lines.push("")
let details = self.assets[i].lines()
let mut j = 0
while j < details.length() {
lines.push("- " + details[j])
j = j + 1
}
i = i + 1
}
join_strings(lines, "\n")
}
///|
/// Compile, lint, execute, and measure one behavior tree asset.
pub fn audit_asset(spec : AssetSpec) -> AssetReport {
match parse_dsl(spec.source) {
Ok(doc) => {
let lint = lint_tree(doc.tree)
let engine = new_engine(doc.tree, blackboard=doc.blackboard)
match engine.run_until_done(max_ticks=spec.max_ticks) {
Ok(result) => {
let issues = Array::new()
if result.status != spec.expected_status {
issues.push(
"expected status " +
spec.expected_status.to_text() +
", got " +
result.status.to_text(),
)
}
if !lint.ok {
let lint_lines = lint.lines()
let mut i = 0
while i < lint_lines.length() {
issues.push("lint: " + lint_lines[i])
i = i + 1
}
}
let coverage = coverage_from_trace(doc.tree, engine.trace)
{
name: spec.name,
parsed: true,
lint_ok: lint.ok,
run_ok: result.status == spec.expected_status,
expected_status: spec.expected_status,
actual_status: result.status,
node_count: doc.tree.node_count(),
blackboard_entries: doc.blackboard.len(),
digest: engine.trace_digest(),
coverage: coverage.percent(),
canonical_dsl: canonicalize_document(doc),
issues,
}
}
Err(err) => failed_asset_report(spec, true, lint.ok, err.message())
}
}
Err(err) => failed_asset_report(spec, false, false, err.message())
}
}
///|
/// Audit every catalog entry without stopping after an invalid asset.
pub fn audit_asset_catalog(
name : String,
specs : Array[AssetSpec],
) -> AssetCatalogReport {
let reports = Array::new()
let mut i = 0
while i < specs.length() {
reports.push(audit_asset(specs[i]))
i = i + 1
}
{ name, assets: reports, duplicate_names: duplicate_asset_names(specs) }
}
///|
pub fn fixture_asset_specs() -> Array[AssetSpec] {
let fixtures = fixture_catalog()
let specs = Array::new()
let mut i = 0
while i < fixtures.length() {
specs.push(
asset_spec(
fixtures[i].name,
fixtures[i].dsl,
expected_status=fixtures[i].expected,
max_ticks=fixtures[i].max_ticks,
),
)
i = i + 1
}
specs
}
///|
pub fn audit_fixture_assets() -> AssetCatalogReport {
audit_asset_catalog("built-in-fixtures", fixture_asset_specs())
}
///|
fn failed_asset_report(
spec : AssetSpec,
parsed : Bool,
lint_ok : Bool,
issue : String,
) -> AssetReport {
{
name: spec.name,
parsed,
lint_ok,
run_ok: false,
expected_status: spec.expected_status,
actual_status: Failure,
node_count: 0,
blackboard_entries: 0,
digest: if parsed {
"run-error"
} else {
"parse-error"
},
coverage: 0,
canonical_dsl: "",
issues: [issue],
}
}
///|
fn duplicate_asset_names(specs : Array[AssetSpec]) -> Array[String] {
let duplicates = Array::new()
let mut i = 0
while i < specs.length() {
let mut j = 0
let mut seen_before = false
while j < i {
if specs[j].name == specs[i].name {
seen_before = true
}
j = j + 1
}
if seen_before && !contains_string(duplicates, specs[i].name) {
duplicates.push(specs[i].name)
}
i = i + 1
}
duplicates
}