Skip to main content

jugar_probar/comply/
wasm_threading.rs

1//! WASM Threading Compliance Checker
2//!
3//! Per `PROBAR-SPEC-WASM-001` Section 4.1, this module checks for compliance
4//! with WASM threading best practices.
5//!
6//! ## Compliance Checks
7//!
8//! | Check ID | Description | Required |
9//! |----------|-------------|----------|
10//! | WASM-COMPLY-001 | State sync lint passes | Yes |
11//! | WASM-COMPLY-002 | Mock runtime tests exist | Yes |
12//! | WASM-COMPLY-003 | Property tests on actual code | Warning |
13//! | WASM-COMPLY-004 | Regression tests for known bugs | Yes |
14//! | WASM-COMPLY-005 | No JS files in target/ (post-build) | Yes |
15//! | WASM-COMPLY-006 | No panic paths (unwrap/expect/panic!) | Yes |
16//!
17//! ## Tarantula Integration
18//!
19//! When proptest fails, run `probar comply --wasm-threading --lcov-passed <path> --lcov-failed <path>`
20//! to generate a Tarantula Hotspot Report showing suspicious lines.
21
22use crate::comply::tarantula::TarantulaEngine;
23use crate::lint::{lint_panic_paths, PanicPathSummary, StateSyncLinter};
24use std::path::Path;
25
26/// Status of a compliance check
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum ComplianceStatus {
29    /// Check passed
30    Pass,
31    /// Check failed (blocking)
32    Fail,
33    /// Check has warnings (non-blocking)
34    Warn,
35    /// Check was skipped
36    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/// A single compliance check result
51#[derive(Debug, Clone)]
52pub struct ComplianceCheck {
53    /// Check identifier
54    pub id: String,
55    /// Human-readable name
56    pub name: String,
57    /// Status
58    pub status: ComplianceStatus,
59    /// Details or error message
60    pub details: Option<String>,
61    /// Count of issues found (if applicable)
62    pub issue_count: usize,
63}
64
65impl ComplianceCheck {
66    /// Create a passing check
67    #[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    /// Create a failing check
79    #[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    /// Create a warning check
91    #[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    /// Create a skipped check
103    #[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/// Result of compliance checking
116#[derive(Debug, Default)]
117pub struct ComplianceResult {
118    /// Individual check results
119    pub checks: Vec<ComplianceCheck>,
120    /// Overall compliance status
121    pub compliant: bool,
122    /// Total files analyzed
123    pub files_analyzed: usize,
124}
125
126impl ComplianceResult {
127    /// Create a new result
128    #[must_use]
129    pub fn new() -> Self {
130        Self {
131            checks: Vec::new(),
132            compliant: true,
133            files_analyzed: 0,
134        }
135    }
136
137    /// Add a check result
138    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    /// Get pass count
146    #[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    /// Get fail count
155    #[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    /// Get warn count
164    #[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    /// Get summary string
173    #[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/// WASM Threading Compliance Checker
195///
196/// Checks for compliance with WASM threading best practices per
197/// `PROBAR-SPEC-WASM-001`.
198#[derive(Debug, Default)]
199pub struct WasmThreadingCompliance {
200    /// State sync linter
201    linter: StateSyncLinter,
202    /// Tarantula fault localization engine
203    tarantula: TarantulaEngine,
204    /// LCOV file for passing tests (optional)
205    lcov_passed: Option<std::path::PathBuf>,
206    /// LCOV file for failing tests (optional)
207    lcov_failed: Option<std::path::PathBuf>,
208}
209
210impl WasmThreadingCompliance {
211    /// Create a new compliance checker
212    #[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    /// Set LCOV files for Tarantula analysis
223    ///
224    /// When both passed and failed coverage files are provided,
225    /// Tarantula will generate a hotspot report.
226    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    /// Check compliance for a project directory
233    pub fn check(&mut self, project_path: &Path) -> ComplianceResult {
234        let mut result = ComplianceResult::new();
235
236        // WASM-COMPLY-001: State sync lint
237        self.check_state_sync_lint(project_path, &mut result);
238
239        // WASM-COMPLY-002: Mock runtime tests
240        self.check_mock_runtime_tests(project_path, &mut result);
241
242        // WASM-COMPLY-003: Property tests on actual code (warning only)
243        self.check_property_tests(project_path, &mut result);
244
245        // WASM-COMPLY-004: Regression tests
246        self.check_regression_tests(project_path, &mut result);
247
248        // WASM-COMPLY-005: Post-build JS file check
249        self.check_target_js_files(project_path, &mut result);
250
251        // WASM-COMPLY-006: Panic path detection
252        self.check_panic_paths(project_path, &mut result);
253
254        result
255    }
256
257    /// Generate Tarantula hotspot report if LCOV files are configured
258    ///
259    /// Returns the formatted report string, or None if no coverage data.
260    pub fn tarantula_report(&mut self) -> Option<String> {
261        // Load LCOV files if configured
262        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        // Generate reports
275        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    /// Check state sync lint (WASM-COMPLY-001)
294    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    /// Check for mock runtime tests (WASM-COMPLY-002)
329    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        // Search for MockWasmRuntime or WasmCallbackTestHarness usage
336        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    /// Check for property tests on actual code (WASM-COMPLY-003)
363    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        // Look for proptest! macro usage
368        let proptest_count = count_pattern_in_dir(&tests_path, "proptest!")
369            + count_pattern_in_dir(&src_path, "proptest!");
370
371        // Check if tests also use mock runtime (testing actual code)
372        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    /// Check for regression tests (WASM-COMPLY-004)
398    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        // Required regression test markers
403        let required_markers = [
404            "WAPR-QA-REGRESSION-005",
405            "WAPR-QA-REGRESSION-006",
406            "WAPR-QA-REGRESSION-007",
407            "regression_", // Generic regression test prefix
408        ];
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    /// Check for panic paths in source code (WASM-COMPLY-006)
438    ///
439    /// Detects unwrap(), expect(), panic!(), todo!(), etc. that can
440    /// terminate WASM execution unrecoverably.
441    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        // Use iterative approach with a stack to traverse directories
454        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                        // Skip hidden dirs, target, and test directories
463                        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    /// Check for JS files in target/ directory (WASM-COMPLY-005)
517    ///
518    /// This catches CI loopholes where build.rs writes JS to target/,
519    /// bypassing WASM-only compliance checks.
520    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            // Filter to only suspicious JS files (not from wasm-bindgen)
541            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/// Suspicious file found in target/ directory
574#[derive(Debug, Clone)]
575pub struct SuspiciousFile {
576    /// Path to the suspicious file
577    pub path: std::path::PathBuf,
578    /// Reason it's suspicious
579    pub reason: SuspiciousReason,
580}
581
582/// Why a file is considered suspicious
583#[derive(Debug, Clone, PartialEq, Eq)]
584pub enum SuspiciousReason {
585    /// File has .js extension
586    JsExtension,
587    /// File contains JavaScript-like content
588    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
600/// Find all suspicious files in the target directory
601///
602/// This includes:
603/// - Files with .js extension
604/// - Text files containing JavaScript-like content (MIME-type smuggling defense)
605///
606/// HOTFIX PROBAR-WASM-003: Now traverses hidden directories (no more .hidden bypass)
607fn 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                    // HOTFIX: Only skip node_modules, NOT hidden directories
615                    // Hidden directories in target/ are suspicious by definition
616                    if name != "node_modules" {
617                        visit(&path, suspicious);
618                    }
619                } else {
620                    // Check by extension
621                    if path.extension().map(|e| e == "js").unwrap_or(false) {
622                        suspicious.push(SuspiciousFile {
623                            path,
624                            reason: SuspiciousReason::JsExtension,
625                        });
626                    } else {
627                        // HOTFIX: Content inspection for MIME-type smuggling
628                        // Check non-.js files for JavaScript content
629                        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
645/// Check if a file contains JavaScript-like content
646///
647/// Scans the first 2048 bytes for JS keywords to detect MIME-type smuggling.
648fn check_file_for_js_content(path: &Path) -> Option<SuspiciousReason> {
649    // Skip known safe binary extensions
650    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    // JS keyword patterns that indicate JavaScript content
683    // Using word boundaries via simple checks
684    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    // Only check text-like files (skip binaries, images, etc.)
709    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    // Skip files that are too large (likely not hand-written JS)
716    let metadata = std::fs::metadata(path).ok()?;
717    if metadata.len() > 10 * 1024 * 1024 {
718        // > 10MB
719        return None;
720    }
721
722    // Read first 2048 bytes
723    let content = std::fs::read(path).ok()?;
724    let sample_size = content.len().min(2048);
725    let sample = &content[..sample_size];
726
727    // Check if it looks like text (not binary)
728    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    // Convert to string and check for JS keywords
737    let text = std::str::from_utf8(sample).ok()?;
738
739    // Count how many JS keywords are found
740    let keyword_count = JS_KEYWORDS.iter().filter(|kw| text.contains(*kw)).count();
741
742    // If 2+ keywords found, it's likely JS content
743    if keyword_count >= 2 {
744        return Some(SuspiciousReason::JsContent);
745    }
746
747    None
748}
749
750/// Legacy wrapper for backward compatibility
751fn 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
758/// Check if a JS file is legitimate wasm-bindgen output
759fn is_wasm_bindgen_output(path: &Path) -> bool {
760    // wasm-bindgen outputs go into pkg/ or have specific naming patterns
761    let path_str = path.display().to_string();
762
763    // Legitimate patterns:
764    // - /pkg/*.js (wasm-pack output)
765    // - *_bg.js (wasm-bindgen background module)
766    // - snippets/ directory (wasm-bindgen snippets)
767    path_str.contains("/pkg/")
768        || path_str.contains("_bg.js")
769        || path_str.contains("/snippets/")
770        || path_str.contains("wasm-bindgen")
771}
772
773/// Count occurrences of a pattern in all .rs files in a directory
774fn 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        // Create a file with correct patterns
876        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        // Should have run all 6 checks (WASM-COMPLY-001 through 006)
903        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        // Create a suspicious JS file (not wasm-bindgen)
913        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        // Legitimate wasm-bindgen outputs
924        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        // Suspicious JS files
936        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        // Create legitimate wasm-bindgen output
950        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        // Should pass since it's legitimate wasm-bindgen output
958        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        // Create suspicious JS file (bypassing WASM-only compliance)
970        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        // Should fail since it's not legitimate wasm-bindgen output
978        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    // =========================================================================
995    // Additional tests for 95% coverage
996    // =========================================================================
997
998    #[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        // Create a directory that can't be linted (no .rs files)
1075
1076        let mut checker = WasmThreadingCompliance::new();
1077        let mut result = ComplianceResult::new();
1078        checker.check_state_sync_lint(temp_dir.path(), &mut result);
1079
1080        // Should pass or skip since there's nothing to lint
1081        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        // File with WasmCallbackTestHarness
1091        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        // File without proptest
1109        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        // File with proptest but no mock runtime
1132        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        // File with both proptest and mock runtime
1155        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        // File with all required markers
1177        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        // File with only some markers
1206        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        // Don't create target directory
1221
1222        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        // Create a file that looks like JS but has different extension
1251        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        // Should detect JS content in the .txt file
1256        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        // Create a binary file
1268        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        // WASM file should not be flagged
1273        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        // Create JS file in nested directory
1284        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        // Create a binary file with non-ASCII characters
1299        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        // Create file with only one JS keyword (not enough)
1311        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        // Create file with multiple JS keywords
1323        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        // File in nested directory
1338        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        // File in target should be skipped
1355        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        // File in hidden directory should be skipped
1368        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        // Default derive uses bool::default() = false, unlike new() which sets true
1407        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        // Create multiple suspicious JS files
1419        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        // Should show "..." for more than 5 files
1431        assert!(result.checks[0].details.as_ref().unwrap().contains("..."));
1432    }
1433
1434    // =========================================================================
1435    // Panic path compliance check tests (WASM-COMPLY-006)
1436    // =========================================================================
1437
1438    #[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        // Clean code without panic paths
1445        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        // Code with unwrap() panic path
1480        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        // Code with expect() panic path
1513        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        // Code with panic!() macro
1540        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        // Code with only warnings (unreachable, index)
1566        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        // unreachable! is a warning, not error
1587        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        // No src directory, no .rs files
1599
1600        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        // Clean code in nested directory
1617        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        // Create hidden directory with panic paths (should be skipped)
1643        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        // Clean file in src
1649        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        // Should pass because hidden dir is skipped
1657        assert_eq!(result.checks.len(), 1);
1658        assert_eq!(result.checks[0].status, ComplianceStatus::Pass);
1659    }
1660}