use crate::args::VERSION;
use crate::call_tree::{AnalysisResult, CrateCodePoint};
use crate::panic_cause::PanicCause;
use is_terminal::IsTerminal;
use std::io::{self, Write};
use std::path::Path;
use url::Url;
pub fn generate_text_output(
result: &AnalysisResult,
tree: bool,
summary_only: bool,
no_hyperlinks: bool,
) {
let is_tty = io::stdout().is_terminal();
let mut stdout = io::stdout().lock();
write_text_output(
&mut stdout,
result,
tree,
summary_only,
no_hyperlinks,
is_tty,
)
.expect("Failed to write to stdout");
}
pub fn write_text_output<W: Write>(
w: &mut W,
result: &AnalysisResult,
tree: bool,
summary_only: bool,
no_hyperlinks: bool,
is_tty: bool,
) -> io::Result<()> {
let summary = result.summary();
if !summary_only {
write_panic_points(
w,
&result.code_points,
&result.project_root,
tree,
no_hyperlinks,
is_tty,
)?;
}
writeln!(w, "Summary (jonesy v{}):", VERSION)?;
writeln!(w, " Project: {}", result.project_name)?;
writeln!(w, " Root: {}", result.project_root)?;
writeln!(
w,
" Panic points: {} in {} file(s)",
summary.panic_points(),
summary.files_affected()
)?;
Ok(())
}
fn write_panic_points<W: Write>(
w: &mut W,
code_points: &[CrateCodePoint],
project_root: &str,
include_children: bool,
no_hyperlinks: bool,
is_tty: bool,
) -> io::Result<()> {
if code_points.is_empty() {
writeln!(w, "\nNo panics in crate")?;
return Ok(());
}
let project_root_path = Path::new(project_root);
writeln!(w, "\nPanic code points in crate:")?;
let use_tree_format = !no_hyperlinks && is_tty;
if use_tree_format {
writeln!(w)?;
write_directory_tree(
w,
code_points,
Some(project_root_path),
None,
no_hyperlinks,
is_tty,
include_children,
)?;
} else {
write_flat_format(w, code_points, Some(project_root_path), include_children)?;
}
Ok(())
}
fn write_directory_tree<W: Write>(
w: &mut W,
points: &[CrateCodePoint],
project_root: Option<&Path>,
crate_root: Option<&Path>,
no_hyperlinks: bool,
is_tty: bool,
include_children: bool,
) -> io::Result<()> {
let mut dir_groups: Vec<(String, Vec<&CrateCodePoint>)> = Vec::new();
for point in points {
let display_path = get_display_path(&point.file, project_root, crate_root);
let dir = if let Some(pos) = display_path.rfind('/') {
display_path[..pos].to_string()
} else {
String::new()
};
if let Some((last_dir, files)) = dir_groups.last_mut() {
if last_dir == &dir {
files.push(point);
continue;
}
}
dir_groups.push((dir, vec![point]));
}
let group_count = dir_groups.len();
for (i, (dir, files)) in dir_groups.iter().enumerate() {
let is_last_dir = i == group_count - 1;
let dir_connector = if is_last_dir {
"└── "
} else {
"├── "
};
let child_prefix = if is_last_dir { " " } else { "│ " };
if !dir.is_empty() {
writeln!(w, "{}{}/", dir_connector, dir)?;
}
let file_count = files.len();
for (j, point) in files.iter().enumerate() {
let is_last_file = j == file_count - 1;
write_file_entry(
w,
point,
if dir.is_empty() { "" } else { child_prefix },
is_last_file,
dir.is_empty(),
project_root,
crate_root,
no_hyperlinks,
is_tty,
include_children,
)?;
}
}
Ok(())
}
fn write_flat_format<W: Write>(
w: &mut W,
points: &[CrateCodePoint],
project_root: Option<&Path>,
include_children: bool,
) -> io::Result<()> {
for point in points {
write_flat_point(w, point, project_root, include_children)?;
}
Ok(())
}
fn write_flat_point<W: Write>(
w: &mut W,
point: &CrateCodePoint,
project_root: Option<&Path>,
include_children: bool,
) -> io::Result<()> {
let display_path = get_relative_path(&point.file, project_root);
let column = point.column.unwrap_or(1);
let location = format!("{}:{}:{}", display_path, point.line, column);
let is_leaf = point.children.is_empty() || !include_children;
let sorted_causes = get_sorted_causes(&point.causes);
let cause_str = if is_leaf && !sorted_causes.is_empty() {
format!(" {}", format_causes(&sorted_causes))
} else if !is_leaf && !sorted_causes.is_empty() {
format!(" {}", format_causes(&sorted_causes))
} else {
String::new()
};
writeln!(w, " --> {}{}", location, cause_str)?;
if is_leaf {
if let Some(cause) = sorted_causes.first() {
let suggestion =
cause.format_suggestion(point.is_direct_panic, point.called_function.as_deref());
if !suggestion.is_empty() {
writeln!(w, " = help: {}", suggestion)?;
}
if let Some(warning) = cause.release_warning() {
writeln!(w, " = warning: {}", warning)?;
}
}
}
if include_children && !point.children.is_empty() {
let mut children = point.children.clone();
children.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
for child in &children {
write_flat_child(w, child, project_root, " ", include_children)?;
}
}
Ok(())
}
fn write_flat_child<W: Write>(
w: &mut W,
point: &CrateCodePoint,
project_root: Option<&Path>,
indent: &str,
include_children: bool,
) -> io::Result<()> {
let display_path = get_relative_path(&point.file, project_root);
let column = point.column.unwrap_or(1);
let location = format!("{}:{}:{}", display_path, point.line, column);
let is_leaf = point.children.is_empty() || !include_children;
let sorted_causes = get_sorted_causes(&point.causes);
let cause_str = if !sorted_causes.is_empty() {
format!(" {}", format_causes(&sorted_causes))
} else {
String::new()
};
writeln!(w, "{}└── --> {}{}", indent, location, cause_str)?;
if is_leaf {
if let Some(cause) = sorted_causes.first() {
let suggestion =
cause.format_suggestion(point.is_direct_panic, point.called_function.as_deref());
if !suggestion.is_empty() {
writeln!(w, "{} = help: {}", indent, suggestion)?;
}
if let Some(warning) = cause.release_warning() {
writeln!(w, "{} = warning: {}", indent, warning)?;
}
}
}
if include_children && !point.children.is_empty() {
let mut children = point.children.clone();
children.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
let child_indent = format!("{} ", indent);
for child in &children {
write_flat_child(w, child, project_root, &child_indent, include_children)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn write_file_entry<W: Write>(
w: &mut W,
point: &CrateCodePoint,
prefix: &str,
is_last: bool,
is_root_level: bool,
project_root: Option<&Path>,
crate_root: Option<&Path>,
no_hyperlinks: bool,
is_tty: bool,
include_children: bool,
) -> io::Result<()> {
let display_path = get_display_path(&point.file, project_root, crate_root);
let filename = display_path
.rfind('/')
.map(|pos| &display_path[pos + 1..])
.unwrap_or(&display_path);
let absolute_path = make_absolute(&point.file, project_root);
let use_hyperlinks = !no_hyperlinks && is_tty;
let column = point.column.unwrap_or(1);
let location = if use_hyperlinks {
if let Ok(mut file_url) = Url::from_file_path(&absolute_path) {
file_url.set_fragment(Some(&format!("L{}", point.line)));
let display = format!("{}:{}:{}", filename, point.line, column);
format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", file_url, display)
} else {
format!("{}:{}:{}", absolute_path, point.line, column)
}
} else {
format!("{}:{}:{}", absolute_path, point.line, column)
};
let sorted_causes = get_sorted_causes(&point.causes);
let is_leaf = point.children.is_empty() || !include_children;
let cause_str = if !sorted_causes.is_empty() {
format!(" {}", format_causes(&sorted_causes))
} else {
String::new()
};
let connector = if is_root_level {
" -->"
} else if is_last {
"└──>"
} else {
"├──>"
};
writeln!(w, "{}{} {}{}", prefix, connector, location, cause_str)?;
if is_leaf {
if let Some(cause) = sorted_causes.first() {
let suggestion =
cause.format_suggestion(point.is_direct_panic, point.called_function.as_deref());
if !suggestion.is_empty() {
let help_prefix = if is_root_level { " " } else { prefix };
writeln!(w, "{} = help: {}", help_prefix, suggestion)?;
}
if let Some(warning) = cause.release_warning() {
let warn_prefix = if is_root_level { " " } else { prefix };
writeln!(w, "{} = warning: {}", warn_prefix, warning)?;
}
}
}
if include_children && !point.children.is_empty() {
let mut children = point.children.clone();
children.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
let child_prefix = if is_root_level {
" ".to_string()
} else if is_last {
format!("{} ", prefix)
} else {
format!("{}│ ", prefix)
};
let child_count = children.len();
for (k, child) in children.iter().enumerate() {
let is_last_child = k == child_count - 1;
write_crate_point(
w,
child,
&child_prefix,
is_last_child,
false,
project_root,
crate_root,
no_hyperlinks,
is_tty,
include_children,
)?;
}
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
fn write_crate_point<W: Write>(
w: &mut W,
point: &CrateCodePoint,
prefix: &str,
is_last: bool,
is_root: bool,
project_root: Option<&Path>,
crate_root: Option<&Path>,
no_hyperlinks: bool,
is_tty: bool,
include_children: bool,
) -> io::Result<()> {
let absolute_path = make_absolute(&point.file, project_root);
let display_root = crate_root.or(project_root);
let display_path = if let Some(root) = display_root {
Path::new(&absolute_path)
.strip_prefix(root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| absolute_path.clone())
} else {
absolute_path.clone()
};
let use_hyperlinks = !no_hyperlinks && is_tty;
let column = point.column.unwrap_or(1);
let location = if use_hyperlinks {
if let Ok(mut file_url) = Url::from_file_path(&absolute_path) {
file_url.set_fragment(Some(&format!("L{}", point.line)));
let display = format!("{}:{}:{}", display_path, point.line, column);
format!("\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\", file_url, display)
} else {
format!("{}:{}:{}", absolute_path, point.line, column)
}
} else {
format!("{}:{}:{}", absolute_path, point.line, column)
};
let sorted_causes = get_sorted_causes(&point.causes);
let is_leaf = point.children.is_empty() || !include_children;
let cause_str = if !sorted_causes.is_empty() {
format!(" {}", format_causes(&sorted_causes))
} else {
String::new()
};
let connector = if is_root {
" -->"
} else if is_last {
"└──>"
} else {
"├──>"
};
writeln!(w, "{}{} {}{}", prefix, connector, location, cause_str)?;
if is_leaf {
if let Some(cause) = sorted_causes.first() {
let suggestion =
cause.format_suggestion(point.is_direct_panic, point.called_function.as_deref());
if !suggestion.is_empty() {
let help_prefix = if is_root { " " } else { prefix };
writeln!(w, "{} = help: {}", help_prefix, suggestion)?;
}
if let Some(warning) = cause.release_warning() {
let warn_prefix = if is_root { " " } else { prefix };
writeln!(w, "{} = warning: {}", warn_prefix, warning)?;
}
}
}
if include_children && !point.children.is_empty() {
let mut children = point.children.clone();
children.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
let child_prefix = if is_root {
" ".to_string()
} else if is_last {
format!("{} ", prefix)
} else {
format!("{}│ ", prefix)
};
let child_count = children.len();
for (k, child) in children.iter().enumerate() {
let is_last_child = k == child_count - 1;
write_crate_point(
w,
child,
&child_prefix,
is_last_child,
false,
project_root,
crate_root,
no_hyperlinks,
is_tty,
include_children,
)?;
}
}
Ok(())
}
fn get_sorted_causes(causes: &std::collections::HashSet<PanicCause>) -> Vec<&PanicCause> {
let mut sorted: Vec<_> = causes.iter().collect();
sorted.sort_by_key(|c| c.error_code());
sorted
}
fn format_causes(causes: &[&PanicCause]) -> String {
causes
.iter()
.map(|c| format!("[{}/{}: {}]", c.error_code(), c.id(), c.description()))
.collect::<Vec<_>>()
.join(" ")
}
fn make_absolute(file: &str, project_root: Option<&Path>) -> String {
if file.starts_with('/') {
file.to_string()
} else if let Some(root) = project_root {
root.join(file).to_string_lossy().to_string()
} else {
file.to_string()
}
}
fn get_relative_path(file: &str, project_root: Option<&Path>) -> String {
let absolute_path = make_absolute(file, project_root);
if let Some(root) = project_root {
Path::new(&absolute_path)
.strip_prefix(root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or(absolute_path)
} else {
absolute_path
}
}
fn get_display_path(file: &str, project_root: Option<&Path>, crate_root: Option<&Path>) -> String {
let absolute_path = make_absolute(file, project_root);
let display_root = crate_root.or(project_root);
if let Some(root) = display_root {
Path::new(&absolute_path)
.strip_prefix(root)
.map(|p| p.to_string_lossy().to_string())
.unwrap_or(absolute_path)
} else {
absolute_path
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::call_tree::AnalysisResult;
use crate::panic_cause::PanicCause;
use std::collections::HashSet;
fn make_test_point(
name: &str,
file: &str,
line: u32,
causes: Vec<PanicCause>,
) -> CrateCodePoint {
CrateCodePoint {
name: name.to_string(),
file: file.to_string(),
line,
column: Some(5),
causes: causes.into_iter().collect(),
children: vec![],
is_direct_panic: true,
called_function: None,
}
}
fn make_test_result(code_points: Vec<CrateCodePoint>) -> AnalysisResult {
AnalysisResult {
project_name: "test_project".to_string(),
project_root: "/test/project".to_string(),
code_points,
}
}
#[test]
fn test_make_absolute_already_absolute() {
let path = make_absolute("/home/user/src/main.rs", None);
assert_eq!(path, "/home/user/src/main.rs");
}
#[test]
fn test_make_absolute_with_project_root() {
let root = Path::new("/home/user/project");
let path = make_absolute("src/main.rs", Some(root));
assert_eq!(path, "/home/user/project/src/main.rs");
}
#[test]
fn test_make_absolute_no_root() {
let path = make_absolute("src/main.rs", None);
assert_eq!(path, "src/main.rs");
}
#[test]
fn test_get_relative_path_absolute() {
let root = Path::new("/home/user/project");
let path = get_relative_path("/home/user/project/src/main.rs", Some(root));
assert_eq!(path, "src/main.rs");
}
#[test]
fn test_get_relative_path_already_relative() {
let root = Path::new("/home/user/project");
let path = get_relative_path("src/main.rs", Some(root));
assert_eq!(path, "src/main.rs");
}
#[test]
fn test_get_relative_path_no_root() {
let path = get_relative_path("src/main.rs", None);
assert_eq!(path, "src/main.rs");
}
#[test]
fn test_get_display_path_with_crate_root() {
let project_root = Path::new("/workspace");
let crate_root = Path::new("/workspace/crate_a");
let path = get_display_path(
"/workspace/crate_a/src/lib.rs",
Some(project_root),
Some(crate_root),
);
assert_eq!(path, "src/lib.rs");
}
#[test]
fn test_get_display_path_project_root_only() {
let project_root = Path::new("/workspace");
let path = get_display_path("/workspace/src/main.rs", Some(project_root), None);
assert_eq!(path, "src/main.rs");
}
#[test]
fn test_get_sorted_causes_empty() {
let causes: HashSet<PanicCause> = HashSet::new();
let sorted = get_sorted_causes(&causes);
assert!(sorted.is_empty());
}
#[test]
fn test_get_sorted_causes_single() {
let mut causes = HashSet::new();
causes.insert(PanicCause::Unwrap);
let sorted = get_sorted_causes(&causes);
assert_eq!(sorted.len(), 1);
assert_eq!(sorted[0].error_code(), "JP006");
}
#[test]
fn test_get_sorted_causes_multiple() {
let mut causes = HashSet::new();
causes.insert(PanicCause::Unwrap); causes.insert(PanicCause::BoundsCheck); causes.insert(PanicCause::ExplicitPanic); let sorted = get_sorted_causes(&causes);
assert_eq!(sorted.len(), 3);
assert_eq!(sorted[0].error_code(), "JP001");
assert_eq!(sorted[1].error_code(), "JP002");
assert_eq!(sorted[2].error_code(), "JP006");
}
#[test]
fn test_format_causes_empty() {
let causes: Vec<&PanicCause> = vec![];
let formatted = format_causes(&causes);
assert_eq!(formatted, "");
}
#[test]
fn test_format_causes_single() {
let cause = PanicCause::Unwrap;
let causes = vec![&cause];
let formatted = format_causes(&causes);
assert_eq!(formatted, "[JP006/unwrap: unwrap_failed]");
}
#[test]
fn test_format_causes_multiple() {
let cause1 = PanicCause::ExplicitPanic;
let cause2 = PanicCause::BoundsCheck;
let causes = vec![&cause1, &cause2];
let formatted = format_causes(&causes);
assert_eq!(
formatted,
"[JP001/panic: explicit_panic] [JP002/bounds: index_out_of_bounds]"
);
}
#[test]
fn test_write_text_output_empty() {
let result = make_test_result(vec![]);
let mut output = Vec::new();
write_text_output(&mut output, &result, false, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("No panics in crate"));
assert!(output_str.contains("Summary"));
assert!(output_str.contains("test_project"));
assert!(output_str.contains("Panic points: 0"));
}
#[test]
fn test_write_text_output_summary_only() {
let result = make_test_result(vec![make_test_point(
"test",
"src/main.rs",
10,
vec![PanicCause::Unwrap],
)]);
let mut output = Vec::new();
write_text_output(&mut output, &result, false, true, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("Summary"));
assert!(output_str.contains("Panic points: 1"));
assert!(!output_str.contains("Panic code points in crate"));
}
#[test]
fn test_write_text_output_flat_format() {
let result = make_test_result(vec![make_test_point(
"test_func",
"src/main.rs",
42,
vec![PanicCause::Unwrap],
)]);
let mut output = Vec::new();
write_text_output(&mut output, &result, false, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("Panic code points in crate"));
assert!(output_str.contains("src/main.rs:42:5"));
assert!(output_str.contains("[JP006/unwrap: unwrap_failed]"));
assert!(output_str.contains("= help:"));
}
#[test]
fn test_write_text_output_with_children() {
let child = CrateCodePoint {
name: "child_func".to_string(),
file: "src/lib.rs".to_string(),
line: 20,
column: Some(1),
causes: vec![PanicCause::BoundsCheck].into_iter().collect(),
children: vec![],
is_direct_panic: true,
called_function: None,
};
let parent = CrateCodePoint {
name: "parent_func".to_string(),
file: "src/main.rs".to_string(),
line: 10,
column: Some(1),
causes: HashSet::new(),
children: vec![child],
is_direct_panic: false,
called_function: Some("child_func".to_string()),
};
let result = make_test_result(vec![parent]);
let mut output = Vec::new();
write_text_output(&mut output, &result, true, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("src/main.rs:10:1"));
assert!(output_str.contains("src/lib.rs:20:1"));
assert!(output_str.contains("[JP002/bounds: index_out_of_bounds]"));
}
#[test]
fn test_write_text_output_with_warning() {
let result = make_test_result(vec![make_test_point(
"overflow_func",
"src/math.rs",
5,
vec![PanicCause::ArithmeticOverflow("add")],
)]);
let mut output = Vec::new();
write_text_output(&mut output, &result, false, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("[JP003/overflow: arithmetic_overflow]"));
assert!(output_str.contains("= warning:"));
assert!(output_str.contains("overflow-checks"));
}
#[test]
fn test_write_flat_point_multiple_causes() {
let mut causes = HashSet::new();
causes.insert(PanicCause::Unwrap);
causes.insert(PanicCause::BoundsCheck);
let point = CrateCodePoint {
name: "test".to_string(),
file: "src/main.rs".to_string(),
line: 10,
column: Some(1),
causes,
children: vec![],
is_direct_panic: true,
called_function: None,
};
let mut output = Vec::new();
write_flat_point(&mut output, &point, Some(Path::new("/test")), true).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("[JP002/bounds: index_out_of_bounds]"));
assert!(output_str.contains("[JP006/unwrap: unwrap_failed]"));
}
#[test]
fn test_write_directory_tree_format() {
let points = vec![
make_test_point("func1", "/test/src/main.rs", 10, vec![PanicCause::Todo]),
make_test_point(
"func2",
"/test/src/lib.rs",
20,
vec![PanicCause::Unreachable],
),
];
let mut output = Vec::new();
write_directory_tree(
&mut output,
&points,
Some(Path::new("/test")),
None,
true,
false,
true, )
.unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("main.rs"));
assert!(output_str.contains("lib.rs"));
assert!(output_str.contains("[JP014/todo: todo_reached]"));
assert!(output_str.contains("[JP012/unreachable: unreachable_reached]"));
}
#[test]
fn test_write_text_output_indirect_panic() {
let point = CrateCodePoint {
name: "caller".to_string(),
file: "src/main.rs".to_string(),
line: 10,
column: Some(1),
causes: vec![PanicCause::Unwrap].into_iter().collect(),
children: vec![],
is_direct_panic: false,
called_function: Some("parse_config".to_string()),
};
let result = make_test_result(vec![point]);
let mut output = Vec::new();
write_text_output(&mut output, &result, false, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("parse_config"));
}
#[test]
fn test_write_text_output_tree_with_children() {
let child = CrateCodePoint {
name: "child_func".to_string(),
file: "src/child.rs".to_string(),
line: 5,
column: Some(1),
causes: vec![PanicCause::ExplicitPanic].into_iter().collect(),
children: vec![],
is_direct_panic: true,
called_function: None,
};
let parent = CrateCodePoint {
name: "parent_func".to_string(),
file: "src/main.rs".to_string(),
line: 10,
column: Some(1),
causes: std::collections::HashSet::new(),
children: vec![child],
is_direct_panic: false,
called_function: None,
};
let result = make_test_result(vec![parent]);
let mut output = Vec::new();
write_text_output(&mut output, &result, true, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(
output_str.contains("child.rs"),
"Expected child.rs in output:\n{}",
output_str
);
assert!(output_str.contains("[JP001/panic: explicit_panic]"));
}
#[test]
fn test_write_text_output_no_tree_hides_children() {
let child = CrateCodePoint {
name: "child_func".to_string(),
file: "src/child.rs".to_string(),
line: 5,
column: Some(1),
causes: vec![PanicCause::ExplicitPanic].into_iter().collect(),
children: vec![],
is_direct_panic: true,
called_function: None,
};
let parent = CrateCodePoint {
name: "parent_func".to_string(),
file: "src/main.rs".to_string(),
line: 10,
column: Some(1),
causes: std::collections::HashSet::new(),
children: vec![child],
is_direct_panic: false,
called_function: None,
};
let result = make_test_result(vec![parent]);
let mut output = Vec::new();
write_text_output(&mut output, &result, false, false, true, false).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(!output_str.contains("child.rs"));
assert!(output_str.contains("main.rs"));
}
#[test]
fn test_write_flat_child_recursive() {
let grandchild = CrateCodePoint {
name: "grandchild".to_string(),
file: "src/deep.rs".to_string(),
line: 1,
column: Some(1),
causes: vec![PanicCause::Todo].into_iter().collect(),
children: vec![],
is_direct_panic: true,
called_function: None,
};
let child = CrateCodePoint {
name: "child".to_string(),
file: "src/middle.rs".to_string(),
line: 5,
column: Some(1),
causes: std::collections::HashSet::new(),
children: vec![grandchild],
is_direct_panic: false,
called_function: None,
};
let parent = CrateCodePoint {
name: "parent".to_string(),
file: "src/top.rs".to_string(),
line: 10,
column: Some(1),
causes: std::collections::HashSet::new(),
children: vec![child],
is_direct_panic: false,
called_function: None,
};
let mut output = Vec::new();
write_flat_point(&mut output, &parent, Some(Path::new("/test")), true).unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("top.rs"));
assert!(output_str.contains("middle.rs"));
assert!(output_str.contains("deep.rs"));
assert!(output_str.contains("[JP014/todo: todo_reached]"));
}
#[test]
fn test_write_directory_tree_multiple_dirs() {
let points = vec![
make_test_point("func1", "/test/src/a/file1.rs", 10, vec![PanicCause::Todo]),
make_test_point(
"func2",
"/test/src/b/file2.rs",
20,
vec![PanicCause::Unreachable],
),
make_test_point(
"func3",
"/test/src/a/file3.rs",
30,
vec![PanicCause::Unimplemented],
),
];
let mut output = Vec::new();
write_directory_tree(
&mut output,
&points,
Some(Path::new("/test")),
None,
true,
false,
true,
)
.unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("file1.rs"));
assert!(output_str.contains("file2.rs"));
assert!(output_str.contains("file3.rs"));
assert!(output_str.contains("src/a/"));
assert!(output_str.contains("src/b/"));
}
#[test]
fn test_write_directory_tree_root_level_files() {
let points = vec![
make_test_point("func1", "/test/main.rs", 10, vec![PanicCause::Todo]),
make_test_point("func2", "/test/lib.rs", 20, vec![PanicCause::Unreachable]),
];
let mut output = Vec::new();
write_directory_tree(
&mut output,
&points,
Some(Path::new("/test")),
None,
true,
false,
true,
)
.unwrap();
let output_str = String::from_utf8(output).unwrap();
assert!(output_str.contains("main.rs"));
assert!(output_str.contains("lib.rs"));
}
#[test]
fn test_format_causes_with_three_causes() {
let causes = vec![
&PanicCause::Unwrap,
&PanicCause::BoundsCheck,
&PanicCause::ExplicitPanic,
];
let result = format_causes(&causes);
assert!(result.contains("[JP001/panic:"));
assert!(result.contains("[JP002/bounds:"));
assert!(result.contains("[JP006/unwrap:"));
}
#[test]
fn test_get_display_path_no_roots() {
let result = get_display_path("src/main.rs", None, None);
assert_eq!(result, "src/main.rs");
}
#[test]
fn test_make_absolute_relative_path() {
let result = make_absolute("src/lib.rs", Some(Path::new("/project")));
assert_eq!(result, "/project/src/lib.rs");
}
}