1use std::path::Path;
23
24use crate::binding::{BindingRegistry, ImplStatus};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum BindingPolicy {
29 AllImplemented,
32
33 WarnOnGaps,
36
37 TieredEnforcement,
39}
40
41#[derive(Debug)]
43pub struct VerifyResult {
44 pub bound_count: usize,
46 pub partial_count: usize,
48 pub not_implemented_count: usize,
50}
51
52#[allow(clippy::too_many_lines)]
66pub fn verify_bindings(binding_yaml_path: &str, policy: BindingPolicy) -> VerifyResult {
67 let path = Path::new(binding_yaml_path);
68
69 println!("cargo:rerun-if-changed={binding_yaml_path}");
71
72 if let Some(parent) = path.parent() {
74 if let Some(grandparent) = parent.parent() {
75 println!("cargo:rerun-if-changed={}", grandparent.display());
76 }
77 }
78
79 let yaml_content = std::fs::read_to_string(path).unwrap_or_else(|e| {
80 panic!(
81 "CONTRACT BUILD ERROR: Cannot read binding YAML at '{}': {e}\n\
82 Hint: Ensure provable-contracts is checked out as a sibling directory.",
83 path.display()
84 );
85 });
86
87 let registry: BindingRegistry = serde_yaml::from_str(&yaml_content).unwrap_or_else(|e| {
88 panic!(
89 "CONTRACT BUILD ERROR: Cannot parse binding YAML at '{}': {e}",
90 path.display()
91 );
92 });
93
94 let mut result = VerifyResult {
95 bound_count: 0,
96 partial_count: 0,
97 not_implemented_count: 0,
98 };
99
100 for binding in ®istry.bindings {
101 let env_key = make_env_key(&binding.contract, &binding.equation);
102
103 match binding.status {
104 ImplStatus::Implemented => {
105 println!("cargo:rustc-env={env_key}=bound");
106 result.bound_count += 1;
107 }
108 ImplStatus::Partial => {
109 result.partial_count += 1;
110 match policy {
111 BindingPolicy::AllImplemented => {
112 panic!(
113 "CONTRACT BUILD ERROR: Binding {}.{} has status 'partial'. \
114 Policy requires all bindings to be 'implemented'.\n\
115 Module: {}\n\
116 See: unified-contract-by-design.md §10",
117 binding.contract,
118 binding.equation,
119 binding.module_path.as_deref().unwrap_or("(unknown)"),
120 );
121 }
122 BindingPolicy::WarnOnGaps | BindingPolicy::TieredEnforcement => {
123 println!(
124 "cargo:warning=CONTRACT: partial binding {}.{} ({})",
125 binding.contract,
126 binding.equation,
127 binding.module_path.as_deref().unwrap_or("?"),
128 );
129 println!("cargo:rustc-env={env_key}=partial");
131 }
132 }
133 }
134 ImplStatus::NotImplemented => {
135 result.not_implemented_count += 1;
136 match policy {
137 BindingPolicy::AllImplemented | BindingPolicy::TieredEnforcement => {
138 panic!(
139 "CONTRACT BUILD ERROR: Binding {}.{} has status 'not_implemented'. \
140 All bindings must be implemented.\n\
141 Equation: {}\n\
142 Target: {}\n\
143 See: unified-contract-by-design.md §10",
144 binding.contract,
145 binding.equation,
146 binding.equation,
147 binding.module_path.as_deref().unwrap_or("(unassigned)"),
148 );
149 }
150 BindingPolicy::WarnOnGaps => {
151 println!(
152 "cargo:warning=CONTRACT: not_implemented binding {}.{} ({})",
153 binding.contract,
154 binding.equation,
155 binding.module_path.as_deref().unwrap_or("?"),
156 );
157 }
158 }
159 }
160 ImplStatus::Pending => {
161 result.not_implemented_count += 1;
162 println!(
163 "cargo:warning=CONTRACT: pending binding {}.{} ({})",
164 binding.contract,
165 binding.equation,
166 binding.module_path.as_deref().unwrap_or("?"),
167 );
168 }
169 }
170 }
171
172 println!(
173 "cargo:warning=CONTRACT: {}/{} bindings bound ({} partial, {} not_implemented)",
174 result.bound_count,
175 registry.bindings.len(),
176 result.partial_count,
177 result.not_implemented_count,
178 );
179
180 result
181}
182
183pub fn verify_source_functions(
204 binding_yaml_path: &str,
205 src_dir: &str,
206 hard_fail: bool,
207) -> Vec<String> {
208 let path = Path::new(binding_yaml_path);
209 let Ok(yaml_content) = std::fs::read_to_string(path) else {
210 println!("cargo:warning=verify_source_functions: cannot read {binding_yaml_path}");
211 return vec![];
212 };
213 let Ok(registry) = serde_yaml::from_str::<BindingRegistry>(&yaml_content) else {
214 println!("cargo:warning=verify_source_functions: cannot parse {binding_yaml_path}");
215 return vec![];
216 };
217
218 let mut expected_fns: std::collections::HashSet<String> = std::collections::HashSet::new();
220 for b in ®istry.bindings {
221 if b.status != ImplStatus::Implemented {
222 continue;
223 }
224 if let Some(ref func) = b.function {
225 let short = func.rsplit("::").next().unwrap_or(func);
227 expected_fns.insert(short.to_lowercase());
228 }
229 }
230
231 if expected_fns.is_empty() {
232 return vec![];
233 }
234
235 let mut found_fns: std::collections::HashSet<String> = std::collections::HashSet::new();
237 let src = Path::new(src_dir);
238 if src.exists() {
239 scan_source_fns(src, &mut found_fns);
240 }
241 let crates_dir = Path::new("crates");
243 if crates_dir.exists() {
244 scan_source_fns(crates_dir, &mut found_fns);
245 }
246
247 let mut missing: Vec<String> = expected_fns
248 .iter()
249 .filter(|name| !found_fns.contains(name.as_str()))
250 .cloned()
251 .collect();
252 missing.sort();
253
254 if !missing.is_empty() {
255 let count = missing.len();
256 let sample: Vec<_> = missing.iter().take(10).collect();
257 let msg = format!(
258 "[contract] verify_source_functions: {count} bound function(s) not found in source: {}{}",
259 sample
260 .iter()
261 .map(|s| s.as_str())
262 .collect::<Vec<_>>()
263 .join(", "),
264 if count > 10 {
265 format!(" (and {} more)", count - 10)
266 } else {
267 String::new()
268 },
269 );
270
271 if hard_fail {
272 panic!("{msg}");
273 } else {
274 println!("cargo:warning={msg}");
275 }
276 }
277
278 missing
279}
280
281fn scan_source_fns(dir: &Path, found: &mut std::collections::HashSet<String>) {
283 let Ok(entries) = std::fs::read_dir(dir) else {
284 return;
285 };
286 for entry in entries.flatten() {
287 let path = entry.path();
288 if path.is_dir() {
289 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
290 if name != "target" && name != ".git" {
291 scan_source_fns(&path, found);
292 }
293 } else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
294 if let Ok(content) = std::fs::read_to_string(&path) {
295 for line in content.lines() {
296 let trimmed = line.trim();
297 if trimmed.starts_with("pub fn ")
298 || trimmed.starts_with("pub async fn ")
299 || trimmed.starts_with("pub(crate) fn ")
300 {
301 let fn_part = trimmed
302 .trim_start_matches("pub async fn ")
303 .trim_start_matches("pub(crate) fn ")
304 .trim_start_matches("pub fn ");
305 let fn_name = fn_part
306 .split('(')
307 .next()
308 .unwrap_or("")
309 .split('<')
310 .next()
311 .unwrap_or("")
312 .trim()
313 .to_lowercase();
314 if !fn_name.is_empty() {
315 found.insert(fn_name);
316 }
317 }
318 }
319 }
320 }
321 }
322}
323
324fn make_env_key(contract: &str, equation: &str) -> String {
328 let contract_part = contract.to_uppercase().replace(['-', '.'], "_");
329 let equation_part = equation.to_uppercase().replace(['-', '.'], "_");
330 format!("CONTRACT_{contract_part}_{equation_part}")
331}
332
333#[cfg(test)]
334mod tests {
335 #![allow(clippy::all)]
336 use super::*;
337 fn write_temp_yaml(content: &str) -> (tempfile::TempDir, String) {
339 let dir = tempfile::tempdir().expect("create tempdir");
340 let path = dir.path().join("binding.yaml");
341 std::fs::write(&path, content).expect("write yaml");
342 let path_str = path.to_str().unwrap().to_string();
343 (dir, path_str)
344 }
345
346 fn yaml_all_implemented() -> String {
348 r#"
349version: "1.0.0"
350target_crate: test_crate
351bindings:
352 - contract: softmax-v1.yaml
353 equation: softmax
354 module_path: "test::softmax"
355 function: my_softmax
356 status: implemented
357 - contract: relu-v1.yaml
358 equation: relu
359 module_path: "test::relu"
360 function: my_relu
361 status: implemented
362"#
363 .to_string()
364 }
365
366 fn yaml_with_partial() -> String {
368 r#"
369version: "1.0.0"
370target_crate: test_crate
371bindings:
372 - contract: softmax-v1.yaml
373 equation: softmax
374 module_path: "test::softmax"
375 function: my_softmax
376 status: implemented
377 - contract: relu-v1.yaml
378 equation: relu
379 module_path: "test::relu"
380 function: my_relu
381 status: partial
382"#
383 .to_string()
384 }
385
386 fn yaml_with_not_implemented() -> String {
388 r#"
389version: "1.0.0"
390target_crate: test_crate
391bindings:
392 - contract: softmax-v1.yaml
393 equation: softmax
394 module_path: "test::softmax"
395 function: my_softmax
396 status: implemented
397 - contract: gelu-v1.yaml
398 equation: gelu
399 status: not_implemented
400"#
401 .to_string()
402 }
403
404 fn yaml_with_pending() -> String {
406 r#"
407version: "1.0.0"
408target_crate: test_crate
409bindings:
410 - contract: softmax-v1.yaml
411 equation: softmax
412 module_path: "test::softmax"
413 function: my_softmax
414 status: implemented
415 - contract: silu-v1.yaml
416 equation: silu
417 module_path: "test::silu"
418 status: pending
419"#
420 .to_string()
421 }
422
423 fn yaml_mixed() -> String {
425 r#"
426version: "1.0.0"
427target_crate: test_crate
428bindings:
429 - contract: softmax-v1.yaml
430 equation: softmax
431 module_path: "test::softmax"
432 function: my_softmax
433 status: implemented
434 - contract: relu-v1.yaml
435 equation: relu
436 module_path: "test::relu"
437 function: my_relu
438 status: partial
439 - contract: gelu-v1.yaml
440 equation: gelu
441 status: not_implemented
442 - contract: silu-v1.yaml
443 equation: silu
444 status: pending
445"#
446 .to_string()
447 }
448
449 #[test]
452 fn test_make_env_key_matches_macro_convention() {
453 assert_eq!(
454 make_env_key("rmsnorm-kernel-v1", "rmsnorm"),
455 "CONTRACT_RMSNORM_KERNEL_V1_RMSNORM"
456 );
457 assert_eq!(
458 make_env_key("gated-delta-net-v1", "decay"),
459 "CONTRACT_GATED_DELTA_NET_V1_DECAY"
460 );
461 }
462
463 #[test]
464 fn make_env_key_with_yaml_extension() {
465 assert_eq!(
466 make_env_key("softmax-kernel-v1.yaml", "softmax"),
467 "CONTRACT_SOFTMAX_KERNEL_V1_YAML_SOFTMAX"
468 );
469 }
470
471 #[test]
472 fn make_env_key_dots_replaced() {
473 assert_eq!(
474 make_env_key("my.contract.v1", "eq.1"),
475 "CONTRACT_MY_CONTRACT_V1_EQ_1"
476 );
477 }
478
479 #[test]
482 fn test_verify_result_defaults() {
483 let r = VerifyResult {
484 bound_count: 0,
485 partial_count: 0,
486 not_implemented_count: 0,
487 };
488 assert_eq!(r.bound_count, 0);
489 assert_eq!(r.partial_count, 0);
490 assert_eq!(r.not_implemented_count, 0);
491 }
492
493 #[test]
494 fn verify_result_debug_display() {
495 let r = VerifyResult {
496 bound_count: 5,
497 partial_count: 2,
498 not_implemented_count: 1,
499 };
500 let dbg = format!("{r:?}");
501 assert!(dbg.contains("bound_count: 5"));
502 assert!(dbg.contains("partial_count: 2"));
503 assert!(dbg.contains("not_implemented_count: 1"));
504 }
505
506 #[test]
509 fn binding_policy_debug() {
510 assert_eq!(
511 format!("{:?}", BindingPolicy::AllImplemented),
512 "AllImplemented"
513 );
514 assert_eq!(
515 format!("{:?}", BindingPolicy::TieredEnforcement),
516 "TieredEnforcement"
517 );
518 assert_eq!(format!("{:?}", BindingPolicy::WarnOnGaps), "WarnOnGaps");
519 }
520
521 #[test]
522 fn binding_policy_clone_eq() {
523 let a = BindingPolicy::AllImplemented;
524 let b = a;
525 assert_eq!(a, b);
526
527 let c = BindingPolicy::WarnOnGaps;
528 assert_ne!(a, c);
529 }
530
531 #[test]
534 fn verify_bindings_all_implemented_all_ok() {
535 let (_dir, path) = write_temp_yaml(&yaml_all_implemented());
536 let result = verify_bindings(&path, BindingPolicy::AllImplemented);
537 assert_eq!(result.bound_count, 2);
538 assert_eq!(result.partial_count, 0);
539 assert_eq!(result.not_implemented_count, 0);
540 }
541
542 #[test]
543 fn verify_bindings_warn_on_gaps_all_implemented() {
544 let (_dir, path) = write_temp_yaml(&yaml_all_implemented());
545 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
546 assert_eq!(result.bound_count, 2);
547 assert_eq!(result.partial_count, 0);
548 assert_eq!(result.not_implemented_count, 0);
549 }
550
551 #[test]
552 fn verify_bindings_tiered_all_implemented() {
553 let (_dir, path) = write_temp_yaml(&yaml_all_implemented());
554 let result = verify_bindings(&path, BindingPolicy::TieredEnforcement);
555 assert_eq!(result.bound_count, 2);
556 assert_eq!(result.partial_count, 0);
557 assert_eq!(result.not_implemented_count, 0);
558 }
559
560 #[test]
563 fn verify_bindings_warn_on_gaps_partial() {
564 let (_dir, path) = write_temp_yaml(&yaml_with_partial());
565 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
566 assert_eq!(result.bound_count, 1);
567 assert_eq!(result.partial_count, 1);
568 assert_eq!(result.not_implemented_count, 0);
569 }
570
571 #[test]
574 fn verify_bindings_warn_on_gaps_not_implemented() {
575 let (_dir, path) = write_temp_yaml(&yaml_with_not_implemented());
576 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
577 assert_eq!(result.bound_count, 1);
578 assert_eq!(result.partial_count, 0);
579 assert_eq!(result.not_implemented_count, 1);
580 }
581
582 #[test]
585 fn verify_bindings_warn_on_gaps_mixed() {
586 let (_dir, path) = write_temp_yaml(&yaml_mixed());
587 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
588 assert_eq!(result.bound_count, 1);
589 assert_eq!(result.partial_count, 1);
590 assert_eq!(result.not_implemented_count, 2);
592 }
593
594 #[test]
597 fn verify_bindings_tiered_partial_warns_but_ok() {
598 let (_dir, path) = write_temp_yaml(&yaml_with_partial());
599 let result = verify_bindings(&path, BindingPolicy::TieredEnforcement);
600 assert_eq!(result.bound_count, 1);
601 assert_eq!(result.partial_count, 1);
602 assert_eq!(result.not_implemented_count, 0);
603 }
604
605 #[test]
608 fn verify_bindings_pending_status_warns() {
609 let (_dir, path) = write_temp_yaml(&yaml_with_pending());
610 let result = verify_bindings(&path, BindingPolicy::AllImplemented);
611 assert_eq!(result.bound_count, 1);
612 assert_eq!(result.not_implemented_count, 1); }
614
615 #[test]
616 fn verify_bindings_pending_with_tiered() {
617 let (_dir, path) = write_temp_yaml(&yaml_with_pending());
618 let result = verify_bindings(&path, BindingPolicy::TieredEnforcement);
619 assert_eq!(result.bound_count, 1);
620 assert_eq!(result.not_implemented_count, 1);
621 }
622
623 #[test]
624 fn verify_bindings_pending_with_warn_on_gaps() {
625 let (_dir, path) = write_temp_yaml(&yaml_with_pending());
626 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
627 assert_eq!(result.bound_count, 1);
628 assert_eq!(result.not_implemented_count, 1);
629 }
630
631 #[test]
634 #[should_panic(expected = "CONTRACT BUILD ERROR")]
635 fn verify_bindings_all_implemented_panics_on_partial() {
636 let (_dir, path) = write_temp_yaml(&yaml_with_partial());
637 verify_bindings(&path, BindingPolicy::AllImplemented);
638 }
639
640 #[test]
641 #[should_panic(expected = "CONTRACT BUILD ERROR")]
642 fn verify_bindings_all_implemented_panics_on_not_implemented() {
643 let (_dir, path) = write_temp_yaml(&yaml_with_not_implemented());
644 verify_bindings(&path, BindingPolicy::AllImplemented);
645 }
646
647 #[test]
648 #[should_panic(expected = "CONTRACT BUILD ERROR")]
649 fn verify_bindings_tiered_panics_on_not_implemented() {
650 let (_dir, path) = write_temp_yaml(&yaml_with_not_implemented());
651 verify_bindings(&path, BindingPolicy::TieredEnforcement);
652 }
653
654 #[test]
655 #[should_panic(expected = "Cannot read binding YAML")]
656 fn verify_bindings_panics_on_missing_yaml() {
657 verify_bindings("/nonexistent/path/binding.yaml", BindingPolicy::WarnOnGaps);
658 }
659
660 #[test]
661 #[should_panic(expected = "Cannot parse binding YAML")]
662 fn verify_bindings_panics_on_invalid_yaml() {
663 let (_dir, path) = write_temp_yaml("not: [valid: {{yaml");
664 verify_bindings(&path, BindingPolicy::AllImplemented);
665 }
666
667 #[test]
670 fn verify_bindings_empty_bindings() {
671 let yaml = r#"
672version: "1.0.0"
673target_crate: test_crate
674bindings: []
675"#;
676 let (_dir, path) = write_temp_yaml(yaml);
677 let result = verify_bindings(&path, BindingPolicy::AllImplemented);
678 assert_eq!(result.bound_count, 0);
679 assert_eq!(result.partial_count, 0);
680 assert_eq!(result.not_implemented_count, 0);
681 }
682
683 #[test]
686 fn verify_bindings_warn_on_gaps_real_file() {
687 let binding_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
688 .join("../../contracts/aprender/binding.yaml");
689 let result = verify_bindings(binding_path.to_str().unwrap(), BindingPolicy::WarnOnGaps);
690 assert!(
691 result.bound_count > 0,
692 "Should have some implemented bindings"
693 );
694 }
695
696 #[test]
699 fn verify_bindings_handles_nested_path() {
700 let dir = tempfile::tempdir().expect("create tempdir");
702 let nested = dir.path().join("contracts").join("crate");
703 std::fs::create_dir_all(&nested).expect("create nested dirs");
704 let yaml_path = nested.join("binding.yaml");
705 std::fs::write(&yaml_path, &yaml_all_implemented()).expect("write yaml");
706 let result = verify_bindings(yaml_path.to_str().unwrap(), BindingPolicy::AllImplemented);
707 assert_eq!(result.bound_count, 2);
708 }
709
710 #[test]
711 fn verify_bindings_partial_no_module_path() {
712 let yaml = r#"
713version: "1.0.0"
714target_crate: test_crate
715bindings:
716 - contract: relu-v1.yaml
717 equation: relu
718 status: partial
719"#;
720 let (_dir, path) = write_temp_yaml(yaml);
721 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
723 assert_eq!(result.partial_count, 1);
724 }
725
726 #[test]
727 fn verify_bindings_not_implemented_no_module_path() {
728 let yaml = r#"
729version: "1.0.0"
730target_crate: test_crate
731bindings:
732 - contract: relu-v1.yaml
733 equation: relu
734 status: not_implemented
735"#;
736 let (_dir, path) = write_temp_yaml(yaml);
737 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
738 assert_eq!(result.not_implemented_count, 1);
739 }
740
741 #[test]
742 fn verify_bindings_pending_no_module_path() {
743 let yaml = r#"
744version: "1.0.0"
745target_crate: test_crate
746bindings:
747 - contract: relu-v1.yaml
748 equation: relu
749 status: pending
750"#;
751 let (_dir, path) = write_temp_yaml(yaml);
752 let result = verify_bindings(&path, BindingPolicy::WarnOnGaps);
753 assert_eq!(result.not_implemented_count, 1);
754 }
755
756 #[test]
759 fn verify_source_functions_missing_yaml() {
760 let missing = verify_source_functions("/nonexistent/binding.yaml", "src/", false);
761 assert!(missing.is_empty());
762 }
763
764 #[test]
765 fn verify_source_functions_invalid_yaml() {
766 let (_dir, path) = write_temp_yaml("not: [valid: {{yaml");
767 let missing = verify_source_functions(&path, "src/", false);
768 assert!(missing.is_empty());
769 }
770
771 #[test]
772 fn verify_source_functions_no_implemented_bindings() {
773 let yaml = r#"
774version: "1.0.0"
775target_crate: test_crate
776bindings:
777 - contract: gelu-v1.yaml
778 equation: gelu
779 status: not_implemented
780"#;
781 let (_dir, path) = write_temp_yaml(yaml);
782 let missing = verify_source_functions(&path, "src/", false);
783 assert!(missing.is_empty());
784 }
785
786 #[test]
787 fn verify_source_functions_all_found() {
788 let dir = tempfile::tempdir().expect("create tempdir");
789
790 let yaml = r#"
792version: "1.0.0"
793target_crate: test_crate
794bindings:
795 - contract: softmax-v1.yaml
796 equation: softmax
797 function: "test_crate::my_softmax"
798 status: implemented
799 - contract: relu-v1.yaml
800 equation: relu
801 function: "test_crate::my_relu"
802 status: implemented
803"#;
804 let yaml_path = dir.path().join("binding.yaml");
805 std::fs::write(&yaml_path, yaml).expect("write yaml");
806
807 let src_dir = dir.path().join("src");
809 std::fs::create_dir_all(&src_dir).expect("create src dir");
810 std::fs::write(
811 src_dir.join("lib.rs"),
812 "pub fn my_softmax(x: &[f32]) -> Vec<f32> { vec![] }\npub fn my_relu(x: f32) -> f32 { x }\n",
813 )
814 .expect("write lib.rs");
815
816 let missing = verify_source_functions(
817 yaml_path.to_str().unwrap(),
818 src_dir.to_str().unwrap(),
819 false,
820 );
821 assert!(
822 missing.is_empty(),
823 "All functions should be found: {missing:?}"
824 );
825 }
826
827 #[test]
828 fn verify_source_functions_some_missing() {
829 let dir = tempfile::tempdir().expect("create tempdir");
830
831 let yaml = r#"
832version: "1.0.0"
833target_crate: test_crate
834bindings:
835 - contract: softmax-v1.yaml
836 equation: softmax
837 function: "test_crate::my_softmax"
838 status: implemented
839 - contract: silu-v1.yaml
840 equation: silu
841 function: "test_crate::my_silu"
842 status: implemented
843"#;
844 let yaml_path = dir.path().join("binding.yaml");
845 std::fs::write(&yaml_path, yaml).expect("write yaml");
846
847 let src_dir = dir.path().join("src");
849 std::fs::create_dir_all(&src_dir).expect("create src dir");
850 std::fs::write(
851 src_dir.join("lib.rs"),
852 "pub fn my_softmax(x: &[f32]) -> Vec<f32> { vec![] }\n",
853 )
854 .expect("write lib.rs");
855
856 let missing = verify_source_functions(
857 yaml_path.to_str().unwrap(),
858 src_dir.to_str().unwrap(),
859 false,
860 );
861 assert_eq!(missing.len(), 1);
862 assert!(missing.contains(&"my_silu".to_string()));
863 }
864
865 #[test]
866 #[should_panic(expected = "verify_source_functions")]
867 fn verify_source_functions_hard_fail_panics() {
868 let dir = tempfile::tempdir().expect("create tempdir");
869
870 let yaml = r#"
871version: "1.0.0"
872target_crate: test_crate
873bindings:
874 - contract: missing-v1.yaml
875 equation: missing
876 function: "test_crate::nonexistent_function"
877 status: implemented
878"#;
879 let yaml_path = dir.path().join("binding.yaml");
880 std::fs::write(&yaml_path, yaml).expect("write yaml");
881
882 let src_dir = dir.path().join("src");
883 std::fs::create_dir_all(&src_dir).expect("create src dir");
884 std::fs::write(src_dir.join("lib.rs"), "pub fn other() {}\n").expect("write lib.rs");
885
886 verify_source_functions(
887 yaml_path.to_str().unwrap(),
888 src_dir.to_str().unwrap(),
889 true, );
891 }
892
893 #[test]
894 fn verify_source_functions_nonexistent_src_dir() {
895 let dir = tempfile::tempdir().expect("create tempdir");
896
897 let yaml = r#"
898version: "1.0.0"
899target_crate: test_crate
900bindings:
901 - contract: softmax-v1.yaml
902 equation: softmax
903 function: "test_crate::softmax"
904 status: implemented
905"#;
906 let yaml_path = dir.path().join("binding.yaml");
907 std::fs::write(&yaml_path, yaml).expect("write yaml");
908
909 let missing = verify_source_functions(
911 yaml_path.to_str().unwrap(),
912 "/tmp/nonexistent_src_dir_test_provable",
913 false,
914 );
915 assert!(!missing.is_empty());
917 }
918
919 #[test]
920 fn verify_source_functions_skips_not_implemented() {
921 let dir = tempfile::tempdir().expect("create tempdir");
922
923 let yaml = r#"
924version: "1.0.0"
925target_crate: test_crate
926bindings:
927 - contract: gelu-v1.yaml
928 equation: gelu
929 function: "test_crate::gelu"
930 status: not_implemented
931 - contract: silu-v1.yaml
932 equation: silu
933 function: "test_crate::silu"
934 status: partial
935"#;
936 let yaml_path = dir.path().join("binding.yaml");
937 std::fs::write(&yaml_path, yaml).expect("write yaml");
938
939 let src_dir = dir.path().join("src");
940 std::fs::create_dir_all(&src_dir).expect("create src dir");
941 std::fs::write(src_dir.join("lib.rs"), "").expect("write empty lib.rs");
942
943 let missing = verify_source_functions(
945 yaml_path.to_str().unwrap(),
946 src_dir.to_str().unwrap(),
947 false,
948 );
949 assert!(
950 missing.is_empty(),
951 "Non-implemented bindings should be skipped"
952 );
953 }
954
955 #[test]
956 fn verify_source_functions_no_function_field() {
957 let dir = tempfile::tempdir().expect("create tempdir");
958
959 let yaml = r#"
960version: "1.0.0"
961target_crate: test_crate
962bindings:
963 - contract: softmax-v1.yaml
964 equation: softmax
965 status: implemented
966"#;
967 let yaml_path = dir.path().join("binding.yaml");
968 std::fs::write(&yaml_path, yaml).expect("write yaml");
969
970 let missing = verify_source_functions(yaml_path.to_str().unwrap(), "/tmp/whatever", false);
972 assert!(missing.is_empty());
973 }
974
975 #[test]
976 fn verify_source_functions_many_missing_truncates() {
977 let dir = tempfile::tempdir().expect("create tempdir");
978
979 let mut bindings = String::new();
981 for i in 0..15 {
982 bindings.push_str(&format!(
983 r#" - contract: fn{i}-v1.yaml
984 equation: fn{i}
985 function: "test_crate::missing_fn_{i}"
986 status: implemented
987"#
988 ));
989 }
990 let yaml = format!(
991 r#"
992version: "1.0.0"
993target_crate: test_crate
994bindings:
995{bindings}"#
996 );
997 let yaml_path = dir.path().join("binding.yaml");
998 std::fs::write(&yaml_path, &yaml).expect("write yaml");
999
1000 let src_dir = dir.path().join("src");
1001 std::fs::create_dir_all(&src_dir).expect("create src dir");
1002 std::fs::write(src_dir.join("lib.rs"), "").expect("write empty lib.rs");
1003
1004 let missing = verify_source_functions(
1005 yaml_path.to_str().unwrap(),
1006 src_dir.to_str().unwrap(),
1007 false,
1008 );
1009 assert_eq!(missing.len(), 15);
1010 }
1011
1012 #[test]
1015 fn scan_source_fns_empty_dir() {
1016 let dir = tempfile::tempdir().expect("create tempdir");
1017 let mut found = std::collections::HashSet::new();
1018 scan_source_fns(dir.path(), &mut found);
1019 assert!(found.is_empty());
1020 }
1021
1022 #[test]
1023 fn scan_source_fns_finds_pub_fn() {
1024 let dir = tempfile::tempdir().expect("create tempdir");
1025 std::fs::write(
1026 dir.path().join("lib.rs"),
1027 "pub fn my_function(x: i32) -> i32 { x }\n",
1028 )
1029 .expect("write");
1030 let mut found = std::collections::HashSet::new();
1031 scan_source_fns(dir.path(), &mut found);
1032 assert!(found.contains("my_function"));
1033 }
1034
1035 #[test]
1036 fn scan_source_fns_finds_pub_async_fn() {
1037 let dir = tempfile::tempdir().expect("create tempdir");
1038 std::fs::write(
1039 dir.path().join("lib.rs"),
1040 "pub async fn fetch_data() -> Vec<u8> { vec![] }\n",
1041 )
1042 .expect("write");
1043 let mut found = std::collections::HashSet::new();
1044 scan_source_fns(dir.path(), &mut found);
1045 assert!(found.contains("fetch_data"));
1046 }
1047
1048 #[test]
1049 fn scan_source_fns_finds_pub_crate_fn() {
1050 let dir = tempfile::tempdir().expect("create tempdir");
1051 std::fs::write(
1052 dir.path().join("lib.rs"),
1053 "pub(crate) fn internal_helper() {}\n",
1054 )
1055 .expect("write");
1056 let mut found = std::collections::HashSet::new();
1057 scan_source_fns(dir.path(), &mut found);
1058 assert!(found.contains("internal_helper"));
1059 }
1060
1061 #[test]
1062 fn scan_source_fns_ignores_private_fn() {
1063 let dir = tempfile::tempdir().expect("create tempdir");
1064 std::fs::write(dir.path().join("lib.rs"), "fn private_function() {}\n").expect("write");
1065 let mut found = std::collections::HashSet::new();
1066 scan_source_fns(dir.path(), &mut found);
1067 assert!(found.is_empty());
1068 }
1069
1070 #[test]
1071 fn scan_source_fns_skips_non_rs_files() {
1072 let dir = tempfile::tempdir().expect("create tempdir");
1073 std::fs::write(
1074 dir.path().join("data.txt"),
1075 "pub fn should_be_ignored() {}\n",
1076 )
1077 .expect("write");
1078 let mut found = std::collections::HashSet::new();
1079 scan_source_fns(dir.path(), &mut found);
1080 assert!(found.is_empty());
1081 }
1082
1083 #[test]
1084 fn scan_source_fns_recursive() {
1085 let dir = tempfile::tempdir().expect("create tempdir");
1086 let subdir = dir.path().join("submod");
1087 std::fs::create_dir_all(&subdir).expect("create subdir");
1088 std::fs::write(subdir.join("inner.rs"), "pub fn nested_function() {}\n").expect("write");
1089 let mut found = std::collections::HashSet::new();
1090 scan_source_fns(dir.path(), &mut found);
1091 assert!(found.contains("nested_function"));
1092 }
1093
1094 #[test]
1095 fn scan_source_fns_skips_target_dir() {
1096 let dir = tempfile::tempdir().expect("create tempdir");
1097 let target_dir = dir.path().join("target");
1098 std::fs::create_dir_all(&target_dir).expect("create target dir");
1099 std::fs::write(
1100 target_dir.join("generated.rs"),
1101 "pub fn should_be_skipped() {}\n",
1102 )
1103 .expect("write");
1104 let mut found = std::collections::HashSet::new();
1105 scan_source_fns(dir.path(), &mut found);
1106 assert!(found.is_empty());
1107 }
1108
1109 #[test]
1110 fn scan_source_fns_skips_git_dir() {
1111 let dir = tempfile::tempdir().expect("create tempdir");
1112 let git_dir = dir.path().join(".git");
1113 std::fs::create_dir_all(&git_dir).expect("create .git dir");
1114 std::fs::write(git_dir.join("hook.rs"), "pub fn should_be_skipped() {}\n").expect("write");
1115 let mut found = std::collections::HashSet::new();
1116 scan_source_fns(dir.path(), &mut found);
1117 assert!(found.is_empty());
1118 }
1119
1120 #[test]
1121 fn scan_source_fns_handles_generic_fn() {
1122 let dir = tempfile::tempdir().expect("create tempdir");
1123 std::fs::write(
1124 dir.path().join("lib.rs"),
1125 "pub fn generic_fn<T: Clone>(x: T) -> T { x }\n",
1126 )
1127 .expect("write");
1128 let mut found = std::collections::HashSet::new();
1129 scan_source_fns(dir.path(), &mut found);
1130 assert!(found.contains("generic_fn"));
1131 }
1132
1133 #[test]
1134 fn scan_source_fns_case_insensitive() {
1135 let dir = tempfile::tempdir().expect("create tempdir");
1136 std::fs::write(dir.path().join("lib.rs"), "pub fn MyMixedCase() {}\n").expect("write");
1137 let mut found = std::collections::HashSet::new();
1138 scan_source_fns(dir.path(), &mut found);
1139 assert!(found.contains("mymixedcase"), "should be lowercased");
1140 }
1141
1142 #[test]
1143 fn scan_source_fns_multiple_fns_one_file() {
1144 let dir = tempfile::tempdir().expect("create tempdir");
1145 std::fs::write(
1146 dir.path().join("lib.rs"),
1147 "pub fn alpha() {}\npub fn beta() {}\npub async fn gamma() {}\n",
1148 )
1149 .expect("write");
1150 let mut found = std::collections::HashSet::new();
1151 scan_source_fns(dir.path(), &mut found);
1152 assert!(found.contains("alpha"));
1153 assert!(found.contains("beta"));
1154 assert!(found.contains("gamma"));
1155 assert_eq!(found.len(), 3);
1156 }
1157
1158 #[test]
1159 fn scan_source_fns_nonexistent_dir() {
1160 let mut found = std::collections::HashSet::new();
1161 scan_source_fns(
1162 Path::new("/tmp/nonexistent_dir_test_provable_xyz"),
1163 &mut found,
1164 );
1165 assert!(found.is_empty());
1166 }
1167
1168 #[test]
1171 #[should_panic(expected = "status 'partial'")]
1172 fn verify_bindings_all_implemented_partial_with_module_path() {
1173 let yaml = r#"
1174version: "1.0.0"
1175target_crate: test_crate
1176bindings:
1177 - contract: relu-v1.yaml
1178 equation: relu
1179 module_path: "test::relu"
1180 function: my_relu
1181 status: partial
1182"#;
1183 let (_dir, path) = write_temp_yaml(yaml);
1184 verify_bindings(&path, BindingPolicy::AllImplemented);
1185 }
1186
1187 #[test]
1188 #[should_panic(expected = "status 'not_implemented'")]
1189 fn verify_bindings_all_implemented_not_impl_with_module_path() {
1190 let yaml = r#"
1191version: "1.0.0"
1192target_crate: test_crate
1193bindings:
1194 - contract: relu-v1.yaml
1195 equation: relu
1196 module_path: "test::relu"
1197 function: my_relu
1198 status: not_implemented
1199"#;
1200 let (_dir, path) = write_temp_yaml(yaml);
1201 verify_bindings(&path, BindingPolicy::AllImplemented);
1202 }
1203
1204 #[test]
1205 #[should_panic(expected = "status 'not_implemented'")]
1206 fn verify_bindings_tiered_not_impl_with_module_path() {
1207 let yaml = r#"
1208version: "1.0.0"
1209target_crate: test_crate
1210bindings:
1211 - contract: relu-v1.yaml
1212 equation: relu
1213 module_path: "test::relu"
1214 function: my_relu
1215 status: not_implemented
1216"#;
1217 let (_dir, path) = write_temp_yaml(yaml);
1218 verify_bindings(&path, BindingPolicy::TieredEnforcement);
1219 }
1220
1221 #[test]
1224 #[should_panic(expected = "Cannot read binding YAML")]
1225 fn verify_bindings_bare_filename() {
1226 verify_bindings("nonexistent.yaml", BindingPolicy::WarnOnGaps);
1228 }
1229}