Skip to main content

batuta/falsification/
cross_platform.rs

1//! Section 9: Cross-Platform & API Completeness (CP-01 to CP-05)
2//!
3//! Portability and API coverage verification.
4//!
5//! # TPS Principles
6//!
7//! - **Portability**: Multi-platform support
8//! - **API completeness**: NumPy/sklearn coverage
9
10use super::types::{CheckItem, Evidence, EvidenceType, Severity};
11use std::path::Path;
12use std::time::Instant;
13
14/// Evaluate all Cross-Platform & API checks.
15pub fn evaluate_all(project_path: &Path) -> Vec<CheckItem> {
16    vec![
17        check_linux_compatibility(project_path),
18        check_macos_windows_compatibility(project_path),
19        check_wasm_browser_compatibility(project_path),
20        check_numpy_api_coverage(project_path),
21        check_sklearn_coverage(project_path),
22    ]
23}
24
25/// CP-01: Linux Distribution Compatibility
26pub fn check_linux_compatibility(project_path: &Path) -> CheckItem {
27    let start = Instant::now();
28    let mut item = CheckItem::new(
29        "CP-01",
30        "Linux Distribution Compatibility",
31        "Stack runs on major Linux distributions",
32    )
33    .with_severity(Severity::Major)
34    .with_tps("Portability");
35
36    let has_linux_ci = check_ci_for_pattern(project_path, &["ubuntu", "linux"]);
37    let has_glibc_docs = check_for_pattern(project_path, &["glibc", "musl", "linux"]);
38
39    item = item.with_evidence(Evidence {
40        evidence_type: EvidenceType::StaticAnalysis,
41        description: format!("Linux: ci={}, docs={}", has_linux_ci, has_glibc_docs),
42        data: None,
43        files: Vec::new(),
44    });
45
46    if has_linux_ci {
47        item = item.pass();
48    } else {
49        item = item.partial("No Linux CI testing");
50    }
51
52    item.finish_timed(start)
53}
54
55/// CP-02: macOS/Windows Compatibility
56pub fn check_macos_windows_compatibility(project_path: &Path) -> CheckItem {
57    let start = Instant::now();
58    let mut item =
59        CheckItem::new("CP-02", "macOS/Windows Compatibility", "Stack runs on macOS and Windows")
60            .with_severity(Severity::Major)
61            .with_tps("Portability");
62
63    let has_macos_ci = check_ci_for_pattern(project_path, &["macos", "darwin"]);
64    let has_windows_ci = check_ci_for_pattern(project_path, &["windows"]);
65    let has_cross_platform_code =
66        check_for_pattern(project_path, &["cfg(target_os", "cfg!(windows)", "cfg!(macos)"]);
67
68    item = item.with_evidence(Evidence {
69        evidence_type: EvidenceType::StaticAnalysis,
70        description: format!(
71            "Cross-platform: macos_ci={}, windows_ci={}, code={}",
72            has_macos_ci, has_windows_ci, has_cross_platform_code
73        ),
74        data: None,
75        files: Vec::new(),
76    });
77
78    if has_macos_ci && has_windows_ci {
79        item = item.pass();
80    } else if has_macos_ci || has_windows_ci {
81        item = item.partial("Partial cross-platform CI");
82    } else if has_cross_platform_code {
83        item = item.partial("Cross-platform code (no CI)");
84    } else {
85        item = item.partial("Linux-only testing");
86    }
87
88    item.finish_timed(start)
89}
90
91/// CP-03: WASM Browser Compatibility
92pub fn check_wasm_browser_compatibility(project_path: &Path) -> CheckItem {
93    let start = Instant::now();
94    let mut item =
95        CheckItem::new("CP-03", "WASM Browser Compatibility", "WASM build works in major browsers")
96            .with_severity(Severity::Major)
97            .with_tps("Edge deployment");
98
99    let has_wasm_build = check_for_pattern(project_path, &["wasm32", "wasm-bindgen", "wasm-pack"]);
100    let has_browser_tests = check_for_pattern(
101        project_path,
102        &["wasm-bindgen-test", "browser_test", "chrome", "firefox"],
103    );
104
105    // Check for WASM feature in Cargo.toml
106    let cargo_toml = project_path.join("Cargo.toml");
107    let has_wasm_feature = cargo_toml
108        .exists()
109        .then(|| std::fs::read_to_string(&cargo_toml).ok())
110        .flatten()
111        .map(|c| c.contains("wasm") || c.contains("wasm32"))
112        .unwrap_or(false);
113
114    item = item.with_evidence(Evidence {
115        evidence_type: EvidenceType::StaticAnalysis,
116        description: format!(
117            "WASM: build={}, tests={}, feature={}",
118            has_wasm_build, has_browser_tests, has_wasm_feature
119        ),
120        data: None,
121        files: Vec::new(),
122    });
123
124    if has_wasm_build && has_browser_tests {
125        item = item.pass();
126    } else if has_wasm_build || has_wasm_feature {
127        item = item.partial("WASM support (verify browser testing)");
128    } else {
129        item = item.partial("No WASM browser support");
130    }
131
132    item.finish_timed(start)
133}
134
135/// CP-04: NumPy API Coverage
136pub fn check_numpy_api_coverage(project_path: &Path) -> CheckItem {
137    let start = Instant::now();
138    let mut item =
139        CheckItem::new("CP-04", "NumPy API Coverage", "Supports >90% of NumPy operations")
140            .with_severity(Severity::Major)
141            .with_tps("API completeness");
142
143    // Check for array/tensor operations that mirror NumPy
144    let numpy_ops = [
145        "reshape",
146        "transpose",
147        "dot",
148        "matmul",
149        "sum",
150        "mean",
151        "std",
152        "var",
153        "min",
154        "max",
155        "argmin",
156        "argmax",
157        "zeros",
158        "ones",
159        "eye",
160        "linspace",
161        "concatenate",
162        "stack",
163        "split",
164    ];
165
166    let mut found_ops = 0;
167    if let Ok(entries) = glob::glob(&format!("{}/src/**/*.rs", project_path.display())) {
168        for entry in entries.flatten() {
169            if let Ok(content) = std::fs::read_to_string(&entry) {
170                for op in &numpy_ops {
171                    if content.contains(op) {
172                        found_ops += 1;
173                    }
174                }
175            }
176        }
177    }
178
179    let coverage = (found_ops as f64 / numpy_ops.len() as f64 * 100.0) as u32;
180
181    item = item.with_evidence(Evidence {
182        evidence_type: EvidenceType::StaticAnalysis,
183        description: format!("NumPy coverage: ~{}% ({}/{})", coverage, found_ops, numpy_ops.len()),
184        data: None,
185        files: Vec::new(),
186    });
187
188    let is_numeric = check_for_pattern(project_path, &["ndarray", "tensor", "Array"]);
189    if !is_numeric || found_ops >= numpy_ops.len() * 80 / 100 {
190        item = item.pass();
191    } else if found_ops >= numpy_ops.len() / 2 {
192        item = item.partial(format!("Partial NumPy coverage (~{}%)", coverage));
193    } else {
194        item = item.partial("Limited NumPy-like API coverage");
195    }
196
197    item.finish_timed(start)
198}
199
200/// CP-05: sklearn Estimator Coverage
201pub fn check_sklearn_coverage(project_path: &Path) -> CheckItem {
202    let start = Instant::now();
203    let mut item = CheckItem::new(
204        "CP-05",
205        "sklearn Estimator Coverage",
206        "Supports >80% of sklearn estimators",
207    )
208    .with_severity(Severity::Major)
209    .with_tps("API completeness");
210
211    // Check for common sklearn estimator equivalents
212    let sklearn_estimators = [
213        "LinearRegression",
214        "LogisticRegression",
215        "Ridge",
216        "Lasso",
217        "RandomForest",
218        "GradientBoosting",
219        "DecisionTree",
220        "KMeans",
221        "DBSCAN",
222        "PCA",
223        "StandardScaler",
224        "SVM",
225        "KNeighbors",
226        "NaiveBayes",
227    ];
228
229    let found_estimators = sklearn_estimators
230        .iter()
231        .filter(|est| {
232            super::helpers::source_contains_pattern(project_path, &[est])
233                || super::helpers::files_contain_pattern_ci(project_path, &["src/**/*.rs"], &[est])
234        })
235        .count();
236
237    let coverage = (found_estimators as f64 / sklearn_estimators.len() as f64 * 100.0) as u32;
238
239    item = item.with_evidence(Evidence {
240        evidence_type: EvidenceType::StaticAnalysis,
241        description: format!(
242            "sklearn coverage: ~{}% ({}/{})",
243            coverage,
244            found_estimators,
245            sklearn_estimators.len()
246        ),
247        data: None,
248        files: Vec::new(),
249    });
250
251    let is_ml = check_for_pattern(project_path, &["train", "fit", "predict", "classifier"]);
252    if !is_ml || found_estimators >= sklearn_estimators.len() * 70 / 100 {
253        item = item.pass();
254    } else if found_estimators >= sklearn_estimators.len() / 3 {
255        item = item.partial(format!("Partial sklearn coverage (~{}%)", coverage));
256    } else {
257        item = item.partial("Limited sklearn-like estimator coverage");
258    }
259
260    item.finish_timed(start)
261}
262
263fn check_for_pattern(project_path: &Path, patterns: &[&str]) -> bool {
264    super::helpers::source_contains_pattern(project_path, patterns)
265}
266
267fn check_ci_for_pattern(project_path: &Path, patterns: &[&str]) -> bool {
268    super::helpers::ci_contains_pattern(project_path, patterns)
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use std::path::PathBuf;
275
276    #[test]
277    fn test_evaluate_all_returns_5_items() {
278        let path = PathBuf::from(".");
279        let items = evaluate_all(&path);
280        assert_eq!(items.len(), 5);
281    }
282
283    #[test]
284    fn test_all_items_have_tps_principle() {
285        let path = PathBuf::from(".");
286        for item in evaluate_all(&path) {
287            assert!(!item.tps_principle.is_empty(), "Item {} missing TPS", item.id);
288        }
289    }
290
291    #[test]
292    fn test_all_items_have_evidence() {
293        let path = PathBuf::from(".");
294        for item in evaluate_all(&path) {
295            assert!(!item.evidence.is_empty(), "Item {} missing evidence", item.id);
296        }
297    }
298
299    // ========================================================================
300    // Additional Coverage Tests
301    // ========================================================================
302
303    #[test]
304    fn test_cp01_linux_compatibility_id() {
305        let result = check_linux_compatibility(Path::new("."));
306        assert_eq!(result.id, "CP-01");
307        assert_eq!(result.severity, Severity::Major);
308        assert_eq!(result.tps_principle, "Portability");
309    }
310
311    #[test]
312    fn test_cp02_macos_windows_compatibility_id() {
313        let result = check_macos_windows_compatibility(Path::new("."));
314        assert_eq!(result.id, "CP-02");
315        assert_eq!(result.severity, Severity::Major);
316        assert_eq!(result.tps_principle, "Portability");
317    }
318
319    #[test]
320    fn test_cp03_wasm_browser_compatibility_id() {
321        let result = check_wasm_browser_compatibility(Path::new("."));
322        assert_eq!(result.id, "CP-03");
323        assert_eq!(result.severity, Severity::Major);
324        assert_eq!(result.tps_principle, "Edge deployment");
325    }
326
327    #[test]
328    fn test_cp04_numpy_api_coverage_id() {
329        let result = check_numpy_api_coverage(Path::new("."));
330        assert_eq!(result.id, "CP-04");
331        assert_eq!(result.severity, Severity::Major);
332        assert_eq!(result.tps_principle, "API completeness");
333    }
334
335    #[test]
336    fn test_cp05_sklearn_coverage_id() {
337        let result = check_sklearn_coverage(Path::new("."));
338        assert_eq!(result.id, "CP-05");
339        assert_eq!(result.severity, Severity::Major);
340        assert_eq!(result.tps_principle, "API completeness");
341    }
342
343    #[test]
344    fn test_cp_nonexistent_path() {
345        let path = Path::new("/nonexistent/path/for/testing");
346        let items = evaluate_all(path);
347        // Should still return 5 items
348        assert_eq!(items.len(), 5);
349    }
350
351    #[test]
352    fn test_linux_compat_with_ci_dir() {
353        let temp_dir = std::env::temp_dir().join("test_cp_linux");
354        let _ = std::fs::remove_dir_all(&temp_dir);
355        std::fs::create_dir_all(temp_dir.join(".github/workflows")).expect("mkdir failed");
356
357        // Create workflow with ubuntu
358        std::fs::write(temp_dir.join(".github/workflows/ci.yml"), "runs-on: ubuntu-latest")
359            .expect("unexpected failure");
360
361        let result = check_linux_compatibility(&temp_dir);
362        assert_eq!(result.id, "CP-01");
363
364        let _ = std::fs::remove_dir_all(&temp_dir);
365    }
366
367    #[test]
368    fn test_macos_windows_compat_with_ci() {
369        let temp_dir = std::env::temp_dir().join("test_cp_macos");
370        let _ = std::fs::remove_dir_all(&temp_dir);
371        std::fs::create_dir_all(temp_dir.join(".github/workflows")).expect("mkdir failed");
372
373        // Create workflow with macos
374        std::fs::write(temp_dir.join(".github/workflows/ci.yml"), "runs-on: macos-latest")
375            .expect("unexpected failure");
376
377        let result = check_macos_windows_compatibility(&temp_dir);
378        assert_eq!(result.id, "CP-02");
379
380        let _ = std::fs::remove_dir_all(&temp_dir);
381    }
382
383    #[test]
384    fn test_wasm_compat_with_cargo_toml() {
385        let temp_dir = std::env::temp_dir().join("test_cp_wasm");
386        let _ = std::fs::remove_dir_all(&temp_dir);
387        std::fs::create_dir_all(&temp_dir).expect("mkdir failed");
388
389        // Create Cargo.toml with wasm-bindgen
390        std::fs::write(
391            temp_dir.join("Cargo.toml"),
392            r#"[package]
393name = "test"
394version = "0.1.0"
395
396[dependencies]
397wasm-bindgen = "0.2"
398"#,
399        )
400        .expect("unexpected failure");
401
402        let result = check_wasm_browser_compatibility(&temp_dir);
403        assert_eq!(result.id, "CP-03");
404
405        let _ = std::fs::remove_dir_all(&temp_dir);
406    }
407
408    #[test]
409    fn test_numpy_coverage_with_converter() {
410        let temp_dir = std::env::temp_dir().join("test_cp_numpy");
411        let _ = std::fs::remove_dir_all(&temp_dir);
412        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
413
414        // Create file with numpy converter reference
415        std::fs::write(
416            temp_dir.join("src/converter.rs"),
417            "// numpy converter using trueno operations",
418        )
419        .expect("unexpected failure");
420
421        let result = check_numpy_api_coverage(&temp_dir);
422        assert_eq!(result.id, "CP-04");
423
424        let _ = std::fs::remove_dir_all(&temp_dir);
425    }
426
427    #[test]
428    fn test_sklearn_coverage_with_converter() {
429        let temp_dir = std::env::temp_dir().join("test_cp_sklearn");
430        let _ = std::fs::remove_dir_all(&temp_dir);
431        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
432
433        // Create file with sklearn converter reference
434        std::fs::write(
435            temp_dir.join("src/sklearn_converter.rs"),
436            "// sklearn to aprender conversion",
437        )
438        .expect("unexpected failure");
439
440        let result = check_sklearn_coverage(&temp_dir);
441        assert_eq!(result.id, "CP-05");
442
443        let _ = std::fs::remove_dir_all(&temp_dir);
444    }
445
446    #[test]
447    fn test_all_items_have_reasonable_duration() {
448        let path = PathBuf::from(".");
449        for item in evaluate_all(&path) {
450            // Duration should be reasonable (less than 1 minute per check)
451            assert!(
452                item.duration_ms < 60_000,
453                "Item {} took unreasonably long: {}ms",
454                item.id,
455                item.duration_ms
456            );
457        }
458    }
459
460    // =========================================================================
461    // Coverage Gap: check_numpy_api_coverage partial/limited branches
462    // =========================================================================
463
464    #[test]
465    fn test_numpy_coverage_partial_with_numeric_project() {
466        // Create a project that has tensor/ndarray but only ~50% numpy ops
467        let temp_dir = std::env::temp_dir().join("test_cp04_partial");
468        let _ = std::fs::remove_dir_all(&temp_dir);
469        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
470
471        // Include ndarray reference (is_numeric=true) and ~10 numpy ops (above 50%)
472        std::fs::write(
473            temp_dir.join("src/ops.rs"),
474            concat!(
475                "use ndarray::Array;\n",
476                "pub fn reshape() {}\n",
477                "pub fn transpose() {}\n",
478                "pub fn dot() {}\n",
479                "pub fn matmul() {}\n",
480                "pub fn sum() {}\n",
481                "pub fn mean() {}\n",
482                "pub fn std() {}\n",
483                "pub fn var() {}\n",
484                "pub fn min() {}\n",
485                "pub fn max() {}\n",
486            ),
487        )
488        .expect("unexpected failure");
489
490        let result = check_numpy_api_coverage(&temp_dir);
491        assert_eq!(result.id, "CP-04");
492        // With is_numeric=true and ~10/18 ops (55%), should be partial
493        assert_eq!(result.status, super::super::types::CheckStatus::Partial);
494        assert!(result.rejection_reason.as_deref().unwrap_or("").contains("NumPy"));
495
496        let _ = std::fs::remove_dir_all(&temp_dir);
497    }
498
499    #[test]
500    fn test_numpy_coverage_limited_with_numeric_project() {
501        // Create a project that has ndarray (is_numeric=true) but very few numpy ops (< 50%)
502        let temp_dir = std::env::temp_dir().join("test_cp04_limited");
503        let _ = std::fs::remove_dir_all(&temp_dir);
504        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
505
506        // Include ndarray reference for is_numeric=true, but only 2 numpy ops
507        std::fs::write(
508            temp_dir.join("src/lib.rs"),
509            "use ndarray::Array2;\npub fn reshape() {}\npub fn dot() {}\n",
510        )
511        .expect("unexpected failure");
512
513        let result = check_numpy_api_coverage(&temp_dir);
514        assert_eq!(result.id, "CP-04");
515        // With is_numeric=true and only ~2/18 ops (11%), should be partial "Limited"
516        assert_eq!(result.status, super::super::types::CheckStatus::Partial);
517        assert!(result.rejection_reason.as_deref().unwrap_or("").contains("Limited"));
518
519        let _ = std::fs::remove_dir_all(&temp_dir);
520    }
521
522    // =========================================================================
523    // Coverage Gap: check_sklearn_coverage partial/limited branches
524    // =========================================================================
525
526    #[test]
527    fn test_sklearn_coverage_partial_with_ml_project() {
528        // ML project with some sklearn estimators (>= 33%, < 70%)
529        let temp_dir = std::env::temp_dir().join("test_cp05_partial");
530        let _ = std::fs::remove_dir_all(&temp_dir);
531        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
532
533        // has train/fit/predict (is_ml=true) + ~6 estimators
534        std::fs::write(
535            temp_dir.join("src/ml.rs"),
536            concat!(
537                "pub fn train() {}\npub fn fit() {}\npub fn predict() {}\n",
538                "pub struct LinearRegression;\n",
539                "pub struct LogisticRegression;\n",
540                "pub struct Ridge;\n",
541                "pub struct Lasso;\n",
542                "pub struct RandomForest;\n",
543                "pub struct GradientBoosting;\n",
544            ),
545        )
546        .expect("unexpected failure");
547
548        let result = check_sklearn_coverage(&temp_dir);
549        assert_eq!(result.id, "CP-05");
550        // 6/14 ~= 42%, above 33% threshold, should be partial
551        assert_eq!(result.status, super::super::types::CheckStatus::Partial);
552        assert!(result.rejection_reason.as_deref().unwrap_or("").contains("sklearn"));
553
554        let _ = std::fs::remove_dir_all(&temp_dir);
555    }
556
557    #[test]
558    fn test_sklearn_coverage_limited_with_ml_project() {
559        // ML project with very few sklearn estimators (< 33%)
560        let temp_dir = std::env::temp_dir().join("test_cp05_limited");
561        let _ = std::fs::remove_dir_all(&temp_dir);
562        std::fs::create_dir_all(temp_dir.join("src")).expect("mkdir failed");
563
564        // has train/fit (is_ml=true) but only 1 estimator
565        std::fs::write(
566            temp_dir.join("src/ml.rs"),
567            "pub fn train() {}\npub fn fit() {}\npub fn classifier() {}\npub struct LinearRegression;\n",
568        )
569        .expect("unexpected failure");
570
571        let result = check_sklearn_coverage(&temp_dir);
572        assert_eq!(result.id, "CP-05");
573        // 1/14 ~= 7%, below 33% threshold, should be partial "Limited"
574        assert_eq!(result.status, super::super::types::CheckStatus::Partial);
575        assert!(result.rejection_reason.as_deref().unwrap_or("").contains("Limited"));
576
577        let _ = std::fs::remove_dir_all(&temp_dir);
578    }
579}