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