Add headless runtime tooling and Campaign.win analysis
This commit is contained in:
parent
57bf0666e0
commit
27172e3786
37 changed files with 11867 additions and 302 deletions
277
crates/rrt-fixtures/src/schema.rs
Normal file
277
crates/rrt-fixtures/src/schema.rs
Normal 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"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue