rrt/crates/rrt-fixtures/src/load.rs

526 lines
21 KiB
Rust

use std::path::{Path, PathBuf};
use rrt_runtime::{
load_runtime_save_slice_document, load_runtime_snapshot_document, load_runtime_state_import,
project_save_slice_to_runtime_state_import, validate_runtime_save_slice_document,
validate_runtime_snapshot_document,
};
use crate::{FixtureDocument, FixtureStateOrigin, RawFixtureDocument};
pub fn load_fixture_document(path: &Path) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)?;
let base_dir = path.parent().unwrap_or_else(|| Path::new("."));
load_fixture_document_from_str_with_base(&text, base_dir)
}
pub fn load_fixture_document_from_str(
text: &str,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
load_fixture_document_from_str_with_base(text, Path::new("."))
}
pub fn load_fixture_document_from_str_with_base(
text: &str,
base_dir: &Path,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
let raw: RawFixtureDocument = serde_json::from_str(text)?;
resolve_raw_fixture_document(raw, base_dir)
}
fn resolve_raw_fixture_document(
raw: RawFixtureDocument,
base_dir: &Path,
) -> Result<FixtureDocument, Box<dyn std::error::Error>> {
let specified_state_inputs = usize::from(raw.state.is_some())
+ usize::from(raw.state_snapshot_path.is_some())
+ usize::from(raw.state_save_slice_path.is_some())
+ usize::from(raw.state_import_path.is_some());
if specified_state_inputs != 1 {
return Err(
"fixture must specify exactly one of inline state, state_snapshot_path, state_save_slice_path, or state_import_path"
.into(),
);
}
let state = match (
&raw.state,
&raw.state_snapshot_path,
&raw.state_save_slice_path,
&raw.state_import_path,
) {
(Some(state), None, None, None) => state.clone(),
(None, Some(snapshot_path), None, None) => {
let snapshot_path = resolve_snapshot_path(base_dir, snapshot_path);
let snapshot = load_runtime_snapshot_document(&snapshot_path)?;
validate_runtime_snapshot_document(&snapshot).map_err(|err| {
format!(
"invalid runtime snapshot {}: {err}",
snapshot_path.display()
)
})?;
snapshot.state
}
(None, None, Some(save_slice_path), None) => {
let save_slice_path = resolve_snapshot_path(base_dir, save_slice_path);
let document = load_runtime_save_slice_document(&save_slice_path)?;
validate_runtime_save_slice_document(&document).map_err(|err| {
format!(
"invalid runtime save slice document {}: {err}",
save_slice_path.display()
)
})?;
project_save_slice_to_runtime_state_import(
&document.save_slice,
&document.save_slice_id,
document.source.description.clone(),
)
.map_err(|err| {
format!(
"failed to project runtime save slice document {}: {err}",
save_slice_path.display()
)
})?
.state
}
(None, None, None, Some(import_path)) => {
let import_path = resolve_snapshot_path(base_dir, import_path);
load_runtime_state_import(&import_path)
.map_err(|err| {
format!(
"failed to load runtime import {}: {err}",
import_path.display()
)
})?
.state
}
_ => unreachable!("state input exclusivity checked above"),
};
let state_origin = match (
raw.state_snapshot_path,
raw.state_save_slice_path,
raw.state_import_path,
) {
(Some(snapshot_path), None, None) => FixtureStateOrigin::SnapshotPath(snapshot_path),
(None, Some(save_slice_path), None) => FixtureStateOrigin::SaveSlicePath(save_slice_path),
(None, None, Some(import_path)) => FixtureStateOrigin::ImportPath(import_path),
_ => FixtureStateOrigin::Inline,
};
Ok(FixtureDocument {
format_version: raw.format_version,
fixture_id: raw.fixture_id,
source: raw.source,
state,
state_origin,
commands: raw.commands,
expected_summary: raw.expected_summary,
expected_state_fragment: raw.expected_state_fragment,
})
}
fn resolve_snapshot_path(base_dir: &Path, snapshot_path: &str) -> PathBuf {
let candidate = PathBuf::from(snapshot_path);
if candidate.is_absolute() {
candidate
} else {
base_dir.join(candidate)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FixtureStateOrigin;
use rrt_runtime::{
CalendarPoint, OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION, RuntimeOverlayImportDocument,
RuntimeOverlayImportDocumentSource, RuntimeSaveProfileState, RuntimeSaveSliceDocument,
RuntimeSaveSliceDocumentSource, RuntimeServiceState, RuntimeSnapshotDocument,
RuntimeSnapshotSource, RuntimeState, RuntimeTrackPieceCounts, RuntimeWorldRestoreState,
SAVE_SLICE_DOCUMENT_FORMAT_VERSION, SNAPSHOT_FORMAT_VERSION,
save_runtime_overlay_import_document, save_runtime_save_slice_document,
save_runtime_snapshot_document,
};
use std::collections::BTreeMap;
#[test]
fn loads_fixture_from_relative_snapshot_path() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-load-{nonce}"));
std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");
let snapshot_path = fixture_dir.join("state.json");
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: "snapshot-backed-fixture-state".to_string(),
source: RuntimeSnapshotSource {
source_fixture_id: Some("snapshot-backed-fixture".to_string()),
description: Some("test snapshot".to_string()),
},
state: RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 5,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: Vec::new(),
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},
};
save_runtime_snapshot_document(&snapshot_path, &snapshot).expect("snapshot should save");
let fixture_json = r#"
{
"format_version": 1,
"fixture_id": "snapshot-backed-fixture",
"source": {
"kind": "captured-runtime"
},
"state_snapshot_path": "state.json",
"commands": [
{
"kind": "step_count",
"steps": 1
}
],
"expected_summary": {
"calendar": {
"year": 1830,
"month_slot": 0,
"phase_slot": 0,
"tick_slot": 6
},
"world_flag_count": 0,
"company_count": 0,
"event_runtime_record_count": 0,
"total_company_cash": 0
}
}
"#;
let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
.expect("snapshot-backed fixture should load");
assert_eq!(
fixture.state_origin,
FixtureStateOrigin::SnapshotPath("state.json".to_string())
);
assert_eq!(fixture.state.calendar.tick_slot, 5);
let _ = std::fs::remove_file(snapshot_path);
let _ = std::fs::remove_dir(fixture_dir);
}
#[test]
fn loads_fixture_from_relative_save_slice_path() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-save-slice-{nonce}"));
std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");
let save_slice_path = fixture_dir.join("state-save-slice.json");
let save_slice = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "save-slice-backed-fixture-state".to_string(),
source: RuntimeSaveSliceDocumentSource {
description: Some("test save slice".to_string()),
original_save_filename: Some("fixture.gms".to_string()),
original_save_sha256: None,
notes: vec![],
},
save_slice: rrt_runtime::SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
world_issue_37_state: None,
world_economic_tuning_state: None,
company_roster: None,
chairman_profile_table: None,
special_conditions_table: None,
event_runtime_collection: None,
notes: vec![],
},
};
save_runtime_save_slice_document(&save_slice_path, &save_slice)
.expect("save slice should save");
let fixture_json = r#"
{
"format_version": 1,
"fixture_id": "save-slice-backed-fixture",
"source": {
"kind": "captured-runtime"
},
"state_save_slice_path": "state-save-slice.json",
"commands": [],
"expected_summary": {
"calendar_projection_is_placeholder": true,
"packed_event_collection_present": false
}
}
"#;
let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
.expect("save-slice-backed fixture should load");
assert_eq!(
fixture.state_origin,
FixtureStateOrigin::SaveSlicePath("state-save-slice.json".to_string())
);
assert_eq!(
fixture
.state
.metadata
.get("save_slice.import_projection")
.map(String::as_str),
Some("partial-runtime-restore-v1")
);
let _ = std::fs::remove_file(save_slice_path);
let _ = std::fs::remove_dir(fixture_dir);
}
#[test]
fn loads_fixture_from_relative_import_path() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time should be after epoch")
.as_nanos();
let fixture_dir = std::env::temp_dir().join(format!("rrt-fixture-import-{nonce}"));
std::fs::create_dir_all(&fixture_dir).expect("fixture dir should be created");
let snapshot_path = fixture_dir.join("base.json");
let save_slice_path = fixture_dir.join("slice.json");
let import_path = fixture_dir.join("overlay.json");
let snapshot = RuntimeSnapshotDocument {
format_version: SNAPSHOT_FORMAT_VERSION,
snapshot_id: "base".to_string(),
source: RuntimeSnapshotSource::default(),
state: RuntimeState {
calendar: CalendarPoint {
year: 1830,
month_slot: 0,
phase_slot: 0,
tick_slot: 5,
},
world_flags: BTreeMap::new(),
save_profile: RuntimeSaveProfileState::default(),
world_restore: RuntimeWorldRestoreState::default(),
metadata: BTreeMap::new(),
companies: vec![rrt_runtime::RuntimeCompany {
company_id: 42,
controller_kind: rrt_runtime::RuntimeCompanyControllerKind::Human,
current_cash: 100,
debt: 0,
credit_rating_score: None,
prime_rate: None,
track_piece_counts: RuntimeTrackPieceCounts::default(),
active: true,
available_track_laying_capacity: None,
linked_chairman_profile_id: None,
book_value_per_share: 0,
investor_confidence: 0,
management_attitude: 0,
takeover_cooldown_year: None,
merger_cooldown_year: None,
}],
selected_company_id: Some(42),
players: Vec::new(),
selected_player_id: None,
chairman_profiles: Vec::new(),
selected_chairman_profile_id: None,
trains: Vec::new(),
locomotive_catalog: Vec::new(),
cargo_catalog: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
event_runtime_records: Vec::new(),
candidate_availability: BTreeMap::new(),
named_locomotive_availability: BTreeMap::new(),
named_locomotive_cost: BTreeMap::new(),
all_cargo_price_override: None,
named_cargo_price_overrides: BTreeMap::new(),
all_cargo_production_override: None,
factory_cargo_production_override: None,
farm_mine_cargo_production_override: None,
named_cargo_production_overrides: BTreeMap::new(),
cargo_production_overrides: BTreeMap::new(),
world_runtime_variables: BTreeMap::new(),
company_runtime_variables: BTreeMap::new(),
player_runtime_variables: BTreeMap::new(),
territory_runtime_variables: BTreeMap::new(),
world_scalar_overrides: BTreeMap::new(),
special_conditions: BTreeMap::new(),
service_state: RuntimeServiceState::default(),
},
};
save_runtime_snapshot_document(&snapshot_path, &snapshot).expect("snapshot should save");
let save_slice = RuntimeSaveSliceDocument {
format_version: SAVE_SLICE_DOCUMENT_FORMAT_VERSION,
save_slice_id: "slice".to_string(),
source: RuntimeSaveSliceDocumentSource::default(),
save_slice: rrt_runtime::SmpLoadedSaveSlice {
file_extension_hint: Some("gms".to_string()),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
trailer_family: None,
bridge_family: None,
profile: None,
candidate_availability_table: None,
named_locomotive_availability_table: None,
locomotive_catalog: None,
cargo_catalog: None,
world_issue_37_state: None,
world_economic_tuning_state: None,
company_roster: None,
chairman_profile_table: None,
special_conditions_table: None,
event_runtime_collection: Some(
rrt_runtime::SmpLoadedEventRuntimeCollectionSummary {
source_kind: "packed-event-runtime-collection".to_string(),
mechanism_family: "classic-save-rehydrate-v1".to_string(),
mechanism_confidence: "grounded".to_string(),
container_profile_family: Some("rt3-classic-save-container-v1".to_string()),
metadata_tag_offset: 0x7100,
records_tag_offset: 0x7200,
close_tag_offset: 0x7600,
packed_state_version: 0x3e9,
packed_state_version_hex: "0x000003e9".to_string(),
live_id_bound: 7,
live_record_count: 1,
live_entry_ids: vec![7],
decoded_record_count: 1,
imported_runtime_record_count: 0,
records: vec![rrt_runtime::SmpLoadedPackedEventRecordSummary {
record_index: 0,
live_entry_id: 7,
payload_offset: Some(0x7202),
payload_len: Some(48),
decode_status: "parity_only".to_string(),
payload_family: "synthetic_harness".to_string(),
trigger_kind: Some(7),
active: Some(true),
marks_collection_dirty: Some(false),
one_shot: Some(false),
compact_control: None,
text_bands: vec![],
standalone_condition_row_count: 0,
standalone_condition_rows: vec![],
negative_sentinel_scope: None,
grouped_effect_row_counts: vec![0, 0, 0, 0],
grouped_effect_rows: vec![],
decoded_conditions: Vec::new(),
decoded_actions: vec![rrt_runtime::RuntimeEffect::AdjustCompanyCash {
target: rrt_runtime::RuntimeCompanyTarget::Ids { ids: vec![42] },
delta: 25,
}],
executable_import_ready: false,
notes: vec![],
}],
},
),
notes: vec![],
},
};
save_runtime_save_slice_document(&save_slice_path, &save_slice)
.expect("save slice should save");
let overlay = RuntimeOverlayImportDocument {
format_version: OVERLAY_IMPORT_DOCUMENT_FORMAT_VERSION,
import_id: "overlay-backed-fixture-state".to_string(),
source: RuntimeOverlayImportDocumentSource::default(),
base_snapshot_path: "base.json".to_string(),
save_slice_path: "slice.json".to_string(),
};
save_runtime_overlay_import_document(&import_path, &overlay)
.expect("overlay import should save");
let fixture_json = r#"
{
"format_version": 1,
"fixture_id": "overlay-backed-fixture",
"source": {
"kind": "captured-runtime"
},
"state_import_path": "overlay.json",
"commands": [
{
"kind": "service_trigger_kind",
"trigger_kind": 7
}
],
"expected_summary": {
"company_count": 1,
"packed_event_imported_runtime_record_count": 1
}
}
"#;
let fixture = load_fixture_document_from_str_with_base(fixture_json, &fixture_dir)
.expect("overlay-backed fixture should load");
assert_eq!(
fixture.state_origin,
FixtureStateOrigin::ImportPath("overlay.json".to_string())
);
assert_eq!(fixture.state.event_runtime_records.len(), 1);
let _ = std::fs::remove_file(snapshot_path);
let _ = std::fs::remove_file(save_slice_path);
let _ = std::fs::remove_file(import_path);
let _ = std::fs::remove_dir(fixture_dir);
}
}