1use presentar_core::{
4 widget::{AccessibleRole, LayoutResult, TextStyle},
5 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Point, Rect,
6 Size, TypeId, Widget,
7};
8use serde::{Deserialize, Serialize};
9use std::any::Any;
10use std::collections::HashMap;
11use std::time::Duration;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
15pub enum ModelStatus {
16 #[default]
18 Draft,
19 Review,
21 Published,
23 Deprecated,
25 Archived,
27}
28
29impl ModelStatus {
30 #[must_use]
32 pub fn color(&self) -> Color {
33 match self {
34 Self::Draft => Color::new(0.6, 0.6, 0.6, 1.0),
35 Self::Review => Color::new(0.9, 0.7, 0.1, 1.0),
36 Self::Published => Color::new(0.2, 0.7, 0.3, 1.0),
37 Self::Deprecated => Color::new(0.9, 0.5, 0.1, 1.0),
38 Self::Archived => Color::new(0.5, 0.5, 0.5, 1.0),
39 }
40 }
41
42 #[must_use]
44 pub const fn label(&self) -> &'static str {
45 match self {
46 Self::Draft => "Draft",
47 Self::Review => "Review",
48 Self::Published => "Published",
49 Self::Deprecated => "Deprecated",
50 Self::Archived => "Archived",
51 }
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
57pub struct ModelMetric {
58 pub name: String,
60 pub value: f64,
62 pub unit: Option<String>,
64 pub higher_is_better: bool,
66}
67
68impl ModelMetric {
69 #[must_use]
71 pub fn new(name: impl Into<String>, value: f64) -> Self {
72 Self {
73 name: name.into(),
74 value,
75 unit: None,
76 higher_is_better: true,
77 }
78 }
79
80 #[must_use]
82 pub fn unit(mut self, unit: impl Into<String>) -> Self {
83 self.unit = Some(unit.into());
84 self
85 }
86
87 #[must_use]
89 pub const fn lower_is_better(mut self) -> Self {
90 self.higher_is_better = false;
91 self
92 }
93
94 #[must_use]
96 pub fn formatted_value(&self) -> String {
97 if let Some(ref unit) = self.unit {
98 format!("{:.2}{}", self.value, unit)
99 } else if self.value.abs() < 1.0 {
100 format!("{:.2}%", self.value * 100.0)
101 } else {
102 format!("{:.2}", self.value)
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ModelCard {
110 name: String,
112 version: String,
114 description: Option<String>,
116 status: ModelStatus,
118 framework: Option<String>,
120 task: Option<String>,
122 metrics: Vec<ModelMetric>,
124 parameters: Option<u64>,
126 dataset: Option<String>,
128 author: Option<String>,
130 tags: Vec<String>,
132 metadata: HashMap<String, String>,
134 width: Option<f32>,
136 height: Option<f32>,
138 background: Color,
140 border_color: Color,
142 corner_radius: f32,
144 show_metrics_chart: bool,
146 accessible_name_value: Option<String>,
148 test_id_value: Option<String>,
150 #[serde(skip)]
152 bounds: Rect,
153}
154
155impl Default for ModelCard {
156 fn default() -> Self {
157 Self {
158 name: String::new(),
159 version: String::from("1.0.0"),
160 description: None,
161 status: ModelStatus::Draft,
162 framework: None,
163 task: None,
164 metrics: Vec::new(),
165 parameters: None,
166 dataset: None,
167 author: None,
168 tags: Vec::new(),
169 metadata: HashMap::new(),
170 width: None,
171 height: None,
172 background: Color::WHITE,
173 border_color: Color::new(0.9, 0.9, 0.9, 1.0),
174 corner_radius: 8.0,
175 show_metrics_chart: true,
176 accessible_name_value: None,
177 test_id_value: None,
178 bounds: Rect::default(),
179 }
180 }
181}
182
183impl ModelCard {
184 #[must_use]
186 pub fn new(name: impl Into<String>) -> Self {
187 Self {
188 name: name.into(),
189 ..Self::default()
190 }
191 }
192
193 #[must_use]
195 pub fn name(mut self, name: impl Into<String>) -> Self {
196 self.name = name.into();
197 self
198 }
199
200 #[must_use]
202 pub fn version(mut self, version: impl Into<String>) -> Self {
203 self.version = version.into();
204 self
205 }
206
207 #[must_use]
209 pub fn description(mut self, desc: impl Into<String>) -> Self {
210 self.description = Some(desc.into());
211 self
212 }
213
214 #[must_use]
216 pub const fn status(mut self, status: ModelStatus) -> Self {
217 self.status = status;
218 self
219 }
220
221 #[must_use]
223 pub fn framework(mut self, framework: impl Into<String>) -> Self {
224 self.framework = Some(framework.into());
225 self
226 }
227
228 #[must_use]
230 pub fn task(mut self, task: impl Into<String>) -> Self {
231 self.task = Some(task.into());
232 self
233 }
234
235 #[must_use]
237 pub fn metric(mut self, metric: ModelMetric) -> Self {
238 self.metrics.push(metric);
239 self
240 }
241
242 #[must_use]
244 pub fn metrics(mut self, metrics: impl IntoIterator<Item = ModelMetric>) -> Self {
245 self.metrics.extend(metrics);
246 self
247 }
248
249 #[must_use]
251 pub const fn parameters(mut self, count: u64) -> Self {
252 self.parameters = Some(count);
253 self
254 }
255
256 #[must_use]
258 pub fn dataset(mut self, dataset: impl Into<String>) -> Self {
259 self.dataset = Some(dataset.into());
260 self
261 }
262
263 #[must_use]
265 pub fn author(mut self, author: impl Into<String>) -> Self {
266 self.author = Some(author.into());
267 self
268 }
269
270 #[must_use]
272 pub fn tag(mut self, tag: impl Into<String>) -> Self {
273 self.tags.push(tag.into());
274 self
275 }
276
277 #[must_use]
279 pub fn tags(mut self, tags: impl IntoIterator<Item = impl Into<String>>) -> Self {
280 self.tags.extend(tags.into_iter().map(Into::into));
281 self
282 }
283
284 #[must_use]
286 pub fn metadata_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
287 self.metadata.insert(key.into(), value.into());
288 self
289 }
290
291 #[must_use]
293 pub fn width(mut self, width: f32) -> Self {
294 self.width = Some(width.max(200.0));
295 self
296 }
297
298 #[must_use]
300 pub fn height(mut self, height: f32) -> Self {
301 self.height = Some(height.max(150.0));
302 self
303 }
304
305 #[must_use]
307 pub const fn background(mut self, color: Color) -> Self {
308 self.background = color;
309 self
310 }
311
312 #[must_use]
314 pub const fn border_color(mut self, color: Color) -> Self {
315 self.border_color = color;
316 self
317 }
318
319 #[must_use]
321 pub fn corner_radius(mut self, radius: f32) -> Self {
322 self.corner_radius = radius.max(0.0);
323 self
324 }
325
326 #[must_use]
328 pub const fn show_metrics_chart(mut self, show: bool) -> Self {
329 self.show_metrics_chart = show;
330 self
331 }
332
333 #[must_use]
335 pub fn accessible_name(mut self, name: impl Into<String>) -> Self {
336 self.accessible_name_value = Some(name.into());
337 self
338 }
339
340 #[must_use]
342 pub fn test_id(mut self, id: impl Into<String>) -> Self {
343 self.test_id_value = Some(id.into());
344 self
345 }
346
347 #[must_use]
351 pub fn get_name(&self) -> &str {
352 &self.name
353 }
354
355 #[must_use]
357 pub fn get_version(&self) -> &str {
358 &self.version
359 }
360
361 #[must_use]
363 pub fn get_description(&self) -> Option<&str> {
364 self.description.as_deref()
365 }
366
367 #[must_use]
369 pub const fn get_status(&self) -> ModelStatus {
370 self.status
371 }
372
373 #[must_use]
375 pub fn get_framework(&self) -> Option<&str> {
376 self.framework.as_deref()
377 }
378
379 #[must_use]
381 pub fn get_task(&self) -> Option<&str> {
382 self.task.as_deref()
383 }
384
385 #[must_use]
387 pub fn get_metrics(&self) -> &[ModelMetric] {
388 &self.metrics
389 }
390
391 #[must_use]
393 pub const fn get_parameters(&self) -> Option<u64> {
394 self.parameters
395 }
396
397 #[must_use]
399 pub fn get_dataset(&self) -> Option<&str> {
400 self.dataset.as_deref()
401 }
402
403 #[must_use]
405 pub fn get_author(&self) -> Option<&str> {
406 self.author.as_deref()
407 }
408
409 #[must_use]
411 pub fn get_tags(&self) -> &[String] {
412 &self.tags
413 }
414
415 #[must_use]
417 pub fn get_metadata(&self, key: &str) -> Option<&str> {
418 self.metadata.get(key).map(String::as_str)
419 }
420
421 #[must_use]
423 pub fn has_metrics(&self) -> bool {
424 !self.metrics.is_empty()
425 }
426
427 #[must_use]
429 pub fn formatted_parameters(&self) -> Option<String> {
430 self.parameters.map(|p| {
431 if p >= 1_000_000_000 {
432 format!("{:.1}B", p as f64 / 1_000_000_000.0)
433 } else if p >= 1_000_000 {
434 format!("{:.1}M", p as f64 / 1_000_000.0)
435 } else if p >= 1_000 {
436 format!("{:.1}K", p as f64 / 1_000.0)
437 } else {
438 format!("{p}")
439 }
440 })
441 }
442}
443
444impl Widget for ModelCard {
445 fn type_id(&self) -> TypeId {
446 TypeId::of::<Self>()
447 }
448
449 fn measure(&self, constraints: Constraints) -> Size {
450 let width = self.width.unwrap_or(320.0);
451 let height = self.height.unwrap_or(200.0);
452 constraints.constrain(Size::new(width, height))
453 }
454
455 fn layout(&mut self, bounds: Rect) -> LayoutResult {
456 self.bounds = bounds;
457 LayoutResult {
458 size: bounds.size(),
459 }
460 }
461
462 #[allow(clippy::too_many_lines)]
463 fn paint(&self, canvas: &mut dyn Canvas) {
464 let padding = 16.0;
465
466 canvas.fill_rect(self.bounds, self.background);
468
469 canvas.stroke_rect(self.bounds, self.border_color, 1.0);
471
472 let status_color = self.status.color();
474 let badge_rect = Rect::new(
475 self.bounds.x + self.bounds.width - 80.0,
476 self.bounds.y + padding,
477 70.0,
478 22.0,
479 );
480 canvas.fill_rect(badge_rect, status_color);
481
482 let badge_style = TextStyle {
483 size: 10.0,
484 color: Color::WHITE,
485 ..TextStyle::default()
486 };
487 canvas.draw_text(
488 self.status.label(),
489 Point::new(badge_rect.x + 10.0, badge_rect.y + 15.0),
490 &badge_style,
491 );
492
493 let title_style = TextStyle {
495 size: 18.0,
496 color: Color::new(0.1, 0.1, 0.1, 1.0),
497 ..TextStyle::default()
498 };
499 canvas.draw_text(
500 &self.name,
501 Point::new(self.bounds.x + padding, self.bounds.y + padding + 16.0),
502 &title_style,
503 );
504
505 let version_style = TextStyle {
507 size: 12.0,
508 color: Color::new(0.5, 0.5, 0.5, 1.0),
509 ..TextStyle::default()
510 };
511 canvas.draw_text(
512 &format!("v{}", self.version),
513 Point::new(self.bounds.x + padding, self.bounds.y + padding + 36.0),
514 &version_style,
515 );
516
517 let mut y_offset = padding + 50.0;
519 if let Some(ref desc) = self.description {
520 let desc_style = TextStyle {
521 size: 12.0,
522 color: Color::new(0.3, 0.3, 0.3, 1.0),
523 ..TextStyle::default()
524 };
525 canvas.draw_text(
526 desc,
527 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
528 &desc_style,
529 );
530 y_offset += 24.0;
531 }
532
533 if self.framework.is_some() || self.task.is_some() {
535 let info_style = TextStyle {
536 size: 11.0,
537 color: Color::new(0.4, 0.4, 0.4, 1.0),
538 ..TextStyle::default()
539 };
540 let info_text = match (&self.framework, &self.task) {
541 (Some(f), Some(t)) => format!("{f} • {t}"),
542 (Some(f), None) => f.clone(),
543 (None, Some(t)) => t.clone(),
544 (None, None) => String::new(),
545 };
546 canvas.draw_text(
547 &info_text,
548 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
549 &info_style,
550 );
551 y_offset += 20.0;
552 }
553
554 if let Some(params) = self.formatted_parameters() {
556 let params_style = TextStyle {
557 size: 11.0,
558 color: Color::new(0.4, 0.4, 0.4, 1.0),
559 ..TextStyle::default()
560 };
561 canvas.draw_text(
562 &format!("Parameters: {params}"),
563 Point::new(self.bounds.x + padding, self.bounds.y + y_offset + 12.0),
564 ¶ms_style,
565 );
566 y_offset += 18.0;
567 }
568
569 if self.show_metrics_chart && !self.metrics.is_empty() {
571 let metric_style = TextStyle {
572 size: 11.0,
573 color: Color::new(0.2, 0.2, 0.2, 1.0),
574 ..TextStyle::default()
575 };
576 let value_style = TextStyle {
577 size: 14.0,
578 color: Color::new(0.2, 0.47, 0.96, 1.0),
579 ..TextStyle::default()
580 };
581
582 let metric_width =
583 (self.bounds.width - padding * 2.0) / self.metrics.len().min(4) as f32;
584 for (i, metric) in self.metrics.iter().take(4).enumerate() {
585 let mx = (i as f32).mul_add(metric_width, self.bounds.x + padding);
586 canvas.draw_text(
587 &metric.name,
588 Point::new(mx, self.bounds.y + y_offset + 12.0),
589 &metric_style,
590 );
591 canvas.draw_text(
592 &metric.formatted_value(),
593 Point::new(mx, self.bounds.y + y_offset + 28.0),
594 &value_style,
595 );
596 }
597 y_offset += 40.0;
598 }
599
600 if !self.tags.is_empty() {
602 let tag_style = TextStyle {
603 size: 10.0,
604 color: Color::new(0.3, 0.3, 0.3, 1.0),
605 ..TextStyle::default()
606 };
607 let tag_bg = Color::new(0.95, 0.95, 0.95, 1.0);
608
609 let mut tx = self.bounds.x + padding;
610 for tag in self.tags.iter().take(5) {
611 let tag_width = (tag.len() as f32).mul_add(6.0, 12.0);
612 canvas.fill_rect(
613 Rect::new(tx, self.bounds.y + y_offset, tag_width, 18.0),
614 tag_bg,
615 );
616 canvas.draw_text(
617 tag,
618 Point::new(tx + 6.0, self.bounds.y + y_offset + 13.0),
619 &tag_style,
620 );
621 tx += tag_width + 6.0;
622 }
623 }
624 }
625
626 fn event(&mut self, _event: &presentar_core::Event) -> Option<Box<dyn Any + Send>> {
627 None
628 }
629
630 fn children(&self) -> &[Box<dyn Widget>] {
631 &[]
632 }
633
634 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
635 &mut []
636 }
637
638 fn is_interactive(&self) -> bool {
639 false
640 }
641
642 fn is_focusable(&self) -> bool {
643 false
644 }
645
646 fn accessible_name(&self) -> Option<&str> {
647 self.accessible_name_value.as_deref().or(Some(&self.name))
648 }
649
650 fn accessible_role(&self) -> AccessibleRole {
651 AccessibleRole::Generic
652 }
653
654 fn test_id(&self) -> Option<&str> {
655 self.test_id_value.as_deref()
656 }
657}
658
659impl Brick for ModelCard {
661 fn brick_name(&self) -> &'static str {
662 "ModelCard"
663 }
664
665 fn assertions(&self) -> &[BrickAssertion] {
666 &[BrickAssertion::MaxLatencyMs(16)]
667 }
668
669 fn budget(&self) -> BrickBudget {
670 BrickBudget::uniform(16)
671 }
672
673 fn verify(&self) -> BrickVerification {
674 BrickVerification {
675 passed: self.assertions().to_vec(),
676 failed: vec![],
677 verification_time: Duration::from_micros(10),
678 }
679 }
680
681 fn to_html(&self) -> String {
682 r#"<div class="brick-modelcard"></div>"#.to_string()
683 }
684
685 fn to_css(&self) -> String {
686 ".brick-modelcard { display: block; }".to_string()
687 }
688
689 fn test_id(&self) -> Option<&str> {
690 self.test_id_value.as_deref()
691 }
692}
693
694#[cfg(test)]
695#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
696mod tests {
697 use super::*;
698
699 #[test]
702 fn test_model_status_default() {
703 assert_eq!(ModelStatus::default(), ModelStatus::Draft);
704 }
705
706 #[test]
707 fn test_model_status_color() {
708 let published = ModelStatus::Published;
709 let color = published.color();
710 assert!(color.g > color.r); }
712
713 #[test]
714 fn test_model_status_label() {
715 assert_eq!(ModelStatus::Draft.label(), "Draft");
716 assert_eq!(ModelStatus::Review.label(), "Review");
717 assert_eq!(ModelStatus::Published.label(), "Published");
718 assert_eq!(ModelStatus::Deprecated.label(), "Deprecated");
719 assert_eq!(ModelStatus::Archived.label(), "Archived");
720 }
721
722 #[test]
725 fn test_model_metric_new() {
726 let metric = ModelMetric::new("Accuracy", 0.95);
727 assert_eq!(metric.name, "Accuracy");
728 assert_eq!(metric.value, 0.95);
729 assert!(metric.unit.is_none());
730 assert!(metric.higher_is_better);
731 }
732
733 #[test]
734 fn test_model_metric_unit() {
735 let metric = ModelMetric::new("Latency", 45.0).unit("ms");
736 assert_eq!(metric.unit, Some("ms".to_string()));
737 }
738
739 #[test]
740 fn test_model_metric_lower_is_better() {
741 let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
742 assert!(!metric.higher_is_better);
743 }
744
745 #[test]
746 fn test_model_metric_formatted_value_percentage() {
747 let metric = ModelMetric::new("Accuracy", 0.95);
748 assert_eq!(metric.formatted_value(), "95.00%");
749 }
750
751 #[test]
752 fn test_model_metric_formatted_value_with_unit() {
753 let metric = ModelMetric::new("Latency", 45.0).unit("ms");
754 assert_eq!(metric.formatted_value(), "45.00ms");
755 }
756
757 #[test]
758 fn test_model_metric_formatted_value_large() {
759 let metric = ModelMetric::new("Score", 1234.5);
760 assert_eq!(metric.formatted_value(), "1234.50");
761 }
762
763 #[test]
766 fn test_model_card_new() {
767 let card = ModelCard::new("GPT-4");
768 assert_eq!(card.get_name(), "GPT-4");
769 assert_eq!(card.get_version(), "1.0.0");
770 assert_eq!(card.get_status(), ModelStatus::Draft);
771 }
772
773 #[test]
774 fn test_model_card_default() {
775 let card = ModelCard::default();
776 assert!(card.name.is_empty());
777 assert_eq!(card.version, "1.0.0");
778 }
779
780 #[test]
781 fn test_model_card_builder() {
782 let card = ModelCard::new("ResNet-50")
783 .version("2.1.0")
784 .description("Image classification model")
785 .status(ModelStatus::Published)
786 .framework("PyTorch")
787 .task("classification")
788 .metric(ModelMetric::new("Top-1 Accuracy", 0.761))
789 .metric(ModelMetric::new("Top-5 Accuracy", 0.929))
790 .parameters(25_600_000)
791 .dataset("ImageNet")
792 .author("Deep Learning Team")
793 .tag("vision")
794 .tag("classification")
795 .metadata_entry("license", "Apache-2.0")
796 .width(400.0)
797 .height(300.0)
798 .background(Color::WHITE)
799 .border_color(Color::new(0.8, 0.8, 0.8, 1.0))
800 .corner_radius(12.0)
801 .show_metrics_chart(true)
802 .accessible_name("ResNet-50 model card")
803 .test_id("resnet-card");
804
805 assert_eq!(card.get_name(), "ResNet-50");
806 assert_eq!(card.get_version(), "2.1.0");
807 assert_eq!(card.get_description(), Some("Image classification model"));
808 assert_eq!(card.get_status(), ModelStatus::Published);
809 assert_eq!(card.get_framework(), Some("PyTorch"));
810 assert_eq!(card.get_task(), Some("classification"));
811 assert_eq!(card.get_metrics().len(), 2);
812 assert_eq!(card.get_parameters(), Some(25_600_000));
813 assert_eq!(card.get_dataset(), Some("ImageNet"));
814 assert_eq!(card.get_author(), Some("Deep Learning Team"));
815 assert_eq!(card.get_tags().len(), 2);
816 assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
817 assert_eq!(Widget::accessible_name(&card), Some("ResNet-50 model card"));
818 assert_eq!(Widget::test_id(&card), Some("resnet-card"));
819 }
820
821 #[test]
822 fn test_model_card_metrics() {
823 let metrics = vec![
824 ModelMetric::new("Accuracy", 0.95),
825 ModelMetric::new("F1", 0.92),
826 ];
827 let card = ModelCard::new("Model").metrics(metrics);
828 assert_eq!(card.get_metrics().len(), 2);
829 assert!(card.has_metrics());
830 }
831
832 #[test]
833 fn test_model_card_tags() {
834 let card = ModelCard::new("Model").tags(["nlp", "transformer", "bert"]);
835 assert_eq!(card.get_tags().len(), 3);
836 }
837
838 #[test]
841 fn test_formatted_parameters_none() {
842 let card = ModelCard::new("Model");
843 assert!(card.formatted_parameters().is_none());
844 }
845
846 #[test]
847 fn test_formatted_parameters_small() {
848 let card = ModelCard::new("Model").parameters(500);
849 assert_eq!(card.formatted_parameters(), Some("500".to_string()));
850 }
851
852 #[test]
853 fn test_formatted_parameters_thousands() {
854 let card = ModelCard::new("Model").parameters(25_000);
855 assert_eq!(card.formatted_parameters(), Some("25.0K".to_string()));
856 }
857
858 #[test]
859 fn test_formatted_parameters_millions() {
860 let card = ModelCard::new("Model").parameters(125_000_000);
861 assert_eq!(card.formatted_parameters(), Some("125.0M".to_string()));
862 }
863
864 #[test]
865 fn test_formatted_parameters_billions() {
866 let card = ModelCard::new("Model").parameters(175_000_000_000);
867 assert_eq!(card.formatted_parameters(), Some("175.0B".to_string()));
868 }
869
870 #[test]
873 fn test_model_card_width_min() {
874 let card = ModelCard::new("Model").width(100.0);
875 assert_eq!(card.width, Some(200.0));
876 }
877
878 #[test]
879 fn test_model_card_height_min() {
880 let card = ModelCard::new("Model").height(50.0);
881 assert_eq!(card.height, Some(150.0));
882 }
883
884 #[test]
885 fn test_model_card_corner_radius_min() {
886 let card = ModelCard::new("Model").corner_radius(-5.0);
887 assert_eq!(card.corner_radius, 0.0);
888 }
889
890 #[test]
893 fn test_model_card_type_id() {
894 let card = ModelCard::new("Model");
895 assert_eq!(Widget::type_id(&card), TypeId::of::<ModelCard>());
896 }
897
898 #[test]
899 fn test_model_card_measure_default() {
900 let card = ModelCard::new("Model");
901 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
902 assert_eq!(size.width, 320.0);
903 assert_eq!(size.height, 200.0);
904 }
905
906 #[test]
907 fn test_model_card_measure_custom() {
908 let card = ModelCard::new("Model").width(400.0).height(250.0);
909 let size = card.measure(Constraints::loose(Size::new(1000.0, 1000.0)));
910 assert_eq!(size.width, 400.0);
911 assert_eq!(size.height, 250.0);
912 }
913
914 #[test]
915 fn test_model_card_layout() {
916 let mut card = ModelCard::new("Model");
917 let bounds = Rect::new(10.0, 20.0, 320.0, 200.0);
918 let result = card.layout(bounds);
919 assert_eq!(result.size, Size::new(320.0, 200.0));
920 assert_eq!(card.bounds, bounds);
921 }
922
923 #[test]
924 fn test_model_card_children() {
925 let card = ModelCard::new("Model");
926 assert!(card.children().is_empty());
927 }
928
929 #[test]
930 fn test_model_card_is_interactive() {
931 let card = ModelCard::new("Model");
932 assert!(!card.is_interactive());
933 }
934
935 #[test]
936 fn test_model_card_is_focusable() {
937 let card = ModelCard::new("Model");
938 assert!(!card.is_focusable());
939 }
940
941 #[test]
942 fn test_model_card_accessible_role() {
943 let card = ModelCard::new("Model");
944 assert_eq!(card.accessible_role(), AccessibleRole::Generic);
945 }
946
947 #[test]
948 fn test_model_card_accessible_name_from_name() {
949 let card = ModelCard::new("GPT-4");
950 assert_eq!(Widget::accessible_name(&card), Some("GPT-4"));
951 }
952
953 #[test]
954 fn test_model_card_accessible_name_explicit() {
955 let card = ModelCard::new("GPT-4").accessible_name("Language model card");
956 assert_eq!(Widget::accessible_name(&card), Some("Language model card"));
957 }
958
959 #[test]
960 fn test_model_card_test_id() {
961 let card = ModelCard::new("Model").test_id("model-card");
962 assert_eq!(Widget::test_id(&card), Some("model-card"));
963 }
964
965 #[test]
968 fn test_model_card_has_metrics_false() {
969 let card = ModelCard::new("Model");
970 assert!(!card.has_metrics());
971 }
972
973 #[test]
974 fn test_model_card_has_metrics_true() {
975 let card = ModelCard::new("Model").metric(ModelMetric::new("Acc", 0.9));
976 assert!(card.has_metrics());
977 }
978
979 #[test]
984 fn test_model_status_color_all_variants() {
985 let _ = ModelStatus::Draft.color();
986 let _ = ModelStatus::Review.color();
987 let _ = ModelStatus::Published.color();
988 let _ = ModelStatus::Deprecated.color();
989 let _ = ModelStatus::Archived.color();
990 }
991
992 #[test]
993 fn test_model_card_children_mut() {
994 let mut card = ModelCard::new("Model");
995 assert!(card.children_mut().is_empty());
996 }
997
998 #[test]
999 fn test_model_card_event_returns_none() {
1000 let mut card = ModelCard::new("Model");
1001 let result = card.event(&presentar_core::Event::key_down(presentar_core::Key::Down));
1002 assert!(result.is_none());
1003 }
1004
1005 #[test]
1006 fn test_model_card_test_id_none() {
1007 let card = ModelCard::new("Model");
1008 assert!(Widget::test_id(&card).is_none());
1009 }
1010
1011 #[test]
1012 fn test_model_card_bounds() {
1013 let mut card = ModelCard::new("Model");
1014 card.layout(Rect::new(5.0, 10.0, 320.0, 200.0));
1015 assert_eq!(card.bounds.x, 5.0);
1016 assert_eq!(card.bounds.y, 10.0);
1017 }
1018
1019 #[test]
1020 fn test_model_metric_eq() {
1021 let m1 = ModelMetric::new("Acc", 0.95);
1022 let m2 = ModelMetric::new("Acc", 0.95);
1023 assert_eq!(m1.name, m2.name);
1024 assert_eq!(m1.value, m2.value);
1025 }
1026
1027 #[test]
1028 fn test_model_card_name_setter() {
1029 let card = ModelCard::new("Initial").name("Changed");
1030 assert_eq!(card.get_name(), "Changed");
1031 }
1032
1033 #[test]
1038 fn test_model_card_brick_name() {
1039 let card = ModelCard::new("test");
1040 assert_eq!(card.brick_name(), "ModelCard");
1041 }
1042
1043 #[test]
1044 fn test_model_card_brick_assertions() {
1045 let card = ModelCard::new("test");
1046 let assertions = card.assertions();
1047 assert!(!assertions.is_empty());
1048 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
1049 }
1050
1051 #[test]
1052 fn test_model_card_brick_budget() {
1053 let card = ModelCard::new("test");
1054 let budget = card.budget();
1055 assert!(budget.layout_ms > 0);
1057 assert!(budget.paint_ms > 0);
1058 }
1059
1060 #[test]
1061 fn test_model_card_brick_verify() {
1062 let card = ModelCard::new("test");
1063 let verification = card.verify();
1064 assert!(!verification.passed.is_empty());
1065 assert!(verification.failed.is_empty());
1066 }
1067
1068 #[test]
1069 fn test_model_card_brick_to_html() {
1070 let card = ModelCard::new("test");
1071 let html = card.to_html();
1072 assert!(html.contains("brick-modelcard"));
1073 }
1074
1075 #[test]
1076 fn test_model_card_brick_to_css() {
1077 let card = ModelCard::new("test");
1078 let css = card.to_css();
1079 assert!(css.contains(".brick-modelcard"));
1080 assert!(css.contains("display: block"));
1081 }
1082
1083 #[test]
1084 fn test_model_card_brick_test_id() {
1085 let card = ModelCard::new("test").test_id("card-1");
1086 assert_eq!(Brick::test_id(&card), Some("card-1"));
1087 }
1088
1089 #[test]
1090 fn test_model_card_brick_test_id_none() {
1091 let card = ModelCard::new("test");
1092 assert!(Brick::test_id(&card).is_none());
1093 }
1094
1095 #[test]
1100 fn test_model_status_debug() {
1101 let status = ModelStatus::Published;
1102 let debug_str = format!("{status:?}");
1103 assert!(debug_str.contains("Published"));
1104 }
1105
1106 #[test]
1107 fn test_model_status_eq() {
1108 assert_eq!(ModelStatus::Draft, ModelStatus::Draft);
1109 assert_ne!(ModelStatus::Draft, ModelStatus::Published);
1110 }
1111
1112 #[test]
1113 fn test_model_status_clone() {
1114 let status = ModelStatus::Review;
1115 let cloned = status;
1116 assert_eq!(cloned, ModelStatus::Review);
1117 }
1118
1119 #[test]
1120 fn test_model_status_serde() {
1121 let status = ModelStatus::Deprecated;
1122 let serialized = serde_json::to_string(&status).unwrap();
1123 let deserialized: ModelStatus = serde_json::from_str(&serialized).unwrap();
1124 assert_eq!(deserialized, ModelStatus::Deprecated);
1125 }
1126
1127 #[test]
1128 fn test_model_status_color_all_variants_detailed() {
1129 let draft_color = ModelStatus::Draft.color();
1131 assert!((draft_color.r - 0.6).abs() < 0.01);
1132
1133 let review_color = ModelStatus::Review.color();
1135 assert!(review_color.r > 0.8);
1136 assert!(review_color.g > 0.6);
1137
1138 let published_color = ModelStatus::Published.color();
1140 assert!(published_color.g > published_color.r);
1141
1142 let deprecated_color = ModelStatus::Deprecated.color();
1144 assert!(deprecated_color.r > 0.8);
1145
1146 let archived_color = ModelStatus::Archived.color();
1148 assert!((archived_color.r - 0.5).abs() < 0.01);
1149 }
1150
1151 #[test]
1156 fn test_model_metric_debug() {
1157 let metric = ModelMetric::new("Accuracy", 0.95);
1158 let debug_str = format!("{metric:?}");
1159 assert!(debug_str.contains("Accuracy"));
1160 assert!(debug_str.contains("0.95"));
1161 }
1162
1163 #[test]
1164 fn test_model_metric_clone() {
1165 let metric = ModelMetric::new("F1", 0.88).unit("%").lower_is_better();
1166 let cloned = metric;
1167 assert_eq!(cloned.name, "F1");
1168 assert_eq!(cloned.value, 0.88);
1169 assert_eq!(cloned.unit, Some("%".to_string()));
1170 assert!(!cloned.higher_is_better);
1171 }
1172
1173 #[test]
1174 fn test_model_metric_serde() {
1175 let metric = ModelMetric::new("Loss", 0.05).lower_is_better();
1176 let serialized = serde_json::to_string(&metric).unwrap();
1177 let deserialized: ModelMetric = serde_json::from_str(&serialized).unwrap();
1178 assert_eq!(deserialized.name, "Loss");
1179 assert_eq!(deserialized.value, 0.05);
1180 assert!(!deserialized.higher_is_better);
1181 }
1182
1183 #[test]
1184 fn test_model_metric_formatted_value_negative() {
1185 let metric = ModelMetric::new("Correlation", -0.5);
1186 let formatted = metric.formatted_value();
1187 assert!(formatted.contains("-50.00%"));
1188 }
1189
1190 #[test]
1191 fn test_model_metric_formatted_value_zero() {
1192 let metric = ModelMetric::new("Bias", 0.0);
1193 assert_eq!(metric.formatted_value(), "0.00%");
1194 }
1195
1196 #[test]
1197 fn test_model_metric_formatted_value_exactly_one() {
1198 let metric = ModelMetric::new("Perfect", 1.0);
1199 assert_eq!(metric.formatted_value(), "1.00");
1200 }
1201
1202 #[test]
1207 fn test_model_card_debug() {
1208 let card = ModelCard::new("GPT-4");
1209 let debug_str = format!("{card:?}");
1210 assert!(debug_str.contains("GPT-4"));
1211 }
1212
1213 #[test]
1214 fn test_model_card_clone() {
1215 let card = ModelCard::new("BERT")
1216 .version("2.0.0")
1217 .status(ModelStatus::Published)
1218 .framework("PyTorch");
1219 let cloned = card;
1220 assert_eq!(cloned.get_name(), "BERT");
1221 assert_eq!(cloned.get_version(), "2.0.0");
1222 assert_eq!(cloned.get_status(), ModelStatus::Published);
1223 assert_eq!(cloned.get_framework(), Some("PyTorch"));
1224 }
1225
1226 #[test]
1227 fn test_model_card_serde() {
1228 let card = ModelCard::new("ResNet")
1229 .version("1.0.0")
1230 .status(ModelStatus::Draft);
1231 let serialized = serde_json::to_string(&card).unwrap();
1232 let deserialized: ModelCard = serde_json::from_str(&serialized).unwrap();
1233 assert_eq!(deserialized.get_name(), "ResNet");
1234 assert_eq!(deserialized.get_version(), "1.0.0");
1235 assert_eq!(deserialized.get_status(), ModelStatus::Draft);
1236 }
1237
1238 #[test]
1243 fn test_model_card_getters_none() {
1244 let card = ModelCard::new("test");
1245 assert!(card.get_description().is_none());
1246 assert!(card.get_framework().is_none());
1247 assert!(card.get_task().is_none());
1248 assert!(card.get_parameters().is_none());
1249 assert!(card.get_dataset().is_none());
1250 assert!(card.get_author().is_none());
1251 assert!(card.get_metadata("nonexistent").is_none());
1252 }
1253
1254 #[test]
1255 fn test_model_card_getters_some() {
1256 let card = ModelCard::new("test")
1257 .description("desc")
1258 .framework("TensorFlow")
1259 .task("classification")
1260 .parameters(1_000_000)
1261 .dataset("CIFAR-10")
1262 .author("ML Team")
1263 .metadata_entry("license", "Apache-2.0");
1264
1265 assert_eq!(card.get_description(), Some("desc"));
1266 assert_eq!(card.get_framework(), Some("TensorFlow"));
1267 assert_eq!(card.get_task(), Some("classification"));
1268 assert_eq!(card.get_parameters(), Some(1_000_000));
1269 assert_eq!(card.get_dataset(), Some("CIFAR-10"));
1270 assert_eq!(card.get_author(), Some("ML Team"));
1271 assert_eq!(card.get_metadata("license"), Some("Apache-2.0"));
1272 }
1273
1274 #[test]
1279 fn test_model_card_measure_with_tight_constraints() {
1280 let card = ModelCard::new("test").width(400.0).height(300.0);
1281 let size = card.measure(Constraints::tight(Size::new(200.0, 150.0)));
1282 assert_eq!(size.width, 200.0);
1283 assert_eq!(size.height, 150.0);
1284 }
1285
1286 #[test]
1291 fn test_model_card_empty_metrics() {
1292 let card = ModelCard::new("test").metrics(vec![]);
1293 assert!(!card.has_metrics());
1294 assert!(card.get_metrics().is_empty());
1295 }
1296
1297 #[test]
1298 fn test_model_card_many_metrics() {
1299 let metrics: Vec<ModelMetric> = (0..5)
1300 .map(|i| ModelMetric::new(format!("metric_{i}"), f64::from(i) * 0.1))
1301 .collect();
1302 let card = ModelCard::new("test").metrics(metrics);
1303 assert!(card.has_metrics());
1304 assert_eq!(card.get_metrics().len(), 5);
1305 }
1306
1307 #[test]
1308 fn test_model_card_empty_tags() {
1309 let tags: [&str; 0] = [];
1310 let card = ModelCard::new("test").tags(tags);
1311 assert!(card.get_tags().is_empty());
1312 }
1313
1314 #[test]
1315 fn test_model_card_show_metrics_chart_false() {
1316 let card = ModelCard::new("test")
1317 .metric(ModelMetric::new("acc", 0.95))
1318 .show_metrics_chart(false);
1319 assert!(card.has_metrics());
1320 }
1322
1323 #[test]
1324 fn test_model_card_default_colors() {
1325 let card = ModelCard::default();
1326 assert_eq!(card.background, Color::WHITE);
1327 }
1328
1329 #[test]
1330 fn test_model_card_default_values() {
1331 let card = ModelCard::default();
1332 assert!(card.name.is_empty());
1333 assert_eq!(card.version, "1.0.0");
1334 assert_eq!(card.status, ModelStatus::Draft);
1335 assert!(card.show_metrics_chart);
1336 assert_eq!(card.corner_radius, 8.0);
1337 }
1338
1339 #[test]
1344 fn test_formatted_parameters_edge_cases() {
1345 let card = ModelCard::new("test").parameters(1000);
1347 assert_eq!(card.formatted_parameters(), Some("1.0K".to_string()));
1348
1349 let card = ModelCard::new("test").parameters(1_000_000);
1351 assert_eq!(card.formatted_parameters(), Some("1.0M".to_string()));
1352
1353 let card = ModelCard::new("test").parameters(1_000_000_000);
1355 assert_eq!(card.formatted_parameters(), Some("1.0B".to_string()));
1356 }
1357}