use shifty_engine::profile::{self, ExecutorKind};
use shifty_engine::{ValidationGraphMode, infer_graphs, validate, validate_plan_graphs_with_mode};
use std::path::Path;
pub fn assert_same_verdict(
label: &str,
left: &shifty_engine::ValidationOutcome,
right: &shifty_engine::ValidationOutcome,
) {
assert_eq!(
left.conforms, right.conforms,
"{label}: conforms mismatch (left={}, right={})",
left.conforms, right.conforms,
);
let mut left_foci: Vec<String> = left
.violations
.iter()
.map(|v| v.focus.to_string())
.collect();
let mut right_foci: Vec<String> = right
.violations
.iter()
.map(|v| v.focus.to_string())
.collect();
left_foci.sort();
left_foci.dedup();
right_foci.sort();
right_foci.dedup();
assert_eq!(
left_foci, right_foci,
"{label}: violation focus sets differ\n left: {left_foci:?}\n right: {right_foci:?}",
);
}
#[allow(dead_code)] pub fn assert_same_outcome(
label: &str,
left: &shifty_engine::ValidationOutcome,
right: &shifty_engine::ValidationOutcome,
) {
assert_same_verdict(label, left, right);
let mut foci: Vec<String> = left
.violations
.iter()
.map(|v| v.focus.to_string())
.collect();
foci.sort();
foci.dedup();
for focus in &foci {
let left_reasons = reason_set(left, focus);
let right_reasons = reason_set(right, focus);
assert_eq!(
left_reasons, right_reasons,
"{label}: reasons differ for focus {focus}\n left: {left_reasons:?}\n right: {right_reasons:?}",
);
}
}
fn reason_set(outcome: &shifty_engine::ValidationOutcome, focus: &str) -> Vec<String> {
let mut reasons: Vec<String> = outcome
.violations
.iter()
.filter(|v| v.focus.to_string() == focus)
.flat_map(|v| {
v.reasons.iter().map(|r| match &r.path {
Some(p) => format!("({p}) {}", r.message),
None => r.message.clone(),
})
})
.collect();
reasons.sort();
reasons.dedup();
reasons
}
fn load_ws(rel: &str) -> shifty_parse::Loaded {
let path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../")
.join(rel);
let bytes =
std::fs::read(&path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
shifty_parse::load_turtle(&bytes, None)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display()))
}
#[test]
fn nist_bdg1_known_violations_against_223p_closure() {
let shapes = load_ws("benchmark/s223/223p-closure.ttl");
let data = load_ws("benchmark/s223/models/nist-bdg1-1.ttl");
let parsed = shifty_parse::parse_loaded(&shapes);
let normalized = shifty_opt::normalize(&parsed.schema);
let physical = shifty_opt::plan(&normalized);
let inference = infer_graphs(&data.graph, &shapes.graph, &normalized)
.expect("223P schema must be stratifiable");
let outcome = validate_plan_graphs_with_mode(
&inference.graph,
&shapes.graph,
&physical,
ValidationGraphMode::Union,
)
.expect("validated schema must be stratifiable");
let mut foci: Vec<String> = outcome
.violations
.iter()
.map(|v| v.focus.to_string())
.collect();
foci.sort();
foci.dedup();
assert_eq!(
foci,
[
"<http://qudt.org/vocab/unit/DEG_F>",
"<http://qudt.org/vocab/unit/FT3-PER-MIN>",
"<http://qudt.org/vocab/unit/PSI>",
],
"NIST/223P-closure violation set changed",
);
}
#[test]
fn reference_and_plan_agree_on_nist_bdg1() {
let shapes = load_ws("benchmark/s223/223p.ttl");
let data = load_ws("benchmark/s223/models/nist-bdg1-1.ttl");
let parsed = shifty_parse::parse_loaded(&shapes);
let normalized = shifty_opt::normalize(&parsed.schema);
let physical = shifty_opt::plan(&normalized);
let inference = infer_graphs(&data.graph, &shapes.graph, &normalized).expect("stratifiable");
let ref_outcome = shifty_engine::validate_graphs_with_mode(
&inference.graph,
&shapes.graph,
&parsed.schema,
ValidationGraphMode::Union,
)
.expect("stratifiable");
let plan_outcome = validate_plan_graphs_with_mode(
&inference.graph,
&shapes.graph,
&physical,
ValidationGraphMode::Union,
)
.expect("stratifiable");
assert_same_verdict("223P/NIST bdg1-1", &ref_outcome, &plan_outcome);
}
#[test]
fn native_executor_fires_on_bgp_constraint() {
let ttl = r#"
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix ex: <http://ex/> .
ex:S a sh:NodeShape ;
sh:targetNode ex:a, ex:d ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:select "SELECT $this ?value WHERE { $this <http://ex/p> ?value . ?value <http://ex/flag> <http://ex/bad> }"
] .
ex:a ex:p ex:b, ex:c .
ex:c ex:flag ex:bad .
ex:d ex:p ex:e .
"#;
let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).expect("valid Turtle");
let parsed = shifty_parse::parse_turtle(ttl.as_bytes(), None).expect("valid shapes");
assert!(
parsed.diagnostics.is_empty(),
"diags: {:?}",
parsed.diagnostics
);
profile::enable();
let outcome = validate(&loaded.graph, &parsed.schema).expect("stratifiable");
let profile = profile::take().expect("profiling was enabled");
assert!(!outcome.conforms);
assert_eq!(outcome.violations.len(), 1);
assert_eq!(outcome.violations[0].focus.to_string(), "<http://ex/a>");
assert_eq!(
outcome.violations[0].reasons[0].value.to_string(),
"<http://ex/c>",
);
assert!(
profile
.records()
.iter()
.any(|r| r.executor == ExecutorKind::Native),
"expected a native execution record; got: {:?}",
profile.records(),
);
}