use std::fs;
use std::path::PathBuf;
use std::process::{Command as ProcessCommand, ExitStatus};
use std::time::{SystemTime, UNIX_EPOCH};
use clap::{Parser, Subcommand};
use dbgflow::{EventKind, read_session_json};
#[derive(Parser)]
#[command(name = "dbgflow", about = "Graph-first Rust debugger for Rust code")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Demo {
#[arg(long, default_value = "artifacts/demo-session.json")]
output: PathBuf,
#[arg(long)]
serve: bool,
#[arg(long, default_value_t = 3000)]
port: u16,
},
Serve {
session: PathBuf,
#[arg(long, default_value_t = 3000)]
port: u16,
},
Test {
#[arg(long)]
manifest_path: Option<PathBuf>,
#[arg(long, default_value = "artifacts/test-sessions")]
output_dir: PathBuf,
#[arg(long)]
serve: bool,
#[arg(long, default_value_t = 3000)]
port: u16,
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
cargo_args: Vec<String>,
},
}
fn main() -> std::io::Result<()> {
let cli = Cli::parse();
match cli.command {
Command::Demo {
output,
serve,
port,
} => dbgflow::demo::run(output, serve, port),
Command::Serve { session, port } => dbgflow::demo::serve_saved(session, port),
Command::Test {
manifest_path,
output_dir,
serve,
port,
cargo_args,
} => run_test(manifest_path, output_dir, serve, port, cargo_args),
}
}
#[derive(Clone)]
struct TestRunConfig {
manifest_path: Option<PathBuf>,
output_dir: PathBuf,
cargo_args: Vec<String>,
}
struct TestRunOutput {
status: ExitStatus,
run_dir: PathBuf,
session_paths: Vec<PathBuf>,
preferred_session: Option<PathBuf>,
}
fn run_test(
manifest_path: Option<PathBuf>,
output_dir: PathBuf,
serve: bool,
port: u16,
cargo_args: Vec<String>,
) -> std::io::Result<()> {
let config = TestRunConfig {
manifest_path,
output_dir: std::env::current_dir()?.join(output_dir),
cargo_args,
};
let first_run = execute_test_run(&config)?;
print_test_summary(&first_run);
if first_run.session_paths.is_empty() {
println!(
"No sessions were captured. Annotate tests with #[dbg_test] from dbgflow to emit per-test sessions."
);
return Ok(());
}
let preferred_session = first_run
.preferred_session
.clone()
.expect("preferred session should exist when at least one session was captured");
println!("Open a captured session with:");
println!(" dbgflow serve {}", preferred_session.display());
if serve {
let initial_session = read_session_json(&preferred_session)?;
let rerun_config = config.clone();
println!("Serving {}", preferred_session.display());
dbgflow::serve_session_with_rerun(initial_session, "127.0.0.1", port, move || {
let rerun = execute_test_run(&rerun_config)?;
print_test_summary(&rerun);
if rerun.session_paths.is_empty() {
return Err(std::io::Error::other(
"rerun finished without dbgflow sessions; make sure #[dbg_test] is present",
));
}
let preferred_session = rerun.preferred_session.ok_or_else(|| {
std::io::Error::other("rerun finished without a preferred dbgflow session")
})?;
read_session_json(preferred_session)
})?;
}
if !first_run.status.success() {
return Err(std::io::Error::other(format!(
"cargo test exited with status {}",
first_run.status
)));
}
Ok(())
}
fn execute_test_run(config: &TestRunConfig) -> std::io::Result<TestRunOutput> {
let run_dir = config
.output_dir
.join(format!("run-{}", unix_timestamp_millis()));
fs::create_dir_all(&run_dir)?;
let mut command = ProcessCommand::new("cargo");
command.arg("test");
if let Some(manifest_path) = &config.manifest_path {
command.arg("--manifest-path").arg(manifest_path);
}
command.args(&config.cargo_args);
command.env("DBG_SESSION_DIR", &run_dir);
command.env("RUST_TEST_THREADS", "1");
let status = command.status()?;
let mut session_paths = collect_session_files(&run_dir)?;
session_paths.sort();
let preferred_session =
find_failed_session(&session_paths)?.or_else(|| session_paths.first().cloned());
Ok(TestRunOutput {
status,
run_dir,
session_paths,
preferred_session,
})
}
fn print_test_summary(run: &TestRunOutput) {
println!(
"Captured {} dbgflow session(s) in {}",
run.session_paths.len(),
run.run_dir.display()
);
for path in &run.session_paths {
println!(" {}", path.display());
}
}
fn collect_session_files(dir: &PathBuf) -> std::io::Result<Vec<PathBuf>> {
let mut paths = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|ext| ext.to_str()) == Some("json") {
paths.push(path);
}
}
Ok(paths)
}
fn find_failed_session(paths: &[PathBuf]) -> std::io::Result<Option<PathBuf>> {
for path in paths {
let session = read_session_json(path)?;
let has_failure = session
.events
.iter()
.any(|event| matches!(event.kind, EventKind::TestFailed));
if has_failure {
return Ok(Some(path.clone()));
}
}
Ok(None)
}
fn unix_timestamp_millis() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
}