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 jigs_core::JigMeta;
41 use std::time::Duration;
42
43 fn meta(name: &'static str) -> &'static JigMeta {
44 Box::leak(Box::new(JigMeta {
45 name,
46 file: "",
47 line: 0,
48 kind: "Response",
49 input: "Request",
50 input_type: "",
51 output_type: "",
52 is_async: false,
53 module: "",
54 chain: &[],
55 }))
56 }
57
58 fn entry(name: &'static str, depth: usize, ok: bool, err: Option<&str>) -> Entry {
59 Entry {
60 meta: meta(name),
61 depth,
62 duration: Duration::from_micros(100),
63 ok,
64 error: err.map(|s| s.to_string()),
65 }
66 }
67
68 #[test]
69 fn empty_input_renders_empty_string() {
70 assert_eq!(render_tree(&[]), "");
71 }
72
73 #[test]
74 fn nested_entries_indent_by_depth() {
75 let entries = [
76 entry("handle", 0, true, None),
77 entry("inner", 1, true, None),
78 ];
79 let out = render_tree(&entries);
80 assert!(out.contains("handle"));
81 let inner_line = out.lines().nth(1).unwrap();
82 assert!(inner_line.starts_with("└─ inner"));
83 }
84
85 #[test]
86 fn errors_are_rendered_with_message() {
87 let entries = [entry("step", 0, false, Some("boom"))];
88 let out = render_tree(&entries);
89 assert!(out.contains("err"));
90 assert!(out.contains("ERROR: boom"));
91 }
92}