feat(api): add round-trip serialization tests for city data

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-06-09 20:44:17 -07:00
parent 0d2520a700
commit ef25e2cf8b

View file

@ -1040,3 +1040,78 @@ mod load_items_tests {
assert_eq!(cost_of(&defs, "axe"), Some(10));
}
}
#[cfg(test)]
mod save_round_trip_tests {
use super::*;
use mc_city::{BuildingQueue, CityYields, Queueable, QueueEntry, CITY_CENTER_QUEUE_ID};
/// Punch-list item 4: a city carrying non-default `building_yields`, a
/// non-empty per-building production queue, and both `captured_turn` /
/// `last_attacked_turn` set must survive the gdext save path
/// (`to_json` → `load_from_json` → `to_json`) byte-identically.
///
/// Byte-identity is asserted on the *re-serialized* form (json1 == json2)
/// rather than on the original input string. `building_yields` is a
/// `HashMap`, whose serialization order is only stable when serializing a
/// single map instance; a fresh deserialize could reorder a multi-entry
/// map. A single `building_yields` entry keeps the comparison deterministic
/// while still exercising the non-default-map path. `queues` is a
/// `BTreeMap`, so multi-entry queue state is order-stable regardless.
#[test]
fn city_with_yields_queue_and_capture_turns_round_trips_byte_identical() {
let mut city = City::new("city_smithgate");
city.city_name = "Smithgate".to_string();
city.population = 4;
city.food_stored = 12.5;
city.production_progress = 7.0;
// Non-default building_yields (single entry → deterministic serde order).
city.register_building_yields(
"smithy",
CityYields { food: 0.0, production: 3.0, gold: 1.0, culture: 0.0, science: 0.5 },
);
// Non-empty per-building production queue (BTreeMap → order-stable).
let mut q = BuildingQueue::new();
q.push(QueueEntry {
queueable: Queueable::Item { item_id: "bronze_sword".to_string() },
production_cost: 40,
production_invested: 12,
origin: mc_core::ProductionOrigin::default(),
});
city.queues_mut().insert(CITY_CENTER_QUEUE_ID.to_string(), q);
// Both occupation/siege timestamps set.
city.mark_captured(7);
city.mark_attacked(9);
let slot: Vec<Vec<City>> = vec![vec![city]];
let json1 = to_json(&slot, 0, 0);
assert!(json1.contains("smithy"), "building_yields must serialize");
assert!(json1.contains("bronze_sword"), "queue entry must serialize");
assert!(json1.contains("\"captured_turn\":7"), "captured_turn must serialize");
assert!(json1.contains("\"last_attacked_turn\":9"), "last_attacked_turn must serialize");
// Restore into a fresh slot via the real load path, then re-serialize.
let mut slot2: Vec<Vec<City>> = vec![vec![City::new("placeholder")]];
assert!(load_from_json(&mut slot2, 0, 0, &json1), "load_from_json must succeed");
let json2 = to_json(&slot2, 0, 0);
assert_eq!(json1, json2, "city save round-trip must be byte-identical");
// Field-level proof the restored city kept the four target fields.
let restored = at(&slot2, 0, 0).expect("restored city present");
assert_eq!(restored.captured_turn, Some(7));
assert_eq!(restored.last_attacked_turn, Some(9));
assert_eq!(restored.queues().get(CITY_CENTER_QUEUE_ID).map(|q| q.len()), Some(1));
assert!(
restored.queues().get(CITY_CENTER_QUEUE_ID)
.and_then(|q| q.entries().first())
.map(|e| e.production_invested == 12)
.unwrap_or(false),
"queued entry progress must survive"
);
}
}