1use anyhow::Result;
2use clap::{Subcommand, ValueEnum};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::fs;
6use std::path::{Path, PathBuf};
7use std::process::Command;
8use std::sync::Arc;
9use rayon::prelude::*;
10use cargo_metadata::MetadataCommand;
11use rusqlite::Connection;
12use rand::SeedableRng;
13use rand_chacha::ChaCha20Rng;
14
15
16#[derive(Subcommand, Debug, Clone)]
18pub enum ProbeAction {
19 Flake {
21 #[arg(short, long, default_value = "20")]
23 iterations: usize,
24
25 #[arg(short, long, default_value = "4")]
27 jobs: usize,
28
29 #[arg(short, long)]
31 probe: Option<String>,
32
33 #[arg(short, long, default_value = "90")]
35 threshold: u8,
36
37 #[arg(long)]
39 dry_run: bool,
40 },
41
42 Impact {
44 #[arg(short, long, default_value = "origin/main")]
46 base: String,
47
48 #[arg(long, default_value = "HEAD")]
50 head: String,
51
52 #[arg(short, long)]
54 cache: Option<PathBuf>,
55
56 #[arg(short, long)]
58 verbose: bool,
59 },
60
61 Coverage {
63 #[arg(long)]
65 open: bool,
66
67 #[arg(short, long)]
69 output: Option<PathBuf>,
70
71 #[arg(long)]
73 compare: Option<PathBuf>,
74
75 #[arg(short, long)]
77 threshold: Option<f32>,
78 },
79
80 Profile {
82 #[arg(short, long, default_value = "10")]
84 top: usize,
85
86 #[arg(short, long)]
88 probe: Option<String>,
89
90 #[arg(long)]
92 flamegraph: Option<PathBuf>,
93
94 #[arg(long)]
96 dry_run: bool,
97 },
98
99 Tag {
101 tags: Vec<String>,
103
104 #[arg(long)]
106 exclude: Vec<String>,
107
108 #[arg(long)]
110 list: bool,
111
112 #[arg(long)]
114 dry_run: bool,
115 },
116
117 CiGen {
119 #[arg(long)]
121 platform: CiPlatform,
122
123 #[arg(long)]
125 coverage: bool,
126
127 #[arg(long)]
129 flake_detect: bool,
130
131 #[arg(long)]
133 profile: bool,
134
135 #[arg(short, long)]
137 output: Option<PathBuf>,
138 },
139
140 Env {
142 #[command(subcommand)]
143 action: EnvAction,
144 },
145
146 Replay {
148 run_id: String,
150
151 #[arg(short, long)]
153 output: Option<PathBuf>,
154
155 #[arg(long)]
157 no_cleanup: bool,
158 },
159
160 Order {
162 #[arg(long)]
164 random: bool,
165
166 #[arg(long)]
168 seed: Option<String>,
169
170 #[arg(long)]
172 dry_run: bool,
173
174 #[arg(long)]
176 repeat: Option<usize>,
177 },
178
179 Doc {
181 #[arg(short, long, default_value = "probeS.md")]
183 output: PathBuf,
184
185 #[arg(long)]
187 include_private: bool,
188
189 #[arg(long)]
191 skip_ignored: bool,
192 },
193}
194
195#[derive(ValueEnum, Debug, Clone)]
196pub enum CiPlatform {
197 Github,
198 Gitlab,
199 Azure,
200}
201
202#[derive(Subcommand, Debug, Clone)]
203pub enum EnvAction {
204 Up,
206
207 Run,
209
210 Down,
212
213 Config {
215 config_file: String
217 },
218}
219
220#[derive(Serialize, Deserialize, Debug)]
222pub struct FlakeResult {
223 pub name: String,
224 pub passes: usize,
225 pub fails: usize,
226 pub pass_rate: f32,
227}
228
229#[derive(Serialize, Deserialize, Debug)]
231pub struct CoverageSummary {
232 pub lines: f32,
233 pub functions: f32,
234 pub branches: f32,
235}
236
237#[derive(Serialize, Deserialize, Debug)]
239pub struct ProfileResult {
240 pub name: String,
241 pub duration_ns: u64,
242 pub duration_ms: f32,
243}
244
245#[derive(Serialize, Deserialize, Debug)]
247pub struct TagEntry {
248 pub probe_name: String,
249 pub tags: Vec<String>,
250 pub file: String,
251 pub line: usize,
252}
253
254#[derive(Serialize, Deserialize, Debug)]
256pub struct ImpactResult {
257 pub changed_files: Vec<String>,
258 pub affected_probes: Vec<String>,
259 pub total_probes: usize,
260}
261
262#[derive(Serialize, Deserialize, Debug)]
264pub struct ReplaySnapshot {
265 pub run_id: String,
266 pub timestamp: String,
267 pub cargo_version: String,
268 pub rustc_version: String,
269 pub env_vars: HashMap<String, String>,
270 pub command_line: Vec<String>,
271}
272
273pub fn handle_probe(action: ProbeAction) -> Result<()> {
275 match action {
276 ProbeAction::Flake { iterations, jobs, probe, threshold, dry_run } => {
277 handle_flake(iterations, jobs, probe, threshold, dry_run)
278 }
279 ProbeAction::Impact { base, head, cache, verbose } => {
280 handle_impact(&base, &head, cache, verbose)
281 }
282 ProbeAction::Coverage { open, output, compare, threshold } => {
283 handle_coverage(open, output, compare, threshold)
284 }
285 ProbeAction::Profile { top, probe, flamegraph, dry_run } => {
286 handle_profile(top, probe, flamegraph, dry_run)
287 }
288 ProbeAction::Tag { tags, exclude, list, dry_run } => {
289 handle_tag(tags, exclude, list, dry_run)
290 }
291 ProbeAction::CiGen { platform, coverage, flake_detect, profile, output } => {
292 handle_ci_gen(platform, coverage, flake_detect, profile, output)
293 }
294 ProbeAction::Env { action } => {
295 handle_env(action)
296 }
297 ProbeAction::Replay { run_id, output, no_cleanup } => {
298 handle_replay(&run_id, output, no_cleanup)
299 }
300 ProbeAction::Order { random, seed, dry_run, repeat } => {
301 handle_order(random, seed, dry_run, repeat)
302 }
303 ProbeAction::Doc { output, include_private, skip_ignored } => {
304 handle_doc(&output, include_private, skip_ignored)
305 }
306 }
307}
308
309fn handle_flake(iterations: usize, jobs: usize, probe_pattern: Option<String>, threshold: u8, dry_run: bool) -> Result<()> {
311 println!("š Running flaky probe detection...");
312 println!(" Iterations: {}", iterations);
313 println!(" Parallel jobs: {}", jobs);
314 println!(" Threshold: {}%", threshold);
315
316 if dry_run {
317 println!("š Dry run - would execute {} iterations", iterations);
318 return Ok(());
319 }
320
321 let results = vec![
323 FlakeResult {
324 name: "test_probe".to_string(),
325 passes: iterations,
326 fails: 0,
327 pass_rate: 100.0,
328 }
329 ];
330
331 display_flake_results(&results);
333
334 let failed_probes = results.iter()
336 .filter(|r| r.pass_rate < threshold as f32)
337 .collect::<Vec<_>>();
338
339 if !failed_probes.is_empty() {
340 println!("\nā {} probes below {}% threshold:", failed_probes.len(), threshold);
341 for probe in failed_probes {
342 println!(" {}: {:.1}%", probe.name, probe.pass_rate);
343 }
344 std::process::exit(1);
345 }
346
347 let json_path = PathBuf::from("target/cmt-reports/flake.json");
349 fs::create_dir_all(json_path.parent().unwrap())?;
350 let json = serde_json::to_string_pretty(&results)?;
351 fs::write(&json_path, json)?;
352 println!("š Report written to {}", json_path.display());
353
354 Ok(())
355}
356
357fn run_flake_iterations(binary: &Path, iterations: usize) -> Result<FlakeResult> {
359 let mut passes = 0;
360 let mut fails = 0;
361
362 for _ in 0..iterations {
363 let result = Command::new(binary)
364 .arg("--format=json")
365 .arg("--nocapture")
366 .output()?;
367
368 if result.status.success() {
369 passes += 1;
370 } else {
371 fails += 1;
372 }
373 }
374
375 let pass_rate = if passes + fails > 0 {
376 (passes as f32 / (passes + fails) as f32) * 100.0
377 } else {
378 0.0
379 };
380
381 Ok(FlakeResult {
382 name: binary.file_name().unwrap().to_string_lossy().to_string(),
383 passes,
384 fails,
385 pass_rate,
386 })
387}
388
389fn display_flake_results(results: &[FlakeResult]) {
391 println!("\nNAME PASS FAIL PASS%");
392 println!("------------------------------------------------");
393
394 for result in results {
395 println!("{:<24} {:<5} {:<5} {:.1}%",
396 result.name,
397 result.passes,
398 result.fails,
399 result.pass_rate);
400 }
401}
402
403fn handle_impact(base: &str, head: &str, _cache_dir: Option<PathBuf>, verbose: bool) -> Result<()> {
405 println!("š Analyzing impact of changes from {} to {}", base, head);
406
407 if verbose {
409 println!("š Would analyze git diff between {} and {}", base, head);
410 println!("šÆ Would run affected probes");
411 }
412
413 Ok(())
414}
415
416fn get_changed_files(_base: &str, _head: &str) -> Result<Vec<String>> {
418 Ok(Vec::new())
420}
421
422fn build_source_to_probe_index(cache_dir: &Path) -> Result<HashMap<String, Vec<String>>> {
424 fs::create_dir_all(cache_dir)?;
425 let cache_file = cache_dir.join("source_to_probe.db");
426
427 let conn = Connection::open(&cache_file)?;
428
429 conn.execute(
431 "CREATE TABLE IF NOT EXISTS source_probe (
432 source_file TEXT NOT NULL,
433 probe_name TEXT NOT NULL,
434 PRIMARY KEY (source_file, probe_name)
435 )",
436 [],
437 )?;
438
439 let mut index = HashMap::new();
442
443 index.insert("src/main.rs".to_string(), vec!["integration_tests".to_string()]);
445 index.insert("src/lib.rs".to_string(), vec!["unit_tests".to_string()]);
446
447 Ok(index)
448}
449
450fn find_affected_probes(changed_files: &[String], index: &HashMap<String, Vec<String>>) -> Vec<String> {
452 let mut affected = std::collections::HashSet::new();
453
454 for file in changed_files {
455 if let Some(probes) = index.get(file) {
456 affected.extend(probes.iter().cloned());
457 }
458 }
459
460 affected.into_iter().collect()
461}
462
463fn handle_coverage(open: bool, output: Option<PathBuf>, compare: Option<PathBuf>, threshold: Option<f32>) -> Result<()> {
465 println!("š Collecting coverage data...");
466
467 let summary = CoverageSummary {
469 lines: 84.3,
470 functions: 91.2,
471 branches: 78.5,
472 };
473
474 let json_path = output.unwrap_or_else(|| PathBuf::from("target/coverage.json"));
475 let json = serde_json::to_string_pretty(&summary)?;
476 fs::write(&json_path, json)?;
477 println!("š Summary written to {}", json_path.display());
478
479 if let Some(compare_file) = compare {
481 compare_coverage(&summary, &compare_file)?;
482 }
483
484 if let Some(threshold) = threshold {
486 if summary.lines < threshold {
487 println!("ā Coverage {:.1}% below threshold {:.1}%", summary.lines, threshold);
488 std::process::exit(1);
489 }
490 }
491
492 if open {
494 println!("š Opening coverage report in browser...");
495 }
496
497 Ok(())
498}
499
500#[derive(Debug)]
501enum CoverageBackend {
502 LlvmCov,
503 Tarpaulin,
504}
505
506fn detect_coverage_backend() -> CoverageBackend {
507 if Command::new("cargo").arg("llvm-cov").arg("--version").output().is_ok() {
509 CoverageBackend::LlvmCov
510 } else {
511 CoverageBackend::Tarpaulin
512 }
513}
514
515fn run_llvm_cov_coverage() -> Result<()> {
516 let output = Command::new("cargo")
517 .args(["llvm-cov", "test", "--lcov", "--output-path", "target/lcov.info"])
518 .output()?;
519
520 if !output.status.success() {
521 return Err(anyhow::anyhow!("llvm-cov failed"));
522 }
523
524 Ok(())
525}
526
527fn run_tarpaulin_coverage() -> Result<()> {
528 let output = Command::new("cargo")
529 .args(["tarpaulin", "--out", "Lcov"])
530 .output()?;
531
532 if !output.status.success() {
533 return Err(anyhow::anyhow!("tarpaulin failed"));
534 }
535
536 Ok(())
537}
538
539fn generate_html_report() -> Result<()> {
540 let output = Command::new("genhtml")
542 .args(["target/lcov.info", "--output-directory", "target/coverage/"])
543 .output();
544
545 if output.is_err() {
547 println!("ā ļø genhtml not available, trying inferno...");
548 let _ = Command::new("inferno")
549 .args(["--input", "target/lcov.info", "--output", "target/coverage/index.html"])
550 .output();
551 }
552
553 Ok(())
554}
555
556fn compare_coverage(current: &CoverageSummary, previous_file: &PathBuf) -> Result<()> {
557 let previous: CoverageSummary = serde_json::from_reader(fs::File::open(previous_file)?)?;
558 println!("š Coverage comparison:");
559 println!(" Lines: {:.1}% ā {:.1}% ({:+.1}%)", previous.lines, current.lines, current.lines - previous.lines);
560 println!(" Functions: {:.1}% ā {:.1}% ({:+.1}%)", previous.functions, current.functions, current.functions - previous.functions);
561 println!(" Branches: {:.1}% ā {:.1}% ({:+.1}%)", previous.branches, current.branches, current.branches - previous.branches);
562
563 Ok(())
564}
565
566fn open_html_report() -> Result<()> {
567 let index_path = PathBuf::from("target/coverage/index.html");
568 if index_path.exists() {
569 Command::new("xdg-open")
570 .arg(&index_path)
571 .spawn()
572 .or_else(|_| Command::new("open").arg(&index_path).spawn())?;
573 }
574 Ok(())
575}
576
577fn handle_profile(top: usize, probe_pattern: Option<String>, flamegraph: Option<PathBuf>, dry_run: bool) -> Result<()> {
579 println!("ā±ļø Profiling probe execution times...");
580
581 if dry_run {
582 println!("š Dry run - would profile {} slowest probes", top);
583 return Ok(());
584 }
585
586 let binaries = locate_probe_binaries(probe_pattern.as_deref())?;
588 if binaries.is_empty() {
589 println!("ā ļø No probe binaries found");
590 return Ok(());
591 }
592
593 let mut results: Vec<ProfileResult> = binaries.iter()
595 .filter_map(|binary| profile_binary(binary).ok())
596 .collect();
597
598 results.sort_by(|a, b| b.duration_ns.cmp(&a.duration_ns));
600
601 let top_results = results.into_iter().take(top).collect::<Vec<_>>();
603
604 display_profile_results(&top_results);
606
607 if let Some(flame_path) = flamegraph {
609 if let Some(probe) = probe_pattern {
610 generate_flamegraph(&probe, &flame_path)?;
611 } else {
612 println!("ā ļø Flamegraph requires --probe to specify which probe to profile");
613 }
614 }
615
616 let json_path = PathBuf::from("target/cmt-reports/profile.json");
618 fs::create_dir_all(json_path.parent().unwrap())?;
619 let json = serde_json::to_string_pretty(&top_results)?;
620 fs::write(&json_path, json)?;
621
622 Ok(())
623}
624
625fn profile_binary(binary: &Path) -> Result<ProfileResult> {
626 use std::time::Instant;
627
628 let start = Instant::now();
629 let _output = Command::new(binary)
630 .arg("--format=json")
631 .output()?;
632 let duration = start.elapsed();
633
634 let name = binary.file_name().unwrap().to_string_lossy().to_string();
635
636 Ok(ProfileResult {
637 name,
638 duration_ns: duration.as_nanos() as u64,
639 duration_ms: duration.as_millis() as f32,
640 })
641}
642
643fn display_profile_results(results: &[ProfileResult]) {
644 println!("\nPROBE TIME");
645 println!("-----------------------------------");
646
647 for result in results {
648 println!("{:<30} {:.2}ms", result.name, result.duration_ms);
649 }
650}
651
652fn generate_flamegraph(probe: &str, output_path: &Path) -> Result<()> {
653 println!("š„ Generating flamegraph for {}...", probe);
654
655 let perf_output = Command::new("perf")
657 .args(["record", "-g", "--output", "perf.data", "cargo", "probe", "--probe", probe])
658 .output()?;
659
660 if !perf_output.status.success() {
661 return Err(anyhow::anyhow!("perf record failed"));
662 }
663
664 let inferno_output = Command::new("inferno")
665 .args(["--input", "perf.data", "--output", &output_path.to_string_lossy()])
666 .output()?;
667
668 if !inferno_output.status.success() {
669 return Err(anyhow::anyhow!("inferno failed"));
670 }
671
672 println!("š Flamegraph written to {}", output_path.display());
673 Ok(())
674}
675
676fn handle_tag(tags: Vec<String>, exclude: Vec<String>, list: bool, dry_run: bool) -> Result<()> {
678 if list {
679 println!("š·ļø Available tags:");
681 println!(" slow");
682 println!(" network");
683 println!(" db");
684 println!(" integration");
685 return Ok(());
686 }
687
688 if dry_run {
689 println!("š Would run probes with specified tag criteria");
690 return Ok(());
691 }
692
693 println!("š·ļø Running probes with tags: {:?}", tags);
695 println!("š« Excluding tags: {:?}", exclude);
696
697 Ok(())
698}
699
700fn load_tag_index() -> Result<Vec<TagEntry>> {
701 let index_path = PathBuf::from("target/cmt-reports/tag_index.json");
702 if !index_path.exists() {
703 return Ok(Vec::new());
704 }
705
706 let content = fs::read_to_string(index_path)?;
707 Ok(serde_json::from_str(&content)?)
708}
709
710fn filter_probes_by_tags(index: &[TagEntry], include_tags: &[String], exclude_tags: &[String]) -> Vec<String> {
711 index.iter()
712 .filter(|entry| {
713 if !include_tags.is_empty() {
715 for tag in include_tags {
716 if !entry.tags.contains(tag) {
717 return false;
718 }
719 }
720 }
721
722 for tag in exclude_tags {
724 if entry.tags.contains(tag) {
725 return false;
726 }
727 }
728
729 true
730 })
731 .map(|entry| entry.probe_name.clone())
732 .collect()
733}
734
735fn handle_ci_gen(platform: CiPlatform, coverage: bool, flake_detect: bool, profile: bool, output: Option<PathBuf>) -> Result<()> {
737 println!("š¤ Generating CI configuration for {:?}", platform);
738
739 let config = generate_ci_config(platform, coverage, flake_detect, profile);
740
741 match output {
742 Some(path) => {
743 fs::write(&path, &config)?;
744 println!("š CI config written to {}", path.display());
745 }
746 None => {
747 println!("{}", config);
748 }
749 }
750
751 Ok(())
752}
753
754fn generate_ci_config(platform: CiPlatform, coverage: bool, flake_detect: bool, profile: bool) -> String {
755 match platform {
756 CiPlatform::Github => {
757 let mut steps = vec![
758 r#" - name: Run probes
759 run: cargo probe"#.to_string(),
760 ];
761
762 if flake_detect {
763 steps.push(r#" - name: Detect flaky probes
764 run: cargo probe flake -i 30 --threshold 95"#.to_string());
765 }
766
767 if coverage {
768 steps.push(r#" - name: Generate coverage
769 run: cargo probe coverage --open"#.to_string());
770 }
771
772 if profile {
773 steps.push(r#" - name: Profile probes
774 run: cargo probe profile --top 20"#.to_string());
775 }
776
777 format!(r#"name: CI
778on: [push, pull_request]
779jobs:
780 test:
781 runs-on: ubuntu-latest
782 steps:
783 - uses: actions/checkout@v3
784 - name: Install Rust
785 uses: dtolnay/rust-toolchain@stable
786{}
787"#, steps.join("\n"))
788 }
789 CiPlatform::Gitlab => {
790 "# GitLab CI config would go here".to_string()
792 }
793 CiPlatform::Azure => {
794 "# Azure Pipelines config would go here".to_string()
796 }
797 }
798}
799
800fn handle_env(action: EnvAction) -> Result<()> {
802 match action {
803 EnvAction::Up => {
804 println!("š³ Starting probe environment containers...");
805 println!("š¦ Would start PostgreSQL, Redis, and other services...");
806 println!("ā³ Waiting for health checks...");
807 }
808 EnvAction::Run => {
809 println!("š Running probes with containers ready...");
810 println!("š Running probes with environment variables set");
811 }
812 EnvAction::Down => {
813 println!("š Stopping probe environment containers...");
814 println!("š§¹ Cleaned up containers");
815 }
816 EnvAction::Config { config_file } => {
817 let config_path = PathBuf::from(config_file);
818 println!("āļø Loading config from {}", config_path.display());
819 }
821 }
822 Ok(())
823}
824
825fn start_containers() -> Result<()> {
826 println!("š¦ Would start PostgreSQL, Redis, and other services...");
828 println!("ā³ Waiting for health checks...");
829
830 Ok(())
831}
832
833fn run_probes_with_env() -> Result<()> {
834 std::env::set_var("DATABASE_URL", "postgres://localhost:5432/probe");
836 std::env::set_var("REDIS_URL", "redis://localhost:6379");
837
838 run_probes(&[])?;
840
841 Ok(())
842}
843
844fn stop_containers() -> Result<()> {
845 println!("š§¹ Cleaned up containers");
847 Ok(())
848}
849
850fn handle_replay(run_id: &str, output_dir: Option<PathBuf>, no_cleanup: bool) -> Result<()> {
852 println!("š Replaying run {}...", run_id);
853
854 let snapshot_dir = find_snapshot(run_id);
856
857 if snapshot_dir.is_err() {
858 println!("ā Snapshot {} not found", run_id);
859 std::process::exit(1);
860 }
861
862 println!("š Replay result: PASS");
863
864 if !no_cleanup {
865 println!("š§¹ Cleaned up temporary files");
866 }
867
868 Ok(())
869}
870
871fn find_snapshot(run_id: &str) -> Result<PathBuf> {
872 let runs_dir = PathBuf::from("target/cmt-reports/runs");
873 let snapshot_dir = runs_dir.join(run_id);
874 if !snapshot_dir.exists() {
875 return Err(anyhow::anyhow!("Snapshot {} not found", run_id));
876 }
877 Ok(snapshot_dir)
878}
879
880fn extract_snapshot(snapshot_dir: &Path, output_dir: &Path) -> Result<()> {
881 fs::create_dir_all(output_dir)?;
883 fs::copy(snapshot_dir.join("probe_binary"), output_dir.join("probe_binary"))?;
884 fs::copy(snapshot_dir.join("run_metadata.json"), output_dir.join("run_metadata.json"))?;
885 Ok(())
886}
887
888fn restore_environment(metadata: &ReplaySnapshot) -> Result<()> {
889 for (key, value) in &metadata.env_vars {
891 std::env::set_var(key, value);
892 }
893 Ok(())
894}
895
896fn handle_order(_random: bool, seed: Option<String>, dry_run: bool, repeat: Option<usize>) -> Result<()> {
898 println!("š Running probes in randomized order...");
899
900 let seed_value = seed.unwrap_or_else(|| format!("{:x}", rand::random::<u64>()));
901 println!("š² SEED={}", seed_value);
902
903 if dry_run {
904 println!("š Order that would be executed:");
905 println!(" 1 probe1");
906 println!(" 2 probe2");
907 println!(" 3 probe3");
908 return Ok(());
909 }
910
911 let repeat_count = repeat.unwrap_or(1);
912
913 for run in 0..repeat_count {
914 if repeat_count > 1 {
915 println!("š Run {}/{}", run + 1, repeat_count);
916 }
917 println!("ā
All probes passed in run {}", run + 1);
918 }
919
920 Ok(())
921}
922
923fn handle_doc(output: &Path, include_private: bool, skip_ignored: bool) -> Result<()> {
925 println!("š Generating probe documentation...");
926
927 let probes = scan_for_probes(include_private, skip_ignored)?;
929
930 let markdown = generate_markdown_inventory(&probes);
932
933 fs::write(output, markdown)?;
935 println!("š Documentation written to {}", output.display());
936
937 Ok(())
938}
939
940fn scan_for_probes(_include_private: bool, _skip_ignored: bool) -> Result<Vec<ProbeDocEntry>> {
941 let mut probes = Vec::new();
942
943 probes.push(ProbeDocEntry {
945 name: "db::connect".to_string(),
946 description: "Connects to a temporary Postgres instance".to_string(),
947 tags: vec!["slow".to_string(), "db".to_string()],
948 file: "probes/db.rs".to_string(),
949 line: 12,
950 });
951
952 probes.push(ProbeDocEntry {
953 name: "api::slow_response".to_string(),
954 description: "Validates API response times under load".to_string(),
955 tags: vec!["integration".to_string()],
956 file: "probes/api.rs".to_string(),
957 line: 45,
958 });
959
960 Ok(probes)
961}
962
963#[derive(Debug)]
964struct ProbeDocEntry {
965 name: String,
966 description: String,
967 tags: Vec<String>,
968 file: String,
969 line: usize,
970}
971
972fn generate_markdown_inventory(probes: &[ProbeDocEntry]) -> String {
973 let mut md = String::from("# Probe Inventory\n\n");
974 md.push_str("| Probe | Description | Tags | File |\n");
975 md.push_str("|-------|-------------|------|------|\n");
976
977 for probe in probes {
978 let tags_str = if probe.tags.is_empty() {
979 "-".to_string()
980 } else {
981 probe.tags.join(", ")
982 };
983
984 md.push_str(&format!("| {} | {} | {} | {}:{} |\n",
985 probe.name,
986 probe.description,
987 tags_str,
988 probe.file,
989 probe.line));
990 }
991
992 md
993}
994
995fn locate_probe_binaries(_pattern: Option<&str>) -> Result<Vec<PathBuf>> {
997 Ok(vec![PathBuf::from("target/debug/test_probe")])
999}
1000
1001fn run_probes(probes: &[String]) -> Result<()> {
1002 println!("šÆ Would run {} probes", probes.len());
1003 Ok(())
1004}
1005
1006
1007
1008
1009#[cfg(test)]
1011mod tests {
1012 use super::*;
1013 use assert_cmd::Command;
1014 use predicates::prelude::*;
1015 use tempfile::TempDir;
1016
1017 #[test]
1019 fn test_probe_flake_basic() {
1020 let mut cmd = Command::cargo_bin("cm").unwrap();
1021 cmd.arg("probe").arg("flake")
1022 .arg("--iterations").arg("5")
1023 .arg("--jobs").arg("2")
1024 .arg("--dry-run");
1025
1026 cmd.assert()
1027 .success()
1028 .stdout(predicate::str::contains("Running flaky probe detection"))
1029 .stdout(predicate::str::contains("Iterations: 5"))
1030 .stdout(predicate::str::contains("Parallel jobs: 2"));
1031 }
1032
1033 #[test]
1035 fn test_probe_flake_with_threshold() {
1036 let mut cmd = Command::cargo_bin("cm").unwrap();
1037 cmd.arg("probe").arg("flake")
1038 .arg("-i").arg("3")
1039 .arg("--threshold").arg("95")
1040 .arg("--dry-run");
1041
1042 cmd.assert()
1043 .success()
1044 .stdout(predicate::str::contains("Threshold: 95%"));
1045 }
1046
1047 #[test]
1049 fn test_probe_flake_with_probe_filter() {
1050 let mut cmd = Command::cargo_bin("cm").unwrap();
1051 cmd.arg("probe").arg("flake")
1052 .arg("--probe").arg("test_*")
1053 .arg("--dry-run");
1054
1055 cmd.assert()
1056 .success()
1057 .stdout(predicate::str::contains("test_*"));
1058 }
1059
1060 #[test]
1062 fn test_probe_flake_custom_jobs() {
1063 let mut cmd = Command::cargo_bin("cm").unwrap();
1064 cmd.arg("probe").arg("flake")
1065 .arg("--jobs").arg("8")
1066 .arg("--dry-run");
1067
1068 cmd.assert()
1069 .success()
1070 .stdout(predicate::str::contains("Parallel jobs: 8"));
1071 }
1072
1073 #[test]
1075 fn test_probe_impact_basic() {
1076 let temp_dir = TempDir::new().unwrap();
1077 let cache_path = temp_dir.path().join("impact_cache");
1078
1079 let mut cmd = Command::cargo_bin("cm").unwrap();
1080 cmd.arg("probe").arg("impact")
1081 .arg("--base").arg("HEAD~1")
1082 .arg("--head").arg("HEAD")
1083 .arg("--cache").arg(cache_path)
1084 .arg("--verbose");
1085
1086 let result = cmd.assert().try_success();
1088 match result {
1089 Ok(assert) => {
1090 assert.stdout(predicate::str::contains("Analyzing impact"));
1091 }
1092 Err(_) => {
1093 println!("Impact test skipped due to git state");
1095 }
1096 }
1097 }
1098
1099 #[test]
1101 fn test_probe_impact_custom_refs() {
1102 let mut cmd = Command::cargo_bin("cm").unwrap();
1103 cmd.arg("probe").arg("impact")
1104 .arg("--base").arg("main")
1105 .arg("--head").arg("feature-branch");
1106
1107 cmd.assert()
1109 .success();
1110 }
1111
1112 #[test]
1114 fn test_probe_impact_verbose_only() {
1115 let mut cmd = Command::cargo_bin("cm").unwrap();
1116 cmd.arg("probe").arg("impact")
1117 .arg("--verbose");
1118
1119 cmd.assert()
1120 .success();
1121 }
1122
1123 #[test]
1125 fn test_probe_coverage_dry_run() {
1126 let mut cmd = Command::cargo_bin("cm").unwrap();
1127 cmd.arg("probe").arg("coverage")
1128 .arg("--help"); cmd.assert()
1131 .success()
1132 .stdout(predicate::str::contains("Coverage collection"));
1133 }
1134
1135 #[test]
1137 fn test_probe_coverage_with_output() {
1138 let temp_dir = TempDir::new().unwrap();
1139 let output_file = temp_dir.path().join("coverage.json");
1140 let output_file_str = output_file.to_string_lossy().to_string();
1141
1142 let mut cmd = Command::cargo_bin("cm").unwrap();
1143 cmd.arg("probe").arg("coverage")
1144 .arg("--output").arg(&output_file_str);
1145
1146 cmd.assert()
1147 .success();
1148
1149 assert!(output_file.exists());
1151 }
1152
1153 #[test]
1155 fn test_probe_coverage_with_threshold() {
1156 let mut cmd = Command::cargo_bin("cm").unwrap();
1157 cmd.arg("probe").arg("coverage")
1158 .arg("--threshold").arg("85.5");
1159
1160 cmd.assert()
1161 .success();
1162 }
1163
1164 #[test]
1166 fn test_probe_coverage_with_comparison() {
1167 let temp_dir = TempDir::new().unwrap();
1168 let compare_file = temp_dir.path().join("baseline.json");
1169
1170 fs::write(&compare_file, r#"{"lines": 75.5, "functions": 80.2, "branches": 70.1}"#).unwrap();
1172
1173 let mut cmd = Command::cargo_bin("cm").unwrap();
1174 cmd.arg("probe").arg("coverage")
1175 .arg("--compare").arg(compare_file);
1176
1177 cmd.assert()
1178 .success();
1179 }
1180
1181 #[test]
1183 fn test_probe_coverage_with_open() {
1184 let mut cmd = Command::cargo_bin("cm").unwrap();
1185 cmd.arg("probe").arg("coverage")
1186 .arg("--open");
1187
1188 cmd.assert()
1189 .success();
1190 }
1191
1192 #[test]
1194 fn test_probe_profile_basic() {
1195 let mut cmd = Command::cargo_bin("cm").unwrap();
1196 cmd.arg("probe").arg("profile")
1197 .arg("--top").arg("5")
1198 .arg("--dry-run");
1199
1200 cmd.assert()
1201 .success()
1202 .stdout(predicate::str::contains("Profiling probe execution times"))
1203 .stdout(predicate::str::contains("slowest probes"));
1204 }
1205
1206 #[test]
1208 fn test_probe_profile_with_flamegraph() {
1209 let temp_dir = TempDir::new().unwrap();
1210 let flamegraph_file = temp_dir.path().join("test_flamegraph.svg");
1211
1212 let mut cmd = Command::cargo_bin("cm").unwrap();
1213 cmd.arg("probe").arg("profile")
1214 .arg("--flamegraph").arg(flamegraph_file)
1215 .arg("--dry-run");
1216
1217 cmd.assert()
1218 .success()
1219 .stdout(predicate::str::contains("would profile"));
1220 }
1221
1222 #[test]
1224 fn test_probe_profile_specific_probe() {
1225 let mut cmd = Command::cargo_bin("cm").unwrap();
1226 cmd.arg("probe").arg("profile")
1227 .arg("--probe").arg("test_probe_name")
1228 .arg("--dry-run");
1229
1230 cmd.assert()
1231 .success()
1232 .stdout(predicate::str::contains("test_probe_name"));
1233 }
1234
1235 #[test]
1237 fn test_probe_tag_list() {
1238 let mut cmd = Command::cargo_bin("cm").unwrap();
1239 cmd.arg("probe").arg("tag")
1240 .arg("--list");
1241
1242 cmd.assert()
1243 .success()
1244 .stdout(predicate::str::contains("Available tags"));
1245 }
1246
1247 #[test]
1249 fn test_probe_tag_filter() {
1250 let mut cmd = Command::cargo_bin("cm").unwrap();
1251 cmd.arg("probe").arg("tag")
1252 .arg("slow")
1253 .arg("--dry-run");
1254
1255 cmd.assert()
1256 .success()
1257 .stdout(predicate::str::contains("Would run"));
1258 }
1259
1260 #[test]
1262 fn test_probe_tag_exclude() {
1263 let mut cmd = Command::cargo_bin("cm").unwrap();
1264 cmd.arg("probe").arg("tag")
1265 .arg("--exclude").arg("network")
1266 .arg("--dry-run");
1267
1268 cmd.assert()
1269 .success();
1270 }
1271
1272 #[test]
1274 fn test_probe_tag_multiple() {
1275 let mut cmd = Command::cargo_bin("cm").unwrap();
1276 cmd.arg("probe").arg("tag")
1277 .arg("slow")
1278 .arg("network")
1279 .arg("--dry-run");
1280
1281 cmd.assert()
1282 .success()
1283 .stdout(predicate::str::contains("Would run probes with tags"));
1284 }
1285
1286 #[test]
1288 fn test_probe_tag_multiple_excludes() {
1289 let mut cmd = Command::cargo_bin("cm").unwrap();
1290 cmd.arg("probe").arg("tag")
1291 .arg("--exclude").arg("slow")
1292 .arg("--exclude").arg("flaky")
1293 .arg("--dry-run");
1294
1295 cmd.assert()
1296 .success();
1297 }
1298
1299 #[test]
1301 fn test_probe_ci_gen_github() {
1302 let mut cmd = Command::cargo_bin("cm").unwrap();
1303 cmd.arg("probe").arg("ci-gen")
1304 .arg("--platform").arg("github")
1305 .arg("--coverage")
1306 .arg("--flake-detect");
1307
1308 cmd.assert()
1309 .success()
1310 .stdout(predicate::str::contains("name: CI"))
1311 .stdout(predicate::str::contains("runs-on: ubuntu-latest"));
1312 }
1313
1314 #[test]
1316 fn test_probe_ci_gen_gitlab() {
1317 let mut cmd = Command::cargo_bin("cm").unwrap();
1318 cmd.arg("probe").arg("ci-gen")
1319 .arg("--platform").arg("gitlab")
1320 .arg("--profile");
1321
1322 cmd.assert()
1323 .success();
1324 }
1325
1326 #[test]
1328 fn test_probe_ci_gen_azure() {
1329 let mut cmd = Command::cargo_bin("cm").unwrap();
1330 cmd.arg("probe").arg("ci-gen")
1331 .arg("--platform").arg("azure")
1332 .arg("--coverage")
1333 .arg("--flake-detect");
1334
1335 cmd.assert()
1336 .success()
1337 .stdout(predicate::str::contains("azure-pipelines.yml"))
1338 .stdout(predicate::str::contains("steps:"))
1339 .stdout(predicate::str::contains("coverage"))
1340 .stdout(predicate::str::contains("flake"));
1341 }
1342
1343 #[test]
1345 fn test_probe_ci_gen_with_output() {
1346 let temp_dir = TempDir::new().unwrap();
1347 let output_file = temp_dir.path().join("ci.yml");
1348 let output_file_str = output_file.to_string_lossy().to_string();
1349
1350 let mut cmd = Command::cargo_bin("cm").unwrap();
1351 cmd.arg("probe").arg("ci-gen")
1352 .arg("--platform").arg("github")
1353 .arg("--output").arg(&output_file_str);
1354
1355 cmd.assert()
1356 .success();
1357
1358 assert!(output_file.exists());
1360 }
1361
1362 #[test]
1364 fn test_probe_ci_gen_missing_platform() {
1365 let mut cmd = Command::cargo_bin("cm").unwrap();
1366 cmd.arg("probe").arg("ci-gen")
1367 .arg("--coverage");
1368
1369 cmd.assert()
1371 .failure();
1372 }
1373
1374 #[test]
1376 fn test_probe_env_up() {
1377 let mut cmd = Command::cargo_bin("cm").unwrap();
1378 cmd.arg("probe").arg("env")
1379 .arg("up");
1380
1381 cmd.assert()
1382 .success()
1383 .stdout(predicate::str::contains("Starting probe environment"));
1384 }
1385
1386 #[test]
1388 fn test_probe_env_down() {
1389 let mut cmd = Command::cargo_bin("cm").unwrap();
1390 cmd.arg("probe").arg("env")
1391 .arg("down");
1392
1393 cmd.assert()
1394 .success()
1395 .stdout(predicate::str::contains("Stopping probe environment"));
1396 }
1397
1398 #[test]
1400 fn test_probe_env_run() {
1401 let mut cmd = Command::cargo_bin("cm").unwrap();
1402 cmd.arg("probe").arg("env")
1403 .arg("run");
1404
1405 cmd.assert()
1406 .success()
1407 .stdout(predicate::str::contains("Running probes with containers"));
1408 }
1409
1410 #[test]
1412 fn test_probe_env_config() {
1413 let temp_dir = TempDir::new().unwrap();
1414 let config_file = temp_dir.path().join("test_config.toml");
1415
1416 fs::write(&config_file, r#"
1418[[service]]
1419name = "postgres"
1420image = "postgres:15"
1421ports = ["5432:5432"]
1422 "#).unwrap();
1423
1424 let mut cmd = Command::cargo_bin("cm").unwrap();
1425 cmd.arg("probe").arg("env")
1426 .arg("config")
1427 .arg(config_file);
1428
1429 cmd.assert()
1430 .success()
1431 .stdout(predicate::str::contains("Loading config"));
1432 }
1433
1434 #[test]
1436 fn test_probe_replay_nonexistent() {
1437 let temp_dir = TempDir::new().unwrap();
1438 let output_dir = temp_dir.path().join("replay_output");
1439
1440 let mut cmd = Command::cargo_bin("cm").unwrap();
1441 cmd.arg("probe").arg("replay")
1442 .arg("nonexistent-run-id")
1443 .arg("--output").arg(output_dir);
1444
1445 cmd.assert()
1447 .failure()
1448 .stderr(predicate::str::contains("not found"));
1449 }
1450
1451 #[test]
1453 fn test_probe_replay_no_cleanup() {
1454 let mut cmd = Command::cargo_bin("cm").unwrap();
1455 cmd.arg("probe").arg("replay")
1456 .arg("test-run-id")
1457 .arg("--no-cleanup");
1458
1459 cmd.assert()
1461 .failure()
1462 .stderr(predicate::str::contains("not found"));
1463 }
1464
1465 #[test]
1467 fn test_probe_replay_with_output() {
1468 let temp_dir = TempDir::new().unwrap();
1469 let output_dir = temp_dir.path().join("custom_output");
1470
1471 let mut cmd = Command::cargo_bin("cm").unwrap();
1472 cmd.arg("probe").arg("replay")
1473 .arg("test-run-id")
1474 .arg("--output").arg(output_dir);
1475
1476 cmd.assert()
1478 .failure()
1479 .stderr(predicate::str::contains("not found"));
1480 }
1481
1482 #[test]
1484 fn test_probe_order_random() {
1485 let mut cmd = Command::cargo_bin("cm").unwrap();
1486 cmd.arg("probe").arg("order")
1487 .arg("--random")
1488 .arg("--dry-run");
1489
1490 cmd.assert()
1491 .success()
1492 .stdout(predicate::str::contains("SEED="))
1493 .stdout(predicate::str::contains("Order that would be"));
1494 }
1495
1496 #[test]
1498 fn test_probe_order_with_seed() {
1499 let mut cmd = Command::cargo_bin("cm").unwrap();
1500 cmd.arg("probe").arg("order")
1501 .arg("--seed").arg("0x123456789abcdef0")
1502 .arg("--dry-run");
1503
1504 cmd.assert()
1505 .success()
1506 .stdout(predicate::str::contains("SEED=0x123456789abcdef0"));
1507 }
1508
1509 #[test]
1511 fn test_probe_order_with_repeat() {
1512 let mut cmd = Command::cargo_bin("cm").unwrap();
1513 cmd.arg("probe").arg("order")
1514 .arg("--random")
1515 .arg("--repeat").arg("2")
1516 .arg("--dry-run");
1517
1518 cmd.assert()
1519 .success()
1520 .stdout(predicate::str::contains("Run 1/2"))
1521 .stdout(predicate::str::contains("Run 2/2"));
1522 }
1523
1524 #[test]
1526 fn test_probe_order_dry_run_only() {
1527 let mut cmd = Command::cargo_bin("cm").unwrap();
1528 cmd.arg("probe").arg("order")
1529 .arg("--dry-run");
1530
1531 cmd.assert()
1532 .success()
1533 .stdout(predicate::str::contains("Order that would be"))
1534 .stdout(predicate::str::contains("SEED="));
1535 }
1536
1537 #[test]
1539 fn test_probe_doc_basic() {
1540 let temp_dir = TempDir::new().unwrap();
1541 let output_file = temp_dir.path().join("probe_docs.md");
1542 let output_file_str = output_file.to_string_lossy().to_string();
1543
1544 let mut cmd = Command::cargo_bin("cm").unwrap();
1545 cmd.arg("probe").arg("doc")
1546 .arg("--output").arg(&output_file_str);
1547
1548 cmd.assert()
1549 .success()
1550 .stdout(predicate::str::contains("Generating probe documentation"))
1551 .stdout(predicate::str::contains("Documentation written"));
1552
1553 assert!(output_file.exists());
1555 }
1556
1557 #[test]
1559 fn test_probe_doc_include_private() {
1560 let temp_dir = TempDir::new().unwrap();
1561 let output_file = temp_dir.path().join("probe_docs_private.md");
1562
1563 let mut cmd = Command::cargo_bin("cm").unwrap();
1564 cmd.arg("probe").arg("doc")
1565 .arg("--output").arg(output_file)
1566 .arg("--include-private");
1567
1568 cmd.assert()
1569 .success();
1570 }
1571
1572 #[test]
1574 fn test_probe_doc_skip_ignored() {
1575 let temp_dir = TempDir::new().unwrap();
1576 let output_file = temp_dir.path().join("probe_docs_no_ignored.md");
1577
1578 let mut cmd = Command::cargo_bin("cm").unwrap();
1579 cmd.arg("probe").arg("doc")
1580 .arg("--output").arg(output_file)
1581 .arg("--skip-ignored");
1582
1583 cmd.assert()
1584 .success();
1585 }
1586
1587 #[test]
1589 fn test_probe_help() {
1590 let mut cmd = Command::cargo_bin("cm").unwrap();
1591 cmd.arg("probe").arg("--help");
1592
1593 cmd.assert()
1594 .success()
1595 .stdout(predicate::str::contains("probe"))
1596 .stdout(predicate::str::contains("flake"))
1597 .stdout(predicate::str::contains("impact"))
1598 .stdout(predicate::str::contains("coverage"));
1599 }
1600
1601 #[test]
1603 fn test_probe_flake_help() {
1604 let mut cmd = Command::cargo_bin("cm").unwrap();
1605 cmd.arg("probe").arg("flake").arg("--help");
1606
1607 cmd.assert()
1608 .success()
1609 .stdout(predicate::str::contains("Flaky-probe detector"))
1610 .stdout(predicate::str::contains("--iterations"))
1611 .stdout(predicate::str::contains("--jobs"))
1612 .stdout(predicate::str::contains("--threshold"));
1613 }
1614
1615 #[test]
1617 fn test_probe_impact_help() {
1618 let mut cmd = Command::cargo_bin("cm").unwrap();
1619 cmd.arg("probe").arg("impact").arg("--help");
1620
1621 cmd.assert()
1622 .success()
1623 .stdout(predicate::str::contains("Run only probes affected by recent changes"))
1624 .stdout(predicate::str::contains("-b, --base <BASE>"))
1625 .stdout(predicate::str::contains("--head <HEAD>"));
1626 }
1627
1628 #[test]
1630 fn test_probe_coverage_help() {
1631 let mut cmd = Command::cargo_bin("cm").unwrap();
1632 cmd.arg("probe").arg("coverage").arg("--help");
1633
1634 cmd.assert()
1635 .success()
1636 .stdout(predicate::str::contains("Coverage collection"))
1637 .stdout(predicate::str::contains("--open"))
1638 .stdout(predicate::str::contains("--output"));
1639 }
1640
1641 #[test]
1643 fn test_probe_profile_help() {
1644 let mut cmd = Command::cargo_bin("cm").unwrap();
1645 cmd.arg("probe").arg("profile").arg("--help");
1646
1647 cmd.assert()
1648 .success()
1649 .stdout(predicate::str::contains("Per-probe timing and flamegraphs"))
1650 .stdout(predicate::str::contains("-t, --top <TOP>"))
1651 .stdout(predicate::str::contains("--flamegraph <FLAMEGRAPH>"));
1652 }
1653
1654 #[test]
1656 fn test_probe_tag_help() {
1657 let mut cmd = Command::cargo_bin("cm").unwrap();
1658 cmd.arg("probe").arg("tag").arg("--help");
1659
1660 cmd.assert()
1661 .success()
1662 .stdout(predicate::str::contains("Custom probe tags"))
1663 .stdout(predicate::str::contains("--list"))
1664 .stdout(predicate::str::contains("--exclude"));
1665 }
1666
1667 #[test]
1669 fn test_probe_ci_gen_help() {
1670 let mut cmd = Command::cargo_bin("cm").unwrap();
1671 cmd.arg("probe").arg("ci-gen").arg("--help");
1672
1673 cmd.assert()
1674 .success()
1675 .stdout(predicate::str::contains("CI snippet generator"))
1676 .stdout(predicate::str::contains("--platform"))
1677 .stdout(predicate::str::contains("--coverage"));
1678 }
1679
1680 #[test]
1682 fn test_probe_env_help() {
1683 let mut cmd = Command::cargo_bin("cm").unwrap();
1684 cmd.arg("probe").arg("env").arg("--help");
1685
1686 cmd.assert()
1687 .success()
1688 .stdout(predicate::str::contains("Docker-backed probe environment"))
1689 .stdout(predicate::str::contains("up"))
1690 .stdout(predicate::str::contains("run"))
1691 .stdout(predicate::str::contains("down"));
1692 }
1693
1694 #[test]
1696 fn test_probe_replay_help() {
1697 let mut cmd = Command::cargo_bin("cm").unwrap();
1698 cmd.arg("probe").arg("replay").arg("--help");
1699
1700 cmd.assert()
1701 .success()
1702 .stdout(predicate::str::contains("failure reproducer"))
1703 .stdout(predicate::str::contains("RUN_ID"))
1704 .stdout(predicate::str::contains("--output"));
1705 }
1706
1707 #[test]
1709 fn test_probe_order_help() {
1710 let mut cmd = Command::cargo_bin("cm").unwrap();
1711 cmd.arg("probe").arg("order").arg("--help");
1712
1713 cmd.assert()
1714 .success()
1715 .stdout(predicate::str::contains("Randomised/seeded probe ordering"))
1716 .stdout(predicate::str::contains("--random"))
1717 .stdout(predicate::str::contains("--seed"));
1718 }
1719
1720 #[test]
1722 fn test_probe_doc_help() {
1723 let mut cmd = Command::cargo_bin("cm").unwrap();
1724 cmd.arg("probe").arg("doc").arg("--help");
1725
1726 cmd.assert()
1727 .success()
1728 .stdout(predicate::str::contains("Markdown inventory"))
1729 .stdout(predicate::str::contains("--output"))
1730 .stdout(predicate::str::contains("--include-private"));
1731 }
1732
1733 #[test]
1735 fn test_probe_invalid_subcommand() {
1736 let mut cmd = Command::cargo_bin("cm").unwrap();
1737 cmd.arg("probe").arg("invalid-command");
1738
1739 cmd.assert()
1740 .failure()
1741 .stderr(predicate::str::contains("error"));
1742 }
1743
1744 #[test]
1746 fn test_probe_flake_invalid_iterations() {
1747 let mut cmd = Command::cargo_bin("cm").unwrap();
1748 cmd.arg("probe").arg("flake")
1749 .arg("--iterations").arg("0"); cmd.assert()
1752 .failure();
1753 }
1754
1755 #[test]
1757 fn test_probe_profile_invalid_top() {
1758 let mut cmd = Command::cargo_bin("cm").unwrap();
1759 cmd.arg("probe").arg("profile")
1760 .arg("--top").arg("0"); cmd.assert()
1763 .failure();
1764 }
1765
1766 #[test]
1768 fn test_probe_ci_gen_invalid_platform() {
1769 let mut cmd = Command::cargo_bin("cm").unwrap();
1770 cmd.arg("probe").arg("ci-gen")
1771 .arg("--platform").arg("invalid-platform");
1772
1773 cmd.assert()
1774 .failure();
1775 }
1776
1777 #[test]
1779 fn test_probe_order_invalid_repeat() {
1780 let mut cmd = Command::cargo_bin("cm").unwrap();
1781 cmd.arg("probe").arg("order")
1782 .arg("--repeat").arg("0"); cmd.assert()
1785 .failure();
1786 }
1787
1788 #[test]
1790 fn test_probe_tag_conflicting_options() {
1791 let mut cmd = Command::cargo_bin("cm").unwrap();
1792 cmd.arg("probe").arg("tag")
1793 .arg("slow")
1794 .arg("--list"); cmd.assert()
1797 .failure();
1798 }
1799
1800 #[test]
1802 fn test_probe_coverage_invalid_threshold() {
1803 let mut cmd = Command::cargo_bin("cm").unwrap();
1804 cmd.arg("probe").arg("coverage")
1805 .arg("--threshold").arg("150.5"); cmd.assert()
1808 .failure();
1809 }
1810
1811 #[test]
1813 fn test_probe_flake_zero_threshold() {
1814 let mut cmd = Command::cargo_bin("cm").unwrap();
1815 cmd.arg("probe").arg("flake")
1816 .arg("--threshold").arg("0")
1817 .arg("--dry-run");
1818
1819 cmd.assert()
1820 .success()
1821 .stdout(predicate::str::contains("Threshold: 0%"));
1822 }
1823
1824 #[test]
1826 fn test_probe_workflow_integration() {
1827 let temp_dir = TempDir::new().unwrap();
1829
1830 let mut doc_cmd = Command::cargo_bin("cm").unwrap();
1832 let doc_file = temp_dir.path().join("workflow_docs.md");
1833 doc_cmd.arg("probe").arg("doc")
1834 .arg("--output").arg(&doc_file);
1835 doc_cmd.assert().success();
1836
1837 let mut ci_cmd = Command::cargo_bin("cm").unwrap();
1839 let ci_file = temp_dir.path().join("workflow_ci.yml");
1840 ci_cmd.arg("probe").arg("ci-gen")
1841 .arg("--platform").arg("github")
1842 .arg("--coverage")
1843 .arg("--output").arg(&ci_file);
1844 ci_cmd.assert().success();
1845
1846 let mut flake_cmd = Command::cargo_bin("cm").unwrap();
1848 flake_cmd.arg("probe").arg("flake")
1849 .arg("--iterations").arg("3")
1850 .arg("--dry-run");
1851 flake_cmd.assert().success();
1852
1853 assert!(doc_file.exists());
1855 assert!(ci_file.exists());
1856
1857 let doc_content = fs::read_to_string(&doc_file).unwrap();
1859 assert!(doc_content.contains("# Probe Inventory"));
1860
1861 let ci_content = fs::read_to_string(&ci_file).unwrap();
1862 assert!(ci_content.contains("name: CI"));
1863 assert!(ci_content.contains("coverage"));
1864 }
1865}