1use crate::comply::tarantula::TarantulaEngine;
23use crate::lint::{lint_panic_paths, PanicPathSummary, StateSyncLinter};
24use std::path::Path;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ComplianceStatus {
29 Pass,
31 Fail,
33 Warn,
35 Skip,
37}
38
39impl std::fmt::Display for ComplianceStatus {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Pass => write!(f, "PASS"),
43 Self::Fail => write!(f, "FAIL"),
44 Self::Warn => write!(f, "WARN"),
45 Self::Skip => write!(f, "SKIP"),
46 }
47 }
48}
49
50#[derive(Debug, Clone)]
52pub struct ComplianceCheck {
53 pub id: String,
55 pub name: String,
57 pub status: ComplianceStatus,
59 pub details: Option<String>,
61 pub issue_count: usize,
63}
64
65impl ComplianceCheck {
66 #[must_use]
68 pub fn pass(id: &str, name: &str) -> Self {
69 Self {
70 id: id.to_string(),
71 name: name.to_string(),
72 status: ComplianceStatus::Pass,
73 details: None,
74 issue_count: 0,
75 }
76 }
77
78 #[must_use]
80 pub fn fail(id: &str, name: &str, details: &str, count: usize) -> Self {
81 Self {
82 id: id.to_string(),
83 name: name.to_string(),
84 status: ComplianceStatus::Fail,
85 details: Some(details.to_string()),
86 issue_count: count,
87 }
88 }
89
90 #[must_use]
92 pub fn warn(id: &str, name: &str, details: &str, count: usize) -> Self {
93 Self {
94 id: id.to_string(),
95 name: name.to_string(),
96 status: ComplianceStatus::Warn,
97 details: Some(details.to_string()),
98 issue_count: count,
99 }
100 }
101
102 #[must_use]
104 pub fn skip(id: &str, name: &str, reason: &str) -> Self {
105 Self {
106 id: id.to_string(),
107 name: name.to_string(),
108 status: ComplianceStatus::Skip,
109 details: Some(reason.to_string()),
110 issue_count: 0,
111 }
112 }
113}
114
115#[derive(Debug, Default)]
117pub struct ComplianceResult {
118 pub checks: Vec<ComplianceCheck>,
120 pub compliant: bool,
122 pub files_analyzed: usize,
124}
125
126impl ComplianceResult {
127 #[must_use]
129 pub fn new() -> Self {
130 Self {
131 checks: Vec::new(),
132 compliant: true,
133 files_analyzed: 0,
134 }
135 }
136
137 pub fn add_check(&mut self, check: ComplianceCheck) {
139 if check.status == ComplianceStatus::Fail {
140 self.compliant = false;
141 }
142 self.checks.push(check);
143 }
144
145 #[must_use]
147 pub fn pass_count(&self) -> usize {
148 self.checks
149 .iter()
150 .filter(|c| c.status == ComplianceStatus::Pass)
151 .count()
152 }
153
154 #[must_use]
156 pub fn fail_count(&self) -> usize {
157 self.checks
158 .iter()
159 .filter(|c| c.status == ComplianceStatus::Fail)
160 .count()
161 }
162
163 #[must_use]
165 pub fn warn_count(&self) -> usize {
166 self.checks
167 .iter()
168 .filter(|c| c.status == ComplianceStatus::Warn)
169 .count()
170 }
171
172 #[must_use]
174 pub fn summary(&self) -> String {
175 let total = self.checks.len();
176 let pass = self.pass_count();
177 let fail = self.fail_count();
178 let warn = self.warn_count();
179
180 let status = if self.compliant {
181 if warn > 0 {
182 "COMPLIANT (with warnings)"
183 } else {
184 "COMPLIANT"
185 }
186 } else {
187 "NON-COMPLIANT"
188 };
189
190 format!("{status}: {pass}/{total} passed, {fail} failed, {warn} warnings")
191 }
192}
193
194#[derive(Debug, Default)]
199pub struct WasmThreadingCompliance {
200 linter: StateSyncLinter,
202 tarantula: TarantulaEngine,
204 lcov_passed: Option<std::path::PathBuf>,
206 lcov_failed: Option<std::path::PathBuf>,
208}
209
210impl WasmThreadingCompliance {
211 #[must_use]
213 pub fn new() -> Self {
214 Self {
215 linter: StateSyncLinter::new(),
216 tarantula: TarantulaEngine::new(),
217 lcov_passed: None,
218 lcov_failed: None,
219 }
220 }
221
222 pub fn with_lcov(&mut self, passed: Option<&Path>, failed: Option<&Path>) -> &mut Self {
227 self.lcov_passed = passed.map(|p| p.to_path_buf());
228 self.lcov_failed = failed.map(|p| p.to_path_buf());
229 self
230 }
231
232 pub fn check(&mut self, project_path: &Path) -> ComplianceResult {
234 let mut result = ComplianceResult::new();
235
236 self.check_state_sync_lint(project_path, &mut result);
238
239 self.check_mock_runtime_tests(project_path, &mut result);
241
242 self.check_property_tests(project_path, &mut result);
244
245 self.check_regression_tests(project_path, &mut result);
247
248 self.check_target_js_files(project_path, &mut result);
250
251 self.check_panic_paths(project_path, &mut result);
253
254 result
255 }
256
257 pub fn tarantula_report(&mut self) -> Option<String> {
261 if let Some(ref passed_path) = self.lcov_passed {
263 if let Err(e) = self.tarantula.parse_lcov(passed_path, true) {
264 return Some(format!("Error parsing passed LCOV: {e}"));
265 }
266 }
267
268 if let Some(ref failed_path) = self.lcov_failed {
269 if let Err(e) = self.tarantula.parse_lcov(failed_path, false) {
270 return Some(format!("Error parsing failed LCOV: {e}"));
271 }
272 }
273
274 let reports = self.tarantula.generate_all_reports();
276 if reports.is_empty() {
277 return None;
278 }
279
280 let mut output = String::new();
281 output.push_str("═══════════════════════════════════════════════════════════════════\n");
282 output.push_str(" 🕷️ TARANTULA HOTSPOT REPORT\n");
283 output.push_str("═══════════════════════════════════════════════════════════════════\n\n");
284
285 for report in reports {
286 output.push_str(&report.format_hotspot_report());
287 output.push('\n');
288 }
289
290 Some(output)
291 }
292
293 fn check_state_sync_lint(&mut self, project_path: &Path, result: &mut ComplianceResult) {
295 let src_path = project_path.join("src");
296 let lint_path = if src_path.exists() {
297 src_path
298 } else {
299 project_path.to_path_buf()
300 };
301
302 match self.linter.lint_directory(&lint_path) {
303 Ok(report) => {
304 result.files_analyzed = report.files_analyzed;
305
306 let error_count = report.error_count();
307 if error_count > 0 {
308 result.add_check(ComplianceCheck::fail(
309 "WASM-COMPLY-001",
310 "State sync lint",
311 &format!("{} state sync errors found", error_count),
312 error_count,
313 ));
314 } else {
315 result.add_check(ComplianceCheck::pass("WASM-COMPLY-001", "State sync lint"));
316 }
317 }
318 Err(e) => {
319 result.add_check(ComplianceCheck::skip(
320 "WASM-COMPLY-001",
321 "State sync lint",
322 &format!("Failed to run lint: {e}"),
323 ));
324 }
325 }
326 }
327
328 fn check_mock_runtime_tests(&self, project_path: &Path, result: &mut ComplianceResult) {
330 let tests_path = project_path.join("tests");
331 let src_path = project_path.join("src");
332
333 let mut mock_test_count = 0;
334
335 let search_patterns = [
337 "MockWasmRuntime",
338 "WasmCallbackTestHarness",
339 "MockableWorker",
340 ];
341
342 for pattern in &search_patterns {
343 mock_test_count += count_pattern_in_dir(&tests_path, pattern);
344 mock_test_count += count_pattern_in_dir(&src_path, pattern);
345 }
346
347 if mock_test_count > 0 {
348 result.add_check(ComplianceCheck::pass(
349 "WASM-COMPLY-002",
350 "Mock runtime tests",
351 ));
352 } else {
353 result.add_check(ComplianceCheck::fail(
354 "WASM-COMPLY-002",
355 "Mock runtime tests",
356 "No mock runtime tests found (use MockWasmRuntime or WasmCallbackTestHarness)",
357 0,
358 ));
359 }
360 }
361
362 fn check_property_tests(&self, project_path: &Path, result: &mut ComplianceResult) {
364 let tests_path = project_path.join("tests");
365 let src_path = project_path.join("src");
366
367 let proptest_count = count_pattern_in_dir(&tests_path, "proptest!")
369 + count_pattern_in_dir(&src_path, "proptest!");
370
371 let mock_in_proptest = count_pattern_in_dir(&tests_path, "MockWasmRuntime")
373 + count_pattern_in_dir(&tests_path, "WasmCallbackTestHarness");
374
375 if proptest_count == 0 {
376 result.add_check(ComplianceCheck::warn(
377 "WASM-COMPLY-003",
378 "Property tests on actual code",
379 "No proptest! blocks found - consider adding property-based tests",
380 0,
381 ));
382 } else if mock_in_proptest == 0 {
383 result.add_check(ComplianceCheck::warn(
384 "WASM-COMPLY-003",
385 "Property tests on actual code",
386 "Property tests found but may be testing models instead of actual code",
387 proptest_count,
388 ));
389 } else {
390 result.add_check(ComplianceCheck::pass(
391 "WASM-COMPLY-003",
392 "Property tests on actual code",
393 ));
394 }
395 }
396
397 fn check_regression_tests(&self, project_path: &Path, result: &mut ComplianceResult) {
399 let tests_path = project_path.join("tests");
400 let src_path = project_path.join("src");
401
402 let required_markers = [
404 "WAPR-QA-REGRESSION-005",
405 "WAPR-QA-REGRESSION-006",
406 "WAPR-QA-REGRESSION-007",
407 "regression_", ];
409
410 let mut found_count = 0;
411 for marker in &required_markers {
412 if count_pattern_in_dir(&tests_path, marker) > 0
413 || count_pattern_in_dir(&src_path, marker) > 0
414 {
415 found_count += 1;
416 }
417 }
418
419 if found_count >= 3 {
420 result.add_check(ComplianceCheck::pass(
421 "WASM-COMPLY-004",
422 "Regression tests for known bugs",
423 ));
424 } else {
425 result.add_check(ComplianceCheck::fail(
426 "WASM-COMPLY-004",
427 "Regression tests for known bugs",
428 &format!(
429 "Only {} of 3 required regression test markers found",
430 found_count
431 ),
432 3 - found_count,
433 ));
434 }
435 }
436
437 fn check_panic_paths(&self, project_path: &Path, result: &mut ComplianceResult) {
442 let src_path = project_path.join("src");
443 let lint_path = if src_path.exists() {
444 src_path
445 } else {
446 project_path.to_path_buf()
447 };
448
449 let mut total_errors = 0;
450 let mut total_warnings = 0;
451 let mut files_checked = 0;
452
453 let mut dirs_to_visit = vec![lint_path];
455
456 while let Some(dir) = dirs_to_visit.pop() {
457 if let Ok(entries) = std::fs::read_dir(&dir) {
458 for entry in entries.flatten() {
459 let path = entry.path();
460 if path.is_dir() {
461 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
462 if !name.starts_with('.') && name != "target" {
464 dirs_to_visit.push(path);
465 }
466 } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
467 if let Ok(content) = std::fs::read_to_string(&path) {
468 if let Ok(report) =
469 lint_panic_paths(&content, path.to_str().unwrap_or("unknown"))
470 {
471 let summary = PanicPathSummary::from_report(&report);
472 total_errors += summary.error_count();
473 total_warnings +=
474 summary.total().saturating_sub(summary.error_count());
475 files_checked += 1;
476 }
477 }
478 }
479 }
480 }
481 }
482
483 if files_checked == 0 {
484 result.add_check(ComplianceCheck::skip(
485 "WASM-COMPLY-006",
486 "Panic path detection",
487 "No Rust source files found",
488 ));
489 } else if total_errors > 0 {
490 result.add_check(ComplianceCheck::fail(
491 "WASM-COMPLY-006",
492 "Panic path detection",
493 &format!(
494 "{} panic paths found ({} errors, {} warnings) - use `?` or `ok_or()` instead",
495 total_errors + total_warnings,
496 total_errors,
497 total_warnings
498 ),
499 total_errors,
500 ));
501 } else if total_warnings > 0 {
502 result.add_check(ComplianceCheck::warn(
503 "WASM-COMPLY-006",
504 "Panic path detection",
505 &format!("{} potential panic paths (warnings only)", total_warnings),
506 total_warnings,
507 ));
508 } else {
509 result.add_check(ComplianceCheck::pass(
510 "WASM-COMPLY-006",
511 "Panic path detection",
512 ));
513 }
514 }
515
516 fn check_target_js_files(&self, project_path: &Path, result: &mut ComplianceResult) {
521 let target_path = project_path.join("target");
522
523 if !target_path.exists() {
524 result.add_check(ComplianceCheck::skip(
525 "WASM-COMPLY-005",
526 "No JS files in target/",
527 "No target/ directory found (run cargo build first)",
528 ));
529 return;
530 }
531
532 let js_files = find_js_files_in_target(&target_path);
533
534 if js_files.is_empty() {
535 result.add_check(ComplianceCheck::pass(
536 "WASM-COMPLY-005",
537 "No JS files in target/",
538 ));
539 } else {
540 let suspicious: Vec<_> = js_files
542 .iter()
543 .filter(|p| !is_wasm_bindgen_output(p))
544 .collect();
545
546 if suspicious.is_empty() {
547 result.add_check(ComplianceCheck::pass(
548 "WASM-COMPLY-005",
549 "No JS files in target/",
550 ));
551 } else {
552 let file_list: Vec<_> = suspicious
553 .iter()
554 .take(5)
555 .map(|p| p.display().to_string())
556 .collect();
557 result.add_check(ComplianceCheck::fail(
558 "WASM-COMPLY-005",
559 "No JS files in target/",
560 &format!(
561 "Found {} JS file(s) in target/ (possible build.rs loophole): {}{}",
562 suspicious.len(),
563 file_list.join(", "),
564 if suspicious.len() > 5 { "..." } else { "" }
565 ),
566 suspicious.len(),
567 ));
568 }
569 }
570 }
571}
572
573#[derive(Debug, Clone)]
575pub struct SuspiciousFile {
576 pub path: std::path::PathBuf,
578 pub reason: SuspiciousReason,
580}
581
582#[derive(Debug, Clone, PartialEq, Eq)]
584pub enum SuspiciousReason {
585 JsExtension,
587 JsContent,
589}
590
591impl std::fmt::Display for SuspiciousReason {
592 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593 match self {
594 Self::JsExtension => write!(f, ".js extension"),
595 Self::JsContent => write!(f, "JS content detected"),
596 }
597 }
598}
599
600fn find_suspicious_files_in_target(target_path: &Path) -> Vec<SuspiciousFile> {
608 fn visit(dir: &Path, suspicious: &mut Vec<SuspiciousFile>) {
609 if let Ok(entries) = std::fs::read_dir(dir) {
610 for entry in entries.flatten() {
611 let path = entry.path();
612 if path.is_dir() {
613 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
614 if name != "node_modules" {
617 visit(&path, suspicious);
618 }
619 } else {
620 if path.extension().map(|e| e == "js").unwrap_or(false) {
622 suspicious.push(SuspiciousFile {
623 path,
624 reason: SuspiciousReason::JsExtension,
625 });
626 } else {
627 if let Some(reason) = check_file_for_js_content(&path) {
630 suspicious.push(SuspiciousFile { path, reason });
631 }
632 }
633 }
634 }
635 }
636 }
637
638 let mut suspicious = Vec::new();
639 if target_path.exists() {
640 visit(target_path, &mut suspicious);
641 }
642 suspicious
643}
644
645fn check_file_for_js_content(path: &Path) -> Option<SuspiciousReason> {
649 const SAFE_BINARY_EXTENSIONS: &[&str] = &[
651 "wasm",
652 "png",
653 "jpg",
654 "jpeg",
655 "gif",
656 "ico",
657 "webp",
658 "svg",
659 "ttf",
660 "woff",
661 "woff2",
662 "eot",
663 "otf",
664 "zip",
665 "tar",
666 "gz",
667 "br",
668 "zst",
669 "rlib",
670 "rmeta",
671 "so",
672 "dylib",
673 "dll",
674 "a",
675 "o",
676 "d",
677 "fingerprint",
678 "bin",
679 "dat",
680 ];
681
682 const JS_KEYWORDS: &[&str] = &[
685 "function ",
686 "function(",
687 "const ",
688 "let ",
689 "var ",
690 "=> {",
691 "=>{",
692 "class ",
693 "import ",
694 "export ",
695 "require(",
696 "module.exports",
697 "window.",
698 "document.",
699 "console.log",
700 "addEventListener",
701 "setTimeout(",
702 "setInterval(",
703 "Promise.",
704 "async ",
705 "await ",
706 ];
707
708 let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
710
711 if SAFE_BINARY_EXTENSIONS.contains(&ext) {
712 return None;
713 }
714
715 let metadata = std::fs::metadata(path).ok()?;
717 if metadata.len() > 10 * 1024 * 1024 {
718 return None;
720 }
721
722 let content = std::fs::read(path).ok()?;
724 let sample_size = content.len().min(2048);
725 let sample = &content[..sample_size];
726
727 let is_text = sample.iter().all(|&b| {
729 b.is_ascii_graphic() || b.is_ascii_whitespace() || b == b'\t' || b == b'\n' || b == b'\r'
730 });
731
732 if !is_text {
733 return None;
734 }
735
736 let text = std::str::from_utf8(sample).ok()?;
738
739 let keyword_count = JS_KEYWORDS.iter().filter(|kw| text.contains(*kw)).count();
741
742 if keyword_count >= 2 {
744 return Some(SuspiciousReason::JsContent);
745 }
746
747 None
748}
749
750fn find_js_files_in_target(target_path: &Path) -> Vec<std::path::PathBuf> {
752 find_suspicious_files_in_target(target_path)
753 .into_iter()
754 .map(|s| s.path)
755 .collect()
756}
757
758fn is_wasm_bindgen_output(path: &Path) -> bool {
760 let path_str = path.display().to_string();
762
763 path_str.contains("/pkg/")
768 || path_str.contains("_bg.js")
769 || path_str.contains("/snippets/")
770 || path_str.contains("wasm-bindgen")
771}
772
773fn count_pattern_in_dir(dir: &Path, pattern: &str) -> usize {
775 fn visit(dir: &Path, pattern: &str, count: &mut usize) {
776 if let Ok(entries) = std::fs::read_dir(dir) {
777 for entry in entries.flatten() {
778 let path = entry.path();
779 if path.is_dir() {
780 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
781 if !name.starts_with('.') && name != "target" {
782 visit(&path, pattern, count);
783 }
784 } else if path.extension().map(|e| e == "rs").unwrap_or(false) {
785 if let Ok(content) = std::fs::read_to_string(&path) {
786 *count += content.matches(pattern).count();
787 }
788 }
789 }
790 }
791 }
792
793 let mut count = 0;
794 if dir.exists() {
795 visit(dir, pattern, &mut count);
796 }
797
798 count
799}
800
801#[cfg(test)]
802mod tests {
803 use super::*;
804 use std::fs;
805 use tempfile::TempDir;
806
807 #[test]
808 fn test_compliance_status_display() {
809 assert_eq!(ComplianceStatus::Pass.to_string(), "PASS");
810 assert_eq!(ComplianceStatus::Fail.to_string(), "FAIL");
811 assert_eq!(ComplianceStatus::Warn.to_string(), "WARN");
812 assert_eq!(ComplianceStatus::Skip.to_string(), "SKIP");
813 }
814
815 #[test]
816 fn test_compliance_check_constructors() {
817 let pass = ComplianceCheck::pass("TEST-001", "Test check");
818 assert_eq!(pass.status, ComplianceStatus::Pass);
819 assert!(pass.details.is_none());
820
821 let fail = ComplianceCheck::fail("TEST-002", "Failed check", "Error", 5);
822 assert_eq!(fail.status, ComplianceStatus::Fail);
823 assert_eq!(fail.issue_count, 5);
824
825 let warn = ComplianceCheck::warn("TEST-003", "Warning check", "Warning", 2);
826 assert_eq!(warn.status, ComplianceStatus::Warn);
827
828 let skip = ComplianceCheck::skip("TEST-004", "Skipped check", "Reason");
829 assert_eq!(skip.status, ComplianceStatus::Skip);
830 }
831
832 #[test]
833 fn test_compliance_result_counts() {
834 let mut result = ComplianceResult::new();
835 result.add_check(ComplianceCheck::pass("TEST-001", "Pass"));
836 result.add_check(ComplianceCheck::pass("TEST-002", "Pass"));
837 result.add_check(ComplianceCheck::fail("TEST-003", "Fail", "Error", 1));
838 result.add_check(ComplianceCheck::warn("TEST-004", "Warn", "Warning", 1));
839
840 assert_eq!(result.pass_count(), 2);
841 assert_eq!(result.fail_count(), 1);
842 assert_eq!(result.warn_count(), 1);
843 assert!(!result.compliant);
844 }
845
846 #[test]
847 fn test_compliance_result_summary() {
848 let mut result = ComplianceResult::new();
849 result.add_check(ComplianceCheck::pass("TEST-001", "Pass"));
850 result.add_check(ComplianceCheck::pass("TEST-002", "Pass"));
851
852 let summary = result.summary();
853 assert!(summary.contains("COMPLIANT"));
854 assert!(summary.contains("2/2 passed"));
855 }
856
857 #[test]
858 fn test_count_pattern_in_dir() {
859 let temp_dir = TempDir::new().unwrap();
860 let test_file = temp_dir.path().join("test.rs");
861
862 fs::write(&test_file, "MockWasmRuntime MockWasmRuntime proptest!").unwrap();
863
864 assert_eq!(count_pattern_in_dir(temp_dir.path(), "MockWasmRuntime"), 2);
865 assert_eq!(count_pattern_in_dir(temp_dir.path(), "proptest!"), 1);
866 assert_eq!(count_pattern_in_dir(temp_dir.path(), "nonexistent"), 0);
867 }
868
869 #[test]
870 fn test_wasm_threading_compliance_check() {
871 let temp_dir = TempDir::new().unwrap();
872 let src_dir = temp_dir.path().join("src");
873 fs::create_dir(&src_dir).unwrap();
874
875 let lib_file = src_dir.join("lib.rs");
877 fs::write(
878 &lib_file,
879 r#"
880use probar::mock::MockWasmRuntime;
881
882// regression_test for state sync
883fn regression_state_sync() {
884 let runtime = MockWasmRuntime::new();
885}
886
887// WAPR-QA-REGRESSION-005
888fn test_005() {}
889
890// WAPR-QA-REGRESSION-006
891fn test_006() {}
892
893// WAPR-QA-REGRESSION-007
894fn test_007() {}
895"#,
896 )
897 .unwrap();
898
899 let mut checker = WasmThreadingCompliance::new();
900 let result = checker.check(temp_dir.path());
901
902 assert_eq!(result.checks.len(), 6);
904 }
905
906 #[test]
907 fn test_target_js_files_detection() {
908 let temp_dir = TempDir::new().unwrap();
909 let target_dir = temp_dir.path().join("target");
910 fs::create_dir(&target_dir).unwrap();
911
912 let suspicious_js = target_dir.join("evil_backdoor.js");
914 fs::write(&suspicious_js, "console.log('sneaky');").unwrap();
915
916 let js_files = find_js_files_in_target(&target_dir);
917 assert_eq!(js_files.len(), 1);
918 assert!(!is_wasm_bindgen_output(&js_files[0]));
919 }
920
921 #[test]
922 fn test_wasm_bindgen_output_detection() {
923 assert!(is_wasm_bindgen_output(Path::new("/target/pkg/app.js")));
925 assert!(is_wasm_bindgen_output(Path::new(
926 "/target/wasm32/app_bg.js"
927 )));
928 assert!(is_wasm_bindgen_output(Path::new(
929 "/target/snippets/helper.js"
930 )));
931 assert!(is_wasm_bindgen_output(Path::new(
932 "/target/wasm-bindgen/out.js"
933 )));
934
935 assert!(!is_wasm_bindgen_output(Path::new("/target/debug/evil.js")));
937 assert!(!is_wasm_bindgen_output(Path::new(
938 "/target/release/backdoor.js"
939 )));
940 }
941
942 #[test]
943 fn test_target_js_check_passes_for_wasm_bindgen() {
944 let temp_dir = TempDir::new().unwrap();
945 let target_dir = temp_dir.path().join("target");
946 let pkg_dir = target_dir.join("pkg");
947 fs::create_dir_all(&pkg_dir).unwrap();
948
949 let wasm_bindgen_js = pkg_dir.join("app.js");
951 fs::write(&wasm_bindgen_js, "// wasm-bindgen generated").unwrap();
952
953 let checker = WasmThreadingCompliance::new();
954 let mut result = ComplianceResult::new();
955 checker.check_target_js_files(temp_dir.path(), &mut result);
956
957 assert_eq!(result.checks.len(), 1);
959 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
960 }
961
962 #[test]
963 fn test_target_js_check_fails_for_suspicious_js() {
964 let temp_dir = TempDir::new().unwrap();
965 let target_dir = temp_dir.path().join("target");
966 let debug_dir = target_dir.join("debug");
967 fs::create_dir_all(&debug_dir).unwrap();
968
969 let evil_js = debug_dir.join("build_script_output.js");
971 fs::write(&evil_js, "// generated by build.rs - CI loophole!").unwrap();
972
973 let checker = WasmThreadingCompliance::new();
974 let mut result = ComplianceResult::new();
975 checker.check_target_js_files(temp_dir.path(), &mut result);
976
977 assert_eq!(result.checks.len(), 1);
979 assert_eq!(result.checks[0].status, ComplianceStatus::Fail);
980 assert!(result.checks[0]
981 .details
982 .as_ref()
983 .unwrap()
984 .contains("build.rs loophole"));
985 }
986
987 #[test]
988 fn test_tarantula_report_empty_without_lcov() {
989 let mut checker = WasmThreadingCompliance::new();
990 let report = checker.tarantula_report();
991 assert!(report.is_none());
992 }
993
994 #[test]
999 fn test_suspicious_reason_display() {
1000 assert_eq!(
1001 format!("{}", SuspiciousReason::JsExtension),
1002 ".js extension"
1003 );
1004 assert_eq!(
1005 format!("{}", SuspiciousReason::JsContent),
1006 "JS content detected"
1007 );
1008 }
1009
1010 #[test]
1011 fn test_suspicious_reason_equality() {
1012 assert_eq!(SuspiciousReason::JsExtension, SuspiciousReason::JsExtension);
1013 assert_eq!(SuspiciousReason::JsContent, SuspiciousReason::JsContent);
1014 assert_ne!(SuspiciousReason::JsExtension, SuspiciousReason::JsContent);
1015 }
1016
1017 #[test]
1018 fn test_suspicious_file_struct() {
1019 let file = SuspiciousFile {
1020 path: std::path::PathBuf::from("/target/evil.js"),
1021 reason: SuspiciousReason::JsExtension,
1022 };
1023 assert_eq!(file.path.to_str().unwrap(), "/target/evil.js");
1024 assert_eq!(file.reason, SuspiciousReason::JsExtension);
1025 }
1026
1027 #[test]
1028 fn test_compliance_result_warn_with_warnings() {
1029 let mut result = ComplianceResult::new();
1030 result.add_check(ComplianceCheck::pass("TEST-001", "Pass"));
1031 result.add_check(ComplianceCheck::warn("TEST-002", "Warn", "Warning", 1));
1032
1033 let summary = result.summary();
1034 assert!(summary.contains("COMPLIANT (with warnings)"));
1035 assert!(result.compliant);
1036 }
1037
1038 #[test]
1039 fn test_compliance_result_non_compliant() {
1040 let mut result = ComplianceResult::new();
1041 result.add_check(ComplianceCheck::fail("TEST-001", "Fail", "Error", 1));
1042
1043 let summary = result.summary();
1044 assert!(summary.contains("NON-COMPLIANT"));
1045 assert!(!result.compliant);
1046 }
1047
1048 #[test]
1049 fn test_wasm_threading_compliance_with_lcov() {
1050 let mut checker = WasmThreadingCompliance::new();
1051
1052 checker.with_lcov(
1053 Some(Path::new("/tmp/passed.lcov")),
1054 Some(Path::new("/tmp/failed.lcov")),
1055 );
1056
1057 assert!(checker.lcov_passed.is_some());
1058 assert!(checker.lcov_failed.is_some());
1059 }
1060
1061 #[test]
1062 fn test_wasm_threading_compliance_with_lcov_none() {
1063 let mut checker = WasmThreadingCompliance::new();
1064
1065 checker.with_lcov(None, None);
1066
1067 assert!(checker.lcov_passed.is_none());
1068 assert!(checker.lcov_failed.is_none());
1069 }
1070
1071 #[test]
1072 fn test_check_state_sync_lint_skip_on_error() {
1073 let temp_dir = TempDir::new().unwrap();
1074 let mut checker = WasmThreadingCompliance::new();
1077 let mut result = ComplianceResult::new();
1078 checker.check_state_sync_lint(temp_dir.path(), &mut result);
1079
1080 assert!(result.checks.len() == 1);
1082 }
1083
1084 #[test]
1085 fn test_check_mock_runtime_tests_multiple_patterns() {
1086 let temp_dir = TempDir::new().unwrap();
1087 let tests_dir = temp_dir.path().join("tests");
1088 fs::create_dir(&tests_dir).unwrap();
1089
1090 let test_file = tests_dir.join("callback_test.rs");
1092 fs::write(&test_file, "use WasmCallbackTestHarness;\nfn test() {}").unwrap();
1093
1094 let checker = WasmThreadingCompliance::new();
1095 let mut result = ComplianceResult::new();
1096 checker.check_mock_runtime_tests(temp_dir.path(), &mut result);
1097
1098 assert_eq!(result.checks.len(), 1);
1099 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1100 }
1101
1102 #[test]
1103 fn test_check_property_tests_no_proptest() {
1104 let temp_dir = TempDir::new().unwrap();
1105 let src_dir = temp_dir.path().join("src");
1106 fs::create_dir(&src_dir).unwrap();
1107
1108 let lib_file = src_dir.join("lib.rs");
1110 fs::write(&lib_file, "fn main() {}").unwrap();
1111
1112 let checker = WasmThreadingCompliance::new();
1113 let mut result = ComplianceResult::new();
1114 checker.check_property_tests(temp_dir.path(), &mut result);
1115
1116 assert_eq!(result.checks.len(), 1);
1117 assert_eq!(result.checks[0].status, ComplianceStatus::Warn);
1118 assert!(result.checks[0]
1119 .details
1120 .as_ref()
1121 .unwrap()
1122 .contains("proptest"));
1123 }
1124
1125 #[test]
1126 fn test_check_property_tests_proptest_without_mock() {
1127 let temp_dir = TempDir::new().unwrap();
1128 let tests_dir = temp_dir.path().join("tests");
1129 fs::create_dir(&tests_dir).unwrap();
1130
1131 let test_file = tests_dir.join("prop_test.rs");
1133 fs::write(&test_file, "proptest! { fn test() {} }").unwrap();
1134
1135 let checker = WasmThreadingCompliance::new();
1136 let mut result = ComplianceResult::new();
1137 checker.check_property_tests(temp_dir.path(), &mut result);
1138
1139 assert_eq!(result.checks.len(), 1);
1140 assert_eq!(result.checks[0].status, ComplianceStatus::Warn);
1141 assert!(result.checks[0]
1142 .details
1143 .as_ref()
1144 .unwrap()
1145 .contains("models"));
1146 }
1147
1148 #[test]
1149 fn test_check_property_tests_proptest_with_mock() {
1150 let temp_dir = TempDir::new().unwrap();
1151 let tests_dir = temp_dir.path().join("tests");
1152 fs::create_dir(&tests_dir).unwrap();
1153
1154 let test_file = tests_dir.join("prop_test.rs");
1156 fs::write(
1157 &test_file,
1158 "proptest! { fn test() { let r = MockWasmRuntime::new(); } }",
1159 )
1160 .unwrap();
1161
1162 let checker = WasmThreadingCompliance::new();
1163 let mut result = ComplianceResult::new();
1164 checker.check_property_tests(temp_dir.path(), &mut result);
1165
1166 assert_eq!(result.checks.len(), 1);
1167 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1168 }
1169
1170 #[test]
1171 fn test_check_regression_tests_all_found() {
1172 let temp_dir = TempDir::new().unwrap();
1173 let tests_dir = temp_dir.path().join("tests");
1174 fs::create_dir(&tests_dir).unwrap();
1175
1176 let test_file = tests_dir.join("regression.rs");
1178 fs::write(
1179 &test_file,
1180 r#"
1181 // WAPR-QA-REGRESSION-005
1182 fn test_005() {}
1183 // WAPR-QA-REGRESSION-006
1184 fn test_006() {}
1185 // WAPR-QA-REGRESSION-007
1186 fn test_007() {}
1187 "#,
1188 )
1189 .unwrap();
1190
1191 let checker = WasmThreadingCompliance::new();
1192 let mut result = ComplianceResult::new();
1193 checker.check_regression_tests(temp_dir.path(), &mut result);
1194
1195 assert_eq!(result.checks.len(), 1);
1196 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1197 }
1198
1199 #[test]
1200 fn test_check_regression_tests_partial() {
1201 let temp_dir = TempDir::new().unwrap();
1202 let tests_dir = temp_dir.path().join("tests");
1203 fs::create_dir(&tests_dir).unwrap();
1204
1205 let test_file = tests_dir.join("regression.rs");
1207 fs::write(&test_file, "// WAPR-QA-REGRESSION-005\nfn test() {}").unwrap();
1208
1209 let checker = WasmThreadingCompliance::new();
1210 let mut result = ComplianceResult::new();
1211 checker.check_regression_tests(temp_dir.path(), &mut result);
1212
1213 assert_eq!(result.checks.len(), 1);
1214 assert_eq!(result.checks[0].status, ComplianceStatus::Fail);
1215 }
1216
1217 #[test]
1218 fn test_check_target_no_target_dir() {
1219 let temp_dir = TempDir::new().unwrap();
1220 let checker = WasmThreadingCompliance::new();
1223 let mut result = ComplianceResult::new();
1224 checker.check_target_js_files(temp_dir.path(), &mut result);
1225
1226 assert_eq!(result.checks.len(), 1);
1227 assert_eq!(result.checks[0].status, ComplianceStatus::Skip);
1228 }
1229
1230 #[test]
1231 fn test_check_target_empty_target_dir() {
1232 let temp_dir = TempDir::new().unwrap();
1233 let target_dir = temp_dir.path().join("target");
1234 fs::create_dir(&target_dir).unwrap();
1235
1236 let checker = WasmThreadingCompliance::new();
1237 let mut result = ComplianceResult::new();
1238 checker.check_target_js_files(temp_dir.path(), &mut result);
1239
1240 assert_eq!(result.checks.len(), 1);
1241 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1242 }
1243
1244 #[test]
1245 fn test_find_suspicious_files_js_content_detection() {
1246 let temp_dir = TempDir::new().unwrap();
1247 let target_dir = temp_dir.path().join("target");
1248 fs::create_dir(&target_dir).unwrap();
1249
1250 let hidden_js = target_dir.join("sneaky.txt");
1252 fs::write(&hidden_js, "function test() { console.log('hidden'); }").unwrap();
1253
1254 let suspicious = find_suspicious_files_in_target(&target_dir);
1255 assert!(suspicious
1257 .iter()
1258 .any(|s| s.reason == SuspiciousReason::JsContent));
1259 }
1260
1261 #[test]
1262 fn test_find_suspicious_files_binary_skip() {
1263 let temp_dir = TempDir::new().unwrap();
1264 let target_dir = temp_dir.path().join("target");
1265 fs::create_dir(&target_dir).unwrap();
1266
1267 let wasm_file = target_dir.join("app.wasm");
1269 fs::write(&wasm_file, [0u8, 1, 2, 3, 97, 115, 109]).unwrap();
1270
1271 let suspicious = find_suspicious_files_in_target(&target_dir);
1272 assert!(suspicious.is_empty());
1274 }
1275
1276 #[test]
1277 fn test_find_suspicious_files_nested_directory() {
1278 let temp_dir = TempDir::new().unwrap();
1279 let target_dir = temp_dir.path().join("target");
1280 let nested_dir = target_dir.join("debug").join("build");
1281 fs::create_dir_all(&nested_dir).unwrap();
1282
1283 let js_file = nested_dir.join("backdoor.js");
1285 fs::write(&js_file, "alert('pwned');").unwrap();
1286
1287 let suspicious = find_suspicious_files_in_target(&target_dir);
1288 assert!(!suspicious.is_empty());
1289 assert!(suspicious
1290 .iter()
1291 .any(|s| s.reason == SuspiciousReason::JsExtension));
1292 }
1293
1294 #[test]
1295 fn test_check_file_for_js_content_not_text() {
1296 let temp_dir = TempDir::new().unwrap();
1297
1298 let binary_file = temp_dir.path().join("binary.dat");
1300 fs::write(&binary_file, [0u8, 255, 128, 64, 32]).unwrap();
1301
1302 let result = check_file_for_js_content(&binary_file);
1303 assert!(result.is_none());
1304 }
1305
1306 #[test]
1307 fn test_check_file_for_js_content_single_keyword() {
1308 let temp_dir = TempDir::new().unwrap();
1309
1310 let file = temp_dir.path().join("single.txt");
1312 fs::write(&file, "function main").unwrap();
1313
1314 let result = check_file_for_js_content(&file);
1315 assert!(result.is_none());
1316 }
1317
1318 #[test]
1319 fn test_check_file_for_js_content_multiple_keywords() {
1320 let temp_dir = TempDir::new().unwrap();
1321
1322 let file = temp_dir.path().join("js_content.txt");
1324 fs::write(&file, "function test() { const x = 1; let y = 2; }").unwrap();
1325
1326 let result = check_file_for_js_content(&file);
1327 assert!(result.is_some());
1328 assert_eq!(result.unwrap(), SuspiciousReason::JsContent);
1329 }
1330
1331 #[test]
1332 fn test_count_pattern_in_dir_nested() {
1333 let temp_dir = TempDir::new().unwrap();
1334 let sub_dir = temp_dir.path().join("src").join("nested");
1335 fs::create_dir_all(&sub_dir).unwrap();
1336
1337 let nested_file = sub_dir.join("test.rs");
1339 fs::write(
1340 &nested_file,
1341 "MockWasmRuntime MockWasmRuntime MockWasmRuntime",
1342 )
1343 .unwrap();
1344
1345 assert_eq!(count_pattern_in_dir(temp_dir.path(), "MockWasmRuntime"), 3);
1346 }
1347
1348 #[test]
1349 fn test_count_pattern_in_dir_skips_target() {
1350 let temp_dir = TempDir::new().unwrap();
1351 let target_dir = temp_dir.path().join("target");
1352 fs::create_dir(&target_dir).unwrap();
1353
1354 let target_file = target_dir.join("test.rs");
1356 fs::write(&target_file, "MockWasmRuntime").unwrap();
1357
1358 assert_eq!(count_pattern_in_dir(temp_dir.path(), "MockWasmRuntime"), 0);
1359 }
1360
1361 #[test]
1362 fn test_count_pattern_in_dir_skips_hidden() {
1363 let temp_dir = TempDir::new().unwrap();
1364 let hidden_dir = temp_dir.path().join(".hidden");
1365 fs::create_dir(&hidden_dir).unwrap();
1366
1367 let hidden_file = hidden_dir.join("test.rs");
1369 fs::write(&hidden_file, "MockWasmRuntime").unwrap();
1370
1371 assert_eq!(count_pattern_in_dir(temp_dir.path(), "MockWasmRuntime"), 0);
1372 }
1373
1374 #[test]
1375 fn test_compliance_check_skip_reason() {
1376 let check = ComplianceCheck::skip("TEST-001", "Skipped", "Not applicable");
1377 assert_eq!(check.status, ComplianceStatus::Skip);
1378 assert_eq!(check.details.unwrap(), "Not applicable");
1379 assert_eq!(check.issue_count, 0);
1380 }
1381
1382 #[test]
1383 fn test_compliance_status_variants() {
1384 let pass = ComplianceStatus::Pass;
1385 let fail = ComplianceStatus::Fail;
1386 let warn = ComplianceStatus::Warn;
1387 let skip = ComplianceStatus::Skip;
1388
1389 assert_eq!(pass.to_string(), "PASS");
1390 assert_eq!(fail.to_string(), "FAIL");
1391 assert_eq!(warn.to_string(), "WARN");
1392 assert_eq!(skip.to_string(), "SKIP");
1393 }
1394
1395 #[test]
1396 fn test_wasm_threading_compliance_default() {
1397 let checker = WasmThreadingCompliance::default();
1398 assert!(checker.lcov_passed.is_none());
1399 assert!(checker.lcov_failed.is_none());
1400 }
1401
1402 #[test]
1403 fn test_compliance_result_default() {
1404 let result = ComplianceResult::default();
1405 assert!(result.checks.is_empty());
1406 assert!(!result.compliant);
1408 assert_eq!(result.files_analyzed, 0);
1409 }
1410
1411 #[test]
1412 fn test_multiple_suspicious_js_files() {
1413 let temp_dir = TempDir::new().unwrap();
1414 let target_dir = temp_dir.path().join("target");
1415 let debug_dir = target_dir.join("debug");
1416 fs::create_dir_all(&debug_dir).unwrap();
1417
1418 for i in 0..6 {
1420 let js_file = debug_dir.join(format!("file{i}.js"));
1421 fs::write(&js_file, format!("console.log({i});")).unwrap();
1422 }
1423
1424 let checker = WasmThreadingCompliance::new();
1425 let mut result = ComplianceResult::new();
1426 checker.check_target_js_files(temp_dir.path(), &mut result);
1427
1428 assert_eq!(result.checks.len(), 1);
1429 assert_eq!(result.checks[0].status, ComplianceStatus::Fail);
1430 assert!(result.checks[0].details.as_ref().unwrap().contains("..."));
1432 }
1433
1434 #[test]
1439 fn test_check_panic_paths_clean_code() {
1440 let temp_dir = TempDir::new().unwrap();
1441 let src_dir = temp_dir.path().join("src");
1442 fs::create_dir(&src_dir).unwrap();
1443
1444 let lib_file = src_dir.join("lib.rs");
1446 fs::write(
1447 &lib_file,
1448 r#"
1449fn example() -> Option<i32> {
1450 let x = Some(5);
1451 let y = x?;
1452 Some(y + 1)
1453}
1454
1455fn example2() -> Result<i32, &'static str> {
1456 let x: Option<i32> = Some(5);
1457 let y = x.ok_or("missing")?;
1458 Ok(y + 1)
1459}
1460"#,
1461 )
1462 .unwrap();
1463
1464 let checker = WasmThreadingCompliance::new();
1465 let mut result = ComplianceResult::new();
1466 checker.check_panic_paths(temp_dir.path(), &mut result);
1467
1468 assert_eq!(result.checks.len(), 1);
1469 assert_eq!(result.checks[0].id, "WASM-COMPLY-006");
1470 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1471 }
1472
1473 #[test]
1474 fn test_check_panic_paths_with_unwrap() {
1475 let temp_dir = TempDir::new().unwrap();
1476 let src_dir = temp_dir.path().join("src");
1477 fs::create_dir(&src_dir).unwrap();
1478
1479 let lib_file = src_dir.join("lib.rs");
1481 fs::write(
1482 &lib_file,
1483 r#"
1484fn bad_code() {
1485 let x = Some(5);
1486 let y = x.unwrap(); // PANIC PATH!
1487}
1488"#,
1489 )
1490 .unwrap();
1491
1492 let checker = WasmThreadingCompliance::new();
1493 let mut result = ComplianceResult::new();
1494 checker.check_panic_paths(temp_dir.path(), &mut result);
1495
1496 assert_eq!(result.checks.len(), 1);
1497 assert_eq!(result.checks[0].id, "WASM-COMPLY-006");
1498 assert_eq!(result.checks[0].status, ComplianceStatus::Fail);
1499 assert!(result.checks[0]
1500 .details
1501 .as_ref()
1502 .unwrap()
1503 .contains("panic paths"));
1504 }
1505
1506 #[test]
1507 fn test_check_panic_paths_with_expect() {
1508 let temp_dir = TempDir::new().unwrap();
1509 let src_dir = temp_dir.path().join("src");
1510 fs::create_dir(&src_dir).unwrap();
1511
1512 let lib_file = src_dir.join("lib.rs");
1514 fs::write(
1515 &lib_file,
1516 r#"
1517fn bad_code() {
1518 let x = Some(5);
1519 let y = x.expect("should exist"); // PANIC PATH!
1520}
1521"#,
1522 )
1523 .unwrap();
1524
1525 let checker = WasmThreadingCompliance::new();
1526 let mut result = ComplianceResult::new();
1527 checker.check_panic_paths(temp_dir.path(), &mut result);
1528
1529 assert_eq!(result.checks.len(), 1);
1530 assert_eq!(result.checks[0].status, ComplianceStatus::Fail);
1531 }
1532
1533 #[test]
1534 fn test_check_panic_paths_with_panic_macro() {
1535 let temp_dir = TempDir::new().unwrap();
1536 let src_dir = temp_dir.path().join("src");
1537 fs::create_dir(&src_dir).unwrap();
1538
1539 let lib_file = src_dir.join("lib.rs");
1541 fs::write(
1542 &lib_file,
1543 r#"
1544fn bad_code() {
1545 panic!("something went wrong");
1546}
1547"#,
1548 )
1549 .unwrap();
1550
1551 let checker = WasmThreadingCompliance::new();
1552 let mut result = ComplianceResult::new();
1553 checker.check_panic_paths(temp_dir.path(), &mut result);
1554
1555 assert_eq!(result.checks.len(), 1);
1556 assert_eq!(result.checks[0].status, ComplianceStatus::Fail);
1557 }
1558
1559 #[test]
1560 fn test_check_panic_paths_warnings_only() {
1561 let temp_dir = TempDir::new().unwrap();
1562 let src_dir = temp_dir.path().join("src");
1563 fs::create_dir(&src_dir).unwrap();
1564
1565 let lib_file = src_dir.join("lib.rs");
1567 fs::write(
1568 &lib_file,
1569 r#"
1570fn code_with_warnings(x: bool) {
1571 if x {
1572 return;
1573 }
1574 unreachable!(); // Warning only
1575}
1576"#,
1577 )
1578 .unwrap();
1579
1580 let checker = WasmThreadingCompliance::new();
1581 let mut result = ComplianceResult::new();
1582 checker.check_panic_paths(temp_dir.path(), &mut result);
1583
1584 assert_eq!(result.checks.len(), 1);
1585 assert_eq!(result.checks[0].id, "WASM-COMPLY-006");
1586 assert_eq!(result.checks[0].status, ComplianceStatus::Warn);
1588 assert!(result.checks[0]
1589 .details
1590 .as_ref()
1591 .unwrap()
1592 .contains("warnings only"));
1593 }
1594
1595 #[test]
1596 fn test_check_panic_paths_no_source_files() {
1597 let temp_dir = TempDir::new().unwrap();
1598 let checker = WasmThreadingCompliance::new();
1601 let mut result = ComplianceResult::new();
1602 checker.check_panic_paths(temp_dir.path(), &mut result);
1603
1604 assert_eq!(result.checks.len(), 1);
1605 assert_eq!(result.checks[0].id, "WASM-COMPLY-006");
1606 assert_eq!(result.checks[0].status, ComplianceStatus::Skip);
1607 }
1608
1609 #[test]
1610 fn test_check_panic_paths_nested_directories() {
1611 let temp_dir = TempDir::new().unwrap();
1612 let src_dir = temp_dir.path().join("src");
1613 let nested_dir = src_dir.join("module").join("submodule");
1614 fs::create_dir_all(&nested_dir).unwrap();
1615
1616 let nested_file = nested_dir.join("mod.rs");
1618 fs::write(
1619 &nested_file,
1620 r#"
1621fn clean_code() -> Option<i32> {
1622 Some(42)
1623}
1624"#,
1625 )
1626 .unwrap();
1627
1628 let checker = WasmThreadingCompliance::new();
1629 let mut result = ComplianceResult::new();
1630 checker.check_panic_paths(temp_dir.path(), &mut result);
1631
1632 assert_eq!(result.checks.len(), 1);
1633 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1634 }
1635
1636 #[test]
1637 fn test_check_panic_paths_skips_hidden_dirs() {
1638 let temp_dir = TempDir::new().unwrap();
1639 let src_dir = temp_dir.path().join("src");
1640 fs::create_dir(&src_dir).unwrap();
1641
1642 let hidden_dir = src_dir.join(".hidden");
1644 fs::create_dir(&hidden_dir).unwrap();
1645 let hidden_file = hidden_dir.join("bad.rs");
1646 fs::write(&hidden_file, "fn bad() { panic!(); }").unwrap();
1647
1648 let lib_file = src_dir.join("lib.rs");
1650 fs::write(&lib_file, "fn clean() -> Option<i32> { Some(1) }").unwrap();
1651
1652 let checker = WasmThreadingCompliance::new();
1653 let mut result = ComplianceResult::new();
1654 checker.check_panic_paths(temp_dir.path(), &mut result);
1655
1656 assert_eq!(result.checks.len(), 1);
1658 assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1659 }
1660}