1use super::types::{CheckItem, Evidence, EvidenceType, Severity};
11use std::path::Path;
12use std::time::Instant;
13
14pub 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
25pub 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
55pub 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
91pub 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 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
135pub 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 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
200pub 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 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 #[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 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 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 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 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 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 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 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 #[test]
465 fn test_numpy_coverage_partial_with_numeric_project() {
466 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 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 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 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 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 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 #[test]
527 fn test_sklearn_coverage_partial_with_ml_project() {
528 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 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 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 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 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 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}