526 lines
21 KiB
Rust
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);
|
|
}
|
|
}
|