use std::process::Command;
struct Fixture {
dir: std::path::PathBuf,
}
impl Fixture {
fn new(name: &str) -> Self {
let dir =
std::env::temp_dir().join(format!("shifty-cli-profile-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
Self { dir }
}
fn write(&self, name: &str, contents: &str) -> String {
let path = self.dir.join(name);
std::fs::write(&path, contents).unwrap();
path.to_str().unwrap().to_string()
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn run(args: &[&str]) -> String {
let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args(args)
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap()
}
const SHAPES: &str = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://ex/> .
ex:S a sh:NodeShape ;
sh:targetSubjectsOf ex:p ;
sh:property [ sh:path ex:p ; sh:maxCount 4 ] .
"#;
const LITERATE: &str = r#"# Some notes
## A heading that is also a Turtle comment
@prefix ex: <http://ex/> .
ex:a ex:p ex:b .
ex:b ex:p ex:c .
"#;
#[test]
fn profile_names_the_format_and_count_of_every_input() {
let fx = Fixture::new("inputs");
let shapes = fx.write("shapes.ttl", SHAPES);
let data = fx.write("data.ttl.md", LITERATE);
let stdout = run(&[
"validate",
"--shapes",
&shapes,
"--data",
&data,
"--profile",
]);
assert!(stdout.starts_with("conforms: true"), "stdout: {stdout}");
assert!(
stdout.contains(&format!("profile: data: 2 triples from {data} [turtle]")),
"stdout: {stdout}"
);
assert!(
stdout.contains(&format!(
"profile: shapes: 5 triples from {shapes} [turtle]"
)),
"stdout: {stdout}"
);
assert!(
stdout.contains("profile: inference: 0 triples added before validation"),
"stdout: {stdout}"
);
for stage in [
"shapes load",
"compile",
"data load",
"session and inference",
"first validation",
"export",
] {
assert!(
stdout.contains(&format!("profile: stage: {stage}: ")),
"missing {stage} timing in stdout: {stdout}"
);
}
assert!(stdout.contains("profile: shape cache:"), "stdout: {stdout}");
}
#[test]
fn profile_is_silent_without_the_flag() {
let fx = Fixture::new("silent");
let shapes = fx.write("shapes.ttl", SHAPES);
let data = fx.write("data.ttl.md", LITERATE);
let stdout = run(&["validate", "--shapes", &shapes, "--data", &data]);
assert_eq!(stdout.trim(), "conforms: true");
}
#[test]
fn profile_separates_merged_sources_from_their_overlap() {
let fx = Fixture::new("merged");
let shapes = fx.write("shapes.ttl", SHAPES);
let first = fx.write(
"first.ttl",
"@prefix ex: <http://ex/> .\nex:a ex:p ex:b .\n",
);
let second = fx.write(
"second.ttl",
"@prefix ex: <http://ex/> .\nex:a ex:p ex:b .\nex:b ex:p ex:c .\n",
);
let stdout = run(&[
"validate",
"--shapes",
&shapes,
"--data",
&first,
"--data",
&second,
"--profile",
]);
assert!(
stdout.contains("profile: data: 2 triples from 2 sources (1 triple dropped as duplicate)"),
"stdout: {stdout}"
);
assert!(
stdout.contains(&format!(" {first}: 1 triple [turtle]")),
"stdout: {stdout}"
);
assert!(
stdout.contains(&format!(" {second}: 2 triples [turtle]")),
"stdout: {stdout}"
);
}
#[test]
fn profile_says_when_the_shapes_graph_is_also_the_data_graph() {
let fx = Fixture::new("selfdata");
let shapes = fx.write("shapes.ttl", SHAPES);
let stdout = run(&["validate", "--shapes", &shapes, "--profile"]);
assert!(
stdout.contains("profile: data: none given; the shapes graph is also the data graph"),
"stdout: {stdout}"
);
}
#[test]
fn profile_records_that_inference_was_skipped() {
let fx = Fixture::new("noinfer");
let shapes = fx.write("shapes.ttl", SHAPES);
let data = fx.write("data.ttl.md", LITERATE);
let stdout = run(&[
"validate",
"--shapes",
&shapes,
"--data",
&data,
"--no-infer",
"--profile",
]);
assert!(
stdout.contains("profile: inference: skipped (--no-infer)"),
"stdout: {stdout}"
);
}
#[test]
fn infer_profiles_its_inputs_too() {
let fx = Fixture::new("infer");
let shapes = fx.write("shapes.ttl", SHAPES);
let data = fx.write("data.ttl.md", LITERATE);
let stdout = run(&["infer", "--shapes", &shapes, "--data", &data, "--profile"]);
assert!(
stdout.contains(&format!("profile: data: 2 triples from {data} [turtle]")),
"stdout: {stdout}"
);
for stage in [
"shapes load",
"compile",
"data load",
"session and inference",
"export",
] {
assert!(
stdout.contains(&format!("profile: stage: {stage}:")),
"stdout: {stdout}"
);
}
}
#[test]
fn validation_does_not_project_inferred_data_without_a_dump() {
let fx = Fixture::new("lazy-data");
let shapes = fx.write(
"shapes.ttl",
r#"@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://ex/> .
ex:S a sh:NodeShape ; sh:targetNode ex:a ;
sh:rule [ a sh:TripleRule ; sh:subject sh:this ;
sh:predicate ex:out ; sh:object ex:value ] ."#,
);
let data = fx.write(
"data.ttl",
"<http://ex/a> <http://ex/seed> <http://ex/value> .",
);
for report in [false, true] {
let mut args = vec![
"validate",
"--shapes",
&shapes,
"--data",
&data,
"--profile",
];
if report {
args.push("--report");
}
let stdout = run(&args);
assert!(
stdout.contains("0 compatibility projection(s) / 0 row(s)"),
"stdout: {stdout}"
);
}
}
#[test]
fn report_mode_keeps_the_profile_after_the_document() {
let fx = Fixture::new("report");
let shapes = fx.write("shapes.ttl", SHAPES);
let data = fx.write("data.ttl.md", LITERATE);
let stdout = run(&[
"validate",
"--shapes",
&shapes,
"--data",
&data,
"--report",
"--profile",
]);
let (document, profile) = stdout.split_once("profile: ").unwrap();
assert!(document.contains("sh:ValidationReport"), "stdout: {stdout}");
assert!(profile.starts_with("shapes: "), "stdout: {stdout}");
}