1use super::state::{TrainingSnapshot, TrainingState, TrainingStatus};
15use presentar_core::{
16 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event,
17 LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
18};
19use presentar_terminal::widgets::{
20 Border, GpuDevice, GpuPanel as PresentarGpuPanel, GpuProcess as PresentarGpuProcess, GpuVendor,
21 Layout, LayoutItem, Meter, Sparkline, Text,
22};
23use std::any::Any;
24use std::path::PathBuf;
25use std::time::Duration;
26
27pub struct TrainingDashboard {
34 snapshot: Option<TrainingSnapshot>,
36 experiment_dir: PathBuf,
38 bounds: Rect,
40 widget_tree: Option<Layout>,
42}
43
44#[allow(clippy::missing_fields_in_debug)]
45impl std::fmt::Debug for TrainingDashboard {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 f.debug_struct("TrainingDashboard")
48 .field("experiment_dir", &self.experiment_dir)
49 .field("has_snapshot", &self.snapshot.is_some())
50 .field("has_widget_tree", &self.widget_tree.is_some())
51 .finish()
52 }
53}
54
55impl TrainingDashboard {
56 #[must_use]
58 pub fn new(experiment_dir: PathBuf) -> Self {
59 Self { snapshot: None, experiment_dir, bounds: Rect::default(), widget_tree: None }
60 }
61
62 pub fn refresh(&mut self) {
64 let mut state = TrainingState::new(&self.experiment_dir);
65 if let Ok(Some(snap)) = state.read() {
66 self.snapshot = Some(snap);
67 }
68 }
69
70 pub fn is_finished(&self) -> bool {
72 self.snapshot.as_ref().is_some_and(|s| {
73 matches!(s.status, TrainingStatus::Completed | TrainingStatus::Failed(_))
74 })
75 }
76
77 fn rebuild_widgets(&mut self) {
82 let Some(snap) = &self.snapshot else {
83 self.widget_tree = None;
84 return;
85 };
86
87 let mut items = Vec::with_capacity(5);
88
89 items.push(build_header(snap).into_item().fixed(2.0));
91
92 items.push(LayoutItem::new(build_metrics_panel(snap)).fixed(6.0));
94
95 if snap.gpu.is_some() {
97 items.push(LayoutItem::new(build_gpu_panel(snap)).fixed(5.0));
98 }
99
100 if !snap.loss_history.is_empty() {
102 items.push(LayoutItem::new(build_loss_panel(snap)).fixed(4.0));
103 }
104
105 if let TrainingStatus::Failed(msg) = &snap.status {
107 let err = Text::new(format!("ERROR: {msg}")).with_color(Color::new(1.0, 0.3, 0.3, 1.0));
108 items.push(LayoutItem::new(err).fixed(1.0));
109 }
110
111 self.widget_tree = Some(Layout::rows(items));
112 }
113}
114
115fn build_header(snap: &TrainingSnapshot) -> Layout {
121 let (status_str, status_color) = status_display(&snap.status);
122
123 let title = Text::new(format!("apr monitor — {}", truncate_str(&snap.experiment_id, 30)))
124 .with_color(Color::WHITE)
125 .bold();
126
127 let status = Text::new(status_str).with_color(status_color).right();
128
129 let title_row =
130 Layout::columns([LayoutItem::new(title).expanded(), LayoutItem::new(status).fixed(10.0)]);
131
132 let info = Text::new(format!(
133 "Model: {} | Opt: {} | Batch: {}",
134 truncate_str(&snap.model_name, 30),
135 snap.optimizer_name,
136 snap.batch_size
137 ))
138 .with_color(Color::new(0.6, 0.6, 0.6, 1.0));
139
140 Layout::rows([title_row.into_item().fixed(1.0), LayoutItem::new(info).fixed(1.0)])
141}
142
143fn build_metrics_panel(snap: &TrainingSnapshot) -> Border {
145 let progress = snap.progress_percent();
146 let trend = snap.loss_trend();
147
148 let progress_text = Text::new(format!(
149 "Epoch {}/{} Step {}/{}",
150 snap.epoch, snap.total_epochs, snap.step, snap.steps_per_epoch
151 ))
152 .with_color(Color::WHITE);
153
154 let meter = Meter::percentage(f64::from(progress))
155 .with_label(format!("{progress:.1}%"))
156 .with_color(Color::new(0.3, 0.9, 0.3, 1.0));
157
158 let loss_text = Text::new(format!(
159 "Loss: {:.6} {} LR: {:.2e} Tok/s: {:.0} Grad: {:.2}",
160 snap.loss,
161 trend.arrow(),
162 snap.learning_rate,
163 snap.tokens_per_second,
164 snap.gradient_norm
165 ))
166 .with_color(Color::WHITE);
167
168 let elapsed = snap.elapsed();
169 let eta = snap
170 .estimated_remaining()
171 .map(|r| format!("ETA: {}", format_duration(r)))
172 .unwrap_or_default();
173 let time_text = Text::new(format!("Elapsed: {} {}", format_duration(elapsed), eta))
174 .with_color(Color::new(1.0, 0.9, 0.3, 1.0));
175
176 let content = Layout::rows([
177 LayoutItem::new(progress_text).fixed(1.0),
178 LayoutItem::new(meter).fixed(1.0),
179 LayoutItem::new(loss_text).fixed(1.0),
180 LayoutItem::new(time_text).fixed(1.0),
181 ]);
182
183 Border::rounded("Training").child(content)
184}
185
186fn build_gpu_panel(snap: &TrainingSnapshot) -> Border {
188 let Some(gpu) = &snap.gpu else {
189 return Border::rounded("GPU")
190 .child(Text::new("N/A (CPU training)").with_color(Color::new(0.5, 0.5, 0.5, 1.0)));
191 };
192
193 let device = convert_gpu_device(gpu);
194 let processes = convert_gpu_processes(&gpu.processes);
195
196 let gpu_widget = PresentarGpuPanel::new()
197 .with_device(device)
198 .with_processes(processes)
199 .show_processes(false);
200
201 Border::rounded(format!("GPU: {}", truncate_str(&gpu.device_name, 20))).child(gpu_widget)
202}
203
204fn build_loss_panel(snap: &TrainingSnapshot) -> Border {
206 let values: Vec<f64> = snap.loss_history.iter().map(|v| f64::from(*v)).collect();
207 let first = snap.loss_history.first().copied().unwrap_or(0.0);
208 let last = snap.loss_history.last().copied().unwrap_or(0.0);
209
210 let sparkline =
211 Sparkline::new(values).with_color(Color::new(0.3, 0.7, 1.0, 1.0)).with_trend(true);
212
213 let range =
214 Text::new(format!("{first:.4} → {last:.4}")).with_color(Color::new(0.5, 0.5, 0.5, 1.0));
215
216 let content =
217 Layout::rows([LayoutItem::new(sparkline).fixed(1.0), LayoutItem::new(range).fixed(1.0)]);
218
219 Border::rounded("Loss History").child(content)
220}
221
222fn convert_gpu_device(gpu: &super::state::GpuTelemetry) -> GpuDevice {
228 GpuDevice::new(&gpu.device_name)
229 .with_vendor(GpuVendor::Nvidia)
230 .with_utilization(gpu.utilization_percent)
231 .with_temperature(gpu.temperature_celsius)
232 .with_vram(
233 (gpu.vram_used_gb.max(0.0) * 1_073_741_824.0) as u64,
234 (gpu.vram_total_gb.max(0.0) * 1_073_741_824.0) as u64,
235 )
236 .with_power(gpu.power_watts, Some(gpu.power_limit_watts))
237}
238
239fn convert_gpu_processes(processes: &[super::state::GpuProcessInfo]) -> Vec<PresentarGpuProcess> {
241 processes
242 .iter()
243 .map(|p| {
244 let name = p.exe_path.rsplit('/').next().unwrap_or(&p.exe_path);
245 PresentarGpuProcess::new(name, p.pid, p.gpu_memory_mb * 1024 * 1024)
246 })
247 .collect()
248}
249
250fn status_display(status: &TrainingStatus) -> (&'static str, Color) {
252 match status {
253 TrainingStatus::Initializing => ("INIT", Color::new(0.7, 0.7, 0.7, 1.0)),
254 TrainingStatus::Running => ("RUNNING", Color::new(0.3, 0.9, 0.3, 1.0)),
255 TrainingStatus::Paused => ("PAUSED", Color::new(0.7, 0.7, 0.7, 1.0)),
256 TrainingStatus::Completed => ("DONE", Color::new(0.3, 0.7, 1.0, 1.0)),
257 TrainingStatus::Failed(_) => ("FAILED", Color::new(1.0, 0.3, 0.3, 1.0)),
258 }
259}
260
261fn truncate_str(s: &str, max: usize) -> &str {
263 if s.len() <= max {
264 s
265 } else {
266 &s[..max]
267 }
268}
269
270fn format_duration(d: Duration) -> String {
272 let secs = d.as_secs();
273 if secs > 3600 {
274 format!("{}h {}m {}s", secs / 3600, (secs % 3600) / 60, secs % 60)
275 } else if secs > 60 {
276 format!("{}m {}s", secs / 60, secs % 60)
277 } else {
278 format!("{secs}s")
279 }
280}
281
282impl Brick for TrainingDashboard {
287 fn brick_name(&self) -> &'static str {
288 "training_dashboard"
289 }
290
291 fn assertions(&self) -> &[BrickAssertion] {
292 static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
293 ASSERTIONS
294 }
295
296 fn budget(&self) -> BrickBudget {
297 BrickBudget::uniform(16) }
299
300 fn verify(&self) -> BrickVerification {
301 let start = std::time::Instant::now();
304 if let Some(snap) = &self.snapshot {
305 if !super::panel::layout_can_render(snap) {
306 return BrickVerification {
307 passed: vec![],
308 failed: vec![(
309 BrickAssertion::max_latency_ms(16),
310 "snapshot data failed panel verification".to_string(),
311 )],
312 verification_time: start.elapsed(),
313 };
314 }
315 }
316 BrickVerification {
317 passed: vec![BrickAssertion::max_latency_ms(16)],
318 failed: vec![],
319 verification_time: start.elapsed(),
320 }
321 }
322
323 fn to_html(&self) -> String {
324 String::new() }
326
327 fn to_css(&self) -> String {
328 String::new() }
330}
331
332impl Widget for TrainingDashboard {
337 fn type_id(&self) -> TypeId {
338 TypeId::of::<Self>()
339 }
340
341 fn measure(&self, constraints: Constraints) -> Size {
342 Size { width: constraints.max_width, height: constraints.max_height }
343 }
344
345 fn layout(&mut self, bounds: Rect) -> LayoutResult {
346 self.bounds = bounds;
347 self.refresh();
349 self.rebuild_widgets();
350
351 if let Some(tree) = &mut self.widget_tree {
353 tree.layout(bounds);
354 }
355
356 LayoutResult { size: Size { width: bounds.width, height: bounds.height } }
357 }
358
359 fn paint(&self, canvas: &mut dyn Canvas) {
360 if let Some(tree) = &self.widget_tree {
362 tree.paint(canvas);
363 return;
364 }
365
366 let dim = TextStyle { color: Color::new(0.5, 0.5, 0.5, 1.0), ..Default::default() };
368 canvas.draw_text("Waiting for training data...", Point { x: 1.0, y: 1.0 }, &dim);
369 }
370
371 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
372 if let Some(tree) = &mut self.widget_tree {
373 return tree.event(event);
374 }
375 None
376 }
377
378 fn children(&self) -> &[Box<dyn Widget>] {
379 &[]
380 }
381
382 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
383 &mut []
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 #![allow(clippy::unwrap_used)]
390 use super::*;
391 use std::path::PathBuf;
392
393 fn make_snapshot() -> TrainingSnapshot {
394 TrainingSnapshot {
395 timestamp_ms: 2000,
396 epoch: 2,
397 total_epochs: 10,
398 step: 50,
399 steps_per_epoch: 100,
400 loss: 0.123456,
401 loss_history: vec![0.5, 0.4, 0.35, 0.3, 0.25, 0.2, 0.15],
402 learning_rate: 1e-4,
403 lr_history: vec![],
404 gradient_norm: 1.5,
405 accuracy: 0.85,
406 tokens_per_second: 1234.0,
407 samples_per_second: 10.0,
408 start_timestamp_ms: 1000,
409 gpu: None,
410 sample: None,
411 status: TrainingStatus::Running,
412 experiment_id: "exp-001".to_string(),
413 model_name: "test-model".to_string(),
414 model_path: String::new(),
415 optimizer_name: "AdamW".to_string(),
416 batch_size: 32,
417 checkpoint_path: String::new(),
418 executable_path: String::new(),
419 }
420 }
421
422 fn make_gpu() -> super::super::state::GpuTelemetry {
423 super::super::state::GpuTelemetry {
424 device_name: "RTX 4090".to_string(),
425 utilization_percent: 95.0,
426 vram_used_gb: 20.0,
427 vram_total_gb: 24.0,
428 temperature_celsius: 72.0,
429 power_watts: 350.0,
430 power_limit_watts: 450.0,
431 processes: vec![],
432 }
433 }
434
435 #[test]
438 fn test_format_duration_seconds() {
439 assert_eq!(format_duration(Duration::from_secs(42)), "42s");
440 }
441
442 #[test]
443 fn test_format_duration_minutes() {
444 assert_eq!(format_duration(Duration::from_secs(125)), "2m 5s");
445 }
446
447 #[test]
448 fn test_format_duration_hours() {
449 assert_eq!(format_duration(Duration::from_secs(3723)), "1h 2m 3s");
450 }
451
452 #[test]
453 fn test_format_duration_zero() {
454 assert_eq!(format_duration(Duration::from_secs(0)), "0s");
455 }
456
457 #[test]
458 fn test_format_duration_exact_minute() {
459 assert_eq!(format_duration(Duration::from_secs(60)), "60s");
461 }
462
463 #[test]
464 fn test_format_duration_exact_hour() {
465 assert_eq!(format_duration(Duration::from_secs(3600)), "60m 0s");
467 }
468
469 #[test]
472 fn test_truncate_str_short() {
473 assert_eq!(truncate_str("hello", 10), "hello");
474 }
475
476 #[test]
477 fn test_truncate_str_exact() {
478 assert_eq!(truncate_str("hello", 5), "hello");
479 }
480
481 #[test]
482 fn test_truncate_str_long() {
483 assert_eq!(truncate_str("hello world", 5), "hello");
484 }
485
486 #[test]
487 fn test_truncate_str_empty() {
488 assert_eq!(truncate_str("", 5), "");
489 }
490
491 #[test]
492 fn test_truncate_str_zero_max() {
493 assert_eq!(truncate_str("abc", 0), "");
494 }
495
496 #[test]
499 fn test_status_display_running() {
500 let (text, color) = status_display(&TrainingStatus::Running);
501 assert_eq!(text, "RUNNING");
502 assert!(color.g > 0.5); }
504
505 #[test]
506 fn test_status_display_init() {
507 let (text, _) = status_display(&TrainingStatus::Initializing);
508 assert_eq!(text, "INIT");
509 }
510
511 #[test]
512 fn test_status_display_paused() {
513 let (text, _) = status_display(&TrainingStatus::Paused);
514 assert_eq!(text, "PAUSED");
515 }
516
517 #[test]
518 fn test_status_display_completed() {
519 let (text, _) = status_display(&TrainingStatus::Completed);
520 assert_eq!(text, "DONE");
521 }
522
523 #[test]
524 fn test_status_display_failed() {
525 let (text, color) = status_display(&TrainingStatus::Failed("oops".into()));
526 assert_eq!(text, "FAILED");
527 assert!(color.r > 0.5); }
529
530 #[test]
533 fn test_convert_gpu_device_basic() {
534 let gpu = make_gpu();
535 let _dev = convert_gpu_device(&gpu);
536 }
538
539 #[test]
540 fn test_convert_gpu_device_zero_vram() {
541 let mut gpu = make_gpu();
542 gpu.vram_used_gb = 0.0;
543 gpu.vram_total_gb = 0.0;
544 let _dev = convert_gpu_device(&gpu);
545 }
546
547 #[test]
548 fn test_convert_gpu_device_negative_vram_clamped() {
549 let mut gpu = make_gpu();
550 gpu.vram_used_gb = -1.0;
551 gpu.vram_total_gb = -1.0;
552 let _dev = convert_gpu_device(&gpu);
554 }
555
556 #[test]
559 fn test_convert_gpu_processes_empty() {
560 let procs = convert_gpu_processes(&[]);
561 assert!(procs.is_empty());
562 }
563
564 #[test]
565 fn test_convert_gpu_processes_single() {
566 let procs = convert_gpu_processes(&[super::super::state::GpuProcessInfo {
567 pid: 1234,
568 exe_path: "/usr/bin/python3".to_string(),
569 gpu_memory_mb: 4096,
570 cpu_percent: 50.0,
571 rss_mb: 2048,
572 }]);
573 assert_eq!(procs.len(), 1);
574 }
575
576 #[test]
577 fn test_convert_gpu_processes_basename_extraction() {
578 let procs = convert_gpu_processes(&[super::super::state::GpuProcessInfo {
579 pid: 42,
580 exe_path: "/very/long/path/to/trainer".to_string(),
581 gpu_memory_mb: 100,
582 cpu_percent: 10.0,
583 rss_mb: 100,
584 }]);
585 assert_eq!(procs.len(), 1);
586 }
587
588 #[test]
589 fn test_convert_gpu_processes_no_slash() {
590 let procs = convert_gpu_processes(&[super::super::state::GpuProcessInfo {
591 pid: 1,
592 exe_path: "python3".to_string(),
593 gpu_memory_mb: 50,
594 cpu_percent: 5.0,
595 rss_mb: 50,
596 }]);
597 assert_eq!(procs.len(), 1);
598 }
599
600 #[test]
603 fn test_dashboard_new() {
604 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
605 assert!(dash.snapshot.is_none());
606 assert!(dash.widget_tree.is_none());
607 }
608
609 #[test]
610 fn test_dashboard_is_finished_no_snapshot() {
611 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
612 assert!(!dash.is_finished());
613 }
614
615 #[test]
616 fn test_dashboard_is_finished_running() {
617 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
618 dash.snapshot = Some(make_snapshot());
619 assert!(!dash.is_finished());
620 }
621
622 #[test]
623 fn test_dashboard_is_finished_completed() {
624 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
625 let mut snap = make_snapshot();
626 snap.status = TrainingStatus::Completed;
627 dash.snapshot = Some(snap);
628 assert!(dash.is_finished());
629 }
630
631 #[test]
632 fn test_dashboard_is_finished_failed() {
633 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
634 let mut snap = make_snapshot();
635 snap.status = TrainingStatus::Failed("boom".into());
636 dash.snapshot = Some(snap);
637 assert!(dash.is_finished());
638 }
639
640 #[test]
641 fn test_dashboard_is_finished_init() {
642 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
643 let mut snap = make_snapshot();
644 snap.status = TrainingStatus::Initializing;
645 dash.snapshot = Some(snap);
646 assert!(!dash.is_finished());
647 }
648
649 #[test]
650 fn test_dashboard_is_finished_paused() {
651 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
652 let mut snap = make_snapshot();
653 snap.status = TrainingStatus::Paused;
654 dash.snapshot = Some(snap);
655 assert!(!dash.is_finished());
656 }
657
658 #[test]
661 fn test_dashboard_debug() {
662 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
663 let dbg = format!("{dash:?}");
664 assert!(dbg.contains("TrainingDashboard"));
665 assert!(dbg.contains("/tmp/exp"));
666 assert!(dbg.contains("has_snapshot"));
667 }
668
669 #[test]
672 fn test_rebuild_widgets_no_snapshot() {
673 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
674 dash.rebuild_widgets();
675 assert!(dash.widget_tree.is_none());
676 }
677
678 #[test]
679 fn test_rebuild_widgets_basic() {
680 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
681 dash.snapshot = Some(make_snapshot());
682 dash.rebuild_widgets();
683 assert!(dash.widget_tree.is_some());
684 }
685
686 #[test]
687 fn test_rebuild_widgets_with_gpu() {
688 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
689 let mut snap = make_snapshot();
690 snap.gpu = Some(make_gpu());
691 dash.snapshot = Some(snap);
692 dash.rebuild_widgets();
693 assert!(dash.widget_tree.is_some());
694 }
695
696 #[test]
697 fn test_rebuild_widgets_no_loss_history() {
698 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
699 let mut snap = make_snapshot();
700 snap.loss_history = vec![];
701 dash.snapshot = Some(snap);
702 dash.rebuild_widgets();
703 assert!(dash.widget_tree.is_some());
704 }
705
706 #[test]
707 fn test_rebuild_widgets_failed_status() {
708 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
709 let mut snap = make_snapshot();
710 snap.status = TrainingStatus::Failed("critical error".into());
711 dash.snapshot = Some(snap);
712 dash.rebuild_widgets();
713 assert!(dash.widget_tree.is_some());
714 }
715
716 #[test]
717 fn test_rebuild_widgets_completed() {
718 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
719 let mut snap = make_snapshot();
720 snap.status = TrainingStatus::Completed;
721 dash.snapshot = Some(snap);
722 dash.rebuild_widgets();
723 assert!(dash.widget_tree.is_some());
724 }
725
726 #[test]
729 fn test_build_header() {
730 let snap = make_snapshot();
731 let _header = build_header(&snap);
732 }
734
735 #[test]
736 fn test_build_header_long_experiment_id() {
737 let mut snap = make_snapshot();
738 snap.experiment_id = "a".repeat(100);
739 let _header = build_header(&snap);
740 }
741
742 #[test]
743 fn test_build_metrics_panel() {
744 let snap = make_snapshot();
745 let _panel = build_metrics_panel(&snap);
746 }
747
748 #[test]
749 fn test_build_metrics_panel_zero_progress() {
750 let mut snap = make_snapshot();
751 snap.epoch = 0;
752 snap.step = 0;
753 snap.total_epochs = 0;
754 snap.steps_per_epoch = 0;
755 let _panel = build_metrics_panel(&snap);
756 }
757
758 #[test]
759 fn test_build_gpu_panel_with_gpu() {
760 let mut snap = make_snapshot();
761 snap.gpu = Some(make_gpu());
762 let _panel = build_gpu_panel(&snap);
763 }
764
765 #[test]
766 fn test_build_gpu_panel_no_gpu() {
767 let snap = make_snapshot();
768 let _panel = build_gpu_panel(&snap);
769 }
770
771 #[test]
772 fn test_build_gpu_panel_with_processes() {
773 let mut snap = make_snapshot();
774 let mut gpu = make_gpu();
775 gpu.processes = vec![
776 super::super::state::GpuProcessInfo {
777 pid: 100,
778 exe_path: "/usr/bin/python3".to_string(),
779 gpu_memory_mb: 4096,
780 cpu_percent: 50.0,
781 rss_mb: 2048,
782 },
783 super::super::state::GpuProcessInfo {
784 pid: 200,
785 exe_path: "trainer".to_string(),
786 gpu_memory_mb: 2048,
787 cpu_percent: 25.0,
788 rss_mb: 1024,
789 },
790 ];
791 snap.gpu = Some(gpu);
792 let _panel = build_gpu_panel(&snap);
793 }
794
795 #[test]
796 fn test_build_loss_panel() {
797 let snap = make_snapshot();
798 let _panel = build_loss_panel(&snap);
799 }
800
801 #[test]
802 fn test_build_loss_panel_single_value() {
803 let mut snap = make_snapshot();
804 snap.loss_history = vec![0.5];
805 let _panel = build_loss_panel(&snap);
806 }
807
808 #[test]
811 fn test_brick_name() {
812 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
813 assert_eq!(dash.brick_name(), "training_dashboard");
814 }
815
816 #[test]
817 fn test_brick_assertions() {
818 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
819 assert!(!dash.assertions().is_empty());
820 }
821
822 #[test]
823 fn test_brick_budget() {
824 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
825 let _budget = dash.budget();
826 }
827
828 #[test]
829 fn test_brick_verify_no_snapshot() {
830 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
831 let v = dash.verify();
832 assert!(v.failed.is_empty());
833 }
834
835 #[test]
836 fn test_brick_verify_with_snapshot() {
837 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
838 dash.snapshot = Some(make_snapshot());
839 let v = dash.verify();
840 assert!(!v.passed.is_empty() || !v.failed.is_empty());
842 }
843
844 #[test]
845 fn test_brick_to_html() {
846 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
847 assert!(dash.to_html().is_empty());
848 }
849
850 #[test]
851 fn test_brick_to_css() {
852 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
853 assert!(dash.to_css().is_empty());
854 }
855
856 #[test]
859 fn test_widget_measure() {
860 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
861 let size = dash.measure(Constraints {
862 min_width: 0.0,
863 min_height: 0.0,
864 max_width: 80.0,
865 max_height: 24.0,
866 });
867 assert_eq!(size.width, 80.0);
868 assert_eq!(size.height, 24.0);
869 }
870
871 #[test]
872 fn test_widget_children_empty() {
873 let dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
874 assert!(dash.children().is_empty());
875 }
876
877 #[test]
878 fn test_widget_children_mut_empty() {
879 let mut dash = TrainingDashboard::new(PathBuf::from("/tmp/exp"));
880 assert!(dash.children_mut().is_empty());
881 }
882}