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