use std::path::Path;
use powerio::{
Branch, Bus, BusId, BusType, CoordinateSpace, CoordsKind, GenCaps, Generator, GeoMeta,
Location, Network, SourceFormat,
};
fn golden_v4() -> String {
let path =
Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/data/powerio-json/case30_v4.json");
std::fs::read_to_string(&path).expect("the committed v4 golden snapshot must exist")
}
#[test]
fn golden_v4_snapshot_still_parses() {
let text = golden_v4();
let net = Network::from_json(&text).expect("the v4 golden must still parse");
assert_eq!(net.buses.len(), 30);
assert_eq!(net.branches.len(), 41);
assert_eq!(net.generators.len(), 6);
assert_eq!(net.loads.len(), 20);
assert_eq!(net.shunts.len(), 2);
assert_eq!(net.base_mva.to_bits(), 100.0_f64.to_bits());
assert!(
net.generators.iter().all(|g| g.cost.is_some()),
"case30 gen costs must survive the round trip"
);
assert!(
text.contains(r#""caps":{"#) && !text.contains(r#""caps":["#),
"generator caps must serialize as an object"
);
let again = net.to_json().unwrap();
assert!(
again.contains(r#""caps":{"#) && !again.contains(r#""caps":["#),
"the live serializer must still emit caps as an object"
);
let back = Network::from_json(&again).unwrap();
assert_eq!(back.buses.len(), net.buses.len());
assert_eq!(back.generators.len(), net.generators.len());
}
#[test]
fn snapshot_ignores_unknown_fields_and_defaults_omitted_caps() {
let net = small_net();
let mut v: serde_json::Value = serde_json::from_str(&net.to_json().unwrap()).unwrap();
v["future_field_v5"] = serde_json::json!("ignored");
v["generators"][0].as_object_mut().unwrap().remove("caps");
let text = serde_json::to_string(&v).unwrap();
let parsed =
Network::from_json(&text).expect("an unknown field and an omitted caps must still parse");
assert_eq!(parsed.generators.len(), 1);
assert!(
!parsed.generators[0].has_caps(),
"an omitted caps field defaults to the empty set"
);
}
fn small_net() -> Network {
let bus = |id, kind| Bus::new(BusId(id), kind, 230.0);
let mut caps: GenCaps = GenCaps::default();
caps[8] = Some(1.5); let mut g = Generator::new(BusId(1));
g.pg = 10.0;
g.pmax = 100.0;
g.qmax = 50.0;
g.qmin = -50.0;
g.mbase = 100.0;
g.caps = caps;
let branch = Branch::new(BusId(1), BusId(2), 0.01, 0.1);
let mut net = Network::new("schema_lock", 100.0);
net.buses = vec![bus(1, BusType::Ref), bus(2, BusType::Pq)];
net.branches = vec![branch];
net.generators = vec![g];
net.source_format = SourceFormat::InMemory;
net
}
#[test]
fn uid_survives_snapshot_roundtrip_and_stays_off_the_wire_when_absent() {
let mut net = small_net();
net.generators[0].uid = Some("gen-a".to_owned());
let v: serde_json::Value = serde_json::from_str(&net.to_json().unwrap()).unwrap();
assert_eq!(v["generators"][0]["uid"], serde_json::json!("gen-a"));
assert!(v["buses"][0].get("uid").is_none());
let parsed = Network::from_json(&serde_json::to_string(&v).unwrap()).unwrap();
assert_eq!(parsed.generators[0].uid.as_deref(), Some("gen-a"));
assert_eq!(parsed.buses[0].uid, None);
}
#[test]
fn geo_fields_roundtrip_and_stay_off_the_wire_when_absent() {
let net = small_net();
let text = net.to_json().unwrap();
assert!(!text.contains(r#""geo""#));
assert!(!text.contains(r#""location""#));
let parsed = Network::from_json(&text).unwrap();
assert_eq!(parsed.to_json().unwrap(), text);
let v: serde_json::Value = serde_json::from_str(&text).unwrap();
assert!(v.get("geo").is_none());
assert!(v["buses"][0].get("location").is_none());
let mut with_geo = net;
with_geo.geo = Some(GeoMeta {
space: CoordinateSpace::Geographic { crs: None },
kind: Some(CoordsKind::Source),
});
with_geo.buses[0].location = Some(Location {
x: -80.0,
y: 35.0,
kind: None,
});
let v: serde_json::Value = serde_json::from_str(&with_geo.to_json().unwrap()).unwrap();
assert_eq!(
v["geo"],
serde_json::json!({"space": "geographic", "kind": "source"})
);
assert_eq!(v["buses"][0]["location"]["x"], serde_json::json!(-80.0));
assert_eq!(v["buses"][0]["location"]["y"], serde_json::json!(35.0));
let parsed = Network::from_json(&serde_json::to_string(&v).unwrap()).unwrap();
assert_eq!(parsed.geo, with_geo.geo);
assert_eq!(parsed.buses[0].location, with_geo.buses[0].location);
}