devscout_rs/render/coverage.rs
1use crate::query;
2
3use super::blocks::{compact_block, ref_kind_block_if_any};
4use super::markers::{compact_marker, heuristic_suffix, test_via_suffix};
5use super::refs::bus_hop_line;
6
7/// Default `devscout tests` output.
8///
9/// The zero case is a first-class answer, not
10/// an empty table: "nothing tests this" is exactly what the caller asked, and it
11/// is one line rather than a header over a void. A symbol reached ONLY over a
12/// possible-route bus hop -- no precise or heuristic row at all -- still gets
13/// the header and its `bus-hop` block; only a symbol with neither prints the
14/// zero-case line.
15///
16/// Heuristic file lines carry the SAME word-suffix the refs/impact renderers
17/// use, and the summary counts stay precise-only, so a file listed under a
18/// covered count is a file a test really references.
19pub fn render_tests_text(model: &query::TestsModel) -> String {
20 let mut out: Vec<String> = vec![format!("tests for {}", model.symbol)];
21 if model.rows.is_empty() && model.bus.total == 0 {
22 out.push("no test references found".to_string());
23 return out.join("\n");
24 }
25 out.push(format!(
26 "covered by {} test file(s), {} reference(s)",
27 model.test_file_count, model.ref_count
28 ));
29 out.push(String::new());
30 for r in &model.rows {
31 out.push(format!(
32 "{}{}{}",
33 r.file,
34 heuristic_suffix(r.heuristic, r.tier),
35 test_via_suffix(r.via)
36 ));
37 let lines = r
38 .lines
39 .iter()
40 .map(|l| l.to_string())
41 .collect::<Vec<_>>()
42 .join(", ");
43 if r.test_defs.is_empty() {
44 // A `Project`-vouched row can carry no attributed def at all --
45 // still one line, so the file's referencing lines are never
46 // silently dropped.
47 out.push(format!(" lines: {lines}"));
48 } else {
49 for def_id in &r.test_defs {
50 out.push(format!(" {def_id} lines: {lines}"));
51 }
52 }
53 }
54 // A test file that PUBLISHES to this handler over a bus hop: a possible
55 // route, kept out of `test_file_count`/`ref_count` by construction (see
56 // `TestsModel::bus`'s own doc comment), rendered through the exact same
57 // row formatter `refs`' own `bus-hop` block uses so the two never state
58 // the disclosure in different words.
59 ref_kind_block_if_any(&mut out, "bus-hop", &model.bus, bus_hop_line);
60 out.join("\n")
61}
62
63/// `--compact` `devscout tests` output.
64///
65/// The same model with the def ids and the
66/// per-file indentation dropped: one header line carrying every count, then one
67/// line per file. The `x`/`h` line marker is the compact renderer's existing
68/// convention for a guess.
69pub fn render_tests_compact(model: &query::TestsModel) -> String {
70 let heur = if model.heuristic_file_count != 0 {
71 format!(" heuristic={}", model.heuristic_file_count)
72 } else {
73 String::new()
74 };
75 let mut out: Vec<String> = vec![format!(
76 "tests {} files={} refs={}{heur}",
77 model.symbol, model.test_file_count, model.ref_count
78 )];
79 for r in &model.rows {
80 let lines = r
81 .lines
82 .iter()
83 .map(|l| format!("{l}{}", compact_marker(r.heuristic, r.tier)))
84 .collect::<Vec<_>>()
85 .join(",");
86 out.push(format!("{} {lines}", r.file));
87 }
88 // Same marker-plus-path-to-full-row shape `refs`' own compact `bus-hop`
89 // block established, reused verbatim rather than respelled here.
90 let file_of_bus: fn(&query::BusHopRow) -> &str = |r| r.file.as_str();
91 let line_bus = |r: &query::BusHopRow| format!("{}?", r.line);
92 compact_block(
93 &mut out,
94 "bus-hop (? = possible route, rerun without --compact for the full row)",
95 Some(&model.bus),
96 file_of_bus,
97 line_bus,
98 );
99 out.join("\n")
100}