Skip to main content

shifty_engine/
lib.rs

1//! Validation + SHACL-AF inference execution (Layers 3, 6, 7).
2//!
3//! Layer 3 lives here: the naive denotational evaluator that is the conformance
4//! oracle — relational path evaluation ([`path`]), value-type checks
5//! ([`value`]), and shape/schema satisfaction ([`validate`]). The rule/fixpoint
6//! inference engine (Layer 6) and compiled executors (Layer 7) come later; every
7//! execution mode must agree with this oracle.
8
9pub mod enumerate;
10pub mod frozen;
11pub mod gate;
12pub mod infer;
13mod native_exec;
14pub mod path;
15mod path_plan;
16pub mod profile;
17pub mod report;
18mod sparql;
19pub mod synthesize;
20pub mod validate;
21pub mod value;
22pub mod witness;
23
24pub use enumerate::{
25    EnumOptions, FixpointResult, RepairSolution, candidates, enumerate_repair, repair_to_fixpoint,
26};
27pub use gate::{RepairOutcome, apply, gate};
28pub use infer::{InferenceOutcome, infer, infer_graphs, infer_with_context};
29pub use report::{
30    ValidationReport, ValidationResult, report_to_graph, validate_report, validate_report_graphs,
31    validate_report_graphs_with_mode, validate_report_graphs_with_mode_and_options,
32    validate_report_with_options,
33};
34pub use synthesize::{synthesize, synthesize_focus};
35pub use validate::{
36    NonStratifiable, Reason, ValidationGraphMode, ValidationOptions, ValidationOutcome, Violation,
37    focus_nodes, validate, validate_graphs, validate_graphs_with_mode,
38    validate_graphs_with_mode_and_options, validate_plan, validate_plan_graphs,
39    validate_plan_graphs_with_mode, validate_plan_graphs_with_mode_and_options,
40    validate_plan_with_context, validate_plan_with_context_and_options, validate_plan_with_options,
41    validate_with_context, validate_with_context_and_options, validate_with_options,
42};
43pub use witness::{
44    BlockReason, FocusSat, FocusWitness, PathSupport, RelKind, SatTrace, Witness, satisfy_shape,
45    shape_id_for_iri, witness_node, witness_shape, witness_violations,
46};
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51    use oxrdf::Graph;
52    use shifty_parse::parse_turtle;
53
54    fn run(shapes_and_data: &str) -> ValidationOutcome {
55        let out = parse_turtle(shapes_and_data.as_bytes(), None).unwrap();
56        // data graph = the same graph (shapes + data coexist), as in the suite.
57        let loaded = shifty_parse::load_turtle(shapes_and_data.as_bytes(), None).unwrap();
58        validate(&loaded.graph, &out.schema).expect("stratifiable schema")
59    }
60
61    const PREFIXES: &str = r#"
62        @prefix sh:  <http://www.w3.org/ns/shacl#> .
63        @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
64        @prefix ex:  <http://ex/> .
65        @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
66    "#;
67
68    #[test]
69    fn inference_rules_fire_for_implicit_class_targets() {
70        let ttl = br#"
71            @prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
72            @prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
73            @prefix owl:   <http://www.w3.org/2002/07/owl#> .
74            @prefix sh:    <http://www.w3.org/ns/shacl#> .
75            @prefix ex:    <http://ex/> .
76
77            ex:Parent a owl:Class, sh:NodeShape ;
78                sh:rule [
79                    a sh:TripleRule ;
80                    sh:subject sh:this ;
81                    sh:predicate ex:hasTag ;
82                    sh:object ex:Tag
83                ] .
84
85            ex:Child rdfs:subClassOf ex:Parent .
86            ex:item a ex:Child .
87        "#;
88        let loaded = shifty_parse::load_turtle(ttl, None).unwrap();
89        let parsed = shifty_parse::parse_loaded(&loaded);
90        let normalized = shifty_opt::normalize(&parsed.schema);
91
92        let outcome = infer(&loaded.graph, &normalized).expect("stratifiable schema");
93
94        assert!(outcome.graph.contains(&oxrdf::Triple::new(
95            oxrdf::NamedNode::new_unchecked("http://ex/item"),
96            oxrdf::NamedNode::new_unchecked("http://ex/hasTag"),
97            oxrdf::NamedNode::new_unchecked("http://ex/Tag"),
98        )));
99    }
100
101    #[test]
102    fn planned_validation_preserves_severity_and_applies_threshold() {
103        let ttl = format!(
104            "{PREFIXES}
105            ex:S a sh:NodeShape ;
106                sh:targetNode ex:x ;
107                sh:property ex:InfoShape, ex:WarningShape .
108            ex:InfoShape a sh:PropertyShape ;
109                sh:path ex:required ;
110                sh:minCount 1 ;
111                sh:severity sh:Info .
112            ex:WarningShape a sh:PropertyShape ;
113                sh:path ex:required ;
114                sh:minCount 1 ;
115                sh:severity sh:Warning .
116            "
117        );
118        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
119        let parsed = shifty_parse::parse_loaded(&loaded);
120        let normalized = shifty_opt::normalize(&parsed.schema);
121        let plan = shifty_opt::plan(&normalized);
122
123        let info = validate_plan_with_options(
124            &loaded.graph,
125            &plan,
126            &ValidationOptions {
127                minimum_severity: shifty_algebra::Severity::Info,
128                sort_results: true,
129            },
130        )
131        .unwrap();
132        assert!(!info.conforms);
133        assert_eq!(info.violations.len(), 1);
134        assert_eq!(
135            info.violations[0].severity,
136            shifty_algebra::Severity::Warning
137        );
138        let mut severities: Vec<_> = info.violations[0]
139            .reasons
140            .iter()
141            .map(|reason| reason.severity.clone())
142            .collect();
143        severities.sort_by_key(shifty_algebra::Severity::rank);
144        assert_eq!(
145            severities,
146            vec![
147                shifty_algebra::Severity::Info,
148                shifty_algebra::Severity::Warning
149            ]
150        );
151
152        let warning = validate_plan_with_options(
153            &loaded.graph,
154            &plan,
155            &ValidationOptions {
156                minimum_severity: shifty_algebra::Severity::Warning,
157                sort_results: true,
158            },
159        )
160        .unwrap();
161        assert!(!warning.conforms);
162
163        let violation = validate_plan_with_options(
164            &loaded.graph,
165            &plan,
166            &ValidationOptions {
167                minimum_severity: shifty_algebra::Severity::Violation,
168                sort_results: true,
169            },
170        )
171        .unwrap();
172        assert!(violation.conforms);
173        assert_eq!(violation.violations.len(), 1);
174
175        let report = validate_report_with_options(
176            &loaded,
177            &loaded.graph,
178            &ValidationOptions {
179                minimum_severity: shifty_algebra::Severity::Violation,
180                sort_results: true,
181            },
182        );
183        assert!(report.conforms);
184        assert_eq!(report.results.len(), 2);
185    }
186
187    #[test]
188    fn validation_findings_sort_by_severity_then_focus_node() {
189        let ttl = format!(
190            "{PREFIXES}
191            ex:InfoShape a sh:NodeShape ;
192                sh:targetNode ex:a ;
193                sh:nodeKind sh:Literal ;
194                sh:severity sh:Info .
195            ex:WarningShape a sh:NodeShape ;
196                sh:targetNode ex:z ;
197                sh:nodeKind sh:Literal ;
198                sh:severity sh:Warning .
199            ex:ViolationShape a sh:NodeShape ;
200                sh:targetNode ex:m ;
201                sh:nodeKind sh:Literal .
202            "
203        );
204        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
205        let parsed = shifty_parse::parse_loaded(&loaded);
206        let plan = shifty_opt::plan(&shifty_opt::normalize(&parsed.schema));
207        let outcome = validate_plan(&loaded.graph, &plan).unwrap();
208
209        let ordered: Vec<_> = outcome
210            .violations
211            .iter()
212            .map(|finding| (finding.severity.clone(), finding.focus.to_string()))
213            .collect();
214        assert_eq!(
215            ordered,
216            vec![
217                (
218                    shifty_algebra::Severity::Violation,
219                    "<http://ex/m>".to_string()
220                ),
221                (
222                    shifty_algebra::Severity::Warning,
223                    "<http://ex/z>".to_string()
224                ),
225                (shifty_algebra::Severity::Info, "<http://ex/a>".to_string()),
226            ]
227        );
228    }
229
230    #[test]
231    fn reports_specific_failing_constraints() {
232        let ttl = format!(
233            "{PREFIXES}
234            ex:S a sh:NodeShape ;
235                sh:targetNode ex:x ;
236                sh:closed true ;
237                sh:ignoredProperties ( rdf:type ) ;
238                sh:property [ sh:path ex:age ; sh:datatype xsd:integer ; sh:maxCount 1 ] .
239            ex:x ex:age \"foo\" , 5 ; ex:extra 1 .
240            "
241        );
242        let outcome = run(&ttl);
243        assert!(!outcome.conforms);
244        assert_eq!(outcome.violations.len(), 1);
245        let msgs: Vec<&str> = outcome.violations[0]
246            .reasons
247            .iter()
248            .map(|r| r.message.as_str())
249            .collect();
250        // each distinct constraint is reported, not just "the node failed"
251        assert!(
252            msgs.iter().any(|m| m.contains("datatype(xsd:integer)")),
253            "missing datatype reason: {msgs:?}"
254        );
255        assert!(
256            msgs.iter().any(|m| m.contains("at most 1")),
257            "missing maxCount reason: {msgs:?}"
258        );
259        assert!(
260            msgs.iter()
261                .any(|m| m.contains("closed") && m.contains("extra")),
262            "missing closed reason: {msgs:?}"
263        );
264    }
265
266    #[test]
267    fn cardinality_and_datatype() {
268        let ttl = format!(
269            "{PREFIXES}
270            ex:S a sh:NodeShape ;
271                sh:targetNode ex:alice, ex:bob ;
272                sh:property [ sh:path ex:age ; sh:maxCount 1 ; sh:datatype xsd:integer ] .
273            ex:alice ex:age 30 .
274            ex:bob   ex:age 30 ; ex:age 40 .
275            "
276        );
277        let outcome = run(&ttl);
278        assert!(!outcome.conforms);
279        // only ex:bob violates maxCount 1
280        let bad: Vec<_> = outcome
281            .violations
282            .iter()
283            .map(|r| r.focus.to_string())
284            .collect();
285        assert_eq!(bad, vec!["<http://ex/bob>".to_string()]);
286    }
287
288    #[test]
289    fn qualified_value_shape_disjoint_uses_all_sibling_property_shapes() {
290        let ttl = format!(
291            "{PREFIXES}
292            ex:S a sh:NodeShape ;
293                sh:targetNode ex:x ;
294                sh:property ex:A, ex:B .
295            ex:A a sh:PropertyShape ;
296                sh:path ex:p ;
297                sh:qualifiedValueShape [ sh:class ex:TypeA ] ;
298                sh:qualifiedValueShapesDisjoint true ;
299                sh:qualifiedMinCount 1 .
300            ex:B a sh:PropertyShape ;
301                sh:path ex:q ;
302                sh:qualifiedValueShape [ sh:class ex:TypeB ] ;
303                sh:qualifiedValueShapesDisjoint true ;
304                sh:qualifiedMaxCount 10 .
305            ex:x ex:p ex:value .
306            ex:value a ex:TypeA, ex:TypeB .
307            "
308        );
309        let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
310        assert!(
311            parsed.diagnostics.is_empty(),
312            "diags: {:?}",
313            parsed.diagnostics
314        );
315        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
316
317        let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
318        assert!(!algebra.conforms);
319
320        let report = validate_report(&loaded, &loaded.graph);
321        assert!(!report.conforms);
322        assert_eq!(report.results.len(), 1);
323        assert_eq!(
324            report.results[0].component.as_str(),
325            "http://www.w3.org/ns/shacl#QualifiedMinCountConstraintComponent"
326        );
327    }
328
329    #[test]
330    fn disjoint_on_node_shape_uses_the_focus_node_as_the_value() {
331        let ttl = format!(
332            "{PREFIXES}
333            ex:S a sh:NodeShape ;
334                sh:targetNode ex:valid, ex:invalid ;
335                sh:disjoint ex:p .
336            ex:valid ex:p ex:other .
337            ex:invalid ex:p ex:invalid .
338            "
339        );
340        let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
341        assert!(
342            parsed.diagnostics.is_empty(),
343            "diags: {:?}",
344            parsed.diagnostics
345        );
346        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
347
348        let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
349        assert!(!algebra.conforms);
350        assert_eq!(algebra.violations.len(), 1);
351        assert_eq!(
352            algebra.violations[0].focus.to_string(),
353            "<http://ex/invalid>"
354        );
355
356        let normalized = shifty_opt::normalize(&parsed.schema);
357        let plan = shifty_opt::plan(&normalized);
358        let planned = validate_plan(&loaded.graph, &plan).unwrap();
359        assert_eq!(planned.conforms, algebra.conforms);
360        assert_eq!(planned.violations.len(), algebra.violations.len());
361
362        let report = validate_report(&loaded, &loaded.graph);
363        assert!(!report.conforms);
364        assert_eq!(report.results.len(), 1);
365        assert_eq!(
366            report.results[0].component.as_str(),
367            "http://www.w3.org/ns/shacl#DisjointConstraintComponent"
368        );
369        assert_eq!(
370            report.results[0].value.as_ref().map(ToString::to_string),
371            Some("<http://ex/invalid>".to_string())
372        );
373    }
374
375    #[test]
376    fn equals_on_node_shape_uses_the_focus_node_as_the_value() {
377        let ttl = format!(
378            "{PREFIXES}
379            ex:S a sh:NodeShape ;
380                sh:targetNode ex:valid, ex:extra, ex:missing ;
381                sh:equals ex:p .
382            ex:valid ex:p ex:valid .
383            ex:extra ex:p ex:extra, ex:other .
384            "
385        );
386        let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
387        assert!(
388            parsed.diagnostics.is_empty(),
389            "diags: {:?}",
390            parsed.diagnostics
391        );
392        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
393
394        let algebra = validate(&loaded.graph, &parsed.schema).unwrap();
395        assert!(!algebra.conforms);
396        let mut foci: Vec<_> = algebra
397            .violations
398            .iter()
399            .map(|violation| violation.focus.to_string())
400            .collect();
401        foci.sort();
402        assert_eq!(
403            foci,
404            [
405                "<http://ex/extra>".to_string(),
406                "<http://ex/missing>".to_string()
407            ]
408        );
409
410        let normalized = shifty_opt::normalize(&parsed.schema);
411        let plan = shifty_opt::plan(&normalized);
412        let planned = validate_plan(&loaded.graph, &plan).unwrap();
413        assert_eq!(planned.conforms, algebra.conforms);
414        assert_eq!(planned.violations.len(), algebra.violations.len());
415
416        let report = validate_report(&loaded, &loaded.graph);
417        assert!(!report.conforms);
418        assert_eq!(report.results.len(), 2);
419        assert!(report.results.iter().all(|result| result.component.as_str()
420            == "http://www.w3.org/ns/shacl#EqualsConstraintComponent"));
421    }
422
423    #[test]
424    fn datatype_violation() {
425        let ttl = format!(
426            "{PREFIXES}
427            ex:S a sh:NodeShape ;
428                sh:targetNode ex:x ;
429                sh:property [ sh:path ex:p ; sh:datatype xsd:integer ] .
430            ex:x ex:p \"hello\" .
431            "
432        );
433        assert!(!run(&ttl).conforms);
434    }
435
436    #[test]
437    fn nodekind_and_class_target() {
438        let ttl = format!(
439            "{PREFIXES}
440            ex:S a sh:NodeShape ;
441                sh:targetClass ex:Person ;
442                sh:property [ sh:path ex:knows ; sh:nodeKind sh:IRI ] .
443            ex:alice a ex:Person ; ex:knows ex:bob .
444            ex:carol a ex:Person ; ex:knows \"notaniri\" .
445            "
446        );
447        let outcome = run(&ttl);
448        assert!(!outcome.conforms);
449        let bad: Vec<_> = outcome
450            .violations
451            .iter()
452            .map(|r| r.focus.to_string())
453            .collect();
454        assert_eq!(bad, vec!["<http://ex/carol>".to_string()]);
455    }
456
457    #[test]
458    fn recursion_over_cyclic_data_terminates() {
459        // S requires every ex:knows neighbour to also satisfy S; data is a cycle.
460        let ttl = format!(
461            "{PREFIXES}
462            ex:S a sh:NodeShape ;
463                sh:targetNode ex:a ;
464                sh:property [ sh:path ex:knows ; sh:node ex:S ; sh:nodeKind sh:IRI ] .
465            ex:a ex:knows ex:b .
466            ex:b ex:knows ex:a .
467            "
468        );
469        // Must terminate; with all-IRI neighbours it conforms under the
470        // provisional cycle-breaking semantics.
471        assert!(run(&ttl).conforms);
472    }
473
474    #[test]
475    fn empty_graph_conforms() {
476        let outcome = validate(&Graph::new(), &shifty_algebra::Schema::new()).unwrap();
477        assert!(outcome.conforms);
478    }
479
480    #[test]
481    fn non_stratifiable_schema_is_diagnosed() {
482        // S := ¬∃p.S — recursion through negation; no defined 2-valued semantics.
483        let ttl = format!(
484            "{PREFIXES}
485            ex:S a sh:NodeShape ;
486                sh:targetNode ex:x ;
487                sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ; sh:qualifiedMinCount 1 ] .
488            ex:x ex:p ex:y .
489            "
490        );
491        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
492        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
493        assert!(validate(&loaded.graph, &out.schema).is_err());
494    }
495
496    fn triple(s: &str, p: &str, o: &str) -> oxrdf::Triple {
497        use oxrdf::NamedNode;
498        oxrdf::Triple::new(
499            NamedNode::new(s).unwrap(),
500            NamedNode::new(p).unwrap(),
501            NamedNode::new(o).unwrap(),
502        )
503    }
504
505    #[test]
506    fn triple_rule_infers_from_path() {
507        // copy each ex:knows value to ex:knows2
508        let ttl = format!(
509            "{PREFIXES}
510            ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
511                sh:rule [ a sh:TripleRule ;
512                    sh:subject sh:this ; sh:predicate ex:knows2 ;
513                    sh:object [ sh:path ex:knows ] ] .
514            ex:a a ex:Person ; ex:knows ex:b .
515            "
516        );
517        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
518        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
519        let outcome = infer(&loaded.graph, &out.schema).unwrap();
520        assert_eq!(outcome.inferred.len(), 1);
521        assert!(
522            outcome
523                .graph
524                .contains(&triple("http://ex/a", "http://ex/knows2", "http://ex/b"))
525        );
526    }
527
528    #[test]
529    fn inference_reaches_a_fixpoint() {
530        // ex:reaches := ex:knows ∪ (ex:knows / ex:reaches) — transitive closure
531        // a→b→c, so a reaches c is derivable only after b reaches c.
532        let ttl = format!(
533            "{PREFIXES}
534            ex:S a sh:NodeShape ; sh:targetClass ex:Person ;
535                sh:rule [ a sh:TripleRule ;
536                    sh:subject sh:this ; sh:predicate ex:reaches ;
537                    sh:object [ sh:path [ sh:alternativePath ( ex:knows ( ex:knows ex:reaches ) ) ] ] ] .
538            ex:a a ex:Person ; ex:knows ex:b .
539            ex:b a ex:Person ; ex:knows ex:c .
540            ex:c a ex:Person .
541            "
542        );
543        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
544        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
545        let outcome = infer(&loaded.graph, &out.schema).unwrap();
546        assert!(
547            outcome
548                .graph
549                .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/b"))
550        );
551        assert!(
552            outcome
553                .graph
554                .contains(&triple("http://ex/b", "http://ex/reaches", "http://ex/c"))
555        );
556        // the fixpoint result: a reaches c (only via b reaches c)
557        assert!(
558            outcome
559                .graph
560                .contains(&triple("http://ex/a", "http://ex/reaches", "http://ex/c"))
561        );
562    }
563
564    #[test]
565    fn later_order_output_reactivates_an_earlier_rule() {
566        let ttl = format!(
567            "{PREFIXES}
568            ex:S a sh:NodeShape ; sh:targetNode ex:x ;
569                sh:rule [
570                    a sh:TripleRule ; sh:order 0 ;
571                    sh:subject sh:this ; sh:predicate ex:done ;
572                    sh:object [ sh:path ex:ready ]
573                ] ;
574                sh:rule [
575                    a sh:TripleRule ; sh:order 1 ;
576                    sh:subject sh:this ; sh:predicate ex:ready ;
577                    sh:object ex:y
578                ] .
579            "
580        );
581        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
582        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
583        let outcome = infer(&loaded.graph, &out.schema).unwrap();
584
585        assert!(
586            outcome
587                .graph
588                .contains(&triple("http://ex/x", "http://ex/ready", "http://ex/y"))
589        );
590        assert!(
591            outcome
592                .graph
593                .contains(&triple("http://ex/x", "http://ex/done", "http://ex/y"))
594        );
595    }
596
597    #[test]
598    fn inferred_triples_can_create_new_rule_targets() {
599        let ttl = format!(
600            "{PREFIXES}
601            ex:Seed a sh:NodeShape ; sh:targetNode ex:x ;
602                sh:rule [
603                    a sh:TripleRule ;
604                    sh:subject sh:this ; sh:predicate ex:eligible ;
605                    sh:object ex:y
606                ] .
607            ex:Eligible a sh:NodeShape ; sh:targetSubjectsOf ex:eligible ;
608                sh:rule [
609                    a sh:TripleRule ;
610                    sh:subject sh:this ; sh:predicate ex:classified ;
611                    sh:object ex:yes
612                ] .
613            "
614        );
615        let out = parse_turtle(ttl.as_bytes(), None).unwrap();
616        let loaded = shifty_parse::load_turtle(ttl.as_bytes(), None).unwrap();
617        let outcome = infer(&loaded.graph, &out.schema).unwrap();
618
619        assert!(outcome.graph.contains(&triple(
620            "http://ex/x",
621            "http://ex/classified",
622            "http://ex/yes",
623        )));
624    }
625
626    #[test]
627    fn split_inference_uses_shapes_graph_as_rule_context() {
628        let shapes_ttl = format!(
629            "{PREFIXES}
630            ex:InverseShape a sh:NodeShape ;
631                sh:targetClass ex:Thing ;
632                sh:rule [
633                    a sh:SPARQLRule ;
634                    sh:construct \"\"\"
635                        CONSTRUCT {{ ?o ?inverse $this }}
636                        WHERE {{
637                            $this ?predicate ?o .
638                            ?predicate ex:inverseOf ?inverse .
639                        }}
640                    \"\"\"
641                ] .
642            ex:p ex:inverseOf ex:q .
643            "
644        );
645        let data_ttl = format!(
646            "{PREFIXES}
647            ex:a a ex:Thing ; ex:p ex:b .
648            "
649        );
650        let shapes = shifty_parse::load_turtle(shapes_ttl.as_bytes(), None).unwrap();
651        let parsed = shifty_parse::parse_loaded(&shapes);
652        let data = shifty_parse::load_turtle(data_ttl.as_bytes(), None).unwrap();
653
654        let outcome = infer_graphs(&data.graph, &shapes.graph, &parsed.schema).unwrap();
655
656        assert!(
657            outcome
658                .graph
659                .contains(&triple("http://ex/b", "http://ex/q", "http://ex/a"))
660        );
661        assert_eq!(outcome.inferred.len(), 1);
662    }
663}