1use jigs_trace::Entry;
2
3pub fn render_tree(entries: &[Entry]) -> String {
5 let labels: Vec<String> = entries
6 .iter()
7 .map(|e| {
8 let indent = if e.depth == 0 {
9 String::new()
10 } else {
11 format!("{}└─ ", " ".repeat(e.depth - 1))
12 };
13 format!("{}{}", indent, e.name)
14 })
15 .collect();
16 let width = labels.iter().map(|l| l.chars().count()).max().unwrap_or(0);
17
18 let mut out = String::new();
19 for (label, e) in labels.iter().zip(entries) {
20 let pad = width - label.chars().count();
21 let mark = if e.ok { "ok" } else { "err" };
22 let detail = match &e.error {
23 Some(msg) => format!("ERROR: {msg}"),
24 None => format!("{:?}", e.duration),
25 };
26 out.push_str(&format!(
27 "{}{} {} {}\n",
28 label,
29 " ".repeat(pad),
30 mark,
31 detail
32 ));
33 }
34 out
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40 use std::time::Duration;
41
42 fn entry(name: &'static str, depth: usize, ok: bool, err: Option<&str>) -> Entry {
43 Entry {
44 name,
45 depth,
46 duration: Duration::from_micros(100),
47 ok,
48 error: err.map(|s| s.to_string()),
49 }
50 }
51
52 #[test]
53 fn empty_input_renders_empty_string() {
54 assert_eq!(render_tree(&[]), "");
55 }
56
57 #[test]
58 fn nested_entries_indent_by_depth() {
59 let entries = [
60 entry("handle", 0, true, None),
61 entry("inner", 1, true, None),
62 ];
63 let out = render_tree(&entries);
64 assert!(out.contains("handle"));
65 let inner_line = out.lines().nth(1).unwrap();
66 assert!(inner_line.starts_with("└─ inner"));
67 }
68
69 #[test]
70 fn errors_are_rendered_with_message() {
71 let entries = [entry("step", 0, false, Some("boom"))];
72 let out = render_tree(&entries);
73 assert!(out.contains("err"));
74 assert!(out.contains("ERROR: boom"));
75 }
76}