use std::process::Command;
#[test]
fn version_subcommand_prints_package_version() {
let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
.arg("version")
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
String::from_utf8(output.stdout).unwrap().trim(),
env!("CARGO_PKG_VERSION")
);
}
#[test]
fn validation_executes_over_data_and_shapes_union() {
let dir = std::env::temp_dir().join(format!("shifty-cli-union-class-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let shapes = dir.join("shapes.ttl");
let data = dir.join("data.ttl");
std::fs::write(
&shapes,
r#"
@prefix ex: <http://ex/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
ex:Parent a rdfs:Class, sh:NodeShape ;
sh:property [ sh:path ex:forbidden ; sh:maxCount 0 ] .
ex:Child rdfs:subClassOf ex:Parent .
ex:shapeItem a ex:Child ; ex:forbidden ex:value .
"#,
)
.unwrap();
std::fs::write(
&data,
r#"
@prefix ex: <http://ex/> .
ex:item a ex:Child ; ex:forbidden ex:value .
"#,
)
.unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("conforms: false"), "stdout: {stdout}");
assert!(stdout.contains("<http://ex/item>"), "stdout: {stdout}");
assert!(
!stdout.contains("<http://ex/shapeItem>"),
"stdout: {stdout}"
);
let data_only = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
"--graph-mode",
"data",
])
.output()
.unwrap();
let data_stdout = String::from_utf8(data_only.stdout).unwrap();
assert!(
data_stdout.contains("conforms: true"),
"stdout: {data_stdout}"
);
let union_all = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
"--graph-mode",
"union-all",
])
.output()
.unwrap();
let union_all_stdout = String::from_utf8(union_all.stdout).unwrap();
assert!(
union_all_stdout.contains("<http://ex/item>"),
"stdout: {union_all_stdout}"
);
assert!(
union_all_stdout.contains("<http://ex/shapeItem>"),
"stdout: {union_all_stdout}"
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn sparql_constraints_see_the_shapes_graph() {
let dir = std::env::temp_dir().join(format!("shifty-cli-union-sparql-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let shapes = dir.join("shapes.ttl");
let data = dir.join("data.ttl");
std::fs::write(
&shapes,
r#"
@prefix ex: <http://ex/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
ex:enabled ex:value true .
ex:S a sh:NodeShape ;
sh:targetNode ex:item ;
sh:sparql [
sh:select "SELECT $this WHERE { ex:enabled ex:value true }"
] .
"#,
)
.unwrap();
std::fs::write(&data, "@prefix ex: <http://ex/> . ex:item ex:value 1 .").unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("conforms: false"), "stdout: {stdout}");
assert!(
stdout.contains("SPARQL constraint at") && stdout.contains("not satisfied"),
"stdout: {stdout}"
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn inference_executes_over_data_and_shapes_union() {
let dir = std::env::temp_dir().join(format!("shifty-cli-infer-union-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let shapes = dir.join("shapes.ttl");
let data = dir.join("data.ttl");
std::fs::write(
&shapes,
r#"
@prefix ex: <http://ex/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
ex:S a sh:NodeShape ;
sh:targetClass ex:Thing ;
sh:rule [
a sh:SPARQLRule ;
sh:construct """
CONSTRUCT { ?object ?inverse $this }
WHERE {
$this ?predicate ?object .
?predicate ex:inverseOf ?inverse .
}
"""
] .
ex:p ex:inverseOf ex:q .
"#,
)
.unwrap();
std::fs::write(
&data,
r#"
@prefix ex: <http://ex/> .
ex:a a ex:Thing ; ex:p ex:b .
"#,
)
.unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"infer",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("inferred 1 triple(s)"), "stdout: {stdout}");
assert!(
stdout.contains("<http://ex/b> <http://ex/q> <http://ex/a>"),
"stdout: {stdout}"
);
std::fs::remove_dir_all(dir).unwrap();
}
#[test]
fn validation_runs_inference_first() {
let dir =
std::env::temp_dir().join(format!("shifty-cli-validate-infer-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let shapes = dir.join("shapes.ttl");
let data = dir.join("data.ttl");
std::fs::write(
&shapes,
r#"
@prefix ex: <http://ex/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
ex:S a sh:NodeShape ;
sh:targetNode ex:item ;
sh:property [ sh:path ex:derived ; sh:maxCount 0 ] ;
sh:rule [
a sh:TripleRule ;
sh:subject sh:this ;
sh:predicate ex:derived ;
sh:object ex:value
] .
"#,
)
.unwrap();
std::fs::write(
&data,
"@prefix ex: <http://ex/> . ex:item ex:input ex:value .",
)
.unwrap();
let output = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("conforms: false"), "stdout: {stdout}");
assert!(stdout.contains("at most 0 value(s)"), "stdout: {stdout}");
let no_infer = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
"--no-infer",
])
.output()
.unwrap();
assert!(
no_infer.status.success(),
"stderr: {}",
String::from_utf8_lossy(&no_infer.stderr)
);
let no_infer_stdout = String::from_utf8(no_infer.stdout).unwrap();
assert!(
no_infer_stdout.contains("conforms: true"),
"stdout: {no_infer_stdout}"
);
let embedded_no_infer = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--no-infer",
])
.output()
.unwrap();
assert!(
embedded_no_infer.status.success(),
"stderr: {}",
String::from_utf8_lossy(&embedded_no_infer.stderr)
);
let embedded_no_infer_stdout = String::from_utf8(embedded_no_infer.stdout).unwrap();
assert!(
embedded_no_infer_stdout.contains("conforms: true"),
"stdout: {embedded_no_infer_stdout}"
);
let embedded_report_no_infer = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--no-infer",
"--report",
])
.output()
.unwrap();
assert!(
embedded_report_no_infer.status.success(),
"stderr: {}",
String::from_utf8_lossy(&embedded_report_no_infer.stderr)
);
let embedded_report_no_infer_stdout =
String::from_utf8(embedded_report_no_infer.stdout).unwrap();
assert!(
embedded_report_no_infer_stdout.contains("sh:conforms true"),
"stdout: {embedded_report_no_infer_stdout}"
);
let report = Command::new(env!("CARGO_BIN_EXE_shifty"))
.args([
"validate",
"--shapes",
shapes.to_str().unwrap(),
"--data",
data.to_str().unwrap(),
"--report",
])
.output()
.unwrap();
assert!(
report.status.success(),
"stderr: {}",
String::from_utf8_lossy(&report.stderr)
);
let report_stdout = String::from_utf8(report.stdout).unwrap();
assert!(
report_stdout.contains("sh:conforms false"),
"stdout: {report_stdout}"
);
assert!(
report_stdout.contains("sh:sourceConstraintComponent sh:MaxCountConstraintComponent"),
"stdout: {report_stdout}"
);
std::fs::remove_dir_all(dir).unwrap();
}