use serde::{Deserialize, Serialize}; use serde_json::Value; 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, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct ExpectedRuntimeSummary { #[serde(default)] pub calendar: Option, #[serde(default)] pub calendar_projection_source: Option, #[serde(default)] pub calendar_projection_is_placeholder: Option, #[serde(default)] pub world_flag_count: Option, #[serde(default)] pub world_restore_selected_year_profile_lane: Option, #[serde(default)] pub world_restore_campaign_scenario_enabled: Option, #[serde(default)] pub world_restore_sandbox_enabled: Option, #[serde(default)] pub world_restore_seed_tuple_written_from_raw_lane: Option, #[serde(default)] pub world_restore_absolute_counter_requires_shell_context: Option, #[serde(default)] pub world_restore_absolute_counter_reconstructible_from_save: Option, #[serde(default)] pub world_restore_disable_cargo_economy_special_condition_slot: Option, #[serde(default)] pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save: Option, #[serde(default)] pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option, #[serde(default)] pub world_restore_disable_cargo_economy_special_condition_enabled: Option, #[serde(default)] pub world_restore_use_bio_accelerator_cars_enabled: Option, #[serde(default)] pub world_restore_use_wartime_cargos_enabled: Option, #[serde(default)] pub world_restore_disable_train_crashes_enabled: Option, #[serde(default)] pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option, #[serde(default)] pub world_restore_ai_ignore_territories_at_startup_enabled: Option, #[serde(default)] pub world_restore_limited_track_building_amount: Option, #[serde(default)] pub world_restore_economic_status_code: Option, #[serde(default)] pub world_restore_territory_access_cost: Option, #[serde(default)] pub world_restore_absolute_counter_restore_kind: Option, #[serde(default)] pub world_restore_absolute_counter_adjustment_context: Option, #[serde(default)] pub metadata_count: Option, #[serde(default)] pub company_count: Option, #[serde(default)] pub active_company_count: Option, #[serde(default)] pub player_count: Option, #[serde(default)] pub chairman_profile_count: Option, #[serde(default)] pub active_chairman_profile_count: Option, #[serde(default)] pub selected_chairman_profile_id: Option, #[serde(default)] pub linked_chairman_company_count: Option, #[serde(default)] pub company_takeover_cooldown_count: Option, #[serde(default)] pub company_merger_cooldown_count: Option, #[serde(default)] pub train_count: Option, #[serde(default)] pub active_train_count: Option, #[serde(default)] pub retired_train_count: Option, #[serde(default)] pub locomotive_catalog_count: Option, #[serde(default)] pub cargo_catalog_count: Option, #[serde(default)] pub territory_count: Option, #[serde(default)] pub company_territory_track_count: Option, #[serde(default)] pub packed_event_collection_present: Option, #[serde(default)] pub packed_event_record_count: Option, #[serde(default)] pub packed_event_decoded_record_count: Option, #[serde(default)] pub packed_event_imported_runtime_record_count: Option, #[serde(default)] pub packed_event_parity_only_record_count: Option, #[serde(default)] pub packed_event_unsupported_record_count: Option, #[serde(default)] pub packed_event_blocked_missing_company_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_selection_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_company_role_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_player_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_player_selection_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_player_role_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_chairman_context_count: Option, #[serde(default)] pub packed_event_blocked_chairman_target_scope_count: Option, #[serde(default)] pub packed_event_blocked_missing_condition_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_player_condition_context_count: Option, #[serde(default)] pub packed_event_blocked_company_condition_scope_disabled_count: Option, #[serde(default)] pub packed_event_blocked_player_condition_scope_count: Option, #[serde(default)] pub packed_event_blocked_territory_condition_scope_count: Option, #[serde(default)] pub packed_event_blocked_missing_territory_context_count: Option, #[serde(default)] pub packed_event_blocked_named_territory_binding_count: Option, #[serde(default)] pub packed_event_blocked_unmapped_ordinary_condition_count: Option, #[serde(default)] pub packed_event_blocked_unmapped_world_condition_count: Option, #[serde(default)] pub packed_event_blocked_missing_compact_control_count: Option, #[serde(default)] pub packed_event_blocked_shell_owned_descriptor_count: Option, #[serde(default)] pub packed_event_blocked_evidence_blocked_descriptor_count: Option, #[serde(default)] pub packed_event_blocked_variant_or_scope_blocked_descriptor_count: Option, #[serde(default)] pub packed_event_blocked_unmapped_real_descriptor_count: Option, #[serde(default)] pub packed_event_blocked_unmapped_world_descriptor_count: Option, #[serde(default)] pub packed_event_blocked_territory_access_variant_count: Option, #[serde(default)] pub packed_event_blocked_territory_access_scope_count: Option, #[serde(default)] pub packed_event_blocked_missing_train_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_train_territory_context_count: Option, #[serde(default)] pub packed_event_blocked_missing_locomotive_catalog_context_count: Option, #[serde(default)] pub packed_event_blocked_confiscation_variant_count: Option, #[serde(default)] pub packed_event_blocked_retire_train_variant_count: Option, #[serde(default)] pub packed_event_blocked_retire_train_scope_count: Option, #[serde(default)] pub packed_event_blocked_structural_only_count: Option, #[serde(default)] pub event_runtime_record_count: Option, #[serde(default)] pub candidate_availability_count: Option, #[serde(default)] pub zero_candidate_availability_count: Option, #[serde(default)] pub named_locomotive_availability_count: Option, #[serde(default)] pub zero_named_locomotive_availability_count: Option, #[serde(default)] pub named_locomotive_cost_count: Option, #[serde(default)] pub cargo_production_override_count: Option, #[serde(default)] pub world_scalar_override_count: Option, #[serde(default)] pub special_condition_count: Option, #[serde(default)] pub enabled_special_condition_count: Option, #[serde(default)] pub save_profile_kind: Option, #[serde(default)] pub save_profile_family: Option, #[serde(default)] pub save_profile_map_path: Option, #[serde(default)] pub save_profile_display_name: Option, #[serde(default)] pub save_profile_selected_year_profile_lane: Option, #[serde(default)] pub save_profile_sandbox_enabled: Option, #[serde(default)] pub save_profile_campaign_scenario_enabled: Option, #[serde(default)] pub save_profile_staged_profile_copy_on_restore: Option, #[serde(default)] pub total_event_record_service_count: Option, #[serde(default)] pub periodic_boundary_call_count: Option, #[serde(default)] pub total_trigger_dispatch_count: Option, #[serde(default)] pub dirty_rerun_count: Option, #[serde(default)] pub total_company_cash: Option, } impl ExpectedRuntimeSummary { pub fn compare(&self, actual: &RuntimeSummary) -> Vec { 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(source) = &self.calendar_projection_source { if actual.calendar_projection_source.as_ref() != Some(source) { mismatches.push(format!( "calendar_projection_source mismatch: expected {source:?}, got {:?}", actual.calendar_projection_source )); } } if let Some(is_placeholder) = self.calendar_projection_is_placeholder { if actual.calendar_projection_is_placeholder != is_placeholder { mismatches.push(format!( "calendar_projection_is_placeholder mismatch: expected {is_placeholder}, got {}", actual.calendar_projection_is_placeholder )); } } 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(lane) = self.world_restore_selected_year_profile_lane { if actual.world_restore_selected_year_profile_lane != Some(lane) { mismatches.push(format!( "world_restore_selected_year_profile_lane mismatch: expected {lane}, got {:?}", actual.world_restore_selected_year_profile_lane )); } } if let Some(enabled) = self.world_restore_campaign_scenario_enabled { if actual.world_restore_campaign_scenario_enabled != Some(enabled) { mismatches.push(format!( "world_restore_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_campaign_scenario_enabled )); } } if let Some(enabled) = self.world_restore_sandbox_enabled { if actual.world_restore_sandbox_enabled != Some(enabled) { mismatches.push(format!( "world_restore_sandbox_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_sandbox_enabled )); } } if let Some(enabled) = self.world_restore_seed_tuple_written_from_raw_lane { if actual.world_restore_seed_tuple_written_from_raw_lane != Some(enabled) { mismatches.push(format!( "world_restore_seed_tuple_written_from_raw_lane mismatch: expected {enabled}, got {:?}", actual.world_restore_seed_tuple_written_from_raw_lane )); } } if let Some(enabled) = self.world_restore_absolute_counter_requires_shell_context { if actual.world_restore_absolute_counter_requires_shell_context != Some(enabled) { mismatches.push(format!( "world_restore_absolute_counter_requires_shell_context mismatch: expected {enabled}, got {:?}", actual.world_restore_absolute_counter_requires_shell_context )); } } if let Some(enabled) = self.world_restore_absolute_counter_reconstructible_from_save { if actual.world_restore_absolute_counter_reconstructible_from_save != Some(enabled) { mismatches.push(format!( "world_restore_absolute_counter_reconstructible_from_save mismatch: expected {enabled}, got {:?}", actual.world_restore_absolute_counter_reconstructible_from_save )); } } if let Some(slot) = self.world_restore_disable_cargo_economy_special_condition_slot { if actual.world_restore_disable_cargo_economy_special_condition_slot != Some(slot) { mismatches.push(format!( "world_restore_disable_cargo_economy_special_condition_slot mismatch: expected {slot}, got {:?}", actual.world_restore_disable_cargo_economy_special_condition_slot )); } } if let Some(enabled) = self.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save { if actual .world_restore_disable_cargo_economy_special_condition_reconstructible_from_save != Some(enabled) { mismatches.push(format!( "world_restore_disable_cargo_economy_special_condition_reconstructible_from_save mismatch: expected {enabled}, got {:?}", actual.world_restore_disable_cargo_economy_special_condition_reconstructible_from_save )); } } if let Some(enabled) = self.world_restore_disable_cargo_economy_special_condition_write_side_grounded { if actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded != Some(enabled) { mismatches.push(format!( "world_restore_disable_cargo_economy_special_condition_write_side_grounded mismatch: expected {enabled}, got {:?}", actual.world_restore_disable_cargo_economy_special_condition_write_side_grounded )); } } if let Some(enabled) = self.world_restore_disable_cargo_economy_special_condition_enabled { if actual.world_restore_disable_cargo_economy_special_condition_enabled != Some(enabled) { mismatches.push(format!( "world_restore_disable_cargo_economy_special_condition_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_disable_cargo_economy_special_condition_enabled )); } } if let Some(enabled) = self.world_restore_use_bio_accelerator_cars_enabled { if actual.world_restore_use_bio_accelerator_cars_enabled != Some(enabled) { mismatches.push(format!( "world_restore_use_bio_accelerator_cars_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_use_bio_accelerator_cars_enabled )); } } if let Some(enabled) = self.world_restore_use_wartime_cargos_enabled { if actual.world_restore_use_wartime_cargos_enabled != Some(enabled) { mismatches.push(format!( "world_restore_use_wartime_cargos_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_use_wartime_cargos_enabled )); } } if let Some(enabled) = self.world_restore_disable_train_crashes_enabled { if actual.world_restore_disable_train_crashes_enabled != Some(enabled) { mismatches.push(format!( "world_restore_disable_train_crashes_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_disable_train_crashes_enabled )); } } if let Some(enabled) = self.world_restore_disable_train_crashes_and_breakdowns_enabled { if actual.world_restore_disable_train_crashes_and_breakdowns_enabled != Some(enabled) { mismatches.push(format!( "world_restore_disable_train_crashes_and_breakdowns_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_disable_train_crashes_and_breakdowns_enabled )); } } if let Some(enabled) = self.world_restore_ai_ignore_territories_at_startup_enabled { if actual.world_restore_ai_ignore_territories_at_startup_enabled != Some(enabled) { mismatches.push(format!( "world_restore_ai_ignore_territories_at_startup_enabled mismatch: expected {enabled}, got {:?}", actual.world_restore_ai_ignore_territories_at_startup_enabled )); } } if let Some(value) = self.world_restore_limited_track_building_amount { if actual.world_restore_limited_track_building_amount != Some(value) { mismatches.push(format!( "world_restore_limited_track_building_amount mismatch: expected {value}, got {:?}", actual.world_restore_limited_track_building_amount )); } } if let Some(code) = self.world_restore_economic_status_code { if actual.world_restore_economic_status_code != Some(code) { mismatches.push(format!( "world_restore_economic_status_code mismatch: expected {code}, got {:?}", actual.world_restore_economic_status_code )); } } if let Some(value) = self.world_restore_territory_access_cost { if actual.world_restore_territory_access_cost != Some(value) { mismatches.push(format!( "world_restore_territory_access_cost mismatch: expected {value}, got {:?}", actual.world_restore_territory_access_cost )); } } if let Some(kind) = &self.world_restore_absolute_counter_restore_kind { if actual.world_restore_absolute_counter_restore_kind.as_ref() != Some(kind) { mismatches.push(format!( "world_restore_absolute_counter_restore_kind mismatch: expected {kind:?}, got {:?}", actual.world_restore_absolute_counter_restore_kind )); } } if let Some(context) = &self.world_restore_absolute_counter_adjustment_context { if actual .world_restore_absolute_counter_adjustment_context .as_ref() != Some(context) { mismatches.push(format!( "world_restore_absolute_counter_adjustment_context mismatch: expected {context:?}, got {:?}", actual.world_restore_absolute_counter_adjustment_context )); } } if let Some(count) = self.metadata_count { if actual.metadata_count != count { mismatches.push(format!( "metadata_count mismatch: expected {count}, got {}", actual.metadata_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.active_company_count { if actual.active_company_count != count { mismatches.push(format!( "active_company_count mismatch: expected {count}, got {}", actual.active_company_count )); } } if let Some(count) = self.player_count { if actual.player_count != count { mismatches.push(format!( "player_count mismatch: expected {count}, got {}", actual.player_count )); } } if let Some(count) = self.chairman_profile_count { if actual.chairman_profile_count != count { mismatches.push(format!( "chairman_profile_count mismatch: expected {count}, got {}", actual.chairman_profile_count )); } } if let Some(count) = self.active_chairman_profile_count { if actual.active_chairman_profile_count != count { mismatches.push(format!( "active_chairman_profile_count mismatch: expected {count}, got {}", actual.active_chairman_profile_count )); } } if let Some(selected_id) = self.selected_chairman_profile_id { if actual.selected_chairman_profile_id != Some(selected_id) { mismatches.push(format!( "selected_chairman_profile_id mismatch: expected {selected_id:?}, got {:?}", actual.selected_chairman_profile_id )); } } if let Some(count) = self.linked_chairman_company_count { if actual.linked_chairman_company_count != count { mismatches.push(format!( "linked_chairman_company_count mismatch: expected {count}, got {}", actual.linked_chairman_company_count )); } } if let Some(count) = self.company_takeover_cooldown_count { if actual.company_takeover_cooldown_count != count { mismatches.push(format!( "company_takeover_cooldown_count mismatch: expected {count}, got {}", actual.company_takeover_cooldown_count )); } } if let Some(count) = self.company_merger_cooldown_count { if actual.company_merger_cooldown_count != count { mismatches.push(format!( "company_merger_cooldown_count mismatch: expected {count}, got {}", actual.company_merger_cooldown_count )); } } if let Some(count) = self.train_count { if actual.train_count != count { mismatches.push(format!( "train_count mismatch: expected {count}, got {}", actual.train_count )); } } if let Some(count) = self.active_train_count { if actual.active_train_count != count { mismatches.push(format!( "active_train_count mismatch: expected {count}, got {}", actual.active_train_count )); } } if let Some(count) = self.retired_train_count { if actual.retired_train_count != count { mismatches.push(format!( "retired_train_count mismatch: expected {count}, got {}", actual.retired_train_count )); } } if let Some(count) = self.locomotive_catalog_count { if actual.locomotive_catalog_count != count { mismatches.push(format!( "locomotive_catalog_count mismatch: expected {count}, got {}", actual.locomotive_catalog_count )); } } if let Some(count) = self.cargo_catalog_count { if actual.cargo_catalog_count != count { mismatches.push(format!( "cargo_catalog_count mismatch: expected {count}, got {}", actual.cargo_catalog_count )); } } if let Some(count) = self.territory_count { if actual.territory_count != count { mismatches.push(format!( "territory_count mismatch: expected {count}, got {}", actual.territory_count )); } } if let Some(count) = self.company_territory_track_count { if actual.company_territory_track_count != count { mismatches.push(format!( "company_territory_track_count mismatch: expected {count}, got {}", actual.company_territory_track_count )); } } if let Some(present) = self.packed_event_collection_present { if actual.packed_event_collection_present != present { mismatches.push(format!( "packed_event_collection_present mismatch: expected {present}, got {}", actual.packed_event_collection_present )); } } if let Some(count) = self.packed_event_record_count { if actual.packed_event_record_count != count { mismatches.push(format!( "packed_event_record_count mismatch: expected {count}, got {}", actual.packed_event_record_count )); } } if let Some(count) = self.packed_event_decoded_record_count { if actual.packed_event_decoded_record_count != count { mismatches.push(format!( "packed_event_decoded_record_count mismatch: expected {count}, got {}", actual.packed_event_decoded_record_count )); } } if let Some(count) = self.packed_event_imported_runtime_record_count { if actual.packed_event_imported_runtime_record_count != count { mismatches.push(format!( "packed_event_imported_runtime_record_count mismatch: expected {count}, got {}", actual.packed_event_imported_runtime_record_count )); } } if let Some(count) = self.packed_event_parity_only_record_count { if actual.packed_event_parity_only_record_count != count { mismatches.push(format!( "packed_event_parity_only_record_count mismatch: expected {count}, got {}", actual.packed_event_parity_only_record_count )); } } if let Some(count) = self.packed_event_unsupported_record_count { if actual.packed_event_unsupported_record_count != count { mismatches.push(format!( "packed_event_unsupported_record_count mismatch: expected {count}, got {}", actual.packed_event_unsupported_record_count )); } } if let Some(count) = self.packed_event_blocked_missing_company_context_count { if actual.packed_event_blocked_missing_company_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_company_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_company_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_selection_context_count { if actual.packed_event_blocked_missing_selection_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_selection_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_selection_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_company_role_context_count { if actual.packed_event_blocked_missing_company_role_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_company_role_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_company_role_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_player_context_count { if actual.packed_event_blocked_missing_player_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_player_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_player_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_player_selection_context_count { if actual.packed_event_blocked_missing_player_selection_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_player_selection_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_player_selection_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_player_role_context_count { if actual.packed_event_blocked_missing_player_role_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_player_role_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_player_role_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_chairman_context_count { if actual.packed_event_blocked_missing_chairman_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_chairman_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_chairman_context_count )); } } if let Some(count) = self.packed_event_blocked_chairman_target_scope_count { if actual.packed_event_blocked_chairman_target_scope_count != count { mismatches.push(format!( "packed_event_blocked_chairman_target_scope_count mismatch: expected {count}, got {}", actual.packed_event_blocked_chairman_target_scope_count )); } } if let Some(count) = self.packed_event_blocked_missing_condition_context_count { if actual.packed_event_blocked_missing_condition_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_condition_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_condition_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_player_condition_context_count { if actual.packed_event_blocked_missing_player_condition_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_player_condition_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_player_condition_context_count )); } } if let Some(count) = self.packed_event_blocked_company_condition_scope_disabled_count { if actual.packed_event_blocked_company_condition_scope_disabled_count != count { mismatches.push(format!( "packed_event_blocked_company_condition_scope_disabled_count mismatch: expected {count}, got {}", actual.packed_event_blocked_company_condition_scope_disabled_count )); } } if let Some(count) = self.packed_event_blocked_player_condition_scope_count { if actual.packed_event_blocked_player_condition_scope_count != count { mismatches.push(format!( "packed_event_blocked_player_condition_scope_count mismatch: expected {count}, got {}", actual.packed_event_blocked_player_condition_scope_count )); } } if let Some(count) = self.packed_event_blocked_territory_condition_scope_count { if actual.packed_event_blocked_territory_condition_scope_count != count { mismatches.push(format!( "packed_event_blocked_territory_condition_scope_count mismatch: expected {count}, got {}", actual.packed_event_blocked_territory_condition_scope_count )); } } if let Some(count) = self.packed_event_blocked_missing_territory_context_count { if actual.packed_event_blocked_missing_territory_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_territory_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_territory_context_count )); } } if let Some(count) = self.packed_event_blocked_named_territory_binding_count { if actual.packed_event_blocked_named_territory_binding_count != count { mismatches.push(format!( "packed_event_blocked_named_territory_binding_count mismatch: expected {count}, got {}", actual.packed_event_blocked_named_territory_binding_count )); } } if let Some(count) = self.packed_event_blocked_unmapped_ordinary_condition_count { if actual.packed_event_blocked_unmapped_ordinary_condition_count != count { mismatches.push(format!( "packed_event_blocked_unmapped_ordinary_condition_count mismatch: expected {count}, got {}", actual.packed_event_blocked_unmapped_ordinary_condition_count )); } } if let Some(count) = self.packed_event_blocked_unmapped_world_condition_count { if actual.packed_event_blocked_unmapped_world_condition_count != count { mismatches.push(format!( "packed_event_blocked_unmapped_world_condition_count mismatch: expected {count}, got {}", actual.packed_event_blocked_unmapped_world_condition_count )); } } if let Some(count) = self.packed_event_blocked_missing_compact_control_count { if actual.packed_event_blocked_missing_compact_control_count != count { mismatches.push(format!( "packed_event_blocked_missing_compact_control_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_compact_control_count )); } } if let Some(count) = self.packed_event_blocked_shell_owned_descriptor_count { if actual.packed_event_blocked_shell_owned_descriptor_count != count { mismatches.push(format!( "packed_event_blocked_shell_owned_descriptor_count mismatch: expected {count}, got {}", actual.packed_event_blocked_shell_owned_descriptor_count )); } } if let Some(count) = self.packed_event_blocked_evidence_blocked_descriptor_count { if actual.packed_event_blocked_evidence_blocked_descriptor_count != count { mismatches.push(format!( "packed_event_blocked_evidence_blocked_descriptor_count mismatch: expected {count}, got {}", actual.packed_event_blocked_evidence_blocked_descriptor_count )); } } if let Some(count) = self.packed_event_blocked_variant_or_scope_blocked_descriptor_count { if actual.packed_event_blocked_variant_or_scope_blocked_descriptor_count != count { mismatches.push(format!( "packed_event_blocked_variant_or_scope_blocked_descriptor_count mismatch: expected {count}, got {}", actual.packed_event_blocked_variant_or_scope_blocked_descriptor_count )); } } if let Some(count) = self.packed_event_blocked_unmapped_real_descriptor_count { if actual.packed_event_blocked_unmapped_real_descriptor_count != count { mismatches.push(format!( "packed_event_blocked_unmapped_real_descriptor_count mismatch: expected {count}, got {}", actual.packed_event_blocked_unmapped_real_descriptor_count )); } } if let Some(count) = self.packed_event_blocked_unmapped_world_descriptor_count { if actual.packed_event_blocked_unmapped_world_descriptor_count != count { mismatches.push(format!( "packed_event_blocked_unmapped_world_descriptor_count mismatch: expected {count}, got {}", actual.packed_event_blocked_unmapped_world_descriptor_count )); } } if let Some(count) = self.packed_event_blocked_territory_access_variant_count { if actual.packed_event_blocked_territory_access_variant_count != count { mismatches.push(format!( "packed_event_blocked_territory_access_variant_count mismatch: expected {count}, got {}", actual.packed_event_blocked_territory_access_variant_count )); } } if let Some(count) = self.packed_event_blocked_territory_access_scope_count { if actual.packed_event_blocked_territory_access_scope_count != count { mismatches.push(format!( "packed_event_blocked_territory_access_scope_count mismatch: expected {count}, got {}", actual.packed_event_blocked_territory_access_scope_count )); } } if let Some(count) = self.packed_event_blocked_missing_train_context_count { if actual.packed_event_blocked_missing_train_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_train_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_train_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_train_territory_context_count { if actual.packed_event_blocked_missing_train_territory_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_train_territory_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_train_territory_context_count )); } } if let Some(count) = self.packed_event_blocked_missing_locomotive_catalog_context_count { if actual.packed_event_blocked_missing_locomotive_catalog_context_count != count { mismatches.push(format!( "packed_event_blocked_missing_locomotive_catalog_context_count mismatch: expected {count}, got {}", actual.packed_event_blocked_missing_locomotive_catalog_context_count )); } } if let Some(count) = self.packed_event_blocked_confiscation_variant_count { if actual.packed_event_blocked_confiscation_variant_count != count { mismatches.push(format!( "packed_event_blocked_confiscation_variant_count mismatch: expected {count}, got {}", actual.packed_event_blocked_confiscation_variant_count )); } } if let Some(count) = self.packed_event_blocked_retire_train_variant_count { if actual.packed_event_blocked_retire_train_variant_count != count { mismatches.push(format!( "packed_event_blocked_retire_train_variant_count mismatch: expected {count}, got {}", actual.packed_event_blocked_retire_train_variant_count )); } } if let Some(count) = self.packed_event_blocked_retire_train_scope_count { if actual.packed_event_blocked_retire_train_scope_count != count { mismatches.push(format!( "packed_event_blocked_retire_train_scope_count mismatch: expected {count}, got {}", actual.packed_event_blocked_retire_train_scope_count )); } } if let Some(count) = self.packed_event_blocked_structural_only_count { if actual.packed_event_blocked_structural_only_count != count { mismatches.push(format!( "packed_event_blocked_structural_only_count mismatch: expected {count}, got {}", actual.packed_event_blocked_structural_only_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.candidate_availability_count { if actual.candidate_availability_count != count { mismatches.push(format!( "candidate_availability_count mismatch: expected {count}, got {}", actual.candidate_availability_count )); } } if let Some(count) = self.zero_candidate_availability_count { if actual.zero_candidate_availability_count != count { mismatches.push(format!( "zero_candidate_availability_count mismatch: expected {count}, got {}", actual.zero_candidate_availability_count )); } } if let Some(count) = self.named_locomotive_availability_count { if actual.named_locomotive_availability_count != count { mismatches.push(format!( "named_locomotive_availability_count mismatch: expected {count}, got {}", actual.named_locomotive_availability_count )); } } if let Some(count) = self.zero_named_locomotive_availability_count { if actual.zero_named_locomotive_availability_count != count { mismatches.push(format!( "zero_named_locomotive_availability_count mismatch: expected {count}, got {}", actual.zero_named_locomotive_availability_count )); } } if let Some(count) = self.named_locomotive_cost_count { if actual.named_locomotive_cost_count != count { mismatches.push(format!( "named_locomotive_cost_count mismatch: expected {count}, got {}", actual.named_locomotive_cost_count )); } } if let Some(count) = self.cargo_production_override_count { if actual.cargo_production_override_count != count { mismatches.push(format!( "cargo_production_override_count mismatch: expected {count}, got {}", actual.cargo_production_override_count )); } } if let Some(count) = self.world_scalar_override_count { if actual.world_scalar_override_count != count { mismatches.push(format!( "world_scalar_override_count mismatch: expected {count}, got {}", actual.world_scalar_override_count )); } } if let Some(count) = self.special_condition_count { if actual.special_condition_count != count { mismatches.push(format!( "special_condition_count mismatch: expected {count}, got {}", actual.special_condition_count )); } } if let Some(count) = self.enabled_special_condition_count { if actual.enabled_special_condition_count != count { mismatches.push(format!( "enabled_special_condition_count mismatch: expected {count}, got {}", actual.enabled_special_condition_count )); } } if let Some(kind) = &self.save_profile_kind { if actual.save_profile_kind.as_ref() != Some(kind) { mismatches.push(format!( "save_profile_kind mismatch: expected {kind:?}, got {:?}", actual.save_profile_kind )); } } if let Some(family) = &self.save_profile_family { if actual.save_profile_family.as_ref() != Some(family) { mismatches.push(format!( "save_profile_family mismatch: expected {family:?}, got {:?}", actual.save_profile_family )); } } if let Some(map_path) = &self.save_profile_map_path { if actual.save_profile_map_path.as_ref() != Some(map_path) { mismatches.push(format!( "save_profile_map_path mismatch: expected {map_path:?}, got {:?}", actual.save_profile_map_path )); } } if let Some(display_name) = &self.save_profile_display_name { if actual.save_profile_display_name.as_ref() != Some(display_name) { mismatches.push(format!( "save_profile_display_name mismatch: expected {display_name:?}, got {:?}", actual.save_profile_display_name )); } } if let Some(lane) = self.save_profile_selected_year_profile_lane { if actual.save_profile_selected_year_profile_lane != Some(lane) { mismatches.push(format!( "save_profile_selected_year_profile_lane mismatch: expected {lane}, got {:?}", actual.save_profile_selected_year_profile_lane )); } } if let Some(enabled) = self.save_profile_sandbox_enabled { if actual.save_profile_sandbox_enabled != Some(enabled) { mismatches.push(format!( "save_profile_sandbox_enabled mismatch: expected {enabled}, got {:?}", actual.save_profile_sandbox_enabled )); } } if let Some(enabled) = self.save_profile_campaign_scenario_enabled { if actual.save_profile_campaign_scenario_enabled != Some(enabled) { mismatches.push(format!( "save_profile_campaign_scenario_enabled mismatch: expected {enabled}, got {:?}", actual.save_profile_campaign_scenario_enabled )); } } if let Some(enabled) = self.save_profile_staged_profile_copy_on_restore { if actual.save_profile_staged_profile_copy_on_restore != Some(enabled) { mismatches.push(format!( "save_profile_staged_profile_copy_on_restore mismatch: expected {enabled}, got {:?}", actual.save_profile_staged_profile_copy_on_restore )); } } 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, #[serde(default)] pub expected_summary: ExpectedRuntimeSummary, #[serde(default)] pub expected_state_fragment: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum FixtureStateOrigin { Inline, SnapshotPath(String), SaveSlicePath(String), ImportPath(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, #[serde(default)] pub state_snapshot_path: Option, #[serde(default)] pub state_save_slice_path: Option, #[serde(default)] pub state_import_path: Option, #[serde(default)] pub commands: Vec, #[serde(default)] pub expected_summary: ExpectedRuntimeSummary, #[serde(default)] pub expected_state_fragment: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct FixtureValidationReport { pub fixture_id: String, pub valid: bool, pub issue_count: usize, pub issues: Vec, } pub fn compare_expected_state_fragment(expected: &Value, actual: &Value) -> Vec { let mut mismatches = Vec::new(); compare_expected_state_fragment_at_path("$", expected, actual, &mut mismatches); mismatches } fn compare_expected_state_fragment_at_path( path: &str, expected: &Value, actual: &Value, mismatches: &mut Vec, ) { match (expected, actual) { (Value::Object(expected_map), Value::Object(actual_map)) => { for (key, expected_value) in expected_map { let next_path = format!("{path}.{key}"); match actual_map.get(key) { Some(actual_value) => compare_expected_state_fragment_at_path( &next_path, expected_value, actual_value, mismatches, ), None => mismatches.push(format!("{next_path} missing in actual state")), } } } (Value::Array(expected_items), Value::Array(actual_items)) => { for (index, expected_item) in expected_items.iter().enumerate() { let next_path = format!("{path}[{index}]"); match actual_items.get(index) { Some(actual_item) => compare_expected_state_fragment_at_path( &next_path, expected_item, actual_item, mismatches, ), None => mismatches.push(format!("{next_path} missing in actual state")), } } } _ if expected != actual => mismatches.push(format!( "{path} mismatch: expected {expected:?}, got {actual:?}" )), _ => {} } } 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")); } #[test] fn compares_expected_state_fragment_recursively() { let expected = serde_json::json!({ "world_flags": { "sandbox": false }, "companies": [ { "company_id": 1 } ] }); let actual = serde_json::json!({ "world_flags": { "sandbox": false, "runtime.effect_fired": true }, "companies": [ { "company_id": 1, "current_cash": 250000 } ] }); let mismatches = compare_expected_state_fragment(&expected, &actual); assert!( mismatches.is_empty(), "unexpected mismatches: {mismatches:?}" ); } }