Add headless runtime tooling and Campaign.win analysis

This commit is contained in:
Jan Petykiewicz 2026-04-10 01:22:47 -07:00
commit 27172e3786
37 changed files with 11867 additions and 302 deletions

View file

@ -0,0 +1,277 @@
use serde::{Deserialize, Serialize};
use rrt_runtime::{RuntimeState, RuntimeSummary, StepCommand};
pub const FIXTURE_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct FixtureSource {
pub kind: String,
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct ExpectedRuntimeSummary {
#[serde(default)]
pub calendar: Option<rrt_runtime::CalendarPoint>,
#[serde(default)]
pub world_flag_count: Option<usize>,
#[serde(default)]
pub company_count: Option<usize>,
#[serde(default)]
pub event_runtime_record_count: Option<usize>,
#[serde(default)]
pub total_event_record_service_count: Option<u64>,
#[serde(default)]
pub periodic_boundary_call_count: Option<u64>,
#[serde(default)]
pub total_trigger_dispatch_count: Option<u64>,
#[serde(default)]
pub dirty_rerun_count: Option<u64>,
#[serde(default)]
pub total_company_cash: Option<i64>,
}
impl ExpectedRuntimeSummary {
pub fn compare(&self, actual: &RuntimeSummary) -> Vec<String> {
let mut mismatches = Vec::new();
if let Some(calendar) = self.calendar {
if actual.calendar != calendar {
mismatches.push(format!(
"calendar mismatch: expected {:?}, got {:?}",
calendar, actual.calendar
));
}
}
if let Some(count) = self.world_flag_count {
if actual.world_flag_count != count {
mismatches.push(format!(
"world_flag_count mismatch: expected {count}, got {}",
actual.world_flag_count
));
}
}
if let Some(count) = self.company_count {
if actual.company_count != count {
mismatches.push(format!(
"company_count mismatch: expected {count}, got {}",
actual.company_count
));
}
}
if let Some(count) = self.event_runtime_record_count {
if actual.event_runtime_record_count != count {
mismatches.push(format!(
"event_runtime_record_count mismatch: expected {count}, got {}",
actual.event_runtime_record_count
));
}
}
if let Some(count) = self.total_event_record_service_count {
if actual.total_event_record_service_count != count {
mismatches.push(format!(
"total_event_record_service_count mismatch: expected {count}, got {}",
actual.total_event_record_service_count
));
}
}
if let Some(count) = self.periodic_boundary_call_count {
if actual.periodic_boundary_call_count != count {
mismatches.push(format!(
"periodic_boundary_call_count mismatch: expected {count}, got {}",
actual.periodic_boundary_call_count
));
}
}
if let Some(count) = self.total_trigger_dispatch_count {
if actual.total_trigger_dispatch_count != count {
mismatches.push(format!(
"total_trigger_dispatch_count mismatch: expected {count}, got {}",
actual.total_trigger_dispatch_count
));
}
}
if let Some(count) = self.dirty_rerun_count {
if actual.dirty_rerun_count != count {
mismatches.push(format!(
"dirty_rerun_count mismatch: expected {count}, got {}",
actual.dirty_rerun_count
));
}
}
if let Some(total) = self.total_company_cash {
if actual.total_company_cash != total {
mismatches.push(format!(
"total_company_cash mismatch: expected {total}, got {}",
actual.total_company_cash
));
}
}
mismatches
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureDocument {
pub format_version: u32,
pub fixture_id: String,
#[serde(default)]
pub source: FixtureSource,
pub state: RuntimeState,
pub state_origin: FixtureStateOrigin,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FixtureStateOrigin {
Inline,
SnapshotPath(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RawFixtureDocument {
pub format_version: u32,
pub fixture_id: String,
#[serde(default)]
pub source: FixtureSource,
#[serde(default)]
pub state: Option<RuntimeState>,
#[serde(default)]
pub state_snapshot_path: Option<String>,
#[serde(default)]
pub commands: Vec<StepCommand>,
#[serde(default)]
pub expected_summary: ExpectedRuntimeSummary,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FixtureValidationReport {
pub fixture_id: String,
pub valid: bool,
pub issue_count: usize,
pub issues: Vec<String>,
}
pub fn validate_fixture_document(document: &FixtureDocument) -> FixtureValidationReport {
let mut issues = Vec::new();
if document.format_version != FIXTURE_FORMAT_VERSION {
issues.push(format!(
"unsupported format_version {} (expected {})",
document.format_version, FIXTURE_FORMAT_VERSION
));
}
if document.fixture_id.trim().is_empty() {
issues.push("fixture_id must not be empty".to_string());
}
if document.source.kind.trim().is_empty() {
issues.push("source.kind must not be empty".to_string());
}
if document.commands.is_empty() {
issues.push("fixture must contain at least one command".to_string());
}
if let Err(err) = document.state.validate() {
issues.push(format!("invalid runtime state: {err}"));
}
for (index, command) in document.commands.iter().enumerate() {
if let Err(err) = command.validate() {
issues.push(format!("invalid command at index {index}: {err}"));
}
}
FixtureValidationReport {
fixture_id: document.fixture_id.clone(),
valid: issues.is_empty(),
issue_count: issues.len(),
issues,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::load_fixture_document_from_str;
const FIXTURE_JSON: &str = r#"
{
"format_version": 1,
"fixture_id": "minimal-world-step-smoke",
"source": {
"kind": "synthetic",
"description": "basic milestone parser smoke fixture"
},
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 0
},
"world_flags": {
"sandbox": false
},
"companies": [
{
"company_id": 1,
"current_cash": 250000,
"debt": 0
}
],
"event_runtime_records": [],
"service_state": {
"periodic_boundary_calls": 0,
"trigger_dispatch_counts": {},
"total_event_record_services": 0,
"dirty_rerun_count": 0
}
},
"commands": [
{
"kind": "advance_to",
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
}
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flag_count": 1,
"company_count": 1,
"event_runtime_record_count": 0,
"total_company_cash": 250000
}
}
"#;
#[test]
fn parses_and_validates_fixture() {
let fixture = load_fixture_document_from_str(FIXTURE_JSON).expect("fixture should parse");
let report = validate_fixture_document(&fixture);
assert!(report.valid, "report should be valid: {:?}", report.issues);
assert_eq!(fixture.state_origin, FixtureStateOrigin::Inline);
}
#[test]
fn compares_expected_summary() {
let fixture = load_fixture_document_from_str(FIXTURE_JSON).expect("fixture should parse");
let summary = RuntimeSummary::from_state(&fixture.state);
let mismatches = fixture.expected_summary.compare(&summary);
assert_eq!(mismatches.len(), 1);
assert!(mismatches[0].contains("calendar mismatch"));
}
}