Expand runtime event graph service

This commit is contained in:
Jan Petykiewicz 2026-04-14 19:37:53 -07:00
commit 6ebe5fffeb
14 changed files with 1803 additions and 254 deletions

View file

@ -4,7 +4,10 @@ use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use rrt_fixtures::{FixtureValidationReport, load_fixture_document, validate_fixture_document};
use rrt_fixtures::{
FixtureValidationReport, JsonDiffEntry, compare_expected_state_fragment, diff_json_values,
load_fixture_document, normalize_runtime_state, validate_fixture_document,
};
use rrt_model::{
BINARY_SUMMARY_PATH, CANONICAL_EXE_PATH, CONTROL_LOOP_ATLAS_PATH, FUNCTION_MAP_PATH,
REQUIRED_ATLAS_HEADINGS, REQUIRED_EXPORTS,
@ -95,6 +98,10 @@ enum Command {
fixture_path: PathBuf,
output_path: PathBuf,
},
RuntimeDiffState {
left_path: PathBuf,
right_path: PathBuf,
},
RuntimeSummarizeState {
snapshot_path: PathBuf,
},
@ -195,6 +202,8 @@ struct RuntimeFixtureSummaryReport {
final_summary: RuntimeSummary,
expected_summary_matches: bool,
expected_summary_mismatches: Vec<String>,
expected_state_fragment_matches: bool,
expected_state_fragment_mismatches: Vec<String>,
}
#[derive(Debug, Serialize)]
@ -203,6 +212,13 @@ struct RuntimeStateSummaryReport {
summary: RuntimeSummary,
}
#[derive(Debug, Serialize)]
struct RuntimeStateDiffReport {
matches: bool,
difference_count: usize,
differences: Vec<JsonDiffEntry>,
}
#[derive(Debug, Serialize)]
struct RuntimeSmpInspectionOutput {
path: String,
@ -724,6 +740,12 @@ fn real_main() -> Result<(), Box<dyn std::error::Error>> {
} => {
run_runtime_export_fixture_state(&fixture_path, &output_path)?;
}
Command::RuntimeDiffState {
left_path,
right_path,
} => {
run_runtime_diff_state(&left_path, &right_path)?;
}
Command::RuntimeSummarizeState { snapshot_path } => {
run_runtime_summarize_state(&snapshot_path)?;
}
@ -859,6 +881,14 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
output_path: PathBuf::from(output_path),
})
}
[command, subcommand, left_path, right_path]
if command == "runtime" && subcommand == "diff-state" =>
{
Ok(Command::RuntimeDiffState {
left_path: PathBuf::from(left_path),
right_path: PathBuf::from(right_path),
})
}
[command, subcommand, path] if command == "runtime" && subcommand == "summarize-state" => {
Ok(Command::RuntimeSummarizeState {
snapshot_path: PathBuf::from(path),
@ -1039,7 +1069,7 @@ fn parse_command() -> Result<Command, Box<dyn std::error::Error>> {
})
}
_ => Err(
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
"usage: rrt-cli [validate [repo-root] | finance eval <snapshot.json> | finance diff <left.json> <right.json> | runtime validate-fixture <fixture.json> | runtime summarize-fixture <fixture.json> | runtime export-fixture-state <fixture.json> <snapshot.json> | runtime diff-state <left.json> <right.json> | runtime summarize-state <snapshot.json> | runtime import-state <input.json> <snapshot.json> | runtime inspect-smp <file.smp> | runtime summarize-save-load <file.smp> | runtime load-save-slice <file.smp> | runtime import-save-state <file.smp> <snapshot.json> | runtime inspect-pk4 <file.pk4> | runtime inspect-win <file.win> | runtime extract-pk4-entry <file.pk4> <entry-name> <output-path> | runtime inspect-campaign-exe <RT3.exe> | runtime compare-classic-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-105-profile <save1.gms> <save2.gms> [saveN.gms...] | runtime compare-candidate-table <file1> <file2> [fileN...] | runtime compare-recipe-book-lines <file1> <file2> [fileN...] | runtime compare-setup-payload-core <file1> <file2> [fileN...] | runtime compare-setup-launch-payload <file1> <file2> [fileN...] | runtime compare-post-special-conditions-scalars <file1> <file2> [fileN...] | runtime scan-candidate-table-headers <root-dir> | runtime scan-special-conditions <root-dir> | runtime scan-aligned-runtime-rule-band <root-dir> | runtime scan-post-special-conditions-scalars <root-dir> | runtime scan-post-special-conditions-tail <root-dir> | runtime scan-recipe-book-lines <root-dir> | runtime export-profile-block <save.gms> <profile.json>]"
.into(),
),
}
@ -1083,20 +1113,31 @@ fn run_runtime_summarize_fixture(fixture_path: &Path) -> Result<(), Box<dyn std:
}
let final_summary = RuntimeSummary::from_state(&state);
let mismatches = fixture.expected_summary.compare(&final_summary);
let expected_summary_mismatches = fixture.expected_summary.compare(&final_summary);
let expected_state_fragment_mismatches = match &fixture.expected_state_fragment {
Some(expected_fragment) => {
let normalized_state = normalize_runtime_state(&state)?;
compare_expected_state_fragment(expected_fragment, &normalized_state)
}
None => Vec::new(),
};
let report = RuntimeFixtureSummaryReport {
fixture_id: fixture.fixture_id,
command_count: fixture.commands.len(),
expected_summary_matches: mismatches.is_empty(),
expected_summary_mismatches: mismatches.clone(),
expected_summary_matches: expected_summary_mismatches.is_empty(),
expected_summary_mismatches: expected_summary_mismatches.clone(),
expected_state_fragment_matches: expected_state_fragment_mismatches.is_empty(),
expected_state_fragment_mismatches: expected_state_fragment_mismatches.clone(),
final_summary,
};
println!("{}", serde_json::to_string_pretty(&report)?);
if !mismatches.is_empty() {
if !expected_summary_mismatches.is_empty() || !expected_state_fragment_mismatches.is_empty() {
let mut mismatch_messages = expected_summary_mismatches;
mismatch_messages.extend(expected_state_fragment_mismatches);
return Err(format!(
"fixture summary mismatched expected output: {}",
mismatches.join("; ")
mismatch_messages.join("; ")
)
.into());
}
@ -1159,6 +1200,33 @@ fn run_runtime_summarize_state(snapshot_path: &Path) -> Result<(), Box<dyn std::
Ok(())
}
fn run_runtime_diff_state(
left_path: &Path,
right_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let left = load_normalized_runtime_state(left_path)?;
let right = load_normalized_runtime_state(right_path)?;
let differences = diff_json_values(&left, &right);
let report = RuntimeStateDiffReport {
matches: differences.is_empty(),
difference_count: differences.len(),
differences,
};
println!("{}", serde_json::to_string_pretty(&report)?);
Ok(())
}
fn load_normalized_runtime_state(path: &Path) -> Result<Value, Box<dyn std::error::Error>> {
if let Ok(snapshot) = load_runtime_snapshot_document(path) {
validate_runtime_snapshot_document(&snapshot)
.map_err(|err| format!("invalid runtime snapshot: {err}"))?;
return normalize_runtime_state(&snapshot.state);
}
let import = load_runtime_state_import(path)?;
normalize_runtime_state(&import.state)
}
fn run_runtime_import_state(
input_path: &Path,
output_path: &Path,
@ -3834,6 +3902,14 @@ mod tests {
"company_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
},
"expected_state_fragment": {
"calendar": {
"tick_slot": 3
},
"world_flags": {
"sandbox": false
}
}
});
let path = write_temp_json("runtime-fixture", &fixture);
@ -3938,6 +4014,121 @@ mod tests {
let _ = fs::remove_file(output_path);
}
#[test]
fn diffs_runtime_states_recursively() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": []
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 2
},
"world_flags": {
"sandbox": true
},
"companies": []
}
});
let left_path = write_temp_json("runtime-diff-left", &left);
let right_path = write_temp_json("runtime-diff-right", &right);
run_runtime_diff_state(&left_path, &right_path).expect("runtime diff should succeed");
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_runtime_states_with_event_record_additions_and_removals() {
let left = serde_json::json!({
"format_version": 1,
"snapshot_id": "left-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": [
{
"record_id": 1,
"trigger_kind": 7,
"active": true
},
{
"record_id": 2,
"trigger_kind": 7,
"active": false
}
]
}
});
let right = serde_json::json!({
"format_version": 1,
"snapshot_id": "right-events",
"state": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 1
},
"world_flags": {
"sandbox": false
},
"companies": [],
"event_runtime_records": [
{
"record_id": 1,
"trigger_kind": 7,
"active": true
}
]
}
});
let left_path = write_temp_json("runtime-diff-events-left", &left);
let right_path = write_temp_json("runtime-diff-events-right", &right);
let left_state =
load_normalized_runtime_state(&left_path).expect("left runtime state should load");
let right_state =
load_normalized_runtime_state(&right_path).expect("right runtime state should load");
let differences = diff_json_values(&left_state, &right_state);
assert!(
differences
.iter()
.any(|entry| entry.path == "$.event_runtime_records[1]")
);
let _ = fs::remove_file(left_path);
let _ = fs::remove_file(right_path);
}
#[test]
fn diffs_classic_profile_samples_across_multiple_files() {
let sample_a = RuntimeClassicProfileSample {