use std::path::{Path, PathBuf};
use std::process::{Command, Output};
mod openrao;
fn config(name: &str) -> Option<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/CGMES-Test-Configurations/v3.0")
.join(name);
if !dir.exists() {
eprintln!(
"skipping: {} not found — run `git submodule update --init \
tests/data/CGMES-Test-Configurations`",
dir.display()
);
return None;
}
Some(dir)
}
fn run(args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_gridoxide"))
.args(args)
.output()
.expect("failed to run the gridoxide binary")
}
fn stdout(out: &Output) -> String {
String::from_utf8_lossy(&out.stdout).into_owned()
}
#[test]
fn solve_needs_a_path() {
let out = run(&["solve"]);
assert_eq!(out.status.code(), Some(2));
assert!(String::from_utf8_lossy(&out.stderr).contains("solve needs a network path"));
}
#[test]
fn an_unreadable_network_is_reported_rather_than_panicking() {
let out = run(&["solve", "/nonexistent/network.uct"]);
assert_eq!(out.status.code(), Some(1));
assert!(!String::from_utf8_lossy(&out.stderr).is_empty());
}
#[test]
fn a_bare_solve_runs_no_outer_loop() {
let Some(dir) = config("Svedala/Svedala-Merged") else {
return;
};
let out = run(&["solve", dir.to_str().unwrap()]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
assert!(text.contains("Converged"), "{text}");
assert!(
!text.contains("outer loops:"),
"no loop was configured: {text}"
);
assert!(!text.contains("tap controllers:"), "{text}");
}
#[test]
fn all_three_controls_run_together() {
let Some(dir) = config("Svedala/Svedala-Merged") else {
return;
};
let out = run(&[
"solve",
dir.to_str().unwrap(),
"--control-taps",
"--enforce-q-limits",
"--distribute-slack",
]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
assert!(text.contains("outer loops:"), "{text}");
for loop_name in [
"DistributedSlack",
"ReactiveLimits",
"PhaseControl",
"TransformerVoltageControl",
] {
assert!(text.contains(loop_name), "missing {loop_name} in:\n{text}");
}
assert!(text.contains("converged = true"), "{text}");
assert!(text.contains("11 of 11 inside their deadbands"), "{text}");
assert!(text.contains("slack distribution:"), "{text}");
}
#[test]
fn control_taps_on_an_uncontrolled_network_reports_nothing() {
let Some(dir) = config("SmallGrid/SmallGrid-Merged") else {
return;
};
let out = run(&["solve", dir.to_str().unwrap(), "--control-taps"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
assert!(text.contains("0 regulating control(s)"), "{text}");
assert!(!text.contains("tap controllers:"), "{text}");
}
#[test]
fn the_import_report_is_printed() {
let Some(dir) = config("PowerFlow/PowerFlow") else {
return;
};
let out = run(&["solve", dir.to_str().unwrap(), "--control-taps"]);
let text = stdout(&out);
assert!(text.contains("tap controls: 0 read, 1 disabled"), "{text}");
}
#[test]
fn a_bad_flag_value_is_rejected() {
let Some(dir) = config("PowerFlow/PowerFlow") else {
return;
};
let out = run(&["solve", dir.to_str().unwrap(), "--max-iter", "banana"]);
assert_eq!(out.status.code(), Some(1));
assert!(String::from_utf8_lossy(&out.stderr).contains("--max-iter"));
}
#[test]
fn a_ucte_network_solves_through_the_same_path() {
let path = openrao::fixture("TestCase12Nodes.uct");
let out = run(&["solve", path.to_str().unwrap(), "--enforce-q-limits"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
assert!(text.contains("Converged"), "{text}");
assert!(text.contains("ReactiveLimits"), "{text}");
}
#[test]
fn cgmes_supplies_both_the_areas_and_the_schedule() {
let Some(dir) = config("MicroGrid/MicroGrid-Type1/MicroGrid-Type1-Merged") else {
return;
};
let out = run(&["solve", dir.to_str().unwrap(), "--area-interchange"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
assert!(text.contains("2 control area(s), 10 tie flow(s)"), "{text}");
assert!(text.contains("0 contested bus(es)"), "{text}");
assert!(text.contains("area interchange over"), "{text}");
assert!(text.contains("AreaInterchange"), "{text}");
assert!(text.contains("converged = true"), "{text}");
assert!(text.contains("BE"), "{text}");
assert!(text.contains("-236.977 MW exported"), "{text}");
assert!(text.contains("(dependent: takes the residual)"), "{text}");
}
#[test]
fn ucte_supplies_areas_from_country_codes() {
let path = openrao::fixture("TestCase12Nodes.uct");
let out = run(&["solve", path.to_str().unwrap(), "--area-interchange"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
for country in ["BE", "DE", "FR", "NL"] {
assert!(text.contains(country), "missing {country} in:\n{text}");
}
assert!(text.contains("converged = true"), "{text}");
}
#[test]
fn the_two_active_power_controls_are_refused_together() {
let path = openrao::fixture("TestCase12Nodes.uct");
let out = run(&[
"solve",
path.to_str().unwrap(),
"--area-interchange",
"--distribute-slack",
]);
assert_eq!(out.status.code(), Some(1));
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("subsumes"), "{err}");
}
#[test]
#[cfg(feature = "iidm")]
fn iidm_supplies_areas_from_its_own_elements() {
let path = openrao::fixture("TestCase12Nodes.xiidm");
let out = run(&["solve", path.to_str().unwrap(), "--area-interchange"]);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let text = stdout(&out);
assert!(text.contains("4 control area(s)"), "{text}");
assert!(
text.contains("0 contested bus(es), 0 bus(es) in no area"),
"{text}"
);
for country in ["BE", "DE", "FR", "NL"] {
assert!(text.contains(country), "missing {country} in:\n{text}");
}
assert!(text.contains("converged = true"), "{text}");
}
#[test]
#[cfg(feature = "iidm")]
fn area_interchange_without_areas_is_refused() {
let path = openrao::fixture("nordic32.xiidm");
let out = run(&["solve", path.to_str().unwrap(), "--area-interchange"]);
assert_eq!(out.status.code(), Some(1));
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("states none"), "{err}");
}
#[test]
fn dispatch_attributes_reactive_power_to_individual_machines() {
let Some(dir) = config("RealGrid/RealGrid-Merged") else {
return;
};
let text = stdout(&run(&[
"solve",
dir.to_str().unwrap(),
"--enforce-q-limits",
"--dispatch",
]));
assert!(
text.contains("reactive dispatch:"),
"{}",
&text[..text.len().min(2000)]
);
assert!(
text.contains("held by more than one"),
"shared buses should be called out"
);
assert!(
text.contains("split by Capability"),
"RealGrid's machines state a usable range"
);
assert!(
text.contains("% of the bus)"),
"each machine's share should be shown"
);
assert!(
text.contains("their own machines cannot produce"),
"RealGrid has buses where a co-located load pushes the machine past its limit"
);
}
#[test]
fn dispatch_says_so_when_the_format_carries_no_machines() {
let uct = openrao::fixture("2Nodes.uct");
let text = stdout(&run(&["solve", uct.to_str().unwrap(), "--dispatch"]));
assert!(
text.contains("no per-machine reactive capability"),
"{}",
&text[..text.len().min(1200)]
);
}
#[test]
fn remote_voltage_control_moves_the_control_onto_the_machine() {
let Some(dir) = config("MicroGrid/MicroGrid-Type1/MicroGrid-Type1-Merged") else {
return;
};
let path = dir.to_str().unwrap();
let off = stdout(&run(&["solve", path, "--enforce-q-limits"]));
assert!(!off.contains("remote voltage control"), "off by default");
let on = stdout(&run(&[
"solve",
path,
"--enforce-q-limits",
"--control-remote-voltage",
]));
assert!(on.contains("remote voltage control: 1 machine(s)"), "{on}");
assert!(
on.contains("Held"),
"the controller should reach its target"
);
assert!(on.contains("re-typed PV -> PQ"), "the held bus is freed");
assert!(on.contains("re-typed PQ -> PV"), "the machine is pinned");
}