use std::sync::Arc;
use powerio::{ArtifactPath, Destination, EmittedOutput, Fidelity, PioValue, Source};
fn tree(files: &[(&str, &str)], primary: Option<&str>) -> Source {
Source::from_memory_tree(
"project",
files.iter().map(|(name, text)| {
(
ArtifactPath::new(*name).unwrap(),
Arc::<[u8]>::from(text.as_bytes()),
)
}),
primary.map(|name| ArtifactPath::new(name).unwrap()),
)
.unwrap()
}
#[test]
fn pypsa_memory_directory_parses_and_echoes_every_file() {
let files = [
("network.csv", "name\nexample\n"),
("buses.csv", "name,v_nom\nB1,138.0\nB2,138.0\n"),
("loads.csv", "name,bus,p_set,q_set\nL1,B2,5.0,1.0\n"),
(
"generators.csv",
"name,bus,control,p_nom,p_set\nG1,B1,Slack,100.0,12.0\n",
),
];
let source = tree(&files, None);
assert!(source.is_directory());
assert!(source.primary_buffer().is_err());
let module = powerio::parse(source).unwrap();
let PioValue::BalancedNetwork(network) = module.value() else {
panic!("network expected")
};
assert_eq!(network.buses().len(), 2);
let result = powerio::emit(&module, "pypsa-csv", Destination::memory("out").unwrap()).unwrap();
assert_eq!(result.fidelity(), Fidelity::ExactSameFormat);
let EmittedOutput::Memory { artifacts } = result.output() else {
panic!("memory expected")
};
assert_eq!(artifacts.len(), files.len());
for (name, bytes) in files {
let artifact = artifacts
.iter()
.find(|a| a.name().as_str().ends_with(name))
.unwrap();
assert_eq!(artifact.bytes(), bytes.as_bytes());
}
}
#[test]
fn nested_dss_primary_resolves_sibling_and_parent_paths() {
let files = [
(
"model/Master.dss",
"Clear\nNew Circuit.example basekv=12.47 bus1=source\nRedirect lines.dss\nRedirect ../loads.dss\n",
),
(
"model/lines.dss",
"New Line.l bus1=source bus2=load phases=3 r1=0.1 x1=0.2 r0=0.3 x0=0.4 length=1\n",
),
(
"loads.dss",
"New Load.ld bus1=load phases=3 conn=wye kv=12.47 kw=10 kvar=2\n",
),
];
let source = tree(&files, Some("model/Master.dss"));
assert!(!source.is_directory());
assert_eq!(source.name(), "model/Master.dss");
let primary = source.primary_buffer().unwrap();
assert_eq!(
source
.referenced_buffer(&primary, "../loads.dss")
.unwrap()
.bytes(),
files[2].1.as_bytes()
);
assert!(
source
.referenced_buffer(&primary, "../../outside.dss")
.is_err()
);
let module = powerio::parse(source).unwrap();
let PioValue::MulticonductorNetwork(network) = module.value() else {
panic!("distribution network expected")
};
assert_eq!(network.lines().len(), 1);
assert_eq!(network.loads().len(), 1);
assert!(
!module
.diagnostics()
.iter()
.any(|entry| entry.code().starts_with("READ.DSS.INCLUDE"))
);
assert_eq!(module.sources().len(), 3);
let echoed = powerio::emit(&module, "dss", Destination::memory("out").unwrap()).unwrap();
assert_eq!(echoed.fidelity(), Fidelity::ExactSameFormat);
let EmittedOutput::Memory { artifacts } = echoed.output() else {
panic!("memory artifacts expected")
};
assert_eq!(artifacts.len(), files.len() + 1);
for (path, text) in files {
let artifact = artifacts
.iter()
.find(|artifact| artifact.name().as_str() == format!("out/source/{path}"))
.unwrap();
assert_eq!(artifact.bytes(), text.as_bytes());
}
powerio::emit(
&module,
"pmd-json",
Destination::memory("case.json").unwrap(),
)
.unwrap();
}
#[test]
fn memory_tree_dss_includes_cannot_escape_the_supplied_root() {
let source = tree(
&[(
"model/Master.dss",
"Clear\nNew Circuit.example basekv=12.47 bus1=source\nRedirect ../../outside.dss\n",
)],
Some("model/Master.dss"),
);
let module = powerio::parse(source).unwrap();
assert!(
module
.diagnostics()
.iter()
.any(|entry| entry.code() == "READ.DSS.INCLUDE_REFUSED")
);
assert_eq!(module.sources().len(), 1);
}
#[test]
fn trees_refuse_duplicate_missing_and_over_budget_files() {
let files = || {
[(
ArtifactPath::new("a").unwrap(),
Arc::<[u8]>::from(&b"x"[..]),
)]
};
assert!(Source::from_memory_tree("project", files().into_iter().chain(files()), None).is_err());
assert!(
Source::from_memory_tree(
"project",
files(),
Some(ArtifactPath::new("missing").unwrap())
)
.is_err()
);
let too_many = (0..4097).map(|n| {
(
ArtifactPath::new(format!("file{n}")).unwrap(),
Arc::<[u8]>::from(&b""[..]),
)
});
assert!(Source::from_memory_tree("project", too_many, None).is_err());
let deep = format!("{}a", "d/".repeat(64));
assert!(
Source::from_memory_tree(
"project",
[(
ArtifactPath::new(deep).unwrap(),
Arc::<[u8]>::from(&b""[..])
)],
None
)
.is_err()
);
}
#[test]
fn format_catalog_tokens_resolve_and_read_only_targets_stay_read_only() {
let catalog: Vec<_> = powerio::grid_formats().collect();
for entry in &catalog {
assert_eq!(
powerio::resolve_format(entry.format.token),
Some(entry.format)
);
}
for token in ["pwb", "ieee-cdf", "opfdata-json"] {
assert!(
!catalog
.iter()
.find(|entry| entry.format.token == token)
.unwrap()
.format
.can_emit
);
}
assert!(
catalog
.iter()
.any(|entry| entry.format.token == "bmopf-json@0.2.0")
);
}
#[test]
fn catalog_includes_explicit_cim_and_sincal_reader_families() {
use powerio::NetworkFamily::{Distribution, Transmission};
let catalog: Vec<_> = powerio::grid_formats().collect();
let mut expected = vec![("cim-dist", Distribution, true)];
if cfg!(feature = "sincal") {
expected.push(("sincal-balanced", Transmission, false));
expected.push(("sincal-multiconductor", Distribution, false));
}
assert_eq!(
catalog
.iter()
.any(|entry| entry.format.token.starts_with("sincal")),
cfg!(feature = "sincal")
);
for (token, family, can_emit) in expected {
let entry = catalog
.iter()
.find(|entry| entry.format.token == token)
.unwrap();
assert_eq!(entry.family, family, "{token}");
assert!(entry.can_read, "{token}");
assert_eq!(entry.format.can_emit, can_emit, "{token}");
}
assert!(!catalog.iter().any(|entry| entry.format.token == "sincal"));
}
#[test]
fn dss_project_entry_names_reparse_or_are_refused() {
for name in ["model/entry\"file.dss", "model/entry\"')] }file.dss"] {
let module = powerio::parse(tree(
&[(name, "New Circuit.example basekv=12.47 bus1=source\n")],
Some(name),
))
.unwrap();
let result = powerio::emit(&module, "dss", Destination::memory("out").unwrap());
if name.contains('}') {
assert!(result.is_err());
continue;
}
let EmittedOutput::Memory { artifacts } = result.unwrap().into_output() else {
panic!("memory artifacts expected")
};
let source = Source::from_memory_tree(
"output",
artifacts.into_iter().map(|artifact| {
let path =
ArtifactPath::new(artifact.name().as_str().strip_prefix("out/").unwrap())
.unwrap();
(path, Arc::<[u8]>::from(artifact.into_bytes()))
}),
Some(ArtifactPath::new("case.dss").unwrap()),
)
.unwrap();
let parsed = powerio::parse(source).unwrap();
assert!(
!parsed
.diagnostics()
.iter()
.any(|entry| entry.code().starts_with("READ.DSS.INCLUDE"))
);
assert_eq!(parsed.sources().len(), 2);
}
}