1use crate::frozen::FrozenIndexedDataset;
9use crate::sparql::SparqlExecutor;
10use crate::validate::{
11 NonStratifiable, ShapeEvaluator, ValidationGraphMode, ValidationOptions,
12 entry_shape_any_name_selected, focus_nodes_with_evaluator, graph_union,
13 prefetch_sparql_constraints, uses_shapes_graph,
14};
15use crate::witness::{
16 ChildEvaluation, ConstraintCatalog, ConstraintRecord, EvaluationProgress, EvaluationStatus,
17 Evidence, EvidenceRun, EvidenceSummary, FocusEvaluation, StatementEvaluation, Witness,
18 materialize_evidence,
19};
20use oxrdf::Graph;
21use shifty_algebra::{ConstraintKind, Schema, Severity, Shape, ShapeArena, ShapeId};
22use shifty_opt::{analyze, normalize_with_mapping};
23use std::collections::HashSet;
24
25pub struct PreparedEvidenceValidator {
32 data: Graph,
33 raw_schema: Schema,
34 schema: Schema,
35 raw_by_normalized: Vec<Vec<usize>>,
36 shape_map: Vec<Option<ShapeId>>,
37 sparql: SparqlExecutor,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub struct ConformanceRun {
47 pub conforms: bool,
48 pub selected_pairs: usize,
49 pub passed: usize,
50 pub failed: usize,
51}
52
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
58pub struct ConformanceOptions {
59 pub entry_shape_names: Vec<String>,
60}
61
62impl From<&ValidationOptions> for ConformanceOptions {
63 fn from(options: &ValidationOptions) -> Self {
64 Self {
65 entry_shape_names: options.entry_shape_names.clone(),
66 }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct SelectedPair {
79 normalized_statement: usize,
80 focus: oxrdf::Term,
81 source_statements: Vec<usize>,
82}
83
84impl SelectedPair {
85 fn new(normalized_statement: usize, focus: oxrdf::Term, source_statements: Vec<usize>) -> Self {
86 Self {
87 normalized_statement,
88 focus,
89 source_statements,
90 }
91 }
92
93 pub fn normalized_statement(&self) -> usize {
94 self.normalized_statement
95 }
96
97 pub fn focus(&self) -> &oxrdf::Term {
98 &self.focus
99 }
100
101 pub fn source_statements(&self) -> &[usize] {
102 &self.source_statements
103 }
104}
105
106impl PreparedEvidenceValidator {
107 pub fn new(data: &Graph, schema: &Schema) -> Result<Self, NonStratifiable> {
109 let uses_shapes = uses_shapes_graph(&schema.arena);
110 let frozen = if uses_shapes {
111 FrozenIndexedDataset::from_graphs(data, data)
112 } else {
113 FrozenIndexedDataset::from_graph(data)
114 };
115 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
116 }
117
118 pub fn with_context(
120 data: &Graph,
121 context: &Graph,
122 schema: &Schema,
123 ) -> Result<Self, NonStratifiable> {
124 let uses_shapes = uses_shapes_graph(&schema.arena);
125 let frozen = if uses_shapes {
126 FrozenIndexedDataset::from_graphs(context, context)
127 } else {
128 FrozenIndexedDataset::from_graph(context)
129 };
130 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
131 }
132
133 pub fn with_graphs(
135 data: &Graph,
136 shapes: &Graph,
137 schema: &Schema,
138 mode: ValidationGraphMode,
139 ) -> Result<Self, NonStratifiable> {
140 let uses_shapes = uses_shapes_graph(&schema.arena);
141 match mode {
142 ValidationGraphMode::Data => {
143 let frozen = if uses_shapes {
144 FrozenIndexedDataset::from_graphs(data, shapes)
145 } else {
146 FrozenIndexedDataset::from_graph(data)
147 };
148 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
149 }
150 ValidationGraphMode::Union => {
151 let frozen = if uses_shapes {
152 FrozenIndexedDataset::from_graph_union_with_shapes(data, shapes)
153 } else {
154 FrozenIndexedDataset::from_graph_union(data, shapes)
155 };
156 Self::from_frozen(data.clone(), schema, frozen, uses_shapes)
157 }
158 ValidationGraphMode::UnionAll => {
159 let union = graph_union(data, shapes);
160 let frozen = if uses_shapes {
161 FrozenIndexedDataset::from_graphs(&union, &union)
162 } else {
163 FrozenIndexedDataset::from_graph(&union)
164 };
165 Self::from_frozen(union, schema, frozen, uses_shapes)
166 }
167 }
168 }
169
170 fn from_frozen(
171 data: Graph,
172 raw_schema: &Schema,
173 frozen: FrozenIndexedDataset,
174 has_shapes_graph: bool,
175 ) -> Result<Self, NonStratifiable> {
176 let normalized = normalize_with_mapping(raw_schema);
177 let stratification = analyze(&normalized.schema.arena);
178 if !stratification.stratifiable {
179 let components = stratification
180 .strata
181 .iter()
182 .filter(|stratum| !stratum.stratifiable)
183 .map(|stratum| stratum.shapes.clone())
184 .collect();
185 return Err(NonStratifiable { components });
186 }
187
188 let mut raw_by_normalized = vec![Vec::new(); normalized.schema.statements.len()];
189 for (raw, normalized_id) in normalized.statement_map.iter().copied().enumerate() {
190 raw_by_normalized[normalized_id].push(raw);
191 }
192
193 Ok(Self {
194 data,
195 raw_schema: raw_schema.clone(),
196 schema: normalized.schema,
197 raw_by_normalized,
198 shape_map: normalized.shape_map,
199 sparql: SparqlExecutor::from_frozen(frozen, has_shapes_graph),
200 })
201 }
202
203 pub fn data(&self) -> &Graph {
209 &self.data
210 }
211
212 pub fn validate_conformance(&self, options: &ConformanceOptions) -> ConformanceRun {
224 self.scan_conformance(options, |_, _, _| {})
225 }
226
227 pub fn find_failures(
236 &self,
237 options: &ConformanceOptions,
238 ) -> (ConformanceRun, Vec<SelectedPair>) {
239 let selected_sources: Vec<Vec<usize>> = self
240 .raw_by_normalized
241 .iter()
242 .map(|sources| {
243 sources
244 .iter()
245 .copied()
246 .filter(|&source| {
247 self.source_statement_selected(source, &options.entry_shape_names)
248 })
249 .collect()
250 })
251 .collect();
252 let mut failures = Vec::new();
253 let run = self.scan_conformance(options, |statement, focus, holds| {
254 if !holds {
255 failures.push(SelectedPair::new(
256 statement,
257 focus.clone(),
258 selected_sources[statement].clone(),
259 ));
260 }
261 });
262 (run, failures)
263 }
264
265 fn scan_conformance(
272 &self,
273 options: &ConformanceOptions,
274 mut observe: impl FnMut(usize, &oxrdf::Term, bool),
275 ) -> ConformanceRun {
276 let backend = self
277 .sparql
278 .frozen()
279 .expect("prepared evidence validator always owns a frozen dataset");
280 let mut evaluator = ShapeEvaluator::new(backend, &self.schema.arena, &self.sparql);
281 let mut run = ConformanceRun {
282 conforms: true,
283 selected_pairs: 0,
284 passed: 0,
285 failed: 0,
286 };
287
288 for (statement_id, statement) in self.schema.statements.iter().enumerate() {
289 if !entry_shape_any_name_selected(
290 &options.entry_shape_names,
291 self.schema.names_of(statement.shape),
292 ) {
293 continue;
294 }
295
296 let foci = focus_nodes_with_evaluator(&self.data, &statement.selector, &mut evaluator);
297 prefetch_sparql_constraints(&self.schema.arena, statement.shape, &foci, &self.sparql);
298
299 for focus in foci {
300 run.selected_pairs += 1;
301 let holds = evaluator.holds(&focus, statement.shape);
302 if holds {
303 run.passed += 1;
304 } else {
305 run.failed += 1;
306 run.conforms = false;
307 }
308 observe(statement_id, &focus, holds);
309 }
310 }
311
312 run
313 }
314
315 pub fn explain(&self, pair: &SelectedPair) -> Vec<StatementEvaluation> {
334 self.explain_with_progress(pair, true)
335 }
336
337 pub fn explain_canonical(&self, pair: &SelectedPair) -> Vec<StatementEvaluation> {
343 self.explain_with_progress(pair, false)
344 }
345
346 fn explain_with_progress(
347 &self,
348 pair: &SelectedPair,
349 include_progress: bool,
350 ) -> Vec<StatementEvaluation> {
351 let Some(statement) = self.schema.statements.get(pair.normalized_statement) else {
352 return Vec::new();
353 };
354 let backend = self
355 .sparql
356 .frozen()
357 .expect("prepared evidence validator always owns a frozen dataset");
358 let mut evaluator = ShapeEvaluator::new(backend, &self.schema.arena, &self.sparql);
359 prefetch_sparql_constraints(
360 &self.schema.arena,
361 statement.shape,
362 std::slice::from_ref(&pair.focus),
363 &self.sparql,
364 );
365 let evidence = materialize_evidence(&mut evaluator, &pair.focus, statement.shape);
366
367 self.raw_by_normalized[pair.normalized_statement]
368 .iter()
369 .filter(|raw_statement| pair.source_statements.contains(raw_statement))
370 .map(|&raw_statement| {
371 let raw = &self.raw_schema.statements[raw_statement];
372 let progress = include_progress
373 .then(|| {
374 source_progress(
375 &mut evaluator,
376 &self.raw_schema.arena,
377 raw.shape,
378 &self.shape_map,
379 &pair.focus,
380 )
381 })
382 .flatten();
383 let mut evaluation = statement_evaluation(
384 &self.raw_schema,
385 &self.schema,
386 raw_statement,
387 pair.normalized_statement,
388 );
389 evaluation.selected_foci.push(FocusEvaluation {
390 focus: pair.focus.clone(),
391 evidence: evidence.clone(),
392 progress,
393 });
394 evaluation
395 })
396 .collect()
397 }
398
399 fn source_statement_selected(
406 &self,
407 raw_statement: usize,
408 entry_shape_names: &[String],
409 ) -> bool {
410 let raw = &self.raw_schema.statements[raw_statement];
411 entry_shape_any_name_selected(entry_shape_names, self.raw_schema.names_of(raw.shape))
412 }
413
414 pub fn source_statements(&self, normalized: usize) -> &[usize] {
421 self.raw_by_normalized
422 .get(normalized)
423 .map_or(&[], Vec::as_slice)
424 }
425
426 pub fn explain_constraint(&self, focus: &oxrdf::Term, constraint: ShapeId) -> Option<Evidence> {
439 if (constraint.0 as usize) >= self.schema.arena.len() {
440 return None;
441 }
442 let backend = self
443 .sparql
444 .frozen()
445 .expect("prepared evidence validator always owns a frozen dataset");
446 let mut evaluator = ShapeEvaluator::new(backend, &self.schema.arena, &self.sparql);
447 prefetch_sparql_constraints(
448 &self.schema.arena,
449 constraint,
450 std::slice::from_ref(focus),
451 &self.sparql,
452 );
453 Some(materialize_evidence(&mut evaluator, focus, constraint))
454 }
455
456 pub fn constraints(&self) -> ConstraintCatalog {
462 ConstraintCatalog {
463 source: constraint_catalog(&self.raw_schema.arena),
464 normalized: constraint_catalog(&self.schema.arena),
465 }
466 }
467
468 pub fn validate(&self, options: &ValidationOptions) -> EvidenceRun {
470 self.validate_with_progress(options, true)
471 }
472
473 pub fn validate_canonical(&self, options: &ValidationOptions) -> EvidenceRun {
478 self.validate_with_progress(options, false)
479 }
480
481 fn validate_with_progress(
482 &self,
483 options: &ValidationOptions,
484 include_progress: bool,
485 ) -> EvidenceRun {
486 let backend = self
487 .sparql
488 .frozen()
489 .expect("prepared evidence validator always owns a frozen dataset");
490 let mut evaluator = ShapeEvaluator::new(backend, &self.schema.arena, &self.sparql);
491 let mut statements: Vec<Option<StatementEvaluation>> =
492 vec![None; self.raw_schema.statements.len()];
493 let mut conforms = true;
494 let profiling = crate::profile::is_enabled();
497
498 for (statement_id, statement) in self.schema.statements.iter().enumerate() {
499 if !entry_shape_any_name_selected(
500 &options.entry_shape_names,
501 self.schema.names_of(statement.shape),
502 ) {
503 continue;
504 }
505
506 let label = profiling.then(|| {
507 self.schema
508 .name_of(statement.shape)
509 .map(str::to_string)
510 .unwrap_or_else(|| format!("@{}", statement.shape.0))
511 });
512 let selection_start = profiling.then(web_time::Instant::now);
513 let foci = focus_nodes_with_evaluator(&self.data, &statement.selector, &mut evaluator);
514 prefetch_sparql_constraints(&self.schema.arena, statement.shape, &foci, &self.sparql);
515 if let (Some(start), Some(label)) = (selection_start, label.as_deref()) {
516 crate::profile::record_shape_work(
517 &format!("select:{label}"),
518 start.elapsed().as_micros() as u64,
519 0,
520 );
521 }
522
523 for focus in foci {
524 let pair_start = profiling
525 .then(|| (web_time::Instant::now(), crate::profile::evidence_visits()));
526 let evidence = materialize_evidence(&mut evaluator, &focus, statement.shape);
527 if let (Some((start, visits_before)), Some(label)) = (pair_start, label.as_deref())
528 {
529 crate::profile::record_shape_work(
530 label,
531 start.elapsed().as_micros() as u64,
532 crate::profile::evidence_visits().saturating_sub(visits_before),
533 );
534 }
535 if let Evidence::Failure(failure) = &evidence
536 && failure_meets_threshold(
537 &self.schema.arena,
538 statement.shape,
539 failure,
540 &options.minimum_severity,
541 )
542 {
543 conforms = false;
544 }
545
546 for &raw_statement in &self.raw_by_normalized[statement_id] {
547 if !self.source_statement_selected(raw_statement, &options.entry_shape_names) {
548 continue;
549 }
550 let raw = &self.raw_schema.statements[raw_statement];
551 let progress = include_progress
552 .then(|| {
553 source_progress(
554 &mut evaluator,
555 &self.raw_schema.arena,
556 raw.shape,
557 &self.shape_map,
558 &focus,
559 )
560 })
561 .flatten();
562 statements[raw_statement]
563 .get_or_insert_with(|| {
564 statement_evaluation(
565 &self.raw_schema,
566 &self.schema,
567 raw_statement,
568 statement_id,
569 )
570 })
571 .selected_foci
572 .push(FocusEvaluation {
573 focus: focus.clone(),
574 evidence: evidence.clone(),
575 progress,
576 });
577 }
578 }
579
580 for &raw_statement in &self.raw_by_normalized[statement_id] {
582 if !self.source_statement_selected(raw_statement, &options.entry_shape_names) {
583 continue;
584 }
585 statements[raw_statement].get_or_insert_with(|| {
586 statement_evaluation(
587 &self.raw_schema,
588 &self.schema,
589 raw_statement,
590 statement_id,
591 )
592 });
593 }
594 }
595
596 let mut statements: Vec<StatementEvaluation> = statements.into_iter().flatten().collect();
597 if options.sort_results {
598 statements.sort_by_key(|statement| statement.source_statement_id);
599 for statement in &mut statements {
600 statement
601 .selected_foci
602 .sort_by_key(|focus| focus.focus.to_string());
603 }
604 }
605
606 EvidenceRun {
610 conforms,
611 constraints: ConstraintCatalog {
612 source: constraint_catalog(&self.raw_schema.arena),
613 normalized: constraint_catalog(&self.schema.arena),
614 },
615 statements,
616 }
617 }
618
619 pub fn schema(&self) -> &Schema {
621 &self.schema
622 }
623}
624
625fn constraint_catalog(arena: &ShapeArena) -> Vec<ConstraintRecord> {
626 (0..arena.len())
627 .map(|index| {
628 let id = ShapeId(index as u32);
629 ConstraintRecord {
630 id,
631 constraint_kind: ConstraintKind::of(arena, id),
632 constraint: arena.get(id).clone(),
633 }
634 })
635 .collect()
636}
637
638fn statement_evaluation(
639 raw_schema: &Schema,
640 normalized_schema: &Schema,
641 source_statement_id: usize,
642 normalized_statement_id: usize,
643) -> StatementEvaluation {
644 let source = &raw_schema.statements[source_statement_id];
645 let normalized = &normalized_schema.statements[normalized_statement_id];
646 StatementEvaluation {
647 source_statement_id,
648 normalized_statement_id: Some(normalized_statement_id),
649 source_constraint_id: source.shape,
650 normalized_constraint_id: Some(normalized.shape),
651 constraint_kind: ConstraintKind::of(&normalized_schema.arena, normalized.shape),
652 constraint: normalized_schema.arena.get(normalized.shape).clone(),
653 selector: source.selector.clone(),
654 selected_foci: Vec::new(),
655 }
656}
657
658fn source_children(arena: &ShapeArena, mut id: ShapeId) -> Vec<ShapeId> {
659 while let Shape::Annotated { shape, .. } = arena.get(id) {
660 id = *shape;
661 }
662 match arena.get(id) {
663 Shape::And(children) | Shape::Or(children) => children.clone(),
664 Shape::Not(child) => vec![*child],
665 _ => Vec::new(),
666 }
667}
668
669fn source_progress(
670 evaluator: &mut ShapeEvaluator<'_>,
671 raw_arena: &ShapeArena,
672 raw_shape: ShapeId,
673 shape_map: &[Option<ShapeId>],
674 focus: &oxrdf::Term,
675) -> Option<EvaluationProgress> {
676 let children = source_children(raw_arena, raw_shape);
677 if children.is_empty() {
678 return None;
679 }
680 let evaluated_children = children
681 .into_iter()
682 .map(|source_constraint_ref| {
683 let normalized_constraint_ref = shape_map
684 .get(source_constraint_ref.0 as usize)
685 .copied()
686 .flatten();
687 let status = normalized_constraint_ref
688 .map(|id| {
689 if evaluator.holds(focus, id) {
690 EvaluationStatus::Pass
691 } else {
692 EvaluationStatus::Fail
693 }
694 })
695 .unwrap_or(EvaluationStatus::Pass);
696 let summary_id = normalized_constraint_ref.unwrap_or(source_constraint_ref);
697 let summary_kind = normalized_constraint_ref.map_or_else(
698 || ConstraintKind::of(raw_arena, source_constraint_ref),
699 |id| ConstraintKind::of(evaluator.arena(), id),
700 );
701 ChildEvaluation {
702 source_constraint_ref,
703 normalized_constraint_ref,
704 status,
705 evidence_summary: EvidenceSummary {
706 constraint_id: summary_id,
707 constraint_kind: summary_kind,
708 status,
709 },
710 }
711 })
712 .collect();
713 Some(EvaluationProgress { evaluated_children })
714}
715
716fn witness_shape_ids(witness: &Witness, out: &mut HashSet<ShapeId>) {
717 let shape = match witness {
718 Witness::Atom { shape, .. }
719 | Witness::Relational { shape, .. }
720 | Witness::Closed { shape, .. }
721 | Witness::Not { shape, .. }
722 | Witness::All { shape, .. }
723 | Witness::Any { shape, .. }
724 | Witness::CountLow { shape, .. }
725 | Witness::CountHigh { shape, .. }
726 | Witness::Opaque { shape, .. } => *shape,
727 };
728 out.insert(shape);
729 match witness {
730 Witness::All { failed, .. } => failed
731 .iter()
732 .for_each(|child| witness_shape_ids(child, out)),
733 Witness::Any { branches, .. } => branches
734 .iter()
735 .for_each(|child| witness_shape_ids(child, out)),
736 Witness::CountHigh { per_value, .. } => per_value
737 .iter()
738 .for_each(|(_, child)| witness_shape_ids(child, out)),
739 _ => {}
740 }
741}
742
743fn failure_meets_threshold(
744 arena: &ShapeArena,
745 root: ShapeId,
746 witness: &Witness,
747 minimum: &Severity,
748) -> bool {
749 let mut failed = HashSet::new();
750 witness_shape_ids(witness, &mut failed);
751 let mut active = HashSet::new();
752 failure_shape_meets(
753 arena,
754 root,
755 &Severity::Violation,
756 minimum,
757 &failed,
758 &mut active,
759 )
760}
761
762fn failure_shape_meets(
763 arena: &ShapeArena,
764 id: ShapeId,
765 inherited: &Severity,
766 minimum: &Severity,
767 failed: &HashSet<ShapeId>,
768 active: &mut HashSet<ShapeId>,
769) -> bool {
770 if !active.insert(id) {
771 return false;
772 }
773 let result = match arena.get(id) {
774 Shape::Annotated {
775 severity, shape, ..
776 } => failure_shape_meets(arena, *shape, severity, minimum, failed, active),
777 Shape::And(children) => children
778 .iter()
779 .any(|child| failure_shape_meets(arena, *child, inherited, minimum, failed, active)),
780 Shape::Or(children) => {
781 (failed.contains(&id) && inherited.meets(minimum))
782 || children.iter().any(|child| {
783 failure_shape_meets(arena, *child, inherited, minimum, failed, active)
784 })
785 }
786 Shape::Count { qualifier, .. } => {
787 (failed.contains(&id) && inherited.meets(minimum))
788 || failure_shape_meets(arena, *qualifier, inherited, minimum, failed, active)
789 }
790 Shape::Not(_) => failed.contains(&id) && inherited.meets(minimum),
791 _ => failed.contains(&id) && inherited.meets(minimum),
792 };
793 active.remove(&id);
794 result
795}
796
797pub fn validate_with_evidence(
798 data: &Graph,
799 schema: &Schema,
800) -> Result<EvidenceRun, NonStratifiable> {
801 validate_with_evidence_and_options(data, schema, &ValidationOptions::default())
802}
803
804pub fn validate_with_evidence_and_options(
805 data: &Graph,
806 schema: &Schema,
807 options: &ValidationOptions,
808) -> Result<EvidenceRun, NonStratifiable> {
809 Ok(PreparedEvidenceValidator::new(data, schema)?.validate(options))
810}
811
812pub fn validate_with_context_and_evidence(
813 data: &Graph,
814 context: &Graph,
815 schema: &Schema,
816) -> Result<EvidenceRun, NonStratifiable> {
817 validate_with_context_and_evidence_and_options(
818 data,
819 context,
820 schema,
821 &ValidationOptions::default(),
822 )
823}
824
825pub fn validate_with_context_and_evidence_and_options(
826 data: &Graph,
827 context: &Graph,
828 schema: &Schema,
829 options: &ValidationOptions,
830) -> Result<EvidenceRun, NonStratifiable> {
831 Ok(PreparedEvidenceValidator::with_context(data, context, schema)?.validate(options))
832}
833
834pub fn validate_graphs_with_evidence(
835 data: &Graph,
836 shapes: &Graph,
837 schema: &Schema,
838) -> Result<EvidenceRun, NonStratifiable> {
839 validate_graphs_with_evidence_and_mode_and_options(
840 data,
841 shapes,
842 schema,
843 ValidationGraphMode::default(),
844 &ValidationOptions::default(),
845 )
846}
847
848pub fn validate_graphs_with_evidence_and_mode(
849 data: &Graph,
850 shapes: &Graph,
851 schema: &Schema,
852 mode: ValidationGraphMode,
853) -> Result<EvidenceRun, NonStratifiable> {
854 validate_graphs_with_evidence_and_mode_and_options(
855 data,
856 shapes,
857 schema,
858 mode,
859 &ValidationOptions::default(),
860 )
861}
862
863pub fn validate_graphs_with_evidence_and_mode_and_options(
864 data: &Graph,
865 shapes: &Graph,
866 schema: &Schema,
867 mode: ValidationGraphMode,
868 options: &ValidationOptions,
869) -> Result<EvidenceRun, NonStratifiable> {
870 Ok(PreparedEvidenceValidator::with_graphs(data, shapes, schema, mode)?.validate(options))
871}
872
873#[cfg(test)]
874mod tests {
875 use super::*;
876 use crate::{PathSupport, SatTrace};
877 use oxrdf::Triple;
878 use shifty_parse::{load_turtle, parse_turtle};
879
880 const PREFIXES: &str = r#"
881 @prefix sh: <http://www.w3.org/ns/shacl#> .
882 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
883 @prefix ex: <http://ex/> .
884 "#;
885
886 fn run(ttl: &str) -> (Graph, Schema, EvidenceRun) {
887 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
888 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
889 let outcome = validate_with_evidence(&loaded.graph, &parsed.schema).unwrap();
890 (loaded.graph, parsed.schema, outcome)
891 }
892
893 const COLLAPSING: &str = r#"
895 ex:S1 a sh:NodeShape ; sh:targetClass ex:T ;
896 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
897 ex:S2 a sh:NodeShape ; sh:targetClass ex:T ;
898 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
899 ex:bad a ex:T .
900 "#;
901
902 fn scoped_to(schema: &Schema, data: &Graph, name: &str) -> Vec<String> {
903 let prepared = PreparedEvidenceValidator::new(data, schema).expect("stratifiable");
904 let options = ValidationOptions {
905 entry_shape_names: vec![name.to_string()],
906 ..ValidationOptions::default()
907 };
908 prepared
909 .validate(&options)
910 .statements
911 .iter()
912 .map(|statement| {
913 schema
914 .name_of(schema.statements[statement.source_statement_id].shape)
915 .unwrap_or("<blank>")
916 .to_string()
917 })
918 .collect()
919 }
920
921 #[test]
922 fn entry_shape_names_reach_a_shape_that_cse_collapsed_onto_another() {
923 let ttl = format!("{PREFIXES}{COLLAPSING}");
927 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
928 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
929
930 assert_eq!(
931 scoped_to(&parsed.schema, &loaded.graph, "http://ex/S1"),
932 ["http://ex/S1"],
933 );
934 assert_eq!(
935 scoped_to(&parsed.schema, &loaded.graph, "http://ex/S2"),
936 ["http://ex/S2"],
937 );
938 let prepared =
940 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).expect("stratifiable");
941 assert_eq!(
942 prepared
943 .validate(&ValidationOptions::default())
944 .statements
945 .len(),
946 2,
947 );
948 }
949
950 #[test]
951 fn scoped_failure_handles_explain_only_the_selected_authored_statement() {
952 let ttl = format!("{PREFIXES}{COLLAPSING}");
953 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
954 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
955 let prepared =
956 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).expect("stratifiable");
957 let options = ConformanceOptions {
958 entry_shape_names: vec!["http://ex/S1".to_string()],
959 };
960
961 let (_, failures) = prepared.find_failures(&options);
962 assert_eq!(failures.len(), 1);
963 assert_eq!(failures[0].source_statements(), [0]);
964 assert_eq!(
965 prepared
966 .explain(&failures[0])
967 .iter()
968 .map(|statement| statement.source_statement_id)
969 .collect::<Vec<_>>(),
970 [0],
971 );
972 }
973
974 #[test]
975 fn a_collapsed_shape_answers_to_every_name_that_reached_it() {
976 let ttl = format!("{PREFIXES}{COLLAPSING}");
977 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
978 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
979 let prepared =
980 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).expect("stratifiable");
981
982 let normalized = prepared.schema();
984 assert_eq!(normalized.statements.len(), 1);
985 assert_eq!(
986 normalized.names_of(normalized.statements[0].shape),
987 ["http://ex/S1".to_string(), "http://ex/S2".to_string()],
988 );
989 assert_eq!(
992 normalized.name_of(normalized.statements[0].shape),
993 Some("http://ex/S1"),
994 );
995 assert_eq!(
997 crate::shape_id_for_iri(normalized, "http://ex/S1"),
998 crate::shape_id_for_iri(normalized, "http://ex/S2"),
999 );
1000 assert!(crate::shape_id_for_iri(normalized, "http://ex/S2").is_some());
1001 }
1002
1003 fn foci(outcome: &EvidenceRun) -> Vec<&FocusEvaluation> {
1004 outcome
1005 .statements
1006 .iter()
1007 .flat_map(|statement| &statement.selected_foci)
1008 .collect()
1009 }
1010
1011 fn traces(trace: &SatTrace, pred: &impl Fn(&SatTrace) -> bool) -> bool {
1012 if pred(trace) {
1013 return true;
1014 }
1015 match trace {
1016 SatTrace::AllHeld { children, .. } => children.iter().any(|t| traces(t, pred)),
1017 SatTrace::AnyHeld { satisfied, .. } => satisfied.iter().any(|t| traces(t, pred)),
1018 SatTrace::CountHeld { matches, .. } => matches.iter().any(|(_, _, t)| traces(t, pred)),
1019 SatTrace::ForAllHeld { values, .. } => values.iter().any(|(_, _, t)| traces(t, pred)),
1020 _ => false,
1021 }
1022 }
1023
1024 fn support_edges(support: &PathSupport, out: &mut Vec<Triple>) {
1025 match support {
1026 PathSupport::Edge(edge) => out.push(edge.clone()),
1027 PathSupport::Chain(parts) | PathSupport::Alt(parts) => {
1028 parts.iter().for_each(|part| support_edges(part, out));
1029 }
1030 PathSupport::Empty => {}
1031 }
1032 }
1033
1034 #[test]
1035 fn conformance_only_run_agrees_with_evidence_run() {
1036 let ttl = format!(
1037 "{PREFIXES}
1038 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
1039 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1040 ex:T2 a sh:NodeShape ; sh:targetClass ex:T ;
1041 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1042 ex:good a ex:T ; ex:p ex:value .
1043 ex:bad a ex:T .
1044 ex:unselected ex:p ex:value ."
1045 );
1046 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1047 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1048 let prepared = PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).unwrap();
1049 let options = ValidationOptions::default();
1050 let conformance = prepared.validate_conformance(&ConformanceOptions::from(&options));
1051 let evidence = prepared.validate(&options);
1052
1053 assert_eq!(conformance.conforms, evidence.conforms);
1054 assert_eq!(conformance.selected_pairs, 2);
1055 assert_eq!(conformance.passed, 1);
1056 assert_eq!(conformance.failed, 1);
1057 assert_eq!(foci(&evidence).len(), 4);
1060 }
1061
1062 #[test]
1063 fn selected_pairs_are_partitioned_and_unselected_pairs_are_absent() {
1064 let ttl = format!(
1065 "{PREFIXES}
1066 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
1067 sh:property [ sh:path ex:p ; sh:minCount 1 ] .
1068 ex:good a ex:T ; ex:p ex:value .
1069 ex:bad a ex:T .
1070 ex:unselected ex:p ex:value ."
1071 );
1072 let (_, _, outcome) = run(&ttl);
1073 let results = foci(&outcome);
1074 assert!(!outcome.conforms);
1075 assert_eq!(results.len(), 2);
1076 assert_eq!(
1077 results
1078 .iter()
1079 .filter(|r| matches!(r.evidence, Evidence::Satisfaction(_)))
1080 .count(),
1081 1
1082 );
1083 assert_eq!(
1084 results
1085 .iter()
1086 .filter(|r| matches!(r.evidence, Evidence::Failure(_)))
1087 .count(),
1088 1
1089 );
1090 assert!(
1091 results
1092 .iter()
1093 .all(|r| !r.focus.to_string().contains("unselected"))
1094 );
1095 }
1096
1097 #[test]
1098 fn raw_statements_survive_normalized_deduplication() {
1099 let ttl = format!(
1100 "{PREFIXES}
1101 ex:S1 a sh:NodeShape ; sh:targetNode ex:x ; sh:nodeKind sh:IRI .
1102 ex:S2 a sh:NodeShape ; sh:targetNode ex:x ; sh:nodeKind sh:IRI ."
1103 );
1104 let (_, raw, outcome) = run(&ttl);
1105 assert_eq!(raw.statements.len(), 2);
1106 assert_eq!(outcome.statements.len(), 2);
1107 assert_eq!(outcome.statements[0].source_statement_id, 0);
1108 assert_eq!(outcome.statements[1].source_statement_id, 1);
1109 assert_eq!(
1110 outcome.statements[0].normalized_statement_id,
1111 outcome.statements[1].normalized_statement_id
1112 );
1113 assert_eq!(
1114 outcome.statements[0].normalized_constraint_id,
1115 outcome.statements[1].normalized_constraint_id
1116 );
1117 }
1118
1119 #[test]
1120 fn selector_matching_nothing_has_empty_statement_row() {
1121 let ttl = format!(
1122 "{PREFIXES}
1123 ex:S a sh:NodeShape ; sh:targetClass ex:Missing ; sh:nodeKind sh:IRI .
1124 ex:x a ex:Other ."
1125 );
1126 let (_, _, outcome) = run(&ttl);
1127 assert!(outcome.conforms);
1128 assert_eq!(outcome.statements.len(), 1);
1129 assert!(outcome.statements[0].selected_foci.is_empty());
1130 }
1131
1132 #[test]
1133 fn negation_crosses_to_the_opposite_evidence_polarity() {
1134 let passing = format!(
1135 "{PREFIXES}
1136 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1137 sh:not [ sh:class ex:C ] .
1138 ex:x a ex:Other ."
1139 );
1140 let (_, _, pass) = run(&passing);
1141 let pass_foci = foci(&pass);
1142 let Evidence::Satisfaction(trace) = &pass_foci[0].evidence else {
1143 panic!("expected satisfaction")
1144 };
1145 assert!(traces(trace, &|t| matches!(t, SatTrace::NotHeld { .. })));
1146
1147 let failing = format!(
1148 "{PREFIXES}
1149 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1150 sh:not [ sh:class ex:C ] .
1151 ex:x a ex:C ."
1152 );
1153 let (_, _, fail) = run(&failing);
1154 let fail_foci = foci(&fail);
1155 assert!(matches!(
1156 fail_foci[0].evidence,
1157 Evidence::Failure(Witness::Not { .. }) | Evidence::Failure(Witness::All { .. })
1158 ));
1159 fn contains_not(w: &Witness) -> bool {
1160 match w {
1161 Witness::Not { .. } => true,
1162 Witness::All { failed, .. } => failed.iter().any(contains_not),
1163 Witness::Any { branches, .. } => branches.iter().any(contains_not),
1164 _ => false,
1165 }
1166 }
1167 let Evidence::Failure(witness) = &fail_foci[0].evidence else {
1168 unreachable!()
1169 };
1170 assert!(contains_not(witness));
1171 }
1172
1173 #[test]
1174 fn sequence_path_satisfaction_carries_only_real_graph_edges() {
1175 let ttl = format!(
1176 "{PREFIXES}
1177 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1178 sh:property [ sh:path (ex:p ex:q) ; sh:minCount 1 ] .
1179 ex:x ex:p ex:y . ex:y ex:q ex:z ."
1180 );
1181 let (graph, _, outcome) = run(&ttl);
1182 let results = foci(&outcome);
1183 let Evidence::Satisfaction(trace) = &results[0].evidence else {
1184 panic!("expected satisfaction")
1185 };
1186 let mut edges = Vec::new();
1187 fn gather(trace: &SatTrace, edges: &mut Vec<Triple>) {
1188 match trace {
1189 SatTrace::CountHeld { matches, .. } => {
1190 for (_, support, child) in matches {
1191 support_edges(support, edges);
1192 gather(child, edges);
1193 }
1194 }
1195 SatTrace::ForAllHeld { values, .. } => {
1196 for (_, support, child) in values {
1197 support_edges(support, edges);
1198 gather(child, edges);
1199 }
1200 }
1201 SatTrace::AllHeld { children, .. } => {
1202 children.iter().for_each(|child| gather(child, edges))
1203 }
1204 SatTrace::AnyHeld { satisfied, .. } => {
1205 satisfied.iter().for_each(|child| gather(child, edges))
1206 }
1207 _ => {}
1208 }
1209 }
1210 gather(trace, &mut edges);
1211 assert_eq!(edges.len(), 2);
1212 assert!(edges.iter().all(|edge| graph.contains(edge.as_ref())));
1213 }
1214
1215 #[test]
1216 fn count_evidence_retains_matches_bounds_qualifiers_and_support() {
1217 let low_ttl = format!(
1218 "{PREFIXES}
1219 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1220 sh:property [ sh:path ex:p ; sh:minCount 2 ] ;
1221 sh:property [ sh:path ex:p ; sh:class ex:C ] .
1222 ex:x ex:p ex:one . ex:one a ex:C ."
1223 );
1224 let (_, _, low) = run(&low_ttl);
1225 let low_foci = foci(&low);
1226 let Evidence::Failure(low_witness) = &low_foci[0].evidence else {
1227 panic!("expected low-count failure")
1228 };
1229 fn find_low(witness: &Witness) -> Option<(u64, u64, ShapeId, &[ShapeId])> {
1230 match witness {
1231 Witness::CountLow {
1232 have,
1233 min,
1234 qualifier,
1235 sibling_qualifiers,
1236 ..
1237 } => Some((*have, *min, *qualifier, sibling_qualifiers)),
1238 Witness::All { failed, .. } => failed.iter().find_map(find_low),
1239 Witness::Any { branches, .. } => branches.iter().find_map(find_low),
1240 _ => None,
1241 }
1242 }
1243 let (have, min, qualifier, _siblings) = find_low(low_witness).expect("CountLow");
1244 assert_eq!((have, min), (1, 2));
1245 assert_ne!(qualifier, ShapeId(u32::MAX));
1246
1247 let high_ttl = format!(
1248 "{PREFIXES}
1249 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1250 sh:property [ sh:path ex:p ; sh:maxCount 1 ] .
1251 ex:x ex:p ex:one, ex:two ."
1252 );
1253 let (graph, _, high) = run(&high_ttl);
1254 let high_foci = foci(&high);
1255 let Evidence::Failure(high_witness) = &high_foci[0].evidence else {
1256 panic!("expected high-count failure")
1257 };
1258 fn find_high(witness: &Witness) -> Option<(&[(oxrdf::Term, PathSupport)], u64)> {
1259 match witness {
1260 Witness::CountHigh { matched, max, .. } => Some((matched, *max)),
1261 Witness::All { failed, .. } => failed.iter().find_map(find_high),
1262 Witness::Any { branches, .. } => branches.iter().find_map(find_high),
1263 _ => None,
1264 }
1265 }
1266 let (matched, max) = find_high(high_witness).expect("CountHigh");
1267 assert_eq!(max, 1);
1268 assert_eq!(matched.len(), 2);
1269 let excess = high_foci[0]
1270 .evidence
1271 .walk()
1272 .into_iter()
1273 .find_map(|node| match node {
1274 crate::EvidenceNodeRef::Failure(Witness::CountHigh { excess_values, .. }) => {
1275 Some(excess_values)
1276 }
1277 _ => None,
1278 })
1279 .expect("CountHigh excess values");
1280 assert_eq!(excess.len(), 1);
1281 let mut edges = Vec::new();
1282 matched
1283 .iter()
1284 .for_each(|(_, support)| support_edges(support, &mut edges));
1285 assert_eq!(edges.len(), 2);
1286 assert!(edges.iter().all(|edge| graph.contains(edge.as_ref())));
1287 }
1288
1289 #[test]
1290 fn failed_conjunction_progress_retains_successful_siblings() {
1291 let ttl = format!(
1292 "{PREFIXES}
1293 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1294 sh:nodeKind sh:IRI ;
1295 sh:property [ sh:path ex:p ; sh:minCount 1 ] ."
1296 );
1297 let (_, _, outcome) = run(&ttl);
1298 let results = foci(&outcome);
1299 let focus = results[0];
1300 assert_eq!(focus.status(), EvaluationStatus::Fail);
1301 let progress = focus.progress.as_ref().expect("conjunction progress");
1302 assert_eq!(progress.evaluated_children.len(), 2);
1303 assert_eq!(
1304 progress
1305 .evaluated_children
1306 .iter()
1307 .map(|child| child.status)
1308 .collect::<Vec<_>>(),
1309 vec![EvaluationStatus::Pass, EvaluationStatus::Fail]
1310 );
1311 let Evidence::Failure(Witness::All { failed, .. }) = &focus.evidence else {
1312 panic!("expected compact conjunction failure")
1313 };
1314 assert_eq!(failed.len(), 1);
1315 }
1316
1317 #[test]
1318 fn canonical_interfaces_omit_progress_without_changing_evidence() {
1319 let ttl = format!(
1320 "{PREFIXES}
1321 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1322 sh:nodeKind sh:IRI ;
1323 sh:property [ sh:path ex:p ; sh:minCount 1 ] ."
1324 );
1325 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1326 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1327 let prepared = PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).unwrap();
1328 let options = ValidationOptions::default();
1329
1330 let complete = prepared.validate(&options);
1331 let canonical = prepared.validate_canonical(&options);
1332 assert_eq!(complete.conforms, canonical.conforms);
1333 assert_eq!(complete.constraints, canonical.constraints);
1334 assert_eq!(complete.statements.len(), canonical.statements.len());
1335
1336 for (with_progress, without_progress) in complete
1337 .statements
1338 .iter()
1339 .flat_map(|statement| &statement.selected_foci)
1340 .zip(
1341 canonical
1342 .statements
1343 .iter()
1344 .flat_map(|statement| &statement.selected_foci),
1345 )
1346 {
1347 assert!(with_progress.progress.is_some());
1348 assert!(without_progress.progress.is_none());
1349 assert_eq!(with_progress.focus, without_progress.focus);
1350 assert_eq!(with_progress.evidence, without_progress.evidence);
1351 }
1352
1353 let (_, failures) = prepared.find_failures(&ConformanceOptions::from(&options));
1354 let explained = prepared.explain_canonical(&failures[0]);
1355 assert!(
1356 explained
1357 .iter()
1358 .flat_map(|statement| &statement.selected_foci)
1359 .all(|focus| focus.progress.is_none())
1360 );
1361 }
1362
1363 #[test]
1364 fn qualified_count_low_partitions_matches_and_rejected_candidates() {
1365 let ttl = format!(
1366 "{PREFIXES}
1367 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1368 sh:property [
1369 sh:path ex:p ;
1370 sh:qualifiedValueShape [ sh:class ex:C ] ;
1371 sh:qualifiedMinCount 2
1372 ] .
1373 ex:x ex:p ex:good, ex:near . ex:good a ex:C ."
1374 );
1375 let (graph, _, outcome) = run(&ttl);
1376 let results = foci(&outcome);
1377 let evidence = &results[0].evidence;
1378 let low = evidence.walk().into_iter().find_map(|node| match node {
1379 crate::EvidenceNodeRef::Failure(value @ Witness::CountLow { .. }) => Some(value),
1380 _ => None,
1381 });
1382 let Some(Witness::CountLow {
1383 qualifying_matches,
1384 rejected_candidates,
1385 have,
1386 min,
1387 ..
1388 }) = low
1389 else {
1390 panic!("expected CountLow")
1391 };
1392 assert_eq!((*have, *min), (1, 2));
1393 assert_eq!(qualifying_matches.len(), 1);
1394 assert_eq!(rejected_candidates.len(), 1);
1395 assert_eq!(evidence.missing_obligations()[0].missing, 1);
1396 assert!(
1397 evidence
1398 .supporting_triples()
1399 .iter()
1400 .all(|triple| graph.contains(triple.as_ref()))
1401 );
1402 assert_eq!(
1403 evidence.to_json().unwrap(),
1404 evidence.to_json().unwrap(),
1405 "serialization must be deterministic"
1406 );
1407 }
1408
1409 #[test]
1410 fn positive_recursion_is_coinductive_and_negative_recursion_is_rejected() {
1411 let positive = format!(
1412 "{PREFIXES}
1413 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1414 sh:property [ sh:path ex:p ; sh:node ex:S ] .
1415 ex:x ex:p ex:x ."
1416 );
1417 let (_, _, outcome) = run(&positive);
1418 let results = foci(&outcome);
1419 let Evidence::Satisfaction(trace) = &results[0].evidence else {
1420 panic!("expected coinductive satisfaction")
1421 };
1422 assert!(
1423 traces(trace, &|t| matches!(t, SatTrace::Coinductive { .. })),
1424 "trace did not retain recursion: {trace:?}"
1425 );
1426 let positive_loaded = load_turtle(positive.as_bytes(), None).unwrap();
1427 let positive_parsed = parse_turtle(positive.as_bytes(), None).unwrap();
1428 let ordinary = crate::validate(
1429 &positive_loaded.graph,
1430 &shifty_opt::normalize(&positive_parsed.schema),
1431 )
1432 .unwrap();
1433 assert_eq!(outcome.conforms, ordinary.conforms);
1434
1435 let negative = format!(
1436 "{PREFIXES}
1437 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1438 sh:not [ sh:path ex:p ; sh:qualifiedValueShape ex:S ;
1439 sh:qualifiedMinCount 1 ] .
1440 ex:x ex:p ex:x ."
1441 );
1442 let parsed = parse_turtle(negative.as_bytes(), None).unwrap();
1443 let loaded = load_turtle(negative.as_bytes(), None).unwrap();
1444 assert!(validate_with_evidence(&loaded.graph, &parsed.schema).is_err());
1445 }
1446
1447 #[test]
1448 fn outcome_serializes_and_conforms_matches_ordinary_validation() {
1449 let ttl = format!(
1450 "{PREFIXES}
1451 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1452 sh:property [ sh:path ex:p ; sh:minCount 1 ] ."
1453 );
1454 let (graph, raw, outcome) = run(&ttl);
1455 let normalized = shifty_opt::normalize(&raw);
1456 let ordinary = crate::validate(&graph, &normalized).unwrap();
1457 assert_eq!(outcome.conforms, ordinary.conforms);
1458 let encoded = serde_json::to_string(&outcome).unwrap();
1459 let decoded: EvidenceRun = serde_json::from_str(&encoded).unwrap();
1460 assert_eq!(decoded, outcome);
1461
1462 let sparql = format!(
1463 "{PREFIXES}
1464 ex:S a sh:NodeShape ; sh:targetNode ex:x ;
1465 sh:sparql [ sh:select \"SELECT $this WHERE {{ FILTER NOT EXISTS {{ $this <http://ex/p> ?value }} }}\" ] ."
1466 );
1467 let (graph, raw, evidence) = run(&sparql);
1468 let ordinary = crate::validate(&graph, &shifty_opt::normalize(&raw)).unwrap();
1469 assert_eq!(evidence.conforms, ordinary.conforms);
1470 assert!(matches!(foci(&evidence)[0].evidence, Evidence::Failure(_)));
1471 }
1472
1473 const ON_DEMAND: &str = r#"
1477 ex:S a sh:NodeShape ; sh:targetClass ex:T ;
1478 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
1479 ex:S2 a sh:NodeShape ; sh:targetClass ex:T ;
1480 sh:property [ sh:path ex:p ; sh:minCount 1 ; sh:class ex:C ] .
1481 ex:U a sh:NodeShape ; sh:targetClass ex:T ;
1482 sh:property [ sh:path ex:q ; sh:maxCount 1 ] .
1483 ex:good a ex:T ; ex:p ex:c1 ; ex:q ex:z .
1484 ex:bad a ex:T ; ex:q ex:z ; ex:q ex:y .
1485 ex:also a ex:T ; ex:p ex:missing .
1486 ex:c1 a ex:C .
1487 "#;
1488
1489 fn prepared(ttl: &str) -> PreparedEvidenceValidator {
1490 let parsed = parse_turtle(ttl.as_bytes(), None).unwrap();
1491 let loaded = load_turtle(ttl.as_bytes(), None).unwrap();
1492 PreparedEvidenceValidator::new(&loaded.graph, &parsed.schema).unwrap()
1493 }
1494
1495 #[test]
1496 fn find_failures_agrees_with_the_full_run() {
1497 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1498 let options = ValidationOptions::default();
1499 let full = validator.validate(&options);
1500 let conformance_options = ConformanceOptions::from(&options);
1501 let (conformance, failures) = validator.find_failures(&conformance_options);
1502
1503 assert_eq!(conformance.conforms, full.conforms);
1504 assert!(!failures.is_empty(), "fixture has no failing pair");
1505 assert_eq!(conformance.failed, failures.len());
1506 assert_eq!(
1508 conformance,
1509 validator.validate_conformance(&conformance_options)
1510 );
1511
1512 for pair in &failures {
1515 for evaluation in validator.explain(pair) {
1516 assert!(matches!(
1517 evaluation.selected_foci[0].evidence,
1518 Evidence::Failure(_)
1519 ));
1520 }
1521 }
1522 let full_failures: HashSet<(usize, String)> = full
1523 .statements
1524 .iter()
1525 .flat_map(|statement| {
1526 statement.selected_foci.iter().map(|focus| {
1527 (
1528 statement.normalized_statement_id.unwrap(),
1529 focus.focus.to_string(),
1530 )
1531 })
1532 })
1533 .filter(|_| true)
1534 .collect();
1535 for pair in &failures {
1536 assert!(
1537 full_failures.contains(&(pair.normalized_statement(), pair.focus().to_string())),
1538 "failure {pair:?} was not a pair of the full run"
1539 );
1540 }
1541 }
1542
1543 #[test]
1544 fn explaining_an_unknown_statement_is_empty() {
1545 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1546 let pair = SelectedPair::new(
1547 usize::MAX,
1548 oxrdf::NamedNode::new("http://ex/good").unwrap().into(),
1549 vec![],
1550 );
1551 assert!(validator.explain(&pair).is_empty());
1552 }
1553
1554 #[test]
1555 fn the_catalog_is_the_one_a_full_run_carries() {
1556 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1557 let full = validator.validate(&ValidationOptions::default());
1558 assert_eq!(validator.constraints(), full.constraints);
1559 }
1560 #[test]
1567 fn two_full_runs_agree() {
1568 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1569 let options = ValidationOptions::default();
1570 let first = validator.validate(&options);
1571 let second = validator.validate(&options);
1572 assert_eq!(first, second, "two runs over the same snapshot disagree");
1573 }
1574
1575 #[test]
1578 fn two_independent_validators_agree() {
1579 let options = ValidationOptions::default();
1580 let first = prepared(&format!("{PREFIXES}{ON_DEMAND}")).validate(&options);
1581 let second = prepared(&format!("{PREFIXES}{ON_DEMAND}")).validate(&options);
1582 assert_eq!(first, second, "two independent validators disagree");
1583 }
1584
1585 #[test]
1588 fn explaining_a_pair_matches_the_full_run() {
1589 let validator = prepared(&format!("{PREFIXES}{ON_DEMAND}"));
1590 let full = validator.validate(&ValidationOptions::default());
1591
1592 let mut checked = 0;
1593 for statement in &full.statements {
1594 let normalized = statement.normalized_statement_id.unwrap();
1595 for focus in &statement.selected_foci {
1596 let pair = SelectedPair::new(
1597 normalized,
1598 focus.focus.clone(),
1599 vec![statement.source_statement_id],
1600 );
1601 let explained = validator.explain(&pair);
1602 let matching = explained
1603 .iter()
1604 .find(|candidate| {
1605 candidate.source_statement_id == statement.source_statement_id
1606 })
1607 .expect("explain covers every authored statement of the pair");
1608 assert_eq!(&matching.selected_foci[0], focus);
1609 checked += 1;
1610 }
1611 }
1612 assert!(checked > 0, "fixture selected no pairs");
1613 }
1614}