1pub mod analytics_charts;
7pub mod app;
8pub mod components;
9pub mod data;
10pub mod ftui_adapter;
11pub mod shortcuts;
12pub mod style_system;
13pub mod theme;
14pub mod time_parser;
15pub mod trace;
16pub mod tui;
17
18#[cfg(test)]
19mod legacy_shell_tests {
20 use super::app::CassApp;
21 use super::ftui_adapter::Rect;
22 use super::theme::CassTheme;
23
24 #[test]
25 fn canonical_ui_runtime_types_live_outside_legacy_tui_shell() {
26 let _ = std::mem::size_of::<CassApp>();
27 let _ = std::mem::size_of::<CassTheme>();
28 let _ = std::mem::size_of::<Rect>();
29 }
30}
31
32#[cfg(test)]
39pub mod test_log {
40 use std::cell::RefCell;
41 use std::time::Instant;
42
43 pub const SCHEMA_VERSION: u32 = 1;
45
46 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
48 pub enum Category {
49 Style,
50 Render,
51 Interaction,
52 Degradation,
53 Theme,
54 Layout,
55 }
56
57 impl Category {
58 pub fn as_str(self) -> &'static str {
59 match self {
60 Self::Style => "style",
61 Self::Render => "render",
62 Self::Interaction => "interaction",
63 Self::Degradation => "degradation",
64 Self::Theme => "theme",
65 Self::Layout => "layout",
66 }
67 }
68 }
69
70 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
72 pub enum Event {
73 AssertPass,
74 AssertFail,
75 StepStart,
76 StepEnd,
77 StateSnapshot,
78 }
79
80 impl Event {
81 pub fn as_str(self) -> &'static str {
82 match self {
83 Self::AssertPass => "assert_pass",
84 Self::AssertFail => "assert_fail",
85 Self::StepStart => "step_start",
86 Self::StepEnd => "step_end",
87 Self::StateSnapshot => "state_snapshot",
88 }
89 }
90 }
91
92 #[derive(Debug, Clone)]
94 pub struct LogEntry {
95 pub test_id: String,
96 pub elapsed_us: u64,
97 pub category: Category,
98 pub event: Event,
99 pub detail: String,
100 }
101
102 impl LogEntry {
103 pub fn to_json(&self) -> String {
105 format!(
106 r#"{{"schema_v":{},"test_id":"{}","elapsed_us":{},"category":"{}","event":"{}","detail":{}}}"#,
107 SCHEMA_VERSION,
108 self.test_id.replace('"', r#"\""#),
109 self.elapsed_us,
110 self.category.as_str(),
111 self.event.as_str(),
112 self.detail,
113 )
114 }
115 }
116
117 pub struct TestLogger {
122 test_id: String,
123 start: Instant,
124 entries: RefCell<Vec<LogEntry>>,
125 }
126
127 impl TestLogger {
128 pub fn new(test_id: impl Into<String>) -> Self {
130 Self {
131 test_id: test_id.into(),
132 start: Instant::now(),
133 entries: RefCell::new(Vec::new()),
134 }
135 }
136
137 pub fn log(&self, category: Category, event: Event, detail: impl Into<String>) {
139 let elapsed_us = self.start.elapsed().as_micros() as u64;
140 self.entries.borrow_mut().push(LogEntry {
141 test_id: self.test_id.clone(),
142 elapsed_us,
143 category,
144 event,
145 detail: detail.into(),
146 });
147 }
148
149 pub fn pass(&self, category: Category, detail: impl Into<String>) {
151 self.log(category, Event::AssertPass, detail);
152 }
153
154 pub fn fail(&self, category: Category, detail: impl Into<String>) {
156 self.log(category, Event::AssertFail, detail);
157 }
158
159 pub fn step_start(&self, category: Category, detail: impl Into<String>) {
161 self.log(category, Event::StepStart, detail);
162 }
163
164 pub fn step_end(&self, category: Category, detail: impl Into<String>) {
166 self.log(category, Event::StepEnd, detail);
167 }
168
169 pub fn snapshot(&self, category: Category, detail: impl Into<String>) {
171 self.log(category, Event::StateSnapshot, detail);
172 }
173
174 pub fn to_jsonl(&self) -> String {
176 self.entries
177 .borrow()
178 .iter()
179 .map(|e| e.to_json())
180 .collect::<Vec<_>>()
181 .join("\n")
182 }
183
184 pub fn summary(&self) -> (usize, usize, usize) {
186 let entries = self.entries.borrow();
187 let pass = entries
188 .iter()
189 .filter(|e| e.event == Event::AssertPass)
190 .count();
191 let fail = entries
192 .iter()
193 .filter(|e| e.event == Event::AssertFail)
194 .count();
195 (pass, fail, entries.len())
196 }
197
198 pub fn dump_on_failure(&self) {
200 let (pass, fail, total) = self.summary();
201 if fail > 0 {
202 eprintln!(
203 "--- TestLogger dump for '{}' ({} pass, {} fail, {} total) ---",
204 self.test_id, pass, fail, total
205 );
206 eprintln!("{}", self.to_jsonl());
207 eprintln!("--- end dump ---");
208 }
209 }
210 }
211
212 impl Drop for TestLogger {
213 fn drop(&mut self) {
214 if std::thread::panicking() {
216 let (pass, fail, total) = self.summary();
217 eprintln!(
218 "\n--- TestLogger auto-dump for '{}' ({} pass, {} fail, {} total) ---",
219 self.test_id, pass, fail, total
220 );
221 eprintln!("{}", self.to_jsonl());
222 eprintln!("--- end auto-dump ---\n");
223 }
224 }
225 }
226
227 #[macro_export]
229 macro_rules! assert_style_eq {
230 ($logger:expr, $left:expr, $right:expr, $category:expr, $msg:expr) => {{
231 let left_val = &$left;
232 let right_val = &$right;
233 if left_val == right_val {
234 $logger.pass($category, format!(r#""{}""#, $msg));
235 } else {
236 $logger.fail(
237 $category,
238 format!(
239 r#"{{"msg":"{}","left":"{:?}","right":"{:?}"}}"#,
240 $msg, left_val, right_val
241 ),
242 );
243 panic!(
244 "assert_style_eq failed: {}\n left: {:?}\n right: {:?}",
245 $msg, left_val, right_val
246 );
247 }
248 }};
249 }
250
251 #[macro_export]
253 macro_rules! assert_logged {
254 ($logger:expr, $cond:expr, $category:expr, $msg:expr) => {{
255 if $cond {
256 $logger.pass($category, format!(r#""{}""#, $msg));
257 } else {
258 $logger.fail(
259 $category,
260 format!(r#"{{"msg":"{}","condition":"false"}}"#, $msg),
261 );
262 panic!("assert_logged failed: {}", $msg);
263 }
264 }};
265 }
266
267 #[cfg(test)]
268 mod tests {
269 use super::*;
270
271 #[test]
272 fn test_logger_basic_lifecycle() {
273 let log = TestLogger::new("test_logger_basic");
274 log.step_start(Category::Style, r#""begin style check""#.to_string());
275 log.pass(Category::Style, r#""token resolved""#.to_string());
276 log.step_end(Category::Style, r#""style check done""#.to_string());
277
278 let (pass, fail, total) = log.summary();
279 assert_eq!(pass, 1);
280 assert_eq!(fail, 0);
281 assert_eq!(total, 3);
282 }
283
284 #[test]
285 fn test_logger_jsonl_output() {
286 let log = TestLogger::new("jsonl_test");
287 log.pass(Category::Render, r#""rendered ok""#.to_string());
288 let jsonl = log.to_jsonl();
289 assert!(jsonl.contains(r#""schema_v":1"#));
290 assert!(jsonl.contains(r#""test_id":"jsonl_test""#));
291 assert!(jsonl.contains(r#""category":"render""#));
292 assert!(jsonl.contains(r#""event":"assert_pass""#));
293 }
294
295 #[test]
296 fn test_logger_summary_counts_correctly() {
297 let log = TestLogger::new("summary_test");
298 log.pass(Category::Style, r#""a""#.to_string());
299 log.pass(Category::Theme, r#""b""#.to_string());
300 log.fail(Category::Degradation, r#""c""#.to_string());
301 log.snapshot(Category::Layout, r#""d""#.to_string());
302
303 let (pass, fail, total) = log.summary();
304 assert_eq!(pass, 2);
305 assert_eq!(fail, 1);
306 assert_eq!(total, 4);
307 }
308
309 #[test]
310 fn test_logger_schema_version_stable() {
311 assert_eq!(
312 SCHEMA_VERSION, 1,
313 "schema version must not change without migration"
314 );
315 }
316
317 #[test]
318 fn category_all_variants_have_str() {
319 let cats = [
320 Category::Style,
321 Category::Render,
322 Category::Interaction,
323 Category::Degradation,
324 Category::Theme,
325 Category::Layout,
326 ];
327 for cat in cats {
328 assert!(!cat.as_str().is_empty());
329 }
330 }
331
332 #[test]
333 fn event_all_variants_have_str() {
334 let events = [
335 Event::AssertPass,
336 Event::AssertFail,
337 Event::StepStart,
338 Event::StepEnd,
339 Event::StateSnapshot,
340 ];
341 for ev in events {
342 assert!(!ev.as_str().is_empty());
343 }
344 }
345
346 #[test]
347 fn assert_style_eq_macro_passes() {
348 let log = TestLogger::new("macro_test");
349 let a = 42u32;
350 let b = 42u32;
351 assert_style_eq!(log, a, b, Category::Style, "values should match");
352 let (pass, _, _) = log.summary();
353 assert_eq!(pass, 1);
354 }
355
356 #[test]
357 fn assert_logged_macro_passes() {
358 let log = TestLogger::new("logged_test");
359 assert_logged!(log, true, Category::Render, "condition holds");
360 let (pass, _, _) = log.summary();
361 assert_eq!(pass, 1);
362 }
363 }
364}