返回 CodeWhale
golden_harness.rs
根目录 / crates / tui / src / tui / golden_harness.rs
1 //! Shared golden-buffer harness for the Tideline components (spec §5c).
2 //!
3 //! Every Tideline component proves itself against cell-exact golden buffers
4 //! at the four canonical blocker sizes. This module owns the one dump format
5 //! and the one bless protocol so they cannot drift between components:
6 //!
7 //! - rows are the `Buffer` cell symbols in paint order, `width` per row;
8 //! - rows are joined by `\n` with one trailing newline;
9 //! - goldens live in `crates/tui/src/tui/goldens/{name}_{w}x{h}.txt`;
10 //! - a missing golden fails the test unless `CODEWHALE_BLESS_GOLDENS=1` is
11 //! set, in which case the rendered text is written as the new contract.
12 //!
13 //! Goldens are the design contract — a visual change that cannot show as a
14 //! golden diff did not happen.
15
16 /// The four terminal sizes the v0.8.66 modal blocker (#3732) requires every
17 /// surface to remain readable and fully operable at. Mirrors
18 /// `views/status_picker.rs::BLOCKER_SIZES` (kept private there, so the
19 /// canonical copy is restated here for the Tideline golden suites).
20 pub(crate) const BLOCKER_SIZES: [(u16, u16); 4] = [(80, 24), (100, 30), (120, 32), (160, 40)];
21
22 /// Render one component into a fresh buffer and dump the cell symbols as
23 /// golden text. Deterministic by contract: the caller injects every fact
24 /// (clock strings, counters, hover state), never `Instant::now`.
25 pub(crate) fn render_golden_text(
26 width: u16,
27 height: u16,
28 draw: impl FnOnce(&mut ratatui::buffer::Buffer),
29 ) -> String {
30 let mut buf = ratatui::buffer::Buffer::empty(ratatui::layout::Rect::new(
31 0,
32 0,
33 width.max(1),
34 height.max(1),
35 ));
36 draw(&mut buf);
37 let w = width.max(1) as usize;
38 let content = buf.content();
39 let rows: Vec<String> = (0..height.max(1) as usize)
40 .map(|y| {
41 content[y * w..(y + 1) * w]
42 .iter()
43 .map(|cell| cell.symbol().to_string())
44 .collect()
45 })
46 .collect();
47 format!("{}\n", rows.join("\n"))
48 }
49
50 /// Assert `rendered` equals the golden `name`, blessing it when missing and
51 /// `CODEWHALE_BLESS_GOLDENS=1` is set (topbar protocol, spec §5c).
52 pub(crate) fn assert_matches_golden(name: &str, rendered: &str) {
53 let path = golden_path(name);
54 match std::fs::read_to_string(&path) {
55 // Compare against LF: a Windows checkout can hand us CRLF, and the
56 // dump side always joins rows with LF. Cell symbols never contain CR,
57 // so this can only ever cancel a line-ending difference.
58 Ok(expected) => assert_eq!(
59 rendered,
60 expected.replace("\r\n", "\n"),
61 "golden drift at {name}; re-bless only with an approved design change"
62 ),
63 Err(_) => {
64 if std::env::var("CODEWHALE_BLESS_GOLDENS").is_ok() {
65 if let Some(parent) = path.parent() {
66 std::fs::create_dir_all(parent).expect("create goldens dir");
67 }
68 std::fs::write(&path, rendered).expect("write golden");
69 } else {
70 panic!("missing golden {name}; run with CODEWHALE_BLESS_GOLDENS=1 to write it");
71 }
72 }
73 }
74 }
75
76 pub(crate) fn golden_path(name: &str) -> std::path::PathBuf {
77 std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
78 .join("src/tui/goldens")
79 .join(format!("{name}.txt"))
80 }
81
82 // ---------------------------------------------------------------------------
83 // Ink plane — the colour half of the design contract.
84 //
85 // `render_golden_text` dumps cell *symbols*. That is only half a render: a
86 // screen can go from a gold mark over a blue gradient to uniform grey without
87 // moving a single glyph, and the symbol golden would not notice. The founder's
88 // standing complaint about the startup screen ("everything in the same dim
89 // gray") was, mechanically, invisible to this suite.
90 //
91 // The ink plane closes that hole. Each painted cell becomes one character
92 // keyed to its (fg, bg, modifier) triple, with a legend resolving those keys
93 // to concrete values, so a contrast change shows up as a golden diff.
94 // ---------------------------------------------------------------------------
95
95 lines RUST