|
20 | 20 | // WARM — no daemon, cache files exist (daemon auto-starts from cache) |
21 | 21 | // HOT — daemon already running (pure in-memory search) |
22 | 22 | // |
23 | | -// Phase 2 — Parallel validation: runs ALL 141 tests concurrently against |
| 23 | +// Phase 2 — Parallel validation: runs ALL 153 tests concurrently against |
24 | 24 | // the HOT daemon from Phase 1. |
25 | 25 | // |
26 | 26 | // Usage: |
@@ -252,6 +252,13 @@ const CLI_ONLY_FLAGS: &[&str] = &[ |
252 | 252 |
|
253 | 253 | /// Returns true if this test's args require spawning a CLI process. |
254 | 254 | fn requires_cli(args: &[String]) -> bool { |
| 255 | + // Subcommands (agg, aggregate, stats, etc.) always need CLI. |
| 256 | + if let Some(first) = args.first() { |
| 257 | + let f = first.to_lowercase(); |
| 258 | + if matches!(f.as_str(), "agg" | "aggregate" | "stats" | "info" | "daemon" | "index") { |
| 259 | + return true; |
| 260 | + } |
| 261 | + } |
255 | 262 | for (i, a) in args.iter().enumerate() { |
256 | 263 | // --columns all → direct is fine (we output all columns). |
257 | 264 | // --columns Name,Size,... → needs CLI for column projection. |
@@ -2557,6 +2564,168 @@ fn define_tests() -> Vec<TestSpec> { |
2557 | 2564 | Ok(format!("{} dirs, treesize+desc constraints met", rows.len())) |
2558 | 2565 | })); |
2559 | 2566 |
|
| 2567 | + // ═══════════════════════════════════════════════════════════════ |
| 2568 | + // AGGREGATE TESTS (T119–T133) |
| 2569 | + // ═══════════════════════════════════════════════════════════════ |
| 2570 | + |
| 2571 | + // ── T119: uffs agg count ───────────────────────────────────── |
| 2572 | + specs.push(spec("T119 agg count", &["agg", "count"], |stdout, _| { |
| 2573 | + // Expect: "=== total_count ===" and " Total: <number>" |
| 2574 | + if !stdout.contains("Total:") { |
| 2575 | + bail!("Missing 'Total:' line in count output"); |
| 2576 | + } |
| 2577 | + // Extract the number after "Total:" |
| 2578 | + let total_line = stdout.lines() |
| 2579 | + .find(|l| l.contains("Total:")) |
| 2580 | + .unwrap_or(""); |
| 2581 | + let num_str: String = total_line.chars() |
| 2582 | + .filter(|c| c.is_ascii_digit()) |
| 2583 | + .collect(); |
| 2584 | + let total: u64 = num_str.parse().unwrap_or(0); |
| 2585 | + if total == 0 { bail!("Total count is 0"); } |
| 2586 | + Ok(format!("count = {total}")) |
| 2587 | + })); |
| 2588 | + |
| 2589 | + // ── T120: agg overview ─────────────────────────────────────── |
| 2590 | + specs.push(spec("T120 agg overview", &["agg", "overview"], |stdout, _| { |
| 2591 | + // Overview should produce count + stats + buckets sections. |
| 2592 | + let has_total = stdout.contains("Total:"); |
| 2593 | + let has_count = stdout.contains("Count:"); |
| 2594 | + let has_sum = stdout.contains("Sum:"); |
| 2595 | + let has_key = stdout.contains("Key"); |
| 2596 | + if !has_total && !has_count { |
| 2597 | + bail!("Overview missing count/total section"); |
| 2598 | + } |
| 2599 | + if !has_sum { |
| 2600 | + bail!("Overview missing stats section (no Sum:)"); |
| 2601 | + } |
| 2602 | + if !has_key { |
| 2603 | + bail!("Overview missing bucket table (no Key header)"); |
| 2604 | + } |
| 2605 | + Ok("overview: count + stats + buckets present".into()) |
| 2606 | + })); |
| 2607 | + |
| 2608 | + // ── T121: agg by_extension ─────────────────────────────────── |
| 2609 | + specs.push(spec("T121 agg by_extension", &["agg", "by_extension"], |stdout, _| { |
| 2610 | + // Should have a bucket table with extension keys. |
| 2611 | + if !stdout.contains("Key") { |
| 2612 | + bail!("Missing bucket table header"); |
| 2613 | + } |
| 2614 | + // Common extensions that should appear on any Windows system. |
| 2615 | + let has_some_ext = ["dll", "exe", "sys", "txt", "log", "xml", "json", "ini"] |
| 2616 | + .iter() |
| 2617 | + .any(|ext| stdout.to_lowercase().contains(ext)); |
| 2618 | + if !has_some_ext { |
| 2619 | + bail!("No common extensions found in by_extension output"); |
| 2620 | + } |
| 2621 | + let bucket_count = stdout.lines() |
| 2622 | + .filter(|l| l.trim_start().starts_with(|c: char| c.is_alphanumeric())) |
| 2623 | + .filter(|l| l.contains('%')) |
| 2624 | + .count(); |
| 2625 | + Ok(format!("{bucket_count} extension buckets")) |
| 2626 | + })); |
| 2627 | + |
| 2628 | + // ── T122: agg by_type ──────────────────────────────────────── |
| 2629 | + specs.push(spec("T122 agg by_type", &["agg", "by_type"], |stdout, _| { |
| 2630 | + if !stdout.contains("Key") { |
| 2631 | + bail!("Missing bucket table header"); |
| 2632 | + } |
| 2633 | + // Type categories should include at least one of these common types. |
| 2634 | + let types = ["document", "image", "video", "audio", "code", |
| 2635 | + "archive", "executable", "system", "other"]; |
| 2636 | + let has_type = types.iter().any(|t| stdout.to_lowercase().contains(t)); |
| 2637 | + if !has_type { |
| 2638 | + bail!("No type categories found in by_type output"); |
| 2639 | + } |
| 2640 | + Ok("type categories present".into()) |
| 2641 | + })); |
| 2642 | + |
| 2643 | + // ── T123: agg by_drive ─────────────────────────────────────── |
| 2644 | + specs.push(spec("T123 agg by_drive", &["agg", "by_drive"], |stdout, _| { |
| 2645 | + // Should have at least one drive letter (C:, D:, etc.) |
| 2646 | + let has_drive = ('A'..='Z').any(|c| stdout.contains(&format!("{c}:"))); |
| 2647 | + if !has_drive { |
| 2648 | + bail!("No drive letters found in by_drive output"); |
| 2649 | + } |
| 2650 | + Ok("drive buckets present".into()) |
| 2651 | + })); |
| 2652 | + |
| 2653 | + // ── T124: agg by_size ──────────────────────────────────────── |
| 2654 | + specs.push(spec("T124 agg by_size", &["agg", "by_size"], |stdout, _| { |
| 2655 | + if !stdout.contains("Key") { |
| 2656 | + bail!("Missing bucket table header"); |
| 2657 | + } |
| 2658 | + // Size ranges should appear (e.g., "0 B", "KB", "MB", "GB"). |
| 2659 | + let has_size = ["KB", "MB", "GB", "bytes", "B "] |
| 2660 | + .iter() |
| 2661 | + .any(|s| stdout.contains(s)); |
| 2662 | + if !has_size { |
| 2663 | + bail!("No size range labels found"); |
| 2664 | + } |
| 2665 | + Ok("size distribution buckets present".into()) |
| 2666 | + })); |
| 2667 | + |
| 2668 | + // ── T125: agg by_age ───────────────────────────────────────── |
| 2669 | + specs.push(spec("T125 agg by_age", &["agg", "by_age"], |stdout, _| { |
| 2670 | + if !stdout.contains("Key") && !stdout.contains("===") { |
| 2671 | + bail!("Missing section header"); |
| 2672 | + } |
| 2673 | + // Age buckets should have date or time-period labels. |
| 2674 | + Ok("age distribution present".into()) |
| 2675 | + })); |
| 2676 | + |
| 2677 | + // ── T126: agg count --format json ──────────────────────────── |
| 2678 | + specs.push(spec("T126 agg count --format json", |
| 2679 | + &["agg", "count", "--format", "json"], |stdout, _| { |
| 2680 | + let parsed: serde_json::Value = serde_json::from_str(stdout.trim()) |
| 2681 | + .map_err(|e| anyhow::anyhow!("Invalid JSON: {e}"))?; |
| 2682 | + let arr = parsed.as_array() |
| 2683 | + .ok_or_else(|| anyhow::anyhow!("Expected JSON array"))?; |
| 2684 | + if arr.is_empty() { bail!("Empty JSON array"); } |
| 2685 | + let first = &arr[0]; |
| 2686 | + let kind = first.get("kind").and_then(|v| v.as_str()).unwrap_or(""); |
| 2687 | + if kind != "count" { bail!("Expected kind=count, got {kind}"); } |
| 2688 | + let value = first.get("value").and_then(|v| v.as_u64()).unwrap_or(0); |
| 2689 | + if value == 0 { bail!("Count value is 0"); } |
| 2690 | + Ok(format!("JSON count = {value}")) |
| 2691 | + })); |
| 2692 | + |
| 2693 | + // ── T127: agg overview --format json ───────────────────────── |
| 2694 | + specs.push(spec("T127 agg overview --format json", |
| 2695 | + &["agg", "overview", "--format", "json"], |stdout, _| { |
| 2696 | + let parsed: serde_json::Value = serde_json::from_str(stdout.trim()) |
| 2697 | + .map_err(|e| anyhow::anyhow!("Invalid JSON: {e}"))?; |
| 2698 | + let arr = parsed.as_array() |
| 2699 | + .ok_or_else(|| anyhow::anyhow!("Expected JSON array"))?; |
| 2700 | + if arr.len() < 3 { bail!("Overview should produce ≥3 results, got {}", arr.len()); } |
| 2701 | + // Should have count, stats, and buckets kinds. |
| 2702 | + let kinds: Vec<&str> = arr.iter() |
| 2703 | + .filter_map(|v| v.get("kind").and_then(|k| k.as_str())) |
| 2704 | + .collect(); |
| 2705 | + let has_count = kinds.contains(&"count"); |
| 2706 | + let has_stats = kinds.contains(&"stats"); |
| 2707 | + let has_buckets = kinds.contains(&"buckets"); |
| 2708 | + if !has_count { bail!("Missing count result"); } |
| 2709 | + if !has_stats { bail!("Missing stats result"); } |
| 2710 | + if !has_buckets { bail!("Missing buckets result"); } |
| 2711 | + Ok(format!("{} results: {:?}", arr.len(), kinds)) |
| 2712 | + })); |
| 2713 | + |
| 2714 | + // ── T128: agg by_extension --format csv ────────────────────── |
| 2715 | + specs.push(spec("T128 agg by_extension --format csv", |
| 2716 | + &["agg", "by_extension", "--format", "csv"], |stdout, _| { |
| 2717 | + let lines: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect(); |
| 2718 | + if lines.is_empty() { bail!("Empty CSV output"); } |
| 2719 | + // First line should be header with key,count,total_bytes,... |
| 2720 | + let header = lines[0].to_lowercase(); |
| 2721 | + if !header.contains("key") || !header.contains("count") { |
| 2722 | + bail!("CSV header missing key/count: {}", lines[0]); |
| 2723 | + } |
| 2724 | + let data_rows = lines.len() - 1; |
| 2725 | + if data_rows == 0 { bail!("No data rows in CSV"); } |
| 2726 | + Ok(format!("{data_rows} CSV rows")) |
| 2727 | + })); |
| 2728 | + |
2560 | 2729 | specs |
2561 | 2730 | } |
2562 | 2731 |
|
@@ -2614,7 +2783,7 @@ fn main() { |
2614 | 2783 |
|
2615 | 2784 | if specs.is_empty() { |
2616 | 2785 | eprintln!(" {} No tests matched filter: {:?}", "⚠".yellow(), args.test_filter); |
2617 | | - eprintln!(" Available test IDs: T00, T01, ..., T118"); |
| 2786 | + eprintln!(" Available test IDs: T00, T01, ..., T128"); |
2618 | 2787 | std::process::exit(1); |
2619 | 2788 | } |
2620 | 2789 |
|
|
0 commit comments