use std::path::{Path, PathBuf};
use gridoxide::cgmes::{self, CimDataset, load_profiles};
fn configurations() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/data/CGMES-Test-Configurations/v3.0")
}
fn try_load(dir: PathBuf, prefix: &str) -> Option<CimDataset> {
if !dir.exists() {
eprintln!(
"skipping: {} not found — run `git submodule update --init tests/data/CGMES-Test-Configurations`",
dir.display()
);
return None;
}
let profiles: Vec<PathBuf> = ["EQ", "SSH", "TP", "SV"]
.iter()
.map(|p| dir.join(format!("{prefix}_{p}.xml")))
.filter(|p| p.exists())
.collect();
let refs: Vec<&Path> = profiles.iter().map(|p| p.as_path()).collect();
Some(load_profiles(&refs).expect("failed to decode CGMES profiles"))
}
fn try_load_pst_type1() -> Option<CimDataset> {
try_load(
configurations().join("PST/PST_PhaseTapChangerLinear_Type1"),
"PST_Type1",
)
}
fn try_load_small_grid() -> Option<CimDataset> {
try_load(
configurations().join("SmallGrid/SmallGrid-Merged"),
"SmallGrid",
)
}
#[test]
fn a_pst_fixture_yields_permanent_and_temporary_ratings() {
let Some(ds) = try_load_pst_type1() else {
return;
};
let (limits, report) = cgmes::cgmes_operational_limits(&ds).expect("limits");
assert!(
report.current_limits > 0,
"no CurrentLimit converted: {report:?}"
);
assert_eq!(
report.unattached, 0,
"every limit should resolve to equipment"
);
assert_eq!(report.without_value, 0);
assert!(!limits.is_empty());
let permanent = limits
.values()
.filter(|l| l.tightest_patl().is_some())
.count();
let temporary: usize = limits
.values()
.map(|l| l.terminals.iter().map(|t| t.tatl.len()).sum::<usize>())
.sum();
assert!(permanent > 0, "no permanent rating found");
assert!(temporary > 0, "no temporary rating found");
let has_1200 = limits.values().any(|l| {
l.terminals.iter().any(|t| {
t.tatl
.iter()
.any(|x| x.acceptable_duration_s == Some(1200.0))
})
});
assert!(
has_1200,
"the TATL_1200 type did not survive with its duration"
);
}
#[test]
fn ratings_are_read_from_normal_value_when_value_is_absent() {
let Some(ds) = try_load_pst_type1() else {
return;
};
let (limits, _) = cgmes::cgmes_operational_limits(&ds).expect("limits");
let values: Vec<f64> = limits.values().filter_map(|l| l.tightest_patl()).collect();
assert!(!values.is_empty(), "no PATL values read");
assert!(
values.iter().all(|v| *v > 0.0),
"a rating came through as zero: {values:?}"
);
assert!(
values.iter().any(|v| *v > 100.0),
"values look defaulted: {values:?}"
);
}
#[test]
fn limits_land_on_the_terminal_their_set_names() {
let Some(ds) = try_load_pst_type1() else {
return;
};
let (limits, _) = cgmes::cgmes_operational_limits(&ds).expect("limits");
let two_sided = limits.values().filter(|l| l.terminals.len() >= 2).count();
assert!(
two_sided > 0,
"no equipment received limits on more than one terminal"
);
for l in limits.values() {
assert!(
l.terminals.len() <= 3,
"more terminals than any branch has: {l:?}"
);
}
}
#[test]
fn a_larger_grid_imports_every_limit_it_declares() {
let Some(ds) = try_load_small_grid() else {
return;
};
let (limits, report) = cgmes::cgmes_operational_limits(&ds).expect("limits");
assert!(
report.current_limits > 100,
"only {} limits: {report:?}",
report.current_limits
);
assert_eq!(report.unattached, 0, "unattached limits: {report:?}");
assert!(
limits.len() > 50,
"only {} pieces of equipment carry limits",
limits.len()
);
for (mrid, l) in &limits {
for t in &l.terminals {
if let Some(patl) = t.patl_a {
assert!(patl > 0.0, "{mrid} has a non-positive PATL {patl}");
}
for tatl in &t.tatl {
assert!(tatl.value_a > 0.0, "{mrid} has a non-positive TATL");
}
}
}
}
#[test]
fn no_declared_limit_is_lost_between_the_file_and_the_result() {
for (dir, prefix) in [
("PST/PST_PhaseTapChangerLinear_Type1", "PST_Type1"),
("SmallGrid/SmallGrid-Merged", "SmallGrid"),
(
"MicroGrid/MicroGid-BaseCase/MicroGrid-BE-MAS",
"MicroGrid-BE",
),
] {
let Some(ds) = try_load(configurations().join(dir), prefix) else {
continue;
};
let (limits, report) = cgmes::cgmes_operational_limits(&ds).expect("limits");
let produced: usize = limits
.values()
.map(|l| {
l.terminals
.iter()
.map(|t| usize::from(t.patl_a.is_some()) + t.tatl.len())
.sum::<usize>()
})
.sum();
assert_eq!(
produced, report.current_limits,
"{dir}: {} CurrentLimit objects converted but {produced} ratings produced",
report.current_limits
);
assert_eq!(report.unattached, 0, "{dir}: {report:?}");
assert_eq!(report.without_value, 0, "{dir}: {report:?}");
}
}