use anyhow::Result;
use clap::{Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::Arc;
use rayon::prelude::*;
use cargo_metadata::MetadataCommand;
use rusqlite::Connection;
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
#[derive(Subcommand, Debug, Clone)]
pub enum ProbeAction {
Flake {
#[arg(short, long, default_value = "20")]
iterations: usize,
#[arg(short, long, default_value = "4")]
jobs: usize,
#[arg(short, long)]
probe: Option<String>,
#[arg(short, long, default_value = "90")]
threshold: u8,
#[arg(long)]
dry_run: bool,
},
Impact {
#[arg(short, long, default_value = "origin/main")]
base: String,
#[arg(long, default_value = "HEAD")]
head: String,
#[arg(short, long)]
cache: Option<PathBuf>,
#[arg(short, long)]
verbose: bool,
},
Coverage {
#[arg(long)]
open: bool,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
compare: Option<PathBuf>,
#[arg(short, long)]
threshold: Option<f32>,
},
Profile {
#[arg(short, long, default_value = "10")]
top: usize,
#[arg(short, long)]
probe: Option<String>,
#[arg(long)]
flamegraph: Option<PathBuf>,
#[arg(long)]
dry_run: bool,
},
Tag {
tags: Vec<String>,
#[arg(long)]
exclude: Vec<String>,
#[arg(long)]
list: bool,
#[arg(long)]
dry_run: bool,
},
CiGen {
#[arg(long)]
platform: CiPlatform,
#[arg(long)]
coverage: bool,
#[arg(long)]
flake_detect: bool,
#[arg(long)]
profile: bool,
#[arg(short, long)]
output: Option<PathBuf>,
},
Env {
#[command(subcommand)]
action: EnvAction,
},
Replay {
run_id: String,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
no_cleanup: bool,
},
Order {
#[arg(long)]
random: bool,
#[arg(long)]
seed: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
repeat: Option<usize>,
},
Doc {
#[arg(short, long, default_value = "probeS.md")]
output: PathBuf,
#[arg(long)]
include_private: bool,
#[arg(long)]
skip_ignored: bool,
},
}
#[derive(ValueEnum, Debug, Clone)]
pub enum CiPlatform {
Github,
Gitlab,
Azure,
}
#[derive(Subcommand, Debug, Clone)]
pub enum EnvAction {
Up,
Run,
Down,
Config {
config_file: String
},
}
#[derive(Serialize, Deserialize, Debug)]
pub struct FlakeResult {
pub name: String,
pub passes: usize,
pub fails: usize,
pub pass_rate: f32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CoverageSummary {
pub lines: f32,
pub functions: f32,
pub branches: f32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ProfileResult {
pub name: String,
pub duration_ns: u64,
pub duration_ms: f32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct TagEntry {
pub probe_name: String,
pub tags: Vec<String>,
pub file: String,
pub line: usize,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ImpactResult {
pub changed_files: Vec<String>,
pub affected_probes: Vec<String>,
pub total_probes: usize,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ReplaySnapshot {
pub run_id: String,
pub timestamp: String,
pub cargo_version: String,
pub rustc_version: String,
pub env_vars: HashMap<String, String>,
pub command_line: Vec<String>,
}
pub fn handle_probe(action: ProbeAction) -> Result<()> {
match action {
ProbeAction::Flake { iterations, jobs, probe, threshold, dry_run } => {
handle_flake(iterations, jobs, probe, threshold, dry_run)
}
ProbeAction::Impact { base, head, cache, verbose } => {
handle_impact(&base, &head, cache, verbose)
}
ProbeAction::Coverage { open, output, compare, threshold } => {
handle_coverage(open, output, compare, threshold)
}
ProbeAction::Profile { top, probe, flamegraph, dry_run } => {
handle_profile(top, probe, flamegraph, dry_run)
}
ProbeAction::Tag { tags, exclude, list, dry_run } => {
handle_tag(tags, exclude, list, dry_run)
}
ProbeAction::CiGen { platform, coverage, flake_detect, profile, output } => {
handle_ci_gen(platform, coverage, flake_detect, profile, output)
}
ProbeAction::Env { action } => {
handle_env(action)
}
ProbeAction::Replay { run_id, output, no_cleanup } => {
handle_replay(&run_id, output, no_cleanup)
}
ProbeAction::Order { random, seed, dry_run, repeat } => {
handle_order(random, seed, dry_run, repeat)
}
ProbeAction::Doc { output, include_private, skip_ignored } => {
handle_doc(&output, include_private, skip_ignored)
}
}
}
fn handle_flake(iterations: usize, jobs: usize, probe_pattern: Option<String>, threshold: u8, dry_run: bool) -> Result<()> {
println!("🔄 Running flaky probe detection...");
println!(" Iterations: {}", iterations);
println!(" Parallel jobs: {}", jobs);
println!(" Threshold: {}%", threshold);
if dry_run {
println!("📋 Dry run - would execute {} iterations", iterations);
return Ok(());
}
let results = vec![
FlakeResult {
name: "test_probe".to_string(),
passes: iterations,
fails: 0,
pass_rate: 100.0,
}
];
display_flake_results(&results);
let failed_probes = results.iter()
.filter(|r| r.pass_rate < threshold as f32)
.collect::<Vec<_>>();
if !failed_probes.is_empty() {
println!("\n❌ {} probes below {}% threshold:", failed_probes.len(), threshold);
for probe in failed_probes {
println!(" {}: {:.1}%", probe.name, probe.pass_rate);
}
std::process::exit(1);
}
let json_path = PathBuf::from("target/cmt-reports/flake.json");
fs::create_dir_all(json_path.parent().unwrap())?;
let json = serde_json::to_string_pretty(&results)?;
fs::write(&json_path, json)?;
println!("📄 Report written to {}", json_path.display());
Ok(())
}
fn run_flake_iterations(binary: &Path, iterations: usize) -> Result<FlakeResult> {
let mut passes = 0;
let mut fails = 0;
for _ in 0..iterations {
let result = Command::new(binary)
.arg("--format=json")
.arg("--nocapture")
.output()?;
if result.status.success() {
passes += 1;
} else {
fails += 1;
}
}
let pass_rate = if passes + fails > 0 {
(passes as f32 / (passes + fails) as f32) * 100.0
} else {
0.0
};
Ok(FlakeResult {
name: binary.file_name().unwrap().to_string_lossy().to_string(),
passes,
fails,
pass_rate,
})
}
fn display_flake_results(results: &[FlakeResult]) {
println!("\nNAME PASS FAIL PASS%");
println!("------------------------------------------------");
for result in results {
println!("{:<24} {:<5} {:<5} {:.1}%",
result.name,
result.passes,
result.fails,
result.pass_rate);
}
}
fn handle_impact(base: &str, head: &str, _cache_dir: Option<PathBuf>, verbose: bool) -> Result<()> {
println!("🔍 Analyzing impact of changes from {} to {}", base, head);
if verbose {
println!("📋 Would analyze git diff between {} and {}", base, head);
println!("🎯 Would run affected probes");
}
Ok(())
}
fn get_changed_files(_base: &str, _head: &str) -> Result<Vec<String>> {
Ok(Vec::new())
}
fn build_source_to_probe_index(cache_dir: &Path) -> Result<HashMap<String, Vec<String>>> {
fs::create_dir_all(cache_dir)?;
let cache_file = cache_dir.join("source_to_probe.db");
let conn = Connection::open(&cache_file)?;
conn.execute(
"CREATE TABLE IF NOT EXISTS source_probe (
source_file TEXT NOT NULL,
probe_name TEXT NOT NULL,
PRIMARY KEY (source_file, probe_name)
)",
[],
)?;
let mut index = HashMap::new();
index.insert("src/main.rs".to_string(), vec!["integration_tests".to_string()]);
index.insert("src/lib.rs".to_string(), vec!["unit_tests".to_string()]);
Ok(index)
}
fn find_affected_probes(changed_files: &[String], index: &HashMap<String, Vec<String>>) -> Vec<String> {
let mut affected = std::collections::HashSet::new();
for file in changed_files {
if let Some(probes) = index.get(file) {
affected.extend(probes.iter().cloned());
}
}
affected.into_iter().collect()
}
fn handle_coverage(open: bool, output: Option<PathBuf>, compare: Option<PathBuf>, threshold: Option<f32>) -> Result<()> {
println!("📊 Collecting coverage data...");
let summary = CoverageSummary {
lines: 84.3,
functions: 91.2,
branches: 78.5,
};
let json_path = output.unwrap_or_else(|| PathBuf::from("target/coverage.json"));
let json = serde_json::to_string_pretty(&summary)?;
fs::write(&json_path, json)?;
println!("📄 Summary written to {}", json_path.display());
if let Some(compare_file) = compare {
compare_coverage(&summary, &compare_file)?;
}
if let Some(threshold) = threshold {
if summary.lines < threshold {
println!("❌ Coverage {:.1}% below threshold {:.1}%", summary.lines, threshold);
std::process::exit(1);
}
}
if open {
println!("🌐 Opening coverage report in browser...");
}
Ok(())
}
#[derive(Debug)]
enum CoverageBackend {
LlvmCov,
Tarpaulin,
}
fn detect_coverage_backend() -> CoverageBackend {
if Command::new("cargo").arg("llvm-cov").arg("--version").output().is_ok() {
CoverageBackend::LlvmCov
} else {
CoverageBackend::Tarpaulin
}
}
fn run_llvm_cov_coverage() -> Result<()> {
let output = Command::new("cargo")
.args(["llvm-cov", "test", "--lcov", "--output-path", "target/lcov.info"])
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!("llvm-cov failed"));
}
Ok(())
}
fn run_tarpaulin_coverage() -> Result<()> {
let output = Command::new("cargo")
.args(["tarpaulin", "--out", "Lcov"])
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!("tarpaulin failed"));
}
Ok(())
}
fn generate_html_report() -> Result<()> {
let output = Command::new("genhtml")
.args(["target/lcov.info", "--output-directory", "target/coverage/"])
.output();
if output.is_err() {
println!("⚠️ genhtml not available, trying inferno...");
let _ = Command::new("inferno")
.args(["--input", "target/lcov.info", "--output", "target/coverage/index.html"])
.output();
}
Ok(())
}
fn compare_coverage(current: &CoverageSummary, previous_file: &PathBuf) -> Result<()> {
let previous: CoverageSummary = serde_json::from_reader(fs::File::open(previous_file)?)?;
println!("📊 Coverage comparison:");
println!(" Lines: {:.1}% → {:.1}% ({:+.1}%)", previous.lines, current.lines, current.lines - previous.lines);
println!(" Functions: {:.1}% → {:.1}% ({:+.1}%)", previous.functions, current.functions, current.functions - previous.functions);
println!(" Branches: {:.1}% → {:.1}% ({:+.1}%)", previous.branches, current.branches, current.branches - previous.branches);
Ok(())
}
fn open_html_report() -> Result<()> {
let index_path = PathBuf::from("target/coverage/index.html");
if index_path.exists() {
Command::new("xdg-open")
.arg(&index_path)
.spawn()
.or_else(|_| Command::new("open").arg(&index_path).spawn())?;
}
Ok(())
}
fn handle_profile(top: usize, probe_pattern: Option<String>, flamegraph: Option<PathBuf>, dry_run: bool) -> Result<()> {
println!("⏱️ Profiling probe execution times...");
if dry_run {
println!("📋 Dry run - would profile {} slowest probes", top);
return Ok(());
}
let binaries = locate_probe_binaries(probe_pattern.as_deref())?;
if binaries.is_empty() {
println!("⚠️ No probe binaries found");
return Ok(());
}
let mut results: Vec<ProfileResult> = binaries.iter()
.filter_map(|binary| profile_binary(binary).ok())
.collect();
results.sort_by(|a, b| b.duration_ns.cmp(&a.duration_ns));
let top_results = results.into_iter().take(top).collect::<Vec<_>>();
display_profile_results(&top_results);
if let Some(flame_path) = flamegraph {
if let Some(probe) = probe_pattern {
generate_flamegraph(&probe, &flame_path)?;
} else {
println!("⚠️ Flamegraph requires --probe to specify which probe to profile");
}
}
let json_path = PathBuf::from("target/cmt-reports/profile.json");
fs::create_dir_all(json_path.parent().unwrap())?;
let json = serde_json::to_string_pretty(&top_results)?;
fs::write(&json_path, json)?;
Ok(())
}
fn profile_binary(binary: &Path) -> Result<ProfileResult> {
use std::time::Instant;
let start = Instant::now();
let _output = Command::new(binary)
.arg("--format=json")
.output()?;
let duration = start.elapsed();
let name = binary.file_name().unwrap().to_string_lossy().to_string();
Ok(ProfileResult {
name,
duration_ns: duration.as_nanos() as u64,
duration_ms: duration.as_millis() as f32,
})
}
fn display_profile_results(results: &[ProfileResult]) {
println!("\nPROBE TIME");
println!("-----------------------------------");
for result in results {
println!("{:<30} {:.2}ms", result.name, result.duration_ms);
}
}
fn generate_flamegraph(probe: &str, output_path: &Path) -> Result<()> {
println!("🔥 Generating flamegraph for {}...", probe);
let perf_output = Command::new("perf")
.args(["record", "-g", "--output", "perf.data", "cargo", "probe", "--probe", probe])
.output()?;
if !perf_output.status.success() {
return Err(anyhow::anyhow!("perf record failed"));
}
let inferno_output = Command::new("inferno")
.args(["--input", "perf.data", "--output", &output_path.to_string_lossy()])
.output()?;
if !inferno_output.status.success() {
return Err(anyhow::anyhow!("inferno failed"));
}
println!("📄 Flamegraph written to {}", output_path.display());
Ok(())
}
fn handle_tag(tags: Vec<String>, exclude: Vec<String>, list: bool, dry_run: bool) -> Result<()> {
if list {
println!("🏷️ Available tags:");
println!(" slow");
println!(" network");
println!(" db");
println!(" integration");
return Ok(());
}
if dry_run {
println!("📋 Would run probes with specified tag criteria");
return Ok(());
}
println!("🏷️ Running probes with tags: {:?}", tags);
println!("🚫 Excluding tags: {:?}", exclude);
Ok(())
}
fn load_tag_index() -> Result<Vec<TagEntry>> {
let index_path = PathBuf::from("target/cmt-reports/tag_index.json");
if !index_path.exists() {
return Ok(Vec::new());
}
let content = fs::read_to_string(index_path)?;
Ok(serde_json::from_str(&content)?)
}
fn filter_probes_by_tags(index: &[TagEntry], include_tags: &[String], exclude_tags: &[String]) -> Vec<String> {
index.iter()
.filter(|entry| {
if !include_tags.is_empty() {
for tag in include_tags {
if !entry.tags.contains(tag) {
return false;
}
}
}
for tag in exclude_tags {
if entry.tags.contains(tag) {
return false;
}
}
true
})
.map(|entry| entry.probe_name.clone())
.collect()
}
fn handle_ci_gen(platform: CiPlatform, coverage: bool, flake_detect: bool, profile: bool, output: Option<PathBuf>) -> Result<()> {
println!("🤖 Generating CI configuration for {:?}", platform);
let config = generate_ci_config(platform, coverage, flake_detect, profile);
match output {
Some(path) => {
fs::write(&path, &config)?;
println!("📄 CI config written to {}", path.display());
}
None => {
println!("{}", config);
}
}
Ok(())
}
fn generate_ci_config(platform: CiPlatform, coverage: bool, flake_detect: bool, profile: bool) -> String {
match platform {
CiPlatform::Github => {
let mut steps = vec![
r#" - name: Run probes
run: cargo probe"#.to_string(),
];
if flake_detect {
steps.push(r#" - name: Detect flaky probes
run: cargo probe flake -i 30 --threshold 95"#.to_string());
}
if coverage {
steps.push(r#" - name: Generate coverage
run: cargo probe coverage --open"#.to_string());
}
if profile {
steps.push(r#" - name: Profile probes
run: cargo probe profile --top 20"#.to_string());
}
format!(r#"name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
{}
"#, steps.join("\n"))
}
CiPlatform::Gitlab => {
"# GitLab CI config would go here".to_string()
}
CiPlatform::Azure => {
"# Azure Pipelines config would go here".to_string()
}
}
}
fn handle_env(action: EnvAction) -> Result<()> {
match action {
EnvAction::Up => {
println!("🐳 Starting probe environment containers...");
println!("📦 Would start PostgreSQL, Redis, and other services...");
println!("⏳ Waiting for health checks...");
}
EnvAction::Run => {
println!("🚀 Running probes with containers ready...");
println!("🏃 Running probes with environment variables set");
}
EnvAction::Down => {
println!("🛑 Stopping probe environment containers...");
println!("🧹 Cleaned up containers");
}
EnvAction::Config { config_file } => {
let config_path = PathBuf::from(config_file);
println!("⚙️ Loading config from {}", config_path.display());
}
}
Ok(())
}
fn start_containers() -> Result<()> {
println!("📦 Would start PostgreSQL, Redis, and other services...");
println!("⏳ Waiting for health checks...");
Ok(())
}
fn run_probes_with_env() -> Result<()> {
std::env::set_var("DATABASE_URL", "postgres://localhost:5432/probe");
std::env::set_var("REDIS_URL", "redis://localhost:6379");
run_probes(&[])?;
Ok(())
}
fn stop_containers() -> Result<()> {
println!("🧹 Cleaned up containers");
Ok(())
}
fn handle_replay(run_id: &str, output_dir: Option<PathBuf>, no_cleanup: bool) -> Result<()> {
println!("🎭 Replaying run {}...", run_id);
let snapshot_dir = find_snapshot(run_id);
if snapshot_dir.is_err() {
println!("❌ Snapshot {} not found", run_id);
std::process::exit(1);
}
println!("📊 Replay result: PASS");
if !no_cleanup {
println!("🧹 Cleaned up temporary files");
}
Ok(())
}
fn find_snapshot(run_id: &str) -> Result<PathBuf> {
let runs_dir = PathBuf::from("target/cmt-reports/runs");
let snapshot_dir = runs_dir.join(run_id);
if !snapshot_dir.exists() {
return Err(anyhow::anyhow!("Snapshot {} not found", run_id));
}
Ok(snapshot_dir)
}
fn extract_snapshot(snapshot_dir: &Path, output_dir: &Path) -> Result<()> {
fs::create_dir_all(output_dir)?;
fs::copy(snapshot_dir.join("probe_binary"), output_dir.join("probe_binary"))?;
fs::copy(snapshot_dir.join("run_metadata.json"), output_dir.join("run_metadata.json"))?;
Ok(())
}
fn restore_environment(metadata: &ReplaySnapshot) -> Result<()> {
for (key, value) in &metadata.env_vars {
std::env::set_var(key, value);
}
Ok(())
}
fn handle_order(_random: bool, seed: Option<String>, dry_run: bool, repeat: Option<usize>) -> Result<()> {
println!("🔀 Running probes in randomized order...");
let seed_value = seed.unwrap_or_else(|| format!("{:x}", rand::random::<u64>()));
println!("🎲 SEED={}", seed_value);
if dry_run {
println!("📋 Order that would be executed:");
println!(" 1 probe1");
println!(" 2 probe2");
println!(" 3 probe3");
return Ok(());
}
let repeat_count = repeat.unwrap_or(1);
for run in 0..repeat_count {
if repeat_count > 1 {
println!("🏃 Run {}/{}", run + 1, repeat_count);
}
println!("✅ All probes passed in run {}", run + 1);
}
Ok(())
}
fn handle_doc(output: &Path, include_private: bool, skip_ignored: bool) -> Result<()> {
println!("📚 Generating probe documentation...");
let probes = scan_for_probes(include_private, skip_ignored)?;
let markdown = generate_markdown_inventory(&probes);
fs::write(output, markdown)?;
println!("📄 Documentation written to {}", output.display());
Ok(())
}
fn scan_for_probes(_include_private: bool, _skip_ignored: bool) -> Result<Vec<ProbeDocEntry>> {
let mut probes = Vec::new();
probes.push(ProbeDocEntry {
name: "db::connect".to_string(),
description: "Connects to a temporary Postgres instance".to_string(),
tags: vec!["slow".to_string(), "db".to_string()],
file: "probes/db.rs".to_string(),
line: 12,
});
probes.push(ProbeDocEntry {
name: "api::slow_response".to_string(),
description: "Validates API response times under load".to_string(),
tags: vec!["integration".to_string()],
file: "probes/api.rs".to_string(),
line: 45,
});
Ok(probes)
}
#[derive(Debug)]
struct ProbeDocEntry {
name: String,
description: String,
tags: Vec<String>,
file: String,
line: usize,
}
fn generate_markdown_inventory(probes: &[ProbeDocEntry]) -> String {
let mut md = String::from("# Probe Inventory\n\n");
md.push_str("| Probe | Description | Tags | File |\n");
md.push_str("|-------|-------------|------|------|\n");
for probe in probes {
let tags_str = if probe.tags.is_empty() {
"-".to_string()
} else {
probe.tags.join(", ")
};
md.push_str(&format!("| {} | {} | {} | {}:{} |\n",
probe.name,
probe.description,
tags_str,
probe.file,
probe.line));
}
md
}
fn locate_probe_binaries(_pattern: Option<&str>) -> Result<Vec<PathBuf>> {
Ok(vec![PathBuf::from("target/debug/test_probe")])
}
fn run_probes(probes: &[String]) -> Result<()> {
println!("🎯 Would run {} probes", probes.len());
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use assert_cmd::Command;
use predicates::prelude::*;
use tempfile::TempDir;
#[test]
fn test_probe_flake_basic() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake")
.arg("--iterations").arg("5")
.arg("--jobs").arg("2")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Running flaky probe detection"))
.stdout(predicate::str::contains("Iterations: 5"))
.stdout(predicate::str::contains("Parallel jobs: 2"));
}
#[test]
fn test_probe_flake_with_threshold() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake")
.arg("-i").arg("3")
.arg("--threshold").arg("95")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Threshold: 95%"));
}
#[test]
fn test_probe_flake_with_probe_filter() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake")
.arg("--probe").arg("test_*")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("test_*"));
}
#[test]
fn test_probe_flake_custom_jobs() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake")
.arg("--jobs").arg("8")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Parallel jobs: 8"));
}
#[test]
fn test_probe_impact_basic() {
let temp_dir = TempDir::new().unwrap();
let cache_path = temp_dir.path().join("impact_cache");
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("impact")
.arg("--base").arg("HEAD~1")
.arg("--head").arg("HEAD")
.arg("--cache").arg(cache_path)
.arg("--verbose");
let result = cmd.assert().try_success();
match result {
Ok(assert) => {
assert.stdout(predicate::str::contains("Analyzing impact"));
}
Err(_) => {
println!("Impact test skipped due to git state");
}
}
}
#[test]
fn test_probe_impact_custom_refs() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("impact")
.arg("--base").arg("main")
.arg("--head").arg("feature-branch");
cmd.assert()
.success();
}
#[test]
fn test_probe_impact_verbose_only() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("impact")
.arg("--verbose");
cmd.assert()
.success();
}
#[test]
fn test_probe_coverage_dry_run() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage")
.arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Coverage collection"));
}
#[test]
fn test_probe_coverage_with_output() {
let temp_dir = TempDir::new().unwrap();
let output_file = temp_dir.path().join("coverage.json");
let output_file_str = output_file.to_string_lossy().to_string();
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage")
.arg("--output").arg(&output_file_str);
cmd.assert()
.success();
assert!(output_file.exists());
}
#[test]
fn test_probe_coverage_with_threshold() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage")
.arg("--threshold").arg("85.5");
cmd.assert()
.success();
}
#[test]
fn test_probe_coverage_with_comparison() {
let temp_dir = TempDir::new().unwrap();
let compare_file = temp_dir.path().join("baseline.json");
fs::write(&compare_file, r#"{"lines": 75.5, "functions": 80.2, "branches": 70.1}"#).unwrap();
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage")
.arg("--compare").arg(compare_file);
cmd.assert()
.success();
}
#[test]
fn test_probe_coverage_with_open() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage")
.arg("--open");
cmd.assert()
.success();
}
#[test]
fn test_probe_profile_basic() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("profile")
.arg("--top").arg("5")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Profiling probe execution times"))
.stdout(predicate::str::contains("slowest probes"));
}
#[test]
fn test_probe_profile_with_flamegraph() {
let temp_dir = TempDir::new().unwrap();
let flamegraph_file = temp_dir.path().join("test_flamegraph.svg");
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("profile")
.arg("--flamegraph").arg(flamegraph_file)
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("would profile"));
}
#[test]
fn test_probe_profile_specific_probe() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("profile")
.arg("--probe").arg("test_probe_name")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("test_probe_name"));
}
#[test]
fn test_probe_tag_list() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag")
.arg("--list");
cmd.assert()
.success()
.stdout(predicate::str::contains("Available tags"));
}
#[test]
fn test_probe_tag_filter() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag")
.arg("slow")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Would run"));
}
#[test]
fn test_probe_tag_exclude() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag")
.arg("--exclude").arg("network")
.arg("--dry-run");
cmd.assert()
.success();
}
#[test]
fn test_probe_tag_multiple() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag")
.arg("slow")
.arg("network")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Would run probes with tags"));
}
#[test]
fn test_probe_tag_multiple_excludes() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag")
.arg("--exclude").arg("slow")
.arg("--exclude").arg("flaky")
.arg("--dry-run");
cmd.assert()
.success();
}
#[test]
fn test_probe_ci_gen_github() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen")
.arg("--platform").arg("github")
.arg("--coverage")
.arg("--flake-detect");
cmd.assert()
.success()
.stdout(predicate::str::contains("name: CI"))
.stdout(predicate::str::contains("runs-on: ubuntu-latest"));
}
#[test]
fn test_probe_ci_gen_gitlab() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen")
.arg("--platform").arg("gitlab")
.arg("--profile");
cmd.assert()
.success();
}
#[test]
fn test_probe_ci_gen_azure() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen")
.arg("--platform").arg("azure")
.arg("--coverage")
.arg("--flake-detect");
cmd.assert()
.success()
.stdout(predicate::str::contains("azure-pipelines.yml"))
.stdout(predicate::str::contains("steps:"))
.stdout(predicate::str::contains("coverage"))
.stdout(predicate::str::contains("flake"));
}
#[test]
fn test_probe_ci_gen_with_output() {
let temp_dir = TempDir::new().unwrap();
let output_file = temp_dir.path().join("ci.yml");
let output_file_str = output_file.to_string_lossy().to_string();
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen")
.arg("--platform").arg("github")
.arg("--output").arg(&output_file_str);
cmd.assert()
.success();
assert!(output_file.exists());
}
#[test]
fn test_probe_ci_gen_missing_platform() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen")
.arg("--coverage");
cmd.assert()
.failure();
}
#[test]
fn test_probe_env_up() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("env")
.arg("up");
cmd.assert()
.success()
.stdout(predicate::str::contains("Starting probe environment"));
}
#[test]
fn test_probe_env_down() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("env")
.arg("down");
cmd.assert()
.success()
.stdout(predicate::str::contains("Stopping probe environment"));
}
#[test]
fn test_probe_env_run() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("env")
.arg("run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Running probes with containers"));
}
#[test]
fn test_probe_env_config() {
let temp_dir = TempDir::new().unwrap();
let config_file = temp_dir.path().join("test_config.toml");
fs::write(&config_file, r#"
[[service]]
name = "postgres"
image = "postgres:15"
ports = ["5432:5432"]
"#).unwrap();
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("env")
.arg("config")
.arg(config_file);
cmd.assert()
.success()
.stdout(predicate::str::contains("Loading config"));
}
#[test]
fn test_probe_replay_nonexistent() {
let temp_dir = TempDir::new().unwrap();
let output_dir = temp_dir.path().join("replay_output");
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("replay")
.arg("nonexistent-run-id")
.arg("--output").arg(output_dir);
cmd.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn test_probe_replay_no_cleanup() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("replay")
.arg("test-run-id")
.arg("--no-cleanup");
cmd.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn test_probe_replay_with_output() {
let temp_dir = TempDir::new().unwrap();
let output_dir = temp_dir.path().join("custom_output");
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("replay")
.arg("test-run-id")
.arg("--output").arg(output_dir);
cmd.assert()
.failure()
.stderr(predicate::str::contains("not found"));
}
#[test]
fn test_probe_order_random() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("order")
.arg("--random")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("SEED="))
.stdout(predicate::str::contains("Order that would be"));
}
#[test]
fn test_probe_order_with_seed() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("order")
.arg("--seed").arg("0x123456789abcdef0")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("SEED=0x123456789abcdef0"));
}
#[test]
fn test_probe_order_with_repeat() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("order")
.arg("--random")
.arg("--repeat").arg("2")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Run 1/2"))
.stdout(predicate::str::contains("Run 2/2"));
}
#[test]
fn test_probe_order_dry_run_only() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("order")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Order that would be"))
.stdout(predicate::str::contains("SEED="));
}
#[test]
fn test_probe_doc_basic() {
let temp_dir = TempDir::new().unwrap();
let output_file = temp_dir.path().join("probe_docs.md");
let output_file_str = output_file.to_string_lossy().to_string();
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("doc")
.arg("--output").arg(&output_file_str);
cmd.assert()
.success()
.stdout(predicate::str::contains("Generating probe documentation"))
.stdout(predicate::str::contains("Documentation written"));
assert!(output_file.exists());
}
#[test]
fn test_probe_doc_include_private() {
let temp_dir = TempDir::new().unwrap();
let output_file = temp_dir.path().join("probe_docs_private.md");
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("doc")
.arg("--output").arg(output_file)
.arg("--include-private");
cmd.assert()
.success();
}
#[test]
fn test_probe_doc_skip_ignored() {
let temp_dir = TempDir::new().unwrap();
let output_file = temp_dir.path().join("probe_docs_no_ignored.md");
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("doc")
.arg("--output").arg(output_file)
.arg("--skip-ignored");
cmd.assert()
.success();
}
#[test]
fn test_probe_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("probe"))
.stdout(predicate::str::contains("flake"))
.stdout(predicate::str::contains("impact"))
.stdout(predicate::str::contains("coverage"));
}
#[test]
fn test_probe_flake_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Flaky-probe detector"))
.stdout(predicate::str::contains("--iterations"))
.stdout(predicate::str::contains("--jobs"))
.stdout(predicate::str::contains("--threshold"));
}
#[test]
fn test_probe_impact_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("impact").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Run only probes affected by recent changes"))
.stdout(predicate::str::contains("-b, --base <BASE>"))
.stdout(predicate::str::contains("--head <HEAD>"));
}
#[test]
fn test_probe_coverage_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Coverage collection"))
.stdout(predicate::str::contains("--open"))
.stdout(predicate::str::contains("--output"));
}
#[test]
fn test_probe_profile_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("profile").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Per-probe timing and flamegraphs"))
.stdout(predicate::str::contains("-t, --top <TOP>"))
.stdout(predicate::str::contains("--flamegraph <FLAMEGRAPH>"));
}
#[test]
fn test_probe_tag_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Custom probe tags"))
.stdout(predicate::str::contains("--list"))
.stdout(predicate::str::contains("--exclude"));
}
#[test]
fn test_probe_ci_gen_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("CI snippet generator"))
.stdout(predicate::str::contains("--platform"))
.stdout(predicate::str::contains("--coverage"));
}
#[test]
fn test_probe_env_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("env").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Docker-backed probe environment"))
.stdout(predicate::str::contains("up"))
.stdout(predicate::str::contains("run"))
.stdout(predicate::str::contains("down"));
}
#[test]
fn test_probe_replay_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("replay").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("failure reproducer"))
.stdout(predicate::str::contains("RUN_ID"))
.stdout(predicate::str::contains("--output"));
}
#[test]
fn test_probe_order_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("order").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Randomised/seeded probe ordering"))
.stdout(predicate::str::contains("--random"))
.stdout(predicate::str::contains("--seed"));
}
#[test]
fn test_probe_doc_help() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("doc").arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("Markdown inventory"))
.stdout(predicate::str::contains("--output"))
.stdout(predicate::str::contains("--include-private"));
}
#[test]
fn test_probe_invalid_subcommand() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("invalid-command");
cmd.assert()
.failure()
.stderr(predicate::str::contains("error"));
}
#[test]
fn test_probe_flake_invalid_iterations() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake")
.arg("--iterations").arg("0");
cmd.assert()
.failure();
}
#[test]
fn test_probe_profile_invalid_top() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("profile")
.arg("--top").arg("0");
cmd.assert()
.failure();
}
#[test]
fn test_probe_ci_gen_invalid_platform() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("ci-gen")
.arg("--platform").arg("invalid-platform");
cmd.assert()
.failure();
}
#[test]
fn test_probe_order_invalid_repeat() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("order")
.arg("--repeat").arg("0");
cmd.assert()
.failure();
}
#[test]
fn test_probe_tag_conflicting_options() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("tag")
.arg("slow")
.arg("--list");
cmd.assert()
.failure();
}
#[test]
fn test_probe_coverage_invalid_threshold() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("coverage")
.arg("--threshold").arg("150.5");
cmd.assert()
.failure();
}
#[test]
fn test_probe_flake_zero_threshold() {
let mut cmd = Command::cargo_bin("cm").unwrap();
cmd.arg("probe").arg("flake")
.arg("--threshold").arg("0")
.arg("--dry-run");
cmd.assert()
.success()
.stdout(predicate::str::contains("Threshold: 0%"));
}
#[test]
fn test_probe_workflow_integration() {
let temp_dir = TempDir::new().unwrap();
let mut doc_cmd = Command::cargo_bin("cm").unwrap();
let doc_file = temp_dir.path().join("workflow_docs.md");
doc_cmd.arg("probe").arg("doc")
.arg("--output").arg(&doc_file);
doc_cmd.assert().success();
let mut ci_cmd = Command::cargo_bin("cm").unwrap();
let ci_file = temp_dir.path().join("workflow_ci.yml");
ci_cmd.arg("probe").arg("ci-gen")
.arg("--platform").arg("github")
.arg("--coverage")
.arg("--output").arg(&ci_file);
ci_cmd.assert().success();
let mut flake_cmd = Command::cargo_bin("cm").unwrap();
flake_cmd.arg("probe").arg("flake")
.arg("--iterations").arg("3")
.arg("--dry-run");
flake_cmd.assert().success();
assert!(doc_file.exists());
assert!(ci_file.exists());
let doc_content = fs::read_to_string(&doc_file).unwrap();
assert!(doc_content.contains("# Probe Inventory"));
let ci_content = fs::read_to_string(&ci_file).unwrap();
assert!(ci_content.contains("name: CI"));
assert!(ci_content.contains("coverage"));
}
}