923 lines
38 KiB
Rust
923 lines
38 KiB
Rust
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<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
pub struct ExpectedRuntimeSummary {
|
|
#[serde(default)]
|
|
pub calendar: Option<rrt_runtime::CalendarPoint>,
|
|
#[serde(default)]
|
|
pub calendar_projection_source: Option<String>,
|
|
#[serde(default)]
|
|
pub calendar_projection_is_placeholder: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_flag_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub world_restore_selected_year_profile_lane: Option<u8>,
|
|
#[serde(default)]
|
|
pub world_restore_campaign_scenario_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_sandbox_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_seed_tuple_written_from_raw_lane: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_absolute_counter_requires_shell_context: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_absolute_counter_reconstructible_from_save: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_disable_cargo_economy_special_condition_slot: Option<u8>,
|
|
#[serde(default)]
|
|
pub world_restore_disable_cargo_economy_special_condition_reconstructible_from_save:
|
|
Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_disable_cargo_economy_special_condition_write_side_grounded: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_disable_cargo_economy_special_condition_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_use_bio_accelerator_cars_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_use_wartime_cargos_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_disable_train_crashes_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_disable_train_crashes_and_breakdowns_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_ai_ignore_territories_at_startup_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub world_restore_absolute_counter_restore_kind: Option<String>,
|
|
#[serde(default)]
|
|
pub world_restore_absolute_counter_adjustment_context: Option<String>,
|
|
#[serde(default)]
|
|
pub metadata_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub company_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub active_company_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub territory_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub company_territory_track_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_collection_present: Option<bool>,
|
|
#[serde(default)]
|
|
pub packed_event_record_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_decoded_record_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_imported_runtime_record_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_parity_only_record_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_unsupported_record_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_missing_company_context_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_missing_selection_context_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_missing_company_role_context_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_missing_condition_context_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_company_condition_scope_disabled_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_player_condition_scope_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_territory_condition_scope_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_missing_territory_context_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_named_territory_binding_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_unmapped_ordinary_condition_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_missing_compact_control_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_unmapped_real_descriptor_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub packed_event_blocked_structural_only_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub event_runtime_record_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub candidate_availability_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub zero_candidate_availability_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub special_condition_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub enabled_special_condition_count: Option<usize>,
|
|
#[serde(default)]
|
|
pub save_profile_kind: Option<String>,
|
|
#[serde(default)]
|
|
pub save_profile_family: Option<String>,
|
|
#[serde(default)]
|
|
pub save_profile_map_path: Option<String>,
|
|
#[serde(default)]
|
|
pub save_profile_display_name: Option<String>,
|
|
#[serde(default)]
|
|
pub save_profile_selected_year_profile_lane: Option<u8>,
|
|
#[serde(default)]
|
|
pub save_profile_sandbox_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub save_profile_campaign_scenario_enabled: Option<bool>,
|
|
#[serde(default)]
|
|
pub save_profile_staged_profile_copy_on_restore: Option<bool>,
|
|
#[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(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(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.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_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_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_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_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_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.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<StepCommand>,
|
|
#[serde(default)]
|
|
pub expected_summary: ExpectedRuntimeSummary,
|
|
#[serde(default)]
|
|
pub expected_state_fragment: Option<Value>,
|
|
}
|
|
|
|
#[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<RuntimeState>,
|
|
#[serde(default)]
|
|
pub state_snapshot_path: Option<String>,
|
|
#[serde(default)]
|
|
pub state_save_slice_path: Option<String>,
|
|
#[serde(default)]
|
|
pub state_import_path: Option<String>,
|
|
#[serde(default)]
|
|
pub commands: Vec<StepCommand>,
|
|
#[serde(default)]
|
|
pub expected_summary: ExpectedRuntimeSummary,
|
|
#[serde(default)]
|
|
pub expected_state_fragment: Option<Value>,
|
|
}
|
|
|
|
#[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 compare_expected_state_fragment(expected: &Value, actual: &Value) -> Vec<String> {
|
|
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<String>,
|
|
) {
|
|
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:?}"
|
|
);
|
|
}
|
|
}
|