use crate::cargo::{find_binary, find_library};
use cargo_toml::Manifest;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OutputFormat {
Text {
tree: bool,
summary_only: bool,
quiet: bool,
hyperlinks: bool,
},
Json {
tree: bool,
summary_only: bool,
},
Html {
tree: bool,
summary_only: bool,
},
}
impl Default for OutputFormat {
fn default() -> Self {
OutputFormat::Text {
tree: false,
summary_only: false,
quiet: false,
hyperlinks: true,
}
}
}
impl OutputFormat {
pub fn text(tree: bool, summary_only: bool, quiet: bool, hyperlinks: bool) -> Self {
OutputFormat::Text {
tree,
summary_only,
quiet,
hyperlinks,
}
}
pub fn json(tree: bool, summary_only: bool) -> Self {
OutputFormat::Json { tree, summary_only }
}
pub fn html(tree: bool, summary_only: bool) -> Self {
OutputFormat::Html { tree, summary_only }
}
pub fn quiet() -> Self {
OutputFormat::Text {
tree: false,
summary_only: false,
quiet: true,
hyperlinks: false,
}
}
pub fn is_json(&self) -> bool {
matches!(self, OutputFormat::Json { .. })
}
pub fn is_html(&self) -> bool {
matches!(self, OutputFormat::Html { .. })
}
pub fn is_text(&self) -> bool {
matches!(self, OutputFormat::Text { .. })
}
pub fn show_progress(&self) -> bool {
match self {
OutputFormat::Text {
quiet,
summary_only,
..
} => !quiet && !summary_only,
OutputFormat::Json { .. } | OutputFormat::Html { .. } => false,
}
}
pub fn is_summary_only(&self) -> bool {
match self {
OutputFormat::Text { summary_only, .. }
| OutputFormat::Json { summary_only, .. }
| OutputFormat::Html { summary_only, .. } => *summary_only,
}
}
pub fn show_tree(&self) -> bool {
match self {
OutputFormat::Text { tree, .. }
| OutputFormat::Json { tree, .. }
| OutputFormat::Html { tree, .. } => *tree,
}
}
pub fn use_hyperlinks(&self) -> bool {
match self {
OutputFormat::Text { hyperlinks, .. } => *hyperlinks,
OutputFormat::Json { .. } | OutputFormat::Html { .. } => false,
}
}
}
#[derive(Debug)]
pub struct WorkspaceMember {
pub name: String,
pub path: PathBuf,
pub binaries: Vec<PathBuf>,
}
pub struct Args {
pub binaries: Vec<PathBuf>,
pub workspace_members: Option<Vec<WorkspaceMember>>,
pub show_timings: bool,
pub max_threads: usize,
pub config_path: Option<PathBuf>,
pub output: OutputFormat,
pub lsp_mode: bool,
}
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub fn parse_args(args: &[String]) -> Result<Args, String> {
if args.iter().any(|a| a == "--version" || a == "-V") {
println!("jonesy {}", VERSION);
std::process::exit(0);
}
if args.get(1).is_some_and(|a| a == "lsp") {
return Ok(Args {
binaries: Vec::new(),
workspace_members: None,
show_timings: false,
max_threads: 1,
config_path: None,
output: OutputFormat::default(),
lsp_mode: true,
});
}
let show_tree = args.iter().any(|a| a == "--tree");
let summary_only = args.iter().any(|a| a == "--summary-only");
let show_timings = args.iter().any(|a| a == "--show-timings");
let quiet = args.iter().any(|a| a == "--quiet");
let no_hyperlinks = args.iter().any(|a| a == "--no-hyperlinks");
let output = parse_output_format(args, show_tree, summary_only, quiet, no_hyperlinks)?;
let max_threads = parse_max_threads(args)?;
let config_path = parse_config_path(args)?;
let filtered_args: Vec<&String> = args
.iter()
.enumerate()
.filter(|(i, a)| {
*a != "--tree"
&& *a != "--summary-only"
&& *a != "--show-timings"
&& *a != "--quiet"
&& *a != "--no-hyperlinks"
&& *a != "--max-threads"
&& *a != "--config"
&& *a != "--format"
&& !(*i > 0 && args.get(i - 1).is_some_and(|prev| prev == "--max-threads"))
&& !(*i > 0 && args.get(i - 1).is_some_and(|prev| prev == "--config"))
&& !(*i > 0 && args.get(i - 1).is_some_and(|prev| prev == "--format"))
})
.map(|(_, a)| a)
.collect();
let has_bin_flag = filtered_args.iter().any(|a| *a == "--bin");
let has_lib_flag = filtered_args.iter().any(|a| *a == "--lib");
if has_bin_flag && has_lib_flag {
return Err("--bin and --lib are mutually exclusive".to_string());
}
let at_workspace_root = is_workspace_root();
if at_workspace_root && (has_bin_flag || has_lib_flag) {
return Err("--bin and --lib are not supported at workspace level. \
cd into a member crate directory for target-specific analysis."
.to_string());
}
let (binaries, workspace_members) = if has_bin_flag {
(parse_bin_args(&filtered_args)?, None)
} else if has_lib_flag {
(parse_lib_args(&filtered_args)?, None)
} else if filtered_args.len() == 1 {
if let Some(members) = find_workspace_members()? {
(vec![], Some(members))
} else {
(find_crate_binaries()?, None)
}
} else {
return Err(usage());
};
Ok(Args {
binaries,
workspace_members,
show_timings,
max_threads,
config_path,
output,
lsp_mode: false,
})
}
fn parse_max_threads(args: &[String]) -> Result<usize, String> {
for (i, arg) in args.iter().enumerate() {
if arg == "--max-threads" {
let value = args
.get(i + 1)
.ok_or("--max-threads requires a number argument")?;
let n: usize = value
.parse()
.map_err(|_| format!("Invalid --max-threads value: {}", value))?;
if n == 0 {
return Err("--max-threads must be at least 1".to_string());
}
return Ok(n);
}
}
Ok(std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1))
}
fn parse_config_path(args: &[String]) -> Result<Option<PathBuf>, String> {
for (i, arg) in args.iter().enumerate() {
if arg == "--config" {
let value = args.get(i + 1).ok_or("--config requires a path argument")?;
let path = PathBuf::from(value);
if !path.exists() {
return Err(format!("Config file not found: {}", path.display()));
}
return Ok(Some(path));
}
}
Ok(None)
}
fn parse_output_format(
args: &[String],
show_tree: bool,
summary_only: bool,
quiet: bool,
no_hyperlinks: bool,
) -> Result<OutputFormat, String> {
for (i, arg) in args.iter().enumerate() {
if arg == "--format" {
let value = args
.get(i + 1)
.ok_or("--format requires an argument (text, json, or html)")?;
return match value.to_lowercase().as_str() {
"text" => Ok(OutputFormat::text(
show_tree,
summary_only,
quiet,
!no_hyperlinks,
)),
"json" => Ok(OutputFormat::json(show_tree, summary_only)),
"html" => Ok(OutputFormat::html(show_tree, summary_only)),
_ => Err(format!(
"Invalid format '{}'. Valid options: text, json, html",
value
)),
};
}
}
Ok(OutputFormat::text(
show_tree,
summary_only,
quiet,
!no_hyperlinks,
))
}
fn usage() -> String {
format!(
"jonesy {} - Find panic points in Rust binaries\n\n\
Usage:\n \
jonesy [OPTIONS]\n \
jonesy [OPTIONS] --bin <name_or_path>\n \
jonesy [OPTIONS] --lib [path_to_lib_object]\n \
jonesy lsp\n\n\
When run without --bin or --lib, jonesy looks for Cargo.toml in the current\n\
directory and analyzes all binary targets found in target/debug/.\n\n\
Subcommands:\n \
lsp Start LSP server for IDE integration\n\n\
Options:\n \
--bin <name> Analyze only the specified binary (by name or path)\n \
--lib Analyze only the library target\n \
--tree Show full call tree instead of just crate code points\n \
--summary-only Only show summary, not detailed panic points\n \
--quiet Suppress progress messages (keeps panic points and summary)\n \
--show-timings Show timing information for each analysis step\n \
--max-threads N Maximum threads for parallel analysis (default: CPU count)\n \
--config <path> Path to TOML config file for allow/deny rules\n \
--no-hyperlinks Disable terminal hyperlinks (use plain absolute paths)\n \
--format <fmt> Output format: text (default), json, html\n \
--version, -V Print version and exit",
VERSION
)
}
fn find_target_dir() -> Result<PathBuf, String> {
let mut current =
std::env::current_dir().map_err(|e| format!("Cannot get current dir: {}", e))?;
loop {
let target_dir = current.join("target/debug");
if target_dir.exists() {
return Ok(target_dir);
}
let cargo_toml = current.join("Cargo.toml");
if cargo_toml.exists()
&& let Ok(content) = std::fs::read_to_string(&cargo_toml)
&& content.contains("[workspace]")
{
return Err("target/debug/ directory not found. Run 'cargo build' first.".to_string());
}
if let Some(parent) = current.parent() {
current = parent.to_path_buf();
} else {
break;
}
}
Err("target/debug/ directory not found. Run 'cargo build' first.".to_string())
}
fn is_workspace_root() -> bool {
let cargo_toml_path = PathBuf::from("Cargo.toml");
if !cargo_toml_path.exists() {
return false;
}
let Ok(content) = std::fs::read_to_string(&cargo_toml_path) else {
return false;
};
let Ok(manifest) = Manifest::from_slice(content.as_bytes()) else {
return false;
};
manifest.workspace.is_some()
}
fn find_workspace_members() -> Result<Option<Vec<WorkspaceMember>>, String> {
let cargo_toml_path = PathBuf::from("Cargo.toml");
if !cargo_toml_path.exists() {
return Ok(None);
}
let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| format!("Failed to read Cargo.toml: {}", e))?;
let manifest = Manifest::from_slice(cargo_toml_content.as_bytes())
.map_err(|e| format!("Failed to parse Cargo.toml: {}", e))?;
if manifest.workspace.is_none() {
return Ok(None);
}
let workspace = manifest.workspace.as_ref().unwrap();
let target_dir = PathBuf::from("target/debug");
if !target_dir.exists() {
return Err("target/debug/ directory not found. Run 'cargo build' first.".to_string());
}
let mut members = Vec::new();
if let Some(pkg) = &manifest.package {
let pkg_name = pkg.name.clone();
let mut root_manifest = manifest.clone();
let _ = root_manifest.complete_from_path_and_workspace::<toml::Value>(
&cargo_toml_path,
None::<(&Manifest<toml::Value>, &std::path::Path)>, );
let binaries = collect_binaries_from_manifest(&root_manifest, &pkg_name, &target_dir);
if !binaries.is_empty() {
members.push(WorkspaceMember {
name: pkg_name,
path: PathBuf::from("."),
binaries,
});
}
}
for member_pattern in &workspace.members {
let member_paths = if member_pattern.contains('*') {
let base = member_pattern.trim_end_matches("/*").trim_end_matches("/*");
let base_path = PathBuf::from(base);
if base_path.is_dir() {
std::fs::read_dir(&base_path)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.map(|e| e.path())
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else {
vec![]
}
} else {
vec![PathBuf::from(member_pattern)]
};
for member_path in member_paths {
let member_cargo_toml = member_path.join("Cargo.toml");
if !member_cargo_toml.exists() {
continue;
}
if let Ok(content) = std::fs::read_to_string(&member_cargo_toml)
&& let Ok(mut member_manifest) = Manifest::from_slice(content.as_bytes())
&& let Some(pkg) = &member_manifest.package
{
let pkg_name = pkg.name.clone();
let _ = member_manifest.complete_from_path_and_workspace(
&member_cargo_toml,
Some((&manifest, &cargo_toml_path)),
);
let binaries =
collect_binaries_from_manifest(&member_manifest, &pkg_name, &target_dir);
if !binaries.is_empty() {
members.push(WorkspaceMember {
name: pkg_name,
path: member_path,
binaries,
});
}
}
}
}
if members.is_empty() {
return Err("No binary targets found in workspace. Run 'cargo build' first.".to_string());
}
Ok(Some(members))
}
fn collect_binaries_from_manifest(
manifest: &Manifest,
pkg_name: &str,
target_dir: &Path,
) -> Vec<PathBuf> {
let mut binaries = Vec::new();
for bin in &manifest.bin {
let bin_name = bin.name.as_deref().unwrap_or(pkg_name);
if let Some(bin_path) = find_binary(target_dir, bin_name) {
binaries.push(bin_path);
}
}
if manifest.lib.is_some() {
let lib_name = manifest
.lib
.as_ref()
.and_then(|l| l.name.clone())
.unwrap_or_else(|| pkg_name.replace('-', "_"));
if let Some(lib_path) = find_library(target_dir, &lib_name) {
binaries.push(lib_path);
}
}
binaries
}
fn find_workspace_binaries(manifest: &Manifest) -> Result<Vec<PathBuf>, String> {
let workspace = manifest
.workspace
.as_ref()
.ok_or("No workspace section found")?;
let target_dir = PathBuf::from("target/debug");
if !target_dir.exists() {
return Err("target/debug/ directory not found. Run 'cargo build' first.".to_string());
}
let mut binaries = Vec::new();
for member_pattern in &workspace.members {
let member_paths = if member_pattern.contains('*') {
let base = member_pattern.trim_end_matches("/*").trim_end_matches("/*");
let base_path = PathBuf::from(base);
if base_path.is_dir() {
std::fs::read_dir(&base_path)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.filter(|e| e.path().is_dir())
.map(|e| e.path())
.collect::<Vec<_>>()
})
.unwrap_or_default()
} else {
vec![]
}
} else {
vec![PathBuf::from(member_pattern)]
};
for member_path in member_paths {
let member_cargo_toml = member_path.join("Cargo.toml");
if !member_cargo_toml.exists() {
continue;
}
if let Ok(content) = std::fs::read_to_string(&member_cargo_toml)
&& let Ok(member_manifest) = Manifest::from_slice(content.as_bytes())
&& let Some(pkg) = &member_manifest.package
{
let pkg_name = &pkg.name;
for bin in &member_manifest.bin {
let bin_name = bin.name.as_ref().unwrap_or(pkg_name);
let bin_path = target_dir.join(bin_name);
if bin_path.exists() {
binaries.push(bin_path);
}
}
if member_manifest.bin.is_empty() {
let default_bin = target_dir.join(pkg_name);
if default_bin.exists() {
binaries.push(default_bin);
}
}
}
}
}
if binaries.is_empty() {
return Err("No binary targets found in workspace. Run 'cargo build' first.".to_string());
}
Ok(binaries)
}
fn find_crate_binaries() -> Result<Vec<PathBuf>, String> {
let cargo_toml_path = PathBuf::from("Cargo.toml");
if !cargo_toml_path.exists() {
return Err("No Cargo.toml found in current directory. \
Run jonesy from a crate root or use --bin <path>."
.to_string());
}
let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| format!("Failed to read Cargo.toml: {}", e))?;
let manifest = Manifest::from_slice(cargo_toml_content.as_bytes())
.map_err(|e| format!("Failed to parse Cargo.toml: {}", e))?;
if manifest.workspace.is_some() && manifest.package.is_none() {
return find_workspace_binaries(&manifest);
}
let package = manifest
.package
.as_ref()
.ok_or("Cargo.toml has no [package] section")?;
let package_name = &package.name;
let target_dir = find_target_dir()?;
let mut binaries = Vec::new();
for bin in &manifest.bin {
let bin_name = bin.name.as_ref().unwrap_or(package_name);
let bin_path = target_dir.join(bin_name);
if bin_path.exists() {
binaries.push(bin_path);
}
}
if manifest.bin.is_empty() {
let default_bin = target_dir.join(package_name);
if default_bin.exists() {
binaries.push(default_bin);
}
}
if manifest.lib.is_some() {
let lib_name = manifest
.lib
.as_ref()
.and_then(|l| l.name.clone())
.unwrap_or_else(|| package_name.replace('-', "_"));
let dylib_path = target_dir.join(format!("lib{}.dylib", lib_name));
let rlib_path = target_dir.join(format!("lib{}.rlib", lib_name));
if dylib_path.exists() {
binaries.push(dylib_path);
} else if rlib_path.exists() {
binaries.push(rlib_path);
}
}
if binaries.is_empty() {
return Err(format!(
"No binary targets found in target/debug/ for package '{}'. \
Run 'cargo build' first.",
package_name
));
}
Ok(binaries)
}
fn extract_bin_arg<'a>(args: &[&'a String]) -> Result<&'a str, String> {
let bin_arg_idx = args
.iter()
.position(|a| *a == "--bin")
.ok_or("--bin flag not found")?;
let bin_name = args
.get(bin_arg_idx + 1)
.ok_or("--bin requires a binary name or path")?;
if let Some(extra) = args.get(bin_arg_idx + 2) {
if !extra.starts_with("--") {
return Err(format!(
"Unexpected extra argument '{}' after --bin <name_or_path>",
extra
));
}
}
Ok(bin_name.as_str())
}
fn find_bin_in_manifest(bin_name: &str, manifest: &Manifest, target_dir: &Path) -> Option<PathBuf> {
for bin in &manifest.bin {
let manifest_bin_name = bin
.name
.as_ref()
.or(manifest.package.as_ref().map(|p| &p.name));
if let Some(name) = manifest_bin_name {
if name == bin_name || name.replace('-', "_") == bin_name {
let bin_path = target_dir.join(name);
if bin_path.exists() {
return Some(bin_path);
}
}
}
}
if let Some(pkg) = &manifest.package {
if pkg.name == bin_name || pkg.name.replace('-', "_") == bin_name {
let bin_path = target_dir.join(&pkg.name);
if bin_path.exists() {
return Some(bin_path);
}
}
}
None
}
fn parse_bin_args(args: &[&String]) -> Result<Vec<PathBuf>, String> {
let bin_name = extract_bin_arg(args)?;
let binary_path = PathBuf::from(bin_name);
if binary_path.exists() {
std::fs::File::open(&binary_path)
.map_err(|e| format!("Cannot read binary at {:?}: {}", binary_path, e))?;
return Ok(vec![binary_path]);
}
let cargo_toml_path = PathBuf::from("Cargo.toml");
if !cargo_toml_path.exists() {
return Err(format!(
"Binary '{}' not found and no Cargo.toml in current directory",
bin_name
));
}
let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| format!("Failed to read Cargo.toml: {}", e))?;
let manifest = Manifest::from_slice(cargo_toml_content.as_bytes())
.map_err(|e| format!("Failed to parse Cargo.toml: {}", e))?;
let target_dir = find_target_dir()?;
if let Some(bin_path) = find_bin_in_manifest(bin_name, &manifest, &target_dir) {
return Ok(vec![bin_path]);
}
if manifest.workspace.is_some() {
if let Ok(workspace_binaries) = find_workspace_binaries(&manifest) {
for bin_path in workspace_binaries {
if let Some(name) = bin_path.file_name().and_then(|n| n.to_str()) {
if name == bin_name || name.replace('-', "_") == bin_name {
return Ok(vec![bin_path]);
}
}
}
}
}
Err(format!(
"Binary '{}' not found in Cargo.toml or target/debug/",
bin_name
))
}
fn extract_lib_arg<'a>(args: &[&'a String]) -> Result<Option<&'a str>, String> {
let lib_arg_idx = args
.iter()
.position(|a| *a == "--lib")
.ok_or("--lib flag not found")?;
let lib_path_arg = args.get(lib_arg_idx + 1).filter(|a| !a.starts_with("--"));
if lib_path_arg.is_some() {
if let Some(extra) = args.get(lib_arg_idx + 2) {
if !extra.starts_with("--") {
return Err(format!(
"Unexpected extra argument '{}' after --lib [path_to_lib_object]",
extra
));
}
}
}
Ok(lib_path_arg.map(|s| s.as_str()))
}
fn get_lib_name(manifest: &Manifest) -> Option<String> {
manifest
.lib
.as_ref()
.and_then(|l| l.name.clone())
.or_else(|| manifest.package.as_ref().map(|p| p.name.replace('-', "_")))
}
fn find_lib_in_target(lib_name: &str, target_dir: &Path) -> Option<PathBuf> {
let dylib_path = target_dir.join(format!("lib{}.dylib", lib_name));
let rlib_path = target_dir.join(format!("lib{}.rlib", lib_name));
let staticlib_path = target_dir.join(format!("lib{}.a", lib_name));
if dylib_path.exists() {
Some(dylib_path)
} else if rlib_path.exists() {
Some(rlib_path)
} else if staticlib_path.exists() {
Some(staticlib_path)
} else {
None
}
}
fn parse_lib_args(args: &[&String]) -> Result<Vec<PathBuf>, String> {
let lib_path_arg = extract_lib_arg(args)?;
if let Some(path_str) = lib_path_arg {
let binary_path = PathBuf::from(path_str);
if !binary_path.exists() {
return Err(format!(
"Library shared object not found at {:?}",
binary_path
));
}
std::fs::File::open(&binary_path).map_err(|e| {
format!(
"Cannot read Library shared object at {:?}: {}",
binary_path, e
)
})?;
return Ok(vec![binary_path]);
}
let cargo_toml_path = PathBuf::from("Cargo.toml");
if !cargo_toml_path.exists() {
return Err("No Cargo.toml found. Use --lib <path> to specify library path.".to_string());
}
let cargo_toml_content = std::fs::read_to_string(&cargo_toml_path)
.map_err(|e| format!("Failed to read Cargo.toml: {}", e))?;
let manifest = Manifest::from_slice(cargo_toml_content.as_bytes())
.map_err(|e| format!("Failed to parse Cargo.toml: {}", e))?;
let has_implicit_lib = PathBuf::from("src/lib.rs").exists();
if manifest.lib.is_none() && !has_implicit_lib {
return Err("No library target found in Cargo.toml or src/lib.rs".to_string());
}
let target_dir = find_target_dir()?;
let lib_name = get_lib_name(&manifest).ok_or("Cannot determine library name")?;
find_lib_in_target(&lib_name, &target_dir)
.map(|p| vec![p])
.ok_or_else(|| {
format!(
"Library 'lib{}' not found in target/debug/. Run 'cargo build' first.",
lib_name
)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_format_default() {
let format = OutputFormat::default();
assert!(format.is_text());
assert!(!format.is_json());
assert!(!format.is_html());
assert!(format.show_progress());
assert!(!format.is_summary_only());
assert!(!format.show_tree());
assert!(format.use_hyperlinks());
}
#[test]
fn test_output_format_text_constructor() {
let format = OutputFormat::text(true, true, true, false);
assert!(format.is_text());
assert!(format.show_tree());
assert!(format.is_summary_only());
assert!(!format.show_progress()); assert!(!format.use_hyperlinks());
}
#[test]
fn test_output_format_json_constructor() {
let format = OutputFormat::json(true, false);
assert!(format.is_json());
assert!(!format.is_text());
assert!(!format.is_html());
assert!(format.show_tree());
assert!(!format.is_summary_only());
assert!(!format.show_progress()); assert!(!format.use_hyperlinks()); }
#[test]
fn test_output_format_html_constructor() {
let format = OutputFormat::html(false, true);
assert!(format.is_html());
assert!(!format.is_text());
assert!(!format.is_json());
assert!(!format.show_tree());
assert!(format.is_summary_only());
assert!(!format.show_progress()); assert!(!format.use_hyperlinks()); }
#[test]
fn test_output_format_quiet() {
let format = OutputFormat::quiet();
assert!(format.is_text());
assert!(!format.show_progress());
assert!(!format.use_hyperlinks());
}
#[test]
fn test_output_format_show_progress_logic() {
let format = OutputFormat::text(false, false, false, true);
assert!(format.show_progress());
let format = OutputFormat::text(false, false, true, true);
assert!(!format.show_progress());
let format = OutputFormat::text(false, true, false, true);
assert!(!format.show_progress());
let format = OutputFormat::text(false, true, true, true);
assert!(!format.show_progress());
}
#[test]
fn test_parse_max_threads_default() {
let args = vec!["jonesy".to_string()];
let result = parse_max_threads(&args).unwrap();
assert!(result >= 1);
}
#[test]
fn test_parse_max_threads_explicit() {
let args = vec![
"jonesy".to_string(),
"--max-threads".to_string(),
"4".to_string(),
];
let result = parse_max_threads(&args).unwrap();
assert_eq!(result, 4);
}
#[test]
fn test_parse_max_threads_one() {
let args = vec![
"jonesy".to_string(),
"--max-threads".to_string(),
"1".to_string(),
];
let result = parse_max_threads(&args).unwrap();
assert_eq!(result, 1);
}
#[test]
fn test_parse_max_threads_zero_error() {
let args = vec![
"jonesy".to_string(),
"--max-threads".to_string(),
"0".to_string(),
];
let result = parse_max_threads(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("at least 1"));
}
#[test]
fn test_parse_max_threads_missing_value() {
let args = vec!["jonesy".to_string(), "--max-threads".to_string()];
let result = parse_max_threads(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("requires a number"));
}
#[test]
fn test_parse_max_threads_invalid_value() {
let args = vec![
"jonesy".to_string(),
"--max-threads".to_string(),
"abc".to_string(),
];
let result = parse_max_threads(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid"));
}
#[test]
fn test_parse_output_format_default() {
let args = vec!["jonesy".to_string()];
let result = parse_output_format(&args, false, false, false, false).unwrap();
assert!(result.is_text());
assert!(result.use_hyperlinks());
}
#[test]
fn test_parse_output_format_text_explicit() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"text".to_string(),
];
let result = parse_output_format(&args, false, false, false, false).unwrap();
assert!(result.is_text());
}
#[test]
fn test_parse_output_format_json() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"json".to_string(),
];
let result = parse_output_format(&args, true, false, false, false).unwrap();
assert!(result.is_json());
assert!(result.show_tree());
}
#[test]
fn test_parse_output_format_html() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"html".to_string(),
];
let result = parse_output_format(&args, false, true, false, false).unwrap();
assert!(result.is_html());
assert!(result.is_summary_only());
}
#[test]
fn test_parse_output_format_case_insensitive() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"JSON".to_string(),
];
let result = parse_output_format(&args, false, false, false, false).unwrap();
assert!(result.is_json());
}
#[test]
fn test_parse_output_format_invalid() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"xml".to_string(),
];
let result = parse_output_format(&args, false, false, false, false);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid format"));
}
#[test]
fn test_parse_output_format_missing_value() {
let args = vec!["jonesy".to_string(), "--format".to_string()];
let result = parse_output_format(&args, false, false, false, false);
assert!(result.is_err());
assert!(result.unwrap_err().contains("requires an argument"));
}
#[test]
fn test_parse_output_format_no_hyperlinks() {
let args = vec!["jonesy".to_string()];
let result = parse_output_format(&args, false, false, false, true).unwrap();
assert!(result.is_text());
assert!(!result.use_hyperlinks());
}
#[test]
fn test_parse_output_format_with_flags() {
let args = vec!["jonesy".to_string()];
let result = parse_output_format(&args, true, true, true, true).unwrap();
assert!(result.is_text());
assert!(result.show_tree());
assert!(result.is_summary_only());
assert!(!result.show_progress()); assert!(!result.use_hyperlinks());
}
#[test]
fn test_usage_contains_version() {
let help = usage();
assert!(help.contains(VERSION));
}
#[test]
fn test_usage_contains_key_options() {
let help = usage();
assert!(help.contains("--bin"));
assert!(help.contains("--lib"));
assert!(help.contains("--tree"));
assert!(help.contains("--quiet"));
assert!(help.contains("--format"));
assert!(help.contains("--config"));
assert!(help.contains("--max-threads"));
assert!(help.contains("lsp"));
}
#[test]
fn test_usage_contains_format_options() {
let help = usage();
assert!(help.contains("text"));
assert!(help.contains("json"));
assert!(help.contains("html"));
}
#[test]
fn test_extract_bin_arg_valid() {
let args = [
"jonesy".to_string(),
"--bin".to_string(),
"my-binary".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs).unwrap();
assert_eq!(result, "my-binary");
}
#[test]
fn test_extract_bin_arg_with_path() {
let args = [
"jonesy".to_string(),
"--bin".to_string(),
"/path/to/binary".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs).unwrap();
assert_eq!(result, "/path/to/binary");
}
#[test]
fn test_extract_bin_arg_missing_value() {
let args = ["jonesy".to_string(), "--bin".to_string()];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs);
assert!(result.is_err());
assert!(result.unwrap_err().contains("requires a binary name"));
}
#[test]
fn test_extract_bin_arg_no_flag() {
let args = ["jonesy".to_string()];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs);
assert!(result.is_err());
assert!(result.unwrap_err().contains("flag not found"));
}
#[test]
fn test_extract_bin_arg_extra_positional() {
let args = [
"jonesy".to_string(),
"--bin".to_string(),
"my-binary".to_string(),
"extra-arg".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unexpected extra argument"));
}
#[test]
fn test_extract_bin_arg_allows_trailing_flags() {
let args = [
"jonesy".to_string(),
"--bin".to_string(),
"my-binary".to_string(),
"--quiet".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs).unwrap();
assert_eq!(result, "my-binary");
}
#[test]
fn test_extract_lib_arg_with_path() {
let args = [
"jonesy".to_string(),
"--lib".to_string(),
"/path/to/lib.rlib".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs).unwrap();
assert_eq!(result, Some("/path/to/lib.rlib"));
}
#[test]
fn test_extract_lib_arg_without_path() {
let args = ["jonesy".to_string(), "--lib".to_string()];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_extract_lib_arg_followed_by_flag() {
let args = [
"jonesy".to_string(),
"--lib".to_string(),
"--quiet".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs).unwrap();
assert_eq!(result, None); }
#[test]
fn test_extract_lib_arg_no_flag() {
let args = ["jonesy".to_string()];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs);
assert!(result.is_err());
assert!(result.unwrap_err().contains("flag not found"));
}
#[test]
fn test_extract_lib_arg_extra_positional() {
let args = [
"jonesy".to_string(),
"--lib".to_string(),
"/path/to/lib.rlib".to_string(),
"extra-arg".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Unexpected extra argument"));
}
#[test]
fn test_get_lib_name_from_lib_section() {
let content = r#"
[package]
name = "my-package"
version = "0.1.0"
[lib]
name = "custom_lib_name"
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let result = get_lib_name(&manifest);
assert_eq!(result, Some("custom_lib_name".to_string()));
}
#[test]
fn test_get_lib_name_from_package() {
let content = r#"
[package]
name = "my-package"
version = "0.1.0"
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let result = get_lib_name(&manifest);
assert_eq!(result, Some("my_package".to_string())); }
#[test]
fn test_get_lib_name_no_package() {
let content = r#"
[workspace]
members = ["crate_a"]
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let result = get_lib_name(&manifest);
assert_eq!(result, None);
}
#[test]
fn test_find_lib_in_target_nonexistent() {
let result = find_lib_in_target("nonexistent", Path::new("/tmp"));
assert!(result.is_none());
}
#[test]
fn test_find_bin_in_manifest_no_bins() {
let content = r#"
[package]
name = "my-package"
version = "0.1.0"
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let result = find_bin_in_manifest("nonexistent", &manifest, Path::new("/tmp"));
assert!(result.is_none());
}
#[test]
fn test_parse_config_path_none() {
let args = vec!["jonesy".to_string()];
let result = parse_config_path(&args).unwrap();
assert!(result.is_none());
}
#[test]
fn test_parse_config_path_missing_value() {
let args = vec!["jonesy".to_string(), "--config".to_string()];
let result = parse_config_path(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("requires a path"));
}
#[test]
fn test_parse_config_path_file_not_found() {
let args = vec![
"jonesy".to_string(),
"--config".to_string(),
"/nonexistent/path/config.toml".to_string(),
];
let result = parse_config_path(&args);
assert!(result.is_err());
assert!(result.unwrap_err().contains("not found"));
}
#[test]
fn test_parse_config_path_valid_file() {
let args = vec![
"jonesy".to_string(),
"--config".to_string(),
"Cargo.toml".to_string(),
];
if PathBuf::from("Cargo.toml").exists() {
let result = parse_config_path(&args).unwrap();
assert!(result.is_some());
assert!(result.unwrap().ends_with("Cargo.toml"));
}
}
#[test]
fn test_output_format_text_with_all_options() {
let format = OutputFormat::text(true, true, true, true);
assert!(format.is_text());
assert!(format.show_tree());
assert!(format.is_summary_only());
assert!(!format.show_progress()); assert!(format.use_hyperlinks());
}
#[test]
fn test_output_format_json_no_hyperlinks() {
let format = OutputFormat::json(false, false);
assert!(!format.use_hyperlinks());
}
#[test]
fn test_output_format_html_no_hyperlinks() {
let format = OutputFormat::html(false, false);
assert!(!format.use_hyperlinks());
}
#[test]
fn test_output_format_json_no_progress() {
let format = OutputFormat::json(false, false);
assert!(!format.show_progress());
}
#[test]
fn test_output_format_html_no_progress() {
let format = OutputFormat::html(false, false);
assert!(!format.show_progress());
}
#[test]
fn test_parse_output_format_html_case_insensitive() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"HTML".to_string(),
];
let result = parse_output_format(&args, false, false, false, false).unwrap();
assert!(result.is_html());
}
#[test]
fn test_parse_output_format_text_case_insensitive() {
let args = vec![
"jonesy".to_string(),
"--format".to_string(),
"TEXT".to_string(),
];
let result = parse_output_format(&args, false, false, false, false).unwrap();
assert!(result.is_text());
}
#[test]
fn test_collect_binaries_no_bins() {
let content = r#"
[package]
name = "my-package"
version = "0.1.0"
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let target_dir = PathBuf::from("/tmp");
let binaries = collect_binaries_from_manifest(&manifest, "my-package", &target_dir);
assert!(binaries.is_empty());
}
#[test]
fn test_workspace_member_debug() {
let member = WorkspaceMember {
name: "test-crate".to_string(),
path: PathBuf::from("crates/test-crate"),
binaries: vec![PathBuf::from("target/debug/test-crate")],
};
let debug_str = format!("{:?}", member);
assert!(debug_str.contains("test-crate"));
assert!(debug_str.contains("crates/test-crate"));
}
#[test]
fn test_args_default_values() {
let args = Args {
binaries: vec![],
workspace_members: None,
show_timings: false,
max_threads: 1,
config_path: None,
output: OutputFormat::default(),
lsp_mode: false,
};
assert!(args.binaries.is_empty());
assert!(args.workspace_members.is_none());
assert!(!args.show_timings);
assert!(!args.lsp_mode);
assert!(args.output.is_text());
}
#[test]
fn test_version_not_empty() {
assert!(!VERSION.is_empty());
assert!(
VERSION.contains('.'),
"Version should contain dots: {}",
VERSION
);
}
#[test]
fn test_extract_bin_arg_at_end() {
let args = [
"jonesy".to_string(),
"--quiet".to_string(),
"--bin".to_string(),
"my-binary".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs).unwrap();
assert_eq!(result, "my-binary");
}
#[test]
fn test_extract_bin_arg_with_multiple_flags_after() {
let args = [
"jonesy".to_string(),
"--bin".to_string(),
"my-binary".to_string(),
"--quiet".to_string(),
"--tree".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_bin_arg(&refs).unwrap();
assert_eq!(result, "my-binary");
}
#[test]
fn test_extract_lib_arg_at_end_no_path() {
let args = [
"jonesy".to_string(),
"--quiet".to_string(),
"--lib".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs).unwrap();
assert_eq!(result, None);
}
#[test]
fn test_extract_lib_arg_with_path_and_trailing_flags() {
let args = [
"jonesy".to_string(),
"--lib".to_string(),
"/path/to/lib.rlib".to_string(),
"--quiet".to_string(),
];
let refs: Vec<&String> = args.iter().collect();
let result = extract_lib_arg(&refs).unwrap();
assert_eq!(result, Some("/path/to/lib.rlib"));
}
#[test]
fn test_find_lib_in_target_dylib() {
let temp_dir = std::env::temp_dir().join("jonesy_test_lib_dylib");
let _ = std::fs::create_dir_all(&temp_dir);
let dylib_path = temp_dir.join("libtest.dylib");
std::fs::write(&dylib_path, "fake dylib").unwrap();
let result = find_lib_in_target("test", &temp_dir);
assert!(result.is_some());
assert!(result.unwrap().ends_with("libtest.dylib"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_find_lib_in_target_rlib() {
let temp_dir = std::env::temp_dir().join("jonesy_test_lib_rlib");
let _ = std::fs::create_dir_all(&temp_dir);
let rlib_path = temp_dir.join("libtest.rlib");
std::fs::write(&rlib_path, "fake rlib").unwrap();
let result = find_lib_in_target("test", &temp_dir);
assert!(result.is_some());
assert!(result.unwrap().ends_with("libtest.rlib"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_find_lib_in_target_staticlib() {
let temp_dir = std::env::temp_dir().join("jonesy_test_lib_static");
let _ = std::fs::create_dir_all(&temp_dir);
let static_path = temp_dir.join("libtest.a");
std::fs::write(&static_path, "fake staticlib").unwrap();
let result = find_lib_in_target("test", &temp_dir);
assert!(result.is_some());
assert!(result.unwrap().ends_with("libtest.a"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_find_lib_in_target_prefers_dylib() {
let temp_dir = std::env::temp_dir().join("jonesy_test_lib_prefer");
let _ = std::fs::create_dir_all(&temp_dir);
std::fs::write(temp_dir.join("libtest.dylib"), "dylib").unwrap();
std::fs::write(temp_dir.join("libtest.rlib"), "rlib").unwrap();
std::fs::write(temp_dir.join("libtest.a"), "staticlib").unwrap();
let result = find_lib_in_target("test", &temp_dir);
assert!(result.is_some());
assert!(result.unwrap().ends_with("libtest.dylib"));
let _ = std::fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_get_lib_name_lib_section_no_name() {
let content = r#"
[package]
name = "my-package"
version = "0.1.0"
[lib]
path = "src/lib.rs"
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let result = get_lib_name(&manifest);
assert_eq!(result, Some("my_package".to_string()));
}
#[test]
fn test_get_lib_name_underscore_preserved() {
let content = r#"
[package]
name = "my_package"
version = "0.1.0"
"#;
let manifest = Manifest::from_slice(content.as_bytes()).unwrap();
let result = get_lib_name(&manifest);
assert_eq!(result, Some("my_package".to_string()));
}
}