1use perfgate_render::{format_metric_with_statistic, format_pct, format_value};
8use perfgate_types::{CompareReceipt, Metric, MetricStatus, PerfgateReport, VerdictStatus};
9use std::path::PathBuf;
10
11const COLOR_PASS: &str = "#4c1";
14const COLOR_WARN: &str = "#dfb317";
15const COLOR_FAIL: &str = "#e05d44";
16const COLOR_SKIP: &str = "#9f9f9f";
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum BadgeType {
23 Status,
25 Metric,
27 Trend,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum BadgeStyle {
34 #[default]
36 Flat,
37 FlatSquare,
39}
40
41#[derive(Debug, Clone)]
43pub struct BadgeRequest {
44 pub input_path: PathBuf,
46 pub badge_type: BadgeType,
48 pub style: BadgeStyle,
50 pub metric: Option<String>,
52 pub output_path: Option<PathBuf>,
54}
55
56#[derive(Debug, Clone)]
58pub struct BadgeOutcome {
59 pub svg: String,
61}
62
63#[derive(Debug, Clone, PartialEq)]
65pub struct Badge {
66 pub label: String,
67 pub message: String,
68 pub color: String,
69 pub style: BadgeStyle,
70}
71
72pub struct BadgeUseCase;
75
76pub enum BadgeInput {
79 Compare(CompareReceipt),
80 Report(PerfgateReport),
81}
82
83impl BadgeUseCase {
84 pub fn execute(
86 &self,
87 input: &BadgeInput,
88 badge_type: BadgeType,
89 style: BadgeStyle,
90 metric_name: Option<&str>,
91 ) -> anyhow::Result<BadgeOutcome> {
92 let badge = match badge_type {
93 BadgeType::Status => status_badge(input, style),
94 BadgeType::Metric => {
95 let name = metric_name.ok_or_else(|| {
96 anyhow::anyhow!("--metric is required when --type metric is used")
97 })?;
98 metric_badge(input, style, name)?
99 }
100 BadgeType::Trend => trend_badge(input, style),
101 };
102 Ok(BadgeOutcome {
103 svg: render_svg(&badge),
104 })
105 }
106}
107
108fn status_badge(input: &BadgeInput, style: BadgeStyle) -> Badge {
111 let (status_label, color) = match input {
112 BadgeInput::Compare(c) => verdict_status_label_color(c.verdict.status),
113 BadgeInput::Report(r) => verdict_status_label_color(r.verdict.status),
114 };
115 Badge {
116 label: "performance".to_string(),
117 message: status_label.to_string(),
118 color: color.to_string(),
119 style,
120 }
121}
122
123fn metric_badge(input: &BadgeInput, style: BadgeStyle, metric_name: &str) -> anyhow::Result<Badge> {
124 let compare = match input {
125 BadgeInput::Compare(c) => c,
126 BadgeInput::Report(r) => r
127 .compare
128 .as_ref()
129 .ok_or_else(|| anyhow::anyhow!("report has no compare receipt (no baseline?)"))?,
130 };
131
132 let metric = Metric::parse_key(metric_name)
133 .ok_or_else(|| anyhow::anyhow!("unknown metric: {metric_name}"))?;
134
135 let delta = compare
136 .deltas
137 .get(&metric)
138 .ok_or_else(|| anyhow::anyhow!("metric {metric_name} not found in deltas"))?;
139
140 let label = format_metric_with_statistic(metric, delta.statistic);
141 let value_str = format_value(metric, delta.current);
142 let unit = metric.display_unit();
143 let pct = format_pct(delta.pct);
144 let message = format!("{value_str} {unit} ({pct})");
145 let color = metric_status_color(delta.status).to_string();
146
147 Ok(Badge {
148 label,
149 message,
150 color,
151 style,
152 })
153}
154
155fn trend_badge(input: &BadgeInput, style: BadgeStyle) -> Badge {
156 let compare = match input {
157 BadgeInput::Compare(c) => Some(c),
158 BadgeInput::Report(r) => r.compare.as_ref(),
159 };
160
161 let (trend_label, color) = match compare {
162 Some(c) => {
163 let worst = worst_metric_status(c);
164 match worst {
165 MetricStatus::Pass => ("stable", COLOR_PASS),
166 MetricStatus::Warn => ("degraded", COLOR_WARN),
167 MetricStatus::Fail => ("regressed", COLOR_FAIL),
168 MetricStatus::Skip => ("unknown", COLOR_SKIP),
169 }
170 }
171 None => ("unknown", COLOR_SKIP),
172 };
173
174 Badge {
175 label: "perf trend".to_string(),
176 message: trend_label.to_string(),
177 color: color.to_string(),
178 style,
179 }
180}
181
182fn verdict_status_label_color(status: VerdictStatus) -> (&'static str, &'static str) {
185 match status {
186 VerdictStatus::Pass => ("passing", COLOR_PASS),
187 VerdictStatus::Warn => ("warning", COLOR_WARN),
188 VerdictStatus::Fail => ("failing", COLOR_FAIL),
189 VerdictStatus::Skip => ("skipped", COLOR_SKIP),
190 }
191}
192
193fn metric_status_color(status: MetricStatus) -> &'static str {
194 match status {
195 MetricStatus::Pass => COLOR_PASS,
196 MetricStatus::Warn => COLOR_WARN,
197 MetricStatus::Fail => COLOR_FAIL,
198 MetricStatus::Skip => COLOR_SKIP,
199 }
200}
201
202fn worst_metric_status(c: &CompareReceipt) -> MetricStatus {
203 let mut worst = MetricStatus::Pass;
204 for delta in c.deltas.values() {
205 worst = match (worst, delta.status) {
206 (MetricStatus::Fail, _) | (_, MetricStatus::Fail) => MetricStatus::Fail,
207 (MetricStatus::Warn, _) | (_, MetricStatus::Warn) => MetricStatus::Warn,
208 (MetricStatus::Skip, _) | (_, MetricStatus::Skip) => MetricStatus::Skip,
209 _ => MetricStatus::Pass,
210 };
211 }
212 if c.deltas.is_empty() {
213 return MetricStatus::Skip;
214 }
215 worst
216}
217
218pub fn text_width(text: &str) -> f64 {
224 let mut w: f64 = 0.0;
225 for ch in text.chars() {
226 w += char_width(ch);
227 }
228 w
229}
230
231fn char_width(ch: char) -> f64 {
234 match ch {
235 'i' | 'l' | '!' | '|' | ',' | '.' | ':' | ';' | '\'' => 3.7,
237 'I' | 'j' | 'f' | 'r' | 't' | '(' | ')' | '[' | ']' | '{' | '}' => 4.5,
238 '1' => 5.0,
240 ' ' | '-' | '_' => 5.0,
242 'M' | 'W' => 9.5,
244 'm' | 'w' => 8.5,
245 'A'..='Z' => 7.5,
247 'a'..='z' | '0'..='9' => 6.5,
249 '+' | '=' | '<' | '>' | '~' | '^' | '%' | '#' | '@' | '&' | '*' | '/' | '\\' | '?'
251 | '$' => 6.5,
252 _ => 6.5,
253 }
254}
255
256pub fn render_svg(badge: &Badge) -> String {
260 let label_width = text_width(&badge.label) + 10.0; let msg_width = text_width(&badge.message) + 10.0;
262 let total_width = label_width + msg_width;
263
264 let label_x = label_width / 2.0;
265 let msg_x = label_width + msg_width / 2.0;
266
267 let radius = match badge.style {
268 BadgeStyle::Flat => 3,
269 BadgeStyle::FlatSquare => 0,
270 };
271
272 let gradient = match badge.style {
273 BadgeStyle::Flat => {
274 r##"<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>"##
275 }
276 BadgeStyle::FlatSquare => "",
277 };
278
279 let gradient_fill = match badge.style {
280 BadgeStyle::Flat => r##"<rect rx="3" width="{tw}" height="20" fill="url(#s)"/>"##,
281 BadgeStyle::FlatSquare => "",
282 };
283
284 let gradient_overlay = gradient_fill.replace("{tw}", &format!("{total_width:.0}"));
286
287 format!(
288 r##"<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="{tw:.0}" height="20" role="img" aria-label="{label}: {msg}"><title>{label}: {msg}</title>{gradient}<clipPath id="r"><rect width="{tw:.0}" height="20" rx="{radius}" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="{lw:.0}" height="20" fill="#555"/><rect x="{lw:.0}" width="{mw:.0}" height="20" fill="{color}"/>{gradient_overlay}</g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="{lx:.0}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="{ltl:.0}">{label}</text><text x="{lx:.0}" y="140" transform="scale(.1)" fill="#fff" textLength="{ltl:.0}">{label}</text><text aria-hidden="true" x="{mx:.0}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="{mtl:.0}">{msg}</text><text x="{mx:.0}" y="140" transform="scale(.1)" fill="#fff" textLength="{mtl:.0}">{msg}</text></g></svg>"##,
289 tw = total_width,
290 lw = label_width,
291 mw = msg_width,
292 color = badge.color,
293 gradient = gradient,
294 gradient_overlay = gradient_overlay,
295 radius = radius,
296 lx = label_x * 10.0,
297 mx = msg_x * 10.0,
298 ltl = (label_width - 10.0) * 10.0,
299 mtl = (msg_width - 10.0) * 10.0,
300 label = xml_escape(&badge.label),
301 msg = xml_escape(&badge.message),
302 )
303}
304
305fn xml_escape(s: &str) -> String {
306 s.replace('&', "&")
307 .replace('<', "<")
308 .replace('>', ">")
309 .replace('"', """)
310 .replace('\'', "'")
311}
312
313#[cfg(test)]
316mod tests {
317 use super::*;
318 use perfgate_types::{
319 BenchMeta, Budget, CompareRef, Delta, Direction, MetricStatistic, ToolInfo, Verdict,
320 VerdictCounts,
321 };
322 use std::collections::BTreeMap;
323
324 fn make_compare(verdict_status: VerdictStatus, metric_status: MetricStatus) -> CompareReceipt {
325 let mut budgets = BTreeMap::new();
326 budgets.insert(Metric::WallMs, Budget::new(0.2, 0.1, Direction::Lower));
327
328 let mut deltas = BTreeMap::new();
329 deltas.insert(
330 Metric::WallMs,
331 Delta {
332 baseline: 100.0,
333 current: 115.0,
334 ratio: 1.15,
335 pct: 0.15,
336 regression: 0.15,
337 statistic: MetricStatistic::Median,
338 significance: None,
339 cv: None,
340 noise_threshold: None,
341 status: metric_status,
342 },
343 );
344
345 CompareReceipt {
346 schema: perfgate_types::COMPARE_SCHEMA_V1.to_string(),
347 tool: ToolInfo {
348 name: "perfgate".into(),
349 version: "0.1.0".into(),
350 },
351 bench: BenchMeta {
352 name: "bench".into(),
353 cwd: None,
354 command: vec!["true".into()],
355 repeat: 1,
356 warmup: 0,
357 work_units: None,
358 timeout_ms: None,
359 },
360 baseline_ref: CompareRef {
361 path: None,
362 run_id: None,
363 },
364 current_ref: CompareRef {
365 path: None,
366 run_id: None,
367 },
368 budgets,
369 deltas,
370 verdict: Verdict {
371 status: verdict_status,
372 counts: VerdictCounts {
373 pass: if verdict_status == VerdictStatus::Pass {
374 1
375 } else {
376 0
377 },
378 warn: if verdict_status == VerdictStatus::Warn {
379 1
380 } else {
381 0
382 },
383 fail: if verdict_status == VerdictStatus::Fail {
384 1
385 } else {
386 0
387 },
388 skip: 0,
389 },
390 reasons: vec![],
391 },
392 }
393 }
394
395 fn make_report(verdict_status: VerdictStatus) -> PerfgateReport {
396 let compare = make_compare(verdict_status, MetricStatus::Pass);
397 PerfgateReport {
398 report_type: perfgate_types::REPORT_SCHEMA_V1.to_string(),
399 verdict: compare.verdict.clone(),
400 compare: Some(compare),
401 findings: vec![],
402 summary: perfgate_types::ReportSummary {
403 pass_count: 1,
404 warn_count: 0,
405 fail_count: 0,
406 skip_count: 0,
407 total_count: 1,
408 },
409 profile_path: None,
410 }
411 }
412
413 #[test]
416 fn verdict_pass_is_green() {
417 let (label, color) = verdict_status_label_color(VerdictStatus::Pass);
418 assert_eq!(label, "passing");
419 assert_eq!(color, COLOR_PASS);
420 }
421
422 #[test]
423 fn verdict_warn_is_yellow() {
424 let (label, color) = verdict_status_label_color(VerdictStatus::Warn);
425 assert_eq!(label, "warning");
426 assert_eq!(color, COLOR_WARN);
427 }
428
429 #[test]
430 fn verdict_fail_is_red() {
431 let (label, color) = verdict_status_label_color(VerdictStatus::Fail);
432 assert_eq!(label, "failing");
433 assert_eq!(color, COLOR_FAIL);
434 }
435
436 #[test]
437 fn verdict_skip_is_grey() {
438 let (label, color) = verdict_status_label_color(VerdictStatus::Skip);
439 assert_eq!(label, "skipped");
440 assert_eq!(color, COLOR_SKIP);
441 }
442
443 #[test]
444 fn metric_status_colors_match() {
445 assert_eq!(metric_status_color(MetricStatus::Pass), COLOR_PASS);
446 assert_eq!(metric_status_color(MetricStatus::Warn), COLOR_WARN);
447 assert_eq!(metric_status_color(MetricStatus::Fail), COLOR_FAIL);
448 assert_eq!(metric_status_color(MetricStatus::Skip), COLOR_SKIP);
449 }
450
451 #[test]
454 fn text_width_empty_is_zero() {
455 assert!((text_width("") - 0.0).abs() < f64::EPSILON);
456 }
457
458 #[test]
459 fn text_width_increases_with_length() {
460 let short = text_width("hi");
461 let long = text_width("performance");
462 assert!(long > short, "long={long}, short={short}");
463 }
464
465 #[test]
466 fn narrow_chars_are_narrower_than_wide() {
467 let narrow = text_width("iii");
468 let wide = text_width("MMM");
469 assert!(wide > narrow, "wide={wide}, narrow={narrow}");
470 }
471
472 #[test]
475 fn svg_contains_label_and_message() {
476 let badge = Badge {
477 label: "performance".into(),
478 message: "passing".into(),
479 color: COLOR_PASS.into(),
480 style: BadgeStyle::Flat,
481 };
482 let svg = render_svg(&badge);
483 assert!(svg.contains("performance"), "missing label");
484 assert!(svg.contains("passing"), "missing message");
485 assert!(svg.contains(COLOR_PASS), "missing color");
486 assert!(svg.starts_with("<svg"), "not an SVG");
487 }
488
489 #[test]
490 fn flat_square_has_zero_radius() {
491 let badge = Badge {
492 label: "test".into(),
493 message: "ok".into(),
494 color: COLOR_PASS.into(),
495 style: BadgeStyle::FlatSquare,
496 };
497 let svg = render_svg(&badge);
498 assert!(svg.contains(r#"rx="0""#), "expected rx=0 for flat-square");
499 }
500
501 #[test]
502 fn flat_has_rounded_radius() {
503 let badge = Badge {
504 label: "test".into(),
505 message: "ok".into(),
506 color: COLOR_PASS.into(),
507 style: BadgeStyle::Flat,
508 };
509 let svg = render_svg(&badge);
510 assert!(svg.contains(r#"rx="3""#), "expected rx=3 for flat");
511 }
512
513 #[test]
514 fn svg_escapes_special_characters() {
515 let badge = Badge {
516 label: "a<b".into(),
517 message: "c&d".into(),
518 color: COLOR_PASS.into(),
519 style: BadgeStyle::Flat,
520 };
521 let svg = render_svg(&badge);
522 assert!(svg.contains("a<b"), "< not escaped");
523 assert!(svg.contains("c&d"), "& not escaped");
524 }
525
526 #[test]
529 fn status_badge_from_compare_pass() {
530 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
531 let badge = status_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat);
532 assert_eq!(badge.label, "performance");
533 assert_eq!(badge.message, "passing");
534 assert_eq!(badge.color, COLOR_PASS);
535 }
536
537 #[test]
538 fn status_badge_from_compare_fail() {
539 let compare = make_compare(VerdictStatus::Fail, MetricStatus::Fail);
540 let badge = status_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat);
541 assert_eq!(badge.message, "failing");
542 assert_eq!(badge.color, COLOR_FAIL);
543 }
544
545 #[test]
546 fn status_badge_from_report() {
547 let report = make_report(VerdictStatus::Warn);
548 let badge = status_badge(&BadgeInput::Report(report), BadgeStyle::FlatSquare);
549 assert_eq!(badge.message, "warning");
550 assert_eq!(badge.color, COLOR_WARN);
551 assert_eq!(badge.style, BadgeStyle::FlatSquare);
552 }
553
554 #[test]
557 fn metric_badge_from_compare() {
558 let compare = make_compare(VerdictStatus::Warn, MetricStatus::Warn);
559 let badge =
560 metric_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat, "wall_ms").unwrap();
561 assert_eq!(badge.label, "wall_ms");
562 assert!(badge.message.contains("115"), "missing current value");
563 assert!(badge.message.contains("ms"), "missing unit");
564 assert!(
565 badge.message.contains("+15.00%"),
566 "missing pct: {}",
567 badge.message
568 );
569 assert_eq!(badge.color, COLOR_WARN);
570 }
571
572 #[test]
573 fn metric_badge_unknown_metric_errors() {
574 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
575 let result = metric_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat, "no_such");
576 assert!(result.is_err());
577 }
578
579 #[test]
580 fn metric_badge_missing_delta_errors() {
581 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
582 let result = metric_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat, "cpu_ms");
583 assert!(result.is_err());
584 }
585
586 #[test]
587 fn metric_badge_from_report_without_compare_errors() {
588 let report = PerfgateReport {
589 report_type: perfgate_types::REPORT_SCHEMA_V1.to_string(),
590 verdict: Verdict {
591 status: VerdictStatus::Pass,
592 counts: VerdictCounts {
593 pass: 0,
594 warn: 0,
595 fail: 0,
596 skip: 0,
597 },
598 reasons: vec![],
599 },
600 compare: None,
601 findings: vec![],
602 summary: perfgate_types::ReportSummary {
603 pass_count: 0,
604 warn_count: 0,
605 fail_count: 0,
606 skip_count: 0,
607 total_count: 0,
608 },
609 profile_path: None,
610 };
611 let result = metric_badge(&BadgeInput::Report(report), BadgeStyle::Flat, "wall_ms");
612 assert!(result.is_err());
613 }
614
615 #[test]
618 fn trend_badge_stable_when_all_pass() {
619 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
620 let badge = trend_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat);
621 assert_eq!(badge.label, "perf trend");
622 assert_eq!(badge.message, "stable");
623 assert_eq!(badge.color, COLOR_PASS);
624 }
625
626 #[test]
627 fn trend_badge_degraded_when_warn() {
628 let compare = make_compare(VerdictStatus::Warn, MetricStatus::Warn);
629 let badge = trend_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat);
630 assert_eq!(badge.message, "degraded");
631 assert_eq!(badge.color, COLOR_WARN);
632 }
633
634 #[test]
635 fn trend_badge_regressed_when_fail() {
636 let compare = make_compare(VerdictStatus::Fail, MetricStatus::Fail);
637 let badge = trend_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat);
638 assert_eq!(badge.message, "regressed");
639 assert_eq!(badge.color, COLOR_FAIL);
640 }
641
642 #[test]
643 fn trend_badge_unknown_when_no_compare() {
644 let report = PerfgateReport {
645 report_type: perfgate_types::REPORT_SCHEMA_V1.to_string(),
646 verdict: Verdict {
647 status: VerdictStatus::Skip,
648 counts: VerdictCounts {
649 pass: 0,
650 warn: 0,
651 fail: 0,
652 skip: 0,
653 },
654 reasons: vec![],
655 },
656 compare: None,
657 findings: vec![],
658 summary: perfgate_types::ReportSummary {
659 pass_count: 0,
660 warn_count: 0,
661 fail_count: 0,
662 skip_count: 0,
663 total_count: 0,
664 },
665 profile_path: None,
666 };
667 let badge = trend_badge(&BadgeInput::Report(report), BadgeStyle::Flat);
668 assert_eq!(badge.message, "unknown");
669 assert_eq!(badge.color, COLOR_SKIP);
670 }
671
672 #[test]
673 fn trend_badge_empty_deltas_is_unknown() {
674 let mut compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
675 compare.deltas.clear();
676 let badge = trend_badge(&BadgeInput::Compare(compare), BadgeStyle::Flat);
677 assert_eq!(badge.message, "unknown");
678 }
679
680 #[test]
683 fn usecase_status_from_compare() {
684 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
685 let uc = BadgeUseCase;
686 let outcome = uc
687 .execute(
688 &BadgeInput::Compare(compare),
689 BadgeType::Status,
690 BadgeStyle::Flat,
691 None,
692 )
693 .unwrap();
694 assert!(outcome.svg.starts_with("<svg"));
695 assert!(outcome.svg.contains("passing"));
696 }
697
698 #[test]
699 fn usecase_metric_requires_metric_name() {
700 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
701 let uc = BadgeUseCase;
702 let result = uc.execute(
703 &BadgeInput::Compare(compare),
704 BadgeType::Metric,
705 BadgeStyle::Flat,
706 None,
707 );
708 assert!(result.is_err());
709 }
710
711 #[test]
712 fn usecase_metric_with_name() {
713 let compare = make_compare(VerdictStatus::Pass, MetricStatus::Pass);
714 let uc = BadgeUseCase;
715 let outcome = uc
716 .execute(
717 &BadgeInput::Compare(compare),
718 BadgeType::Metric,
719 BadgeStyle::Flat,
720 Some("wall_ms"),
721 )
722 .unwrap();
723 assert!(outcome.svg.contains("wall_ms"));
724 }
725
726 #[test]
727 fn usecase_trend() {
728 let compare = make_compare(VerdictStatus::Fail, MetricStatus::Fail);
729 let uc = BadgeUseCase;
730 let outcome = uc
731 .execute(
732 &BadgeInput::Compare(compare),
733 BadgeType::Trend,
734 BadgeStyle::FlatSquare,
735 None,
736 )
737 .unwrap();
738 assert!(outcome.svg.contains("regressed"));
739 }
740
741 #[test]
744 fn xml_escape_covers_all_entities() {
745 let raw = r#"<>&"'"#;
746 let escaped = xml_escape(raw);
747 assert_eq!(escaped, "<>&"'");
748 }
749
750 #[test]
753 fn worst_metric_status_picks_fail_over_warn() {
754 let mut compare = make_compare(VerdictStatus::Fail, MetricStatus::Warn);
755 compare.deltas.insert(
756 Metric::MaxRssKb,
757 Delta {
758 baseline: 100.0,
759 current: 200.0,
760 ratio: 2.0,
761 pct: 1.0,
762 regression: 1.0,
763 statistic: MetricStatistic::Median,
764 significance: None,
765 cv: None,
766 noise_threshold: None,
767 status: MetricStatus::Fail,
768 },
769 );
770 assert_eq!(worst_metric_status(&compare), MetricStatus::Fail);
771 }
772}