1pub mod cache;
12mod composition_gate;
13pub mod config;
14pub mod diff;
15pub mod duplicate_stems;
16pub mod finding;
17mod gates;
18pub use gates::collect_yaml_files;
19mod gates_extended;
20pub mod relations_gate;
21pub mod rules;
22pub mod sarif;
23pub mod shapes_gate;
24pub mod sigma_gate;
25pub mod sigma_symbols;
26mod strict_test_binding;
27pub mod trend;
28
29use std::collections::{HashMap, HashSet};
30use std::path::Path;
31use std::time::Instant;
32
33use serde::Serialize;
34
35use self::finding::LintFinding;
36use self::gates::{
37 load_binding, load_contracts, run_audit_gate, run_score_gate, run_validate_gate,
38};
39use self::gates_extended::{
40 check_stale_suppressions, run_enforce_gate, run_enforcement_level_gate,
41 run_reverse_coverage_gate, run_verify_gate,
42};
43use self::rules::RuleSeverity;
44use crate::ontology::arming::{meet_armed, ArmedGates};
45use crate::ontology::verdict::Verdict;
46
47#[derive(Debug, Clone, Serialize)]
49pub struct GateResult {
50 pub name: String,
51 pub passed: bool,
52 pub skipped: bool,
53 pub verdict: Verdict,
55 pub duration_ms: u64,
56 pub detail: GateDetail,
57 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub extra: Option<GateExtra>,
65}
66
67#[derive(Debug, Clone, Serialize)]
86#[serde(tag = "type")]
87pub enum GateDetail {
88 #[serde(rename = "validate")]
89 Validate {
90 contracts: usize,
91 errors: usize,
92 warnings: usize,
93 error_messages: Vec<String>,
94 },
95 #[serde(rename = "audit")]
96 Audit {
97 contracts: usize,
98 findings: usize,
99 finding_messages: Vec<String>,
100 },
101 #[serde(rename = "score")]
102 Score {
103 contracts: usize,
104 min_score: f64,
105 mean_score: f64,
106 threshold: f64,
107 below_threshold: Vec<String>,
108 },
109 #[serde(rename = "verify")]
110 Verify {
111 total_refs: usize,
112 existing: usize,
113 missing: usize,
114 },
115 #[serde(rename = "enforce")]
116 Enforce {
117 equations_total: usize,
118 equations_with_pre: usize,
119 equations_with_post: usize,
120 equations_with_lean: usize,
121 },
122 #[serde(rename = "reverse_coverage")]
123 ReverseCoverage {
124 total_pub_fns: usize,
125 bound_fns: usize,
126 unbound_fns: usize,
127 coverage_pct: f64,
128 threshold_pct: f64,
129 },
130 #[serde(rename = "composition")]
131 Composition {
132 edges_checked: usize,
133 edges_satisfied: usize,
134 edges_broken: usize,
135 },
136 #[serde(rename = "skipped")]
137 Skipped { reason: String },
138}
139
140#[derive(Debug, Clone, Serialize)]
148#[serde(tag = "type")]
149#[non_exhaustive]
150pub enum GateExtra {
151 #[serde(rename = "duplicate_stems")]
153 DuplicateStems {
154 divergent: usize,
156 baselined: usize,
158 unbaselined: Vec<String>,
160 stale: Vec<String>,
162 divergent_stems: Vec<String>,
164 },
165 #[serde(rename = "sigma")]
167 Sigma {
168 entity_types: usize,
170 roles: usize,
172 symbols: usize,
174 contracts_checked: usize,
176 violations: usize,
178 formal_total: usize,
180 formal_prose: usize,
182 },
183 #[serde(rename = "relations")]
185 Relations {
186 relations_n: usize,
188 contracts_with_relations: usize,
190 roles_used: Vec<String>,
192 cycles: Vec<String>,
194 legacy_depends_on: usize,
196 legacy_unresolved_depends_on: usize,
198 violations: usize,
200 },
201 #[serde(rename = "shapes")]
203 Shapes {
204 shapes_n: usize,
206 focus_nodes_n: usize,
208 pc_shape: String,
210 plant_violations: usize,
212 violations: usize,
214 warnings: usize,
216 by_shape: Vec<String>,
218 triples: usize,
220 armed_shapes: Vec<String>,
222 not_armed_shapes: Vec<String>,
224 unarmed_violations: usize,
226 by_entity_type: std::collections::BTreeMap<String, usize>,
228 pc_extract: std::collections::BTreeMap<String, String>,
230 receipts: usize,
232 witnesses: usize,
234 hex_mismatches: usize,
236 unmeasured_rows: usize,
238 w3c_cases_passed: usize,
240 w3c_cases_n: usize,
241 symbols_resolved: usize,
243 symbols_unresolved: usize,
244 lean_statements: usize,
246 lean_refs_unresolved: usize,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 release: Option<Box<crate::ontology::extract::release_evidence::ReleaseStats>>,
250 },
251}
252
253#[derive(Debug, Clone, Serialize)]
255pub struct LintReport {
256 pub passed: bool,
257 pub gates: Vec<GateResult>,
258 pub total_duration_ms: u64,
259 #[serde(skip_serializing_if = "Vec::is_empty")]
260 pub findings: Vec<LintFinding>,
261 #[serde(skip)]
262 pub cache_stats: cache::CacheStats,
263 #[serde(default, skip_serializing_if = "Vec::is_empty")]
265 pub contract_timings: Vec<(String, u64)>,
266 pub verdict: Verdict,
269 pub armed_gates: Vec<ArmedGateVerdict>,
271 pub not_armed: Vec<String>,
273 #[serde(skip_serializing_if = "Option::is_none")]
275 pub armed_monotone: Option<String>,
276 #[serde(skip_serializing_if = "Option::is_none")]
278 pub armed_shapes_monotone: Option<String>,
279}
280
281#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
283pub struct ArmedGateVerdict {
284 pub name: String,
285 pub verdict: Verdict,
286}
287
288impl LintReport {
289 pub fn arm(&mut self, armed: &ArmedGates) {
291 let results: Vec<(String, Verdict)> = self
292 .gates
293 .iter()
294 .map(|g| (g.name.clone(), g.verdict))
295 .collect();
296 let meet = meet_armed(&results, armed);
297 self.verdict = meet.verdict;
298 self.armed_gates = meet
299 .armed
300 .into_iter()
301 .map(|(name, verdict)| ArmedGateVerdict { name, verdict })
302 .collect();
303 self.not_armed = meet.not_armed;
304 }
305}
306
307pub struct LintConfig<'a> {
309 pub contract_dir: &'a Path,
310 pub binding_path: Option<&'a Path>,
311 pub min_score: f64,
312 pub severity_filter: Option<RuleSeverity>,
313 pub severity_overrides: HashMap<String, RuleSeverity>,
314 pub suppressed_findings: Vec<String>,
315 pub suppressed_rules: Vec<String>,
316 pub suppressed_files: Vec<String>,
317 pub strict: bool,
318 pub no_cache: bool,
319 pub cache_stats: bool,
320 pub crate_dir: Option<&'a Path>,
322 pub min_level: Option<crate::schema::EnforcementLevel>,
324 pub strict_test_binding: bool,
326}
327
328impl<'a> LintConfig<'a> {
329 pub fn new(contract_dir: &'a Path, binding_path: Option<&'a Path>, min_score: f64) -> Self {
331 Self {
332 contract_dir,
333 binding_path,
334 min_score,
335 severity_filter: None,
336 severity_overrides: HashMap::new(),
337 suppressed_findings: Vec::new(),
338 suppressed_rules: Vec::new(),
339 suppressed_files: Vec::new(),
340 strict: false,
341 no_cache: false,
342 cache_stats: false,
343 crate_dir: None,
344 min_level: None,
345 strict_test_binding: false,
346 }
347 }
348}
349
350fn push_gate<F>(
355 gates: &mut Vec<GateResult>,
356 all_findings: &mut Vec<LintFinding>,
357 run: F,
358 name: &str,
359 validation_passed: bool,
360) where
361 F: FnOnce() -> (GateResult, Vec<LintFinding>),
362{
363 let (result, mut findings) = if validation_passed {
364 run()
365 } else {
366 (skipped_gate(name, "validation failed"), Vec::new())
367 };
368 gates.push(result);
369 all_findings.append(&mut findings);
370}
371
372fn per_contract_timings(
375 contracts: &[(String, crate::schema::Contract)],
376 binding: Option<&crate::binding::BindingRegistry>,
377) -> Vec<(String, u64)> {
378 let mut contract_timings: Vec<(String, u64)> = Vec::with_capacity(contracts.len());
379 for (stem, contract) in contracts {
380 let ct_start = Instant::now();
381 let _ = crate::schema::validate_contract(contract);
383 let _ = crate::audit::audit_contract(contract);
385 let _ = crate::scoring::score_contract(contract, binding, stem);
387 let ct_ms = u64::try_from(ct_start.elapsed().as_micros() / 1000).unwrap_or(0);
388 contract_timings.push((format!("{stem}.yaml"), ct_ms));
389 }
390 contract_timings.sort_by_key(|b| std::cmp::Reverse(b.1));
392 contract_timings
393}
394
395fn store_findings_in_cache(
397 root: &std::path::Path,
398 config: &LintConfig,
399 contracts: &[(String, crate::schema::Contract)],
400 all_findings: &[LintFinding],
401 stats: &mut cache::CacheStats,
402) {
403 let rule_cfg = format!("{:?}{:?}", config.severity_overrides, config.strict);
404 for (stem, _) in contracts {
405 stats.total += 1;
406 let yaml_path = config.contract_dir.join(format!("{stem}.yaml"));
407 let yaml_content = std::fs::read_to_string(&yaml_path).unwrap_or_default();
408 let hash = cache::content_hash(&yaml_content, &rule_cfg);
409 if cache::cache_get(root, &hash).is_some() {
410 stats.hits += 1;
411 } else {
412 stats.misses += 1;
413 let contract_findings: Vec<_> = all_findings
414 .iter()
415 .filter(|f| f.contract_stem.as_deref() == Some(stem.as_str()))
416 .cloned()
417 .collect();
418 let _ = cache::cache_put(root, &hash, &contract_findings);
419 }
420 }
421}
422
423#[allow(clippy::too_many_lines)]
425pub fn run_lint(config: &LintConfig) -> LintReport {
426 let overall_start = Instant::now();
427 let mut gates = Vec::with_capacity(3);
428 let mut all_findings = Vec::new();
429 let mut stats = cache::CacheStats::default();
430 let mut contract_timings: Vec<(String, u64)> = Vec::new();
431
432 let cache_root = if config.no_cache {
433 None
434 } else {
435 Some(cache::cache_dir(config.contract_dir))
436 };
437
438 let (contracts, parse_errors) = load_contracts(config.contract_dir);
439 let binding = load_binding(config.binding_path);
440
441 let (validate_result, mut validate_findings) = run_validate_gate(&contracts, &parse_errors);
443 let validation_passed = validate_result.passed;
444 gates.push(validate_result);
445
446 push_gate(
448 &mut gates,
449 &mut all_findings,
450 || run_audit_gate(&contracts),
451 "audit",
452 validation_passed,
453 );
454
455 push_gate(
457 &mut gates,
458 &mut all_findings,
459 || run_score_gate(&contracts, binding.as_ref(), config.min_score),
460 "score",
461 validation_passed,
462 );
463
464 push_gate(
466 &mut gates,
467 &mut all_findings,
468 || {
469 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
470 run_verify_gate(&contracts, project_root)
471 },
472 "verify",
473 validation_passed,
474 );
475
476 push_gate(
478 &mut gates,
479 &mut all_findings,
480 || run_enforce_gate(&contracts),
481 "enforce",
482 validation_passed,
483 );
484
485 push_gate(
487 &mut gates,
488 &mut all_findings,
489 || {
490 let min_level = config
491 .min_level
492 .unwrap_or(crate::schema::EnforcementLevel::Standard);
493 run_enforcement_level_gate(&contracts, min_level)
494 },
495 "enforcement-level",
496 validation_passed,
497 );
498
499 push_gate(
501 &mut gates,
502 &mut all_findings,
503 || match (config.binding_path, config.crate_dir) {
504 (Some(bp), Some(cd)) => run_reverse_coverage_gate(bp, cd),
505 _ => (
506 skipped_gate("reverse-coverage", "no --binding or --crate-dir provided"),
507 Vec::new(),
508 ),
509 },
510 "reverse-coverage",
511 validation_passed,
512 );
513
514 let duplicates = duplicate_stems::scan_duplicate_stems(config.contract_dir);
518 let ambiguous = duplicate_stems::ambiguous_stems(&duplicates);
519 push_gate(
520 &mut gates,
521 &mut all_findings,
522 || {
523 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
524 let baseline = duplicate_stems::read_baseline(project_root);
525 duplicate_stems::run_duplicate_stem_gate(&duplicates, &baseline)
526 },
527 "duplicate-stems",
528 validation_passed,
529 );
530
531 push_gate(
533 &mut gates,
534 &mut all_findings,
535 || composition_gate::run_composition_gate(&contracts, &ambiguous),
536 "composition",
537 validation_passed,
538 );
539
540 let (sigma_gate_result, mut sigma_findings) =
543 sigma_result(config.contract_dir, validation_passed);
544 gates.push(sigma_gate_result);
545 all_findings.append(&mut sigma_findings);
546
547 let (relations_gate_result, mut relations_findings) =
549 relations_result(config.contract_dir, validation_passed);
550 gates.push(relations_gate_result);
551 all_findings.append(&mut relations_findings);
552
553 let (shapes_gate_result, mut shapes_findings) =
555 shapes_result(config.contract_dir, validation_passed);
556 gates.push(shapes_gate_result);
557 all_findings.append(&mut shapes_findings);
558
559 if config.strict_test_binding {
561 push_gate(
562 &mut gates,
563 &mut all_findings,
564 || {
565 let project_root = config.contract_dir.parent().unwrap_or(config.contract_dir);
566 strict_test_binding::run_strict_test_binding_gate(
567 &contracts,
568 project_root,
569 config.strict,
570 )
571 },
572 "strict-test-binding",
573 validation_passed,
574 );
575 }
576
577 all_findings.append(&mut validate_findings);
578
579 if validation_passed {
581 contract_timings = per_contract_timings(&contracts, binding.as_ref());
582 }
583
584 let mut stale_findings = check_stale_suppressions(
586 &all_findings,
587 &config.suppressed_rules,
588 &config.suppressed_findings,
589 );
590 all_findings.append(&mut stale_findings);
591
592 mark_new_findings(&mut all_findings, config.contract_dir);
594
595 if let Some(ref root) = cache_root {
597 store_findings_in_cache(root, config, &contracts, &all_findings, &mut stats);
598 }
599
600 apply_suppressions(&mut all_findings, config);
602 apply_severity_overrides(&mut all_findings, config);
603 if let Some(min_sev) = config.severity_filter {
604 all_findings.retain(|f| f.severity >= min_sev);
605 }
606
607 let passed = gates.iter().all(|g| g.passed || g.skipped);
608
609 let mut report = LintReport {
610 passed,
611 gates,
612 total_duration_ms: u64::try_from(overall_start.elapsed().as_millis()).unwrap_or(u64::MAX),
613 findings: all_findings,
614 cache_stats: stats,
615 contract_timings,
616 verdict: Verdict::Unknown(crate::ontology::verdict::Reason::NotArmed),
617 armed_gates: Vec::new(),
618 not_armed: Vec::new(),
619 armed_monotone: None,
620 armed_shapes_monotone: None,
621 };
622 report.arm(&ArmedGates::default_set());
625 report
626}
627
628pub enum NamedGateOutcome {
630 UnknownGate,
632 Sigma(sigma_gate::SigmaOutcome),
634 Relations(relations_gate::RelationsOutcome),
636 Shapes(shapes_gate::ShapesOutcome),
638 Ran {
640 result: Box<GateResult>,
641 findings: Vec<LintFinding>,
642 },
643}
644
645#[must_use]
651pub fn run_named_gate(contract_dir: &Path, name: &str) -> NamedGateOutcome {
652 run_named_gate_with(contract_dir, name, &shapes_gate::ShapesOptions::default())
653}
654
655pub fn run_named_gate_with(
657 contract_dir: &Path,
658 name: &str,
659 shapes_opts: &shapes_gate::ShapesOptions,
660) -> NamedGateOutcome {
661 match name {
662 "relations" => {
663 NamedGateOutcome::Relations(relations_gate::run_relations_gate(contract_dir))
664 }
665 "shapes" => {
666 NamedGateOutcome::Shapes(shapes_gate::run_shapes_gate_with(contract_dir, shapes_opts))
667 }
668 "sigma" => NamedGateOutcome::Sigma(sigma_gate::run_sigma_gate(contract_dir)),
669 "validate" => {
670 let (contracts, parse_errors) = load_contracts(contract_dir);
671 let (result, findings) = run_validate_gate(&contracts, &parse_errors);
672 NamedGateOutcome::Ran {
673 result: Box::new(result),
674 findings,
675 }
676 }
677 _ => NamedGateOutcome::UnknownGate,
678 }
679}
680
681pub const NAMED_GATES: [&str; 4] = ["relations", "shapes", "sigma", "validate"];
683
684fn sigma_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
688 if !validation_passed {
689 return (skipped_gate("sigma", "validation failed"), Vec::new());
690 }
691 match sigma_gate::run_sigma_gate(contract_dir) {
692 sigma_gate::SigmaOutcome::Ran { result, findings } => (*result, findings),
693 sigma_gate::SigmaOutcome::NoSigma => (
694 skipped_gate("sigma", "no contracts/ontology.yaml"),
695 Vec::new(),
696 ),
697 sigma_gate::SigmaOutcome::Malformed(e) => (
698 skipped_gate("sigma", &format!("Σ is malformed: {e}")),
699 Vec::new(),
700 ),
701 }
702}
703
704fn relations_result(
707 contract_dir: &Path,
708 validation_passed: bool,
709) -> (GateResult, Vec<LintFinding>) {
710 if !validation_passed {
711 return (skipped_gate("relations", "validation failed"), Vec::new());
712 }
713 match relations_gate::run_relations_gate(contract_dir) {
714 relations_gate::RelationsOutcome::Ran { result, findings } => (*result, findings),
715 relations_gate::RelationsOutcome::NoSigma => (
716 skipped_gate("relations", "no contracts/ontology.yaml"),
717 Vec::new(),
718 ),
719 relations_gate::RelationsOutcome::Malformed(e) => (
720 skipped_gate("relations", &format!("Σ is malformed: {e}")),
721 Vec::new(),
722 ),
723 relations_gate::RelationsOutcome::NoRelations {
724 contracts_checked,
725 legacy_depends_on,
726 } => (
727 skipped_gate(
728 "relations",
729 &format!("no typed relations in {contracts_checked} contracts ({legacy_depends_on} legacy metadata.depends_on edges) — R-2: zero is a decline"),
730 ),
731 Vec::new(),
732 ),
733 }
734}
735
736fn shapes_result(contract_dir: &Path, validation_passed: bool) -> (GateResult, Vec<LintFinding>) {
739 if !validation_passed {
740 return (skipped_gate("shapes", "validation failed"), Vec::new());
741 }
742 match shapes_gate::run_shapes_gate(contract_dir) {
743 shapes_gate::ShapesOutcome::Ran { result, findings } => (*result, findings),
744 shapes_gate::ShapesOutcome::Unsupported(e) => (skipped_gate("shapes", &format!("{e}")), Vec::new()),
745 shapes_gate::ShapesOutcome::ExtractFailed(e) => (skipped_gate("shapes", &format!("{e}")), Vec::new()),
746 shapes_gate::ShapesOutcome::NoShapes { contracts_checked } => (
747 skipped_gate("shapes", &format!("no `shape:` block in {contracts_checked} contracts — R-2: zero is a decline")),
748 Vec::new(),
749 ),
750 shapes_gate::ShapesOutcome::NoFocus { shapes_n } => (
751 skipped_gate("shapes", &format!("{shapes_n} shape(s), no focus node")),
752 Vec::new(),
753 ),
754 shapes_gate::ShapesOutcome::WrongCorpus { shapes_n, expected, found, refused } => (
755 skipped_gate("shapes", &format!("extract:parity-receipt matched {found} focus node(s) and evidence/parity/EXPECTED_RECEIPTS says {expected} ({shapes_n} shape(s)){} — an extractor that saw the wrong corpus reports the same \"no violations\" as one that saw all of it", if refused.is_empty() { String::new() } else { format!("; refused: {}", refused.join("; ")) })),
756 Vec::new(),
757 ),
758 shapes_gate::ShapesOutcome::NoReceipts { shapes_n, dir } => (
759 skipped_gate("shapes", &format!("{shapes_n} shape(s) resolve receipts and the tree holds none under {dir}/ — R-2: unmeasured is a decline")),
760 Vec::new(),
761 ),
762 shapes_gate::ShapesOutcome::PositiveControlFailed { shapes_n, focus_nodes_n, which } => (
763 skipped_gate("shapes", &format!("positive control {which} did not fire ({shapes_n} shape(s), {focus_nodes_n} focus node(s)) — the gate cannot reject")),
764 Vec::new(),
765 ),
766 shapes_gate::ShapesOutcome::Differential { shapes_n, focus_nodes_n, passed, n, failed } => {
767 let mut g = skipped_gate(
768 "shapes",
769 &format!(
770 "W3C SHACL-Core differential: {passed} of {n} vendored case(s) pass; failed: {} ({shapes_n} shape(s), {focus_nodes_n} focus node(s)) — the validator disagrees with the standard, so no corpus verdict",
771 failed.join(" | ")
772 ),
773 );
774 g.verdict = Verdict::Unknown(crate::ontology::verdict::Reason::Differential);
775 (g, Vec::new())
776 }
777 }
778}
779
780fn skipped_gate(name: &str, reason: &str) -> GateResult {
781 GateResult {
782 name: name.into(),
783 passed: false,
784 skipped: true,
785 verdict: Verdict::from_gate(false, true),
786 duration_ms: 0,
787 detail: GateDetail::Skipped {
788 reason: reason.into(),
789 },
790 extra: None,
791 }
792}
793
794fn apply_suppressions(findings: &mut [LintFinding], config: &LintConfig) {
795 for f in findings.iter_mut() {
796 if config.suppressed_rules.iter().any(|r| r == &f.rule_id) {
797 f.suppressed = true;
798 f.suppression_reason = Some("Suppressed by --suppress-rule".into());
799 }
800 if let Some(ref stem) = f.contract_stem {
801 if config.suppressed_findings.iter().any(|s| s == stem) {
802 f.suppressed = true;
803 f.suppression_reason = Some("Suppressed by --suppress".into());
804 }
805 }
806 if config.suppressed_files.iter().any(|p| f.file.contains(p)) {
807 f.suppressed = true;
808 f.suppression_reason = Some("Suppressed by --suppress-file".into());
809 }
810 }
811}
812
813fn pv_state_dir(contract_dir: &Path) -> std::path::PathBuf {
815 contract_dir.parent().unwrap_or(contract_dir).join(".pv")
816}
817
818fn mark_new_findings(findings: &mut [LintFinding], contract_dir: &Path) {
821 let state_dir = pv_state_dir(contract_dir);
822 let previous_path = state_dir.join("lint-previous.json");
823
824 let previous: HashSet<String> = std::fs::read_to_string(&previous_path)
826 .ok()
827 .and_then(|s| serde_json::from_str(&s).ok())
828 .unwrap_or_default();
829
830 let mut current = HashSet::new();
832 for f in findings.iter_mut() {
833 let fp = f.fingerprint();
834 if !previous.contains(&fp) {
835 f.is_new = true;
836 }
837 current.insert(fp);
838 }
839
840 if let Err(e) = std::fs::create_dir_all(&state_dir) {
842 eprintln!("pv lint: cannot create {}: {e}", state_dir.display());
843 return;
844 }
845 if let Ok(json) = serde_json::to_string(¤t) {
846 let _ = std::fs::write(&previous_path, json);
847 }
848}
849
850fn apply_severity_overrides(findings: &mut [LintFinding], config: &LintConfig) {
851 for f in findings.iter_mut() {
852 if let Some(&sev) = config.severity_overrides.get(&f.rule_id) {
853 f.severity = sev;
854 }
855 }
856 if config.strict {
857 for f in findings.iter_mut() {
858 if f.severity == RuleSeverity::Warning {
859 f.severity = RuleSeverity::Error;
860 }
861 }
862 }
863}
864
865#[cfg(test)]
866#[path = "mod_tests.rs"]
867mod tests;