1use crate::cbs::{self, HyperscaleSummary};
4use crate::repology;
5use serde::Serialize;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct Distros {
10 pub upstream: bool,
11 pub fedora_rawhide: bool,
12 pub fedora_stable: bool,
13 pub centos_stream: bool,
14 pub hyperscale_9: bool,
15 pub hyperscale_10: bool,
16}
17
18impl Distros {
19 pub fn all() -> Self {
21 Self {
22 upstream: true,
23 fedora_rawhide: true,
24 fedora_stable: true,
25 centos_stream: true,
26 hyperscale_9: true,
27 hyperscale_10: true,
28 }
29 }
30
31 pub fn parse(input: &str) -> Result<Self, String> {
36 let mut d = Self {
37 upstream: false,
38 fedora_rawhide: false,
39 fedora_stable: false,
40 centos_stream: false,
41 hyperscale_9: false,
42 hyperscale_10: false,
43 };
44 for token in input.split(',') {
45 match token.trim() {
46 "upstream" => d.upstream = true,
47 "fedora" => {
48 d.fedora_rawhide = true;
49 d.fedora_stable = true;
50 }
51 "fedora-rawhide" => d.fedora_rawhide = true,
52 "fedora-stable" => d.fedora_stable = true,
53 "centos" | "centos-stream" => d.centos_stream = true,
54 "hyperscale" | "hs" => {
55 d.hyperscale_9 = true;
56 d.hyperscale_10 = true;
57 }
58 "hs9" => d.hyperscale_9 = true,
59 "hs10" => d.hyperscale_10 = true,
60 other => return Err(format!("unknown distro: {other:?}")),
61 }
62 }
63 Ok(d)
64 }
65
66 fn needs_repology(&self) -> bool {
67 self.upstream || self.fedora_rawhide || self.fedora_stable || self.centos_stream
68 }
69
70 fn needs_cbs(&self) -> bool {
71 self.hyperscale_9 || self.hyperscale_10
72 }
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
77pub enum TrackRef {
78 Upstream,
79 FedoraRawhide,
80 FedoraStable,
81 CentosStream,
82}
83
84impl TrackRef {
85 pub fn parse(input: &str) -> Result<Self, String> {
90 match input.trim() {
91 "upstream" => Ok(Self::Upstream),
92 "fedora-rawhide" => Ok(Self::FedoraRawhide),
93 "fedora-stable" => Ok(Self::FedoraStable),
94 "centos" | "centos-stream" => Ok(Self::CentosStream),
95 other => Err(format!("unknown track reference: {other:?}")),
96 }
97 }
98
99 fn resolve(&self, packages: &[repology::Package]) -> Option<String> {
101 match self {
102 Self::Upstream => repology::find_newest(packages).map(|p| p.version.clone()),
103 Self::FedoraRawhide => {
104 repology::latest_for_repo(packages, "fedora_rawhide").map(|p| p.version.clone())
105 }
106 Self::FedoraStable => {
107 repology::latest_fedora_stable(packages).map(|p| p.version.clone())
108 }
109 Self::CentosStream => {
110 repology::latest_centos_stream(packages).map(|p| p.version.clone())
111 }
112 }
113 }
114}
115
116#[derive(Debug, Serialize)]
118pub struct HyperscaleResult {
119 #[serde(flatten)]
120 pub summary: HyperscaleSummary,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub newest_version: Option<bool>,
123}
124
125#[derive(Debug, Serialize)]
127pub struct CheckResult {
128 pub package: String,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub upstream: Option<String>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub fedora_rawhide: Option<String>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub fedora_stable: Option<VersionWithDetail>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub centos_stream: Option<VersionWithDetail>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub hs9: Option<HyperscaleResult>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub hs10: Option<HyperscaleResult>,
141 #[serde(skip)]
143 ref_version: Option<String>,
144}
145
146#[derive(Debug, Serialize)]
147pub struct VersionWithDetail {
148 pub version: String,
149 pub detail: String,
150}
151
152impl CheckResult {
153 pub fn is_outdated(&self) -> bool {
155 [&self.hs9, &self.hs10]
156 .iter()
157 .filter_map(|r| r.as_ref())
158 .any(|r| r.newest_version == Some(false))
159 }
160
161 pub fn ref_version(&self) -> Option<&str> {
163 self.ref_version.as_deref()
164 }
165}
166
167pub fn check(
172 repology_client: &repology::Client,
173 cbs_client: &cbs::Client,
174 package: &str,
175 repology_name: &str,
176 distros: &Distros,
177 track: &TrackRef,
178) -> Result<CheckResult, Box<dyn std::error::Error>> {
179 let mut result = CheckResult {
180 package: package.to_string(),
181 upstream: None,
182 fedora_rawhide: None,
183 fedora_stable: None,
184 centos_stream: None,
185 hs9: None,
186 hs10: None,
187 ref_version: None,
188 };
189
190 let fetch_repology = distros.needs_repology() || distros.needs_cbs();
192 let packages = if fetch_repology {
193 repology_client.get_project(repology_name)?
194 } else {
195 Vec::new()
196 };
197
198 if distros.upstream {
199 result.upstream = repology::find_newest(&packages).map(|p| p.version.clone());
200 }
201 if distros.fedora_rawhide {
202 result.fedora_rawhide = repology::latest_for_repo(&packages, "fedora_rawhide")
203 .map(|p| p.version.clone());
204 }
205 if distros.fedora_stable {
206 result.fedora_stable = repology::latest_fedora_stable(&packages).map(|p| {
207 VersionWithDetail {
208 version: p.version.clone(),
209 detail: p.repo.clone(),
210 }
211 });
212 }
213 if distros.centos_stream {
214 result.centos_stream =
215 repology::latest_centos_stream(&packages).map(|p| VersionWithDetail {
216 version: p.version.clone(),
217 detail: p.repo.clone(),
218 });
219 }
220
221 let ref_version = track.resolve(&packages);
222 result.ref_version = ref_version.clone();
223
224 if distros.needs_cbs() {
225 let builds = cbs_client
226 .get_package_id(package)?
227 .map(|id| cbs_client.list_builds(id))
228 .transpose()?;
229 let empty = Vec::new();
230 let builds = builds.as_deref().unwrap_or(&empty);
231
232 if distros.hyperscale_9 {
233 let summary = cbs_client.hyperscale_summary(builds, 9)?;
234 let newest_version = compute_newest_version(&summary, &ref_version);
235 result.hs9 = Some(HyperscaleResult {
236 summary,
237 newest_version,
238 });
239 }
240 if distros.hyperscale_10 {
241 let summary = cbs_client.hyperscale_summary(builds, 10)?;
242 let newest_version = compute_newest_version(&summary, &ref_version);
243 result.hs10 = Some(HyperscaleResult {
244 summary,
245 newest_version,
246 });
247 }
248 }
249
250 Ok(result)
251}
252
253fn compute_newest_version(summary: &HyperscaleSummary, ref_version: &Option<String>) -> Option<bool> {
259 let ref_ver = ref_version.as_ref()?;
260 let effective = summary.release.as_ref().or(summary.testing.as_ref())?;
261 Some(repology::version_cmp(&effective.version, ref_ver) != std::cmp::Ordering::Less)
262}
263
264struct Row {
266 distro: String,
267 version: String,
268 detail: String,
269 status: String,
270}
271
272fn result_to_rows(result: &CheckResult) -> Vec<Row> {
274 let mut rows = Vec::new();
275
276 if let Some(v) = &result.upstream {
277 rows.push(Row {
278 distro: "Upstream".into(),
279 version: v.clone(),
280 detail: String::new(),
281 status: String::new(),
282 });
283 }
284 if let Some(v) = &result.fedora_rawhide {
285 rows.push(Row {
286 distro: "Fedora Rawhide".into(),
287 version: v.clone(),
288 detail: String::new(),
289 status: String::new(),
290 });
291 }
292 if let Some(vd) = &result.fedora_stable {
293 rows.push(Row {
294 distro: "Fedora Stable".into(),
295 version: vd.version.clone(),
296 detail: vd.detail.clone(),
297 status: String::new(),
298 });
299 }
300 if let Some(vd) = &result.centos_stream {
301 rows.push(Row {
302 distro: "CentOS Stream".into(),
303 version: vd.version.clone(),
304 detail: vd.detail.clone(),
305 status: String::new(),
306 });
307 }
308 if let Some(hs_result) = &result.hs9 {
309 hs_rows(&mut rows, "Hyperscale 9", &hs_result.summary, result.ref_version.as_deref());
310 }
311 if let Some(hs_result) = &result.hs10 {
312 hs_rows(&mut rows, "Hyperscale 10", &hs_result.summary, result.ref_version.as_deref());
313 }
314
315 rows
316}
317
318fn version_status(version: &str, ref_version: Option<&str>) -> String {
319 match ref_version {
320 Some(ref_ver) => {
321 if repology::version_cmp(version, ref_ver) != std::cmp::Ordering::Less {
322 "newest".into()
323 } else {
324 "outdated".into()
325 }
326 }
327 None => String::new(),
328 }
329}
330
331fn hs_rows(rows: &mut Vec<Row>, label: &str, summary: &HyperscaleSummary, ref_version: Option<&str>) {
332 match (&summary.release, &summary.testing) {
333 (Some(rel), Some(test)) => {
334 rows.push(Row {
335 distro: format!("{label} (release)"),
336 version: rel.version.clone(),
337 detail: rel.nvr.clone(),
338 status: version_status(&rel.version, ref_version),
339 });
340 rows.push(Row {
341 distro: format!("{label} (testing)"),
342 version: test.version.clone(),
343 detail: test.nvr.clone(),
344 status: version_status(&test.version, ref_version),
345 });
346 }
347 (Some(rel), None) => {
348 rows.push(Row {
349 distro: label.into(),
350 version: rel.version.clone(),
351 detail: rel.nvr.clone(),
352 status: version_status(&rel.version, ref_version),
353 });
354 }
355 (None, Some(test)) => {
356 rows.push(Row {
357 distro: format!("{label} (testing)"),
358 version: test.version.clone(),
359 detail: test.nvr.clone(),
360 status: version_status(&test.version, ref_version),
361 });
362 }
363 (None, None) => {
364 rows.push(Row {
365 distro: label.into(),
366 version: "not found".into(),
367 detail: String::new(),
368 status: String::new(),
369 });
370 }
371 }
372}
373
374pub fn format_table(result: &CheckResult) -> String {
376 let mut buf = Vec::new();
377 let _ = write_table(result, &mut buf);
378 String::from_utf8(buf).unwrap_or_default()
379}
380
381pub fn print_table(result: &CheckResult) {
383 let _ = write_table(result, &mut std::io::stdout().lock());
384}
385
386pub fn print_json(result: &CheckResult) -> Result<(), Box<dyn std::error::Error>> {
388 write_json(result, &mut std::io::stdout().lock())?;
389 Ok(())
390}
391
392fn write_table(
393 result: &CheckResult,
394 w: &mut dyn std::io::Write,
395) -> std::io::Result<()> {
396 let rows = result_to_rows(result);
397 if rows.is_empty() {
398 return Ok(());
399 }
400
401 let distro_w = rows.iter().map(|r| r.distro.len()).max().unwrap_or(0).max("Distribution".len());
402 let version_w = rows.iter().map(|r| r.version.len()).max().unwrap_or(0).max("Version".len());
403 let has_status = rows.iter().any(|r| !r.status.is_empty());
404 let detail_w = rows
405 .iter()
406 .map(|r| r.detail.len())
407 .max()
408 .unwrap_or(0)
409 .max("Detail".len());
410
411 writeln!(w, "{}", result.package)?;
412 if has_status {
413 writeln!(
414 w,
415 " {:<distro_w$} {:<version_w$} {:<detail_w$} {}",
416 "Distribution", "Version", "Detail", "Status"
417 )?;
418 writeln!(
419 w,
420 " {:<distro_w$} {:<version_w$} {:<detail_w$} {}",
421 "─".repeat(distro_w),
422 "─".repeat(version_w),
423 "─".repeat(detail_w),
424 "──────"
425 )?;
426 } else {
427 writeln!(
428 w,
429 " {:<distro_w$} {:<version_w$} {}",
430 "Distribution", "Version", "Detail"
431 )?;
432 writeln!(
433 w,
434 " {:<distro_w$} {:<version_w$} {}",
435 "─".repeat(distro_w),
436 "─".repeat(version_w),
437 "──────"
438 )?;
439 }
440 for row in &rows {
441 if !row.status.is_empty() {
442 writeln!(
443 w,
444 " {:<distro_w$} {:<version_w$} {:<detail_w$} {}",
445 row.distro, row.version, row.detail, row.status
446 )?;
447 } else if row.detail.is_empty() {
448 writeln!(w, " {:<distro_w$} {}", row.distro, row.version)?;
449 } else {
450 writeln!(
451 w,
452 " {:<distro_w$} {:<version_w$} {}",
453 row.distro, row.version, row.detail
454 )?;
455 }
456 }
457 Ok(())
458}
459
460fn write_json(
461 result: &CheckResult,
462 w: &mut dyn std::io::Write,
463) -> Result<(), Box<dyn std::error::Error>> {
464 writeln!(w, "{}", serde_json::to_string_pretty(result)?)?;
465 Ok(())
466}
467
468#[cfg(test)]
469mod tests {
470 use super::*;
471 use crate::cbs::Build;
472
473 #[test]
474 fn test_distros_all() {
475 let d = Distros::all();
476 assert!(d.upstream);
477 assert!(d.fedora_rawhide);
478 assert!(d.fedora_stable);
479 assert!(d.centos_stream);
480 assert!(d.hyperscale_9);
481 assert!(d.hyperscale_10);
482 }
483
484 #[test]
485 fn test_distros_parse_single() {
486 let d = Distros::parse("upstream").unwrap();
487 assert!(d.upstream);
488 assert!(!d.fedora_rawhide);
489 assert!(!d.hyperscale_9);
490 }
491
492 #[test]
493 fn test_distros_parse_fedora_expands() {
494 let d = Distros::parse("fedora").unwrap();
495 assert!(d.fedora_rawhide);
496 assert!(d.fedora_stable);
497 assert!(!d.upstream);
498 }
499
500 #[test]
501 fn test_distros_parse_hyperscale_expands() {
502 let d = Distros::parse("hyperscale").unwrap();
503 assert!(d.hyperscale_9);
504 assert!(d.hyperscale_10);
505 assert!(!d.upstream);
506 }
507
508 #[test]
509 fn test_distros_parse_hs_alias() {
510 let d = Distros::parse("hs").unwrap();
511 assert!(d.hyperscale_9);
512 assert!(d.hyperscale_10);
513 }
514
515 #[test]
516 fn test_distros_parse_comma_separated() {
517 let d = Distros::parse("upstream,fedora-rawhide,hs10").unwrap();
518 assert!(d.upstream);
519 assert!(d.fedora_rawhide);
520 assert!(!d.fedora_stable);
521 assert!(d.hyperscale_10);
522 assert!(!d.hyperscale_9);
523 }
524
525 #[test]
526 fn test_distros_parse_centos_aliases() {
527 let d1 = Distros::parse("centos").unwrap();
528 assert!(d1.centos_stream);
529 let d2 = Distros::parse("centos-stream").unwrap();
530 assert!(d2.centos_stream);
531 }
532
533 #[test]
534 fn test_distros_parse_with_spaces() {
535 let d = Distros::parse("upstream , hs9").unwrap();
536 assert!(d.upstream);
537 assert!(d.hyperscale_9);
538 }
539
540 #[test]
541 fn test_distros_parse_unknown() {
542 let err = Distros::parse("upstream,bogus").unwrap_err();
543 assert!(err.contains("bogus"));
544 }
545
546 #[test]
547 fn test_needs_repology() {
548 let d = Distros::parse("hs9").unwrap();
549 assert!(!d.needs_repology());
550 assert!(d.needs_cbs());
551
552 let d = Distros::parse("upstream").unwrap();
553 assert!(d.needs_repology());
554 assert!(!d.needs_cbs());
555 }
556
557 fn make_build(version: &str, nvr: &str) -> Build {
558 Build {
559 build_id: 1,
560 name: "pkg".into(),
561 version: version.into(),
562 release: String::new(),
563 nvr: nvr.into(),
564 }
565 }
566
567 #[test]
568 fn test_track_ref_parse() {
569 assert_eq!(TrackRef::parse("upstream").unwrap(), TrackRef::Upstream);
570 assert_eq!(
571 TrackRef::parse("fedora-rawhide").unwrap(),
572 TrackRef::FedoraRawhide
573 );
574 assert_eq!(
575 TrackRef::parse("fedora-stable").unwrap(),
576 TrackRef::FedoraStable
577 );
578 assert_eq!(
579 TrackRef::parse("centos").unwrap(),
580 TrackRef::CentosStream
581 );
582 assert_eq!(
583 TrackRef::parse("centos-stream").unwrap(),
584 TrackRef::CentosStream
585 );
586 assert!(TrackRef::parse("bogus").is_err());
587 }
588
589 #[test]
590 fn test_track_ref_parse_trims_spaces() {
591 assert_eq!(
592 TrackRef::parse(" upstream ").unwrap(),
593 TrackRef::Upstream
594 );
595 }
596
597 fn make_hs_result(summary: HyperscaleSummary) -> HyperscaleResult {
598 HyperscaleResult {
599 summary,
600 newest_version: None,
601 }
602 }
603
604 #[test]
605 fn test_result_to_rows_all_fields() {
606 let result = CheckResult {
607 package: "ethtool".into(),
608 upstream: Some("6.19".into()),
609 fedora_rawhide: Some("6.19".into()),
610 fedora_stable: Some(VersionWithDetail {
611 version: "6.19".into(),
612 detail: "fedora_43".into(),
613 }),
614 centos_stream: Some(VersionWithDetail {
615 version: "6.15".into(),
616 detail: "centos_stream_10".into(),
617 }),
618 hs9: Some(make_hs_result(HyperscaleSummary {
619 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
620 testing: None,
621 })),
622 hs10: None,
623 ref_version: None,
624 };
625 let rows = result_to_rows(&result);
626 assert_eq!(rows.len(), 5);
627 assert_eq!(rows[0].distro, "Upstream");
628 assert_eq!(rows[0].version, "6.19");
629 assert_eq!(rows[3].distro, "CentOS Stream");
630 assert_eq!(rows[4].distro, "Hyperscale 9");
631 }
632
633 #[test]
634 fn test_result_to_rows_hs_testing_and_release() {
635 let result = CheckResult {
636 package: "systemd".into(),
637 upstream: None,
638 fedora_rawhide: None,
639 fedora_stable: None,
640 centos_stream: None,
641 hs9: Some(make_hs_result(HyperscaleSummary {
642 release: Some(make_build("258.5", "systemd-258.5-1.1.hs.el9")),
643 testing: Some(make_build("260~rc2", "systemd-260~rc2-20260309.hs.el9")),
644 })),
645 hs10: None,
646 ref_version: None,
647 };
648 let rows = result_to_rows(&result);
649 assert_eq!(rows.len(), 2);
650 assert_eq!(rows[0].distro, "Hyperscale 9 (release)");
651 assert_eq!(rows[0].version, "258.5");
652 assert_eq!(rows[1].distro, "Hyperscale 9 (testing)");
653 assert_eq!(rows[1].version, "260~rc2");
654 }
655
656 #[test]
657 fn test_result_to_rows_hs_testing_only() {
658 let result = CheckResult {
659 package: "pkg".into(),
660 upstream: None,
661 fedora_rawhide: None,
662 fedora_stable: None,
663 centos_stream: None,
664 hs9: Some(make_hs_result(HyperscaleSummary {
665 release: None,
666 testing: Some(make_build("1.0", "pkg-1.0-1.hs.el9")),
667 })),
668 hs10: None,
669 ref_version: None,
670 };
671 let rows = result_to_rows(&result);
672 assert_eq!(rows.len(), 1);
673 assert_eq!(rows[0].distro, "Hyperscale 9 (testing)");
674 }
675
676 #[test]
677 fn test_result_to_rows_hs_not_found() {
678 let result = CheckResult {
679 package: "pkg".into(),
680 upstream: None,
681 fedora_rawhide: None,
682 fedora_stable: None,
683 centos_stream: None,
684 hs9: Some(make_hs_result(HyperscaleSummary {
685 release: None,
686 testing: None,
687 })),
688 hs10: None,
689 ref_version: None,
690 };
691 let rows = result_to_rows(&result);
692 assert_eq!(rows.len(), 1);
693 assert_eq!(rows[0].distro, "Hyperscale 9");
694 assert_eq!(rows[0].version, "not found");
695 }
696
697 #[test]
698 fn test_result_to_rows_with_tracking_outdated() {
699 let result = CheckResult {
700 package: "ethtool".into(),
701 upstream: Some("6.19".into()),
702 fedora_rawhide: None,
703 fedora_stable: None,
704 centos_stream: None,
705 hs9: Some(make_hs_result(HyperscaleSummary {
706 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
707 testing: None,
708 })),
709 hs10: None,
710 ref_version: Some("6.19".into()),
711 };
712 let rows = result_to_rows(&result);
713 assert_eq!(rows[1].status, "outdated");
714 assert_eq!(rows[0].status, ""); }
716
717 #[test]
718 fn test_result_to_rows_with_tracking_newest() {
719 let result = CheckResult {
720 package: "ethtool".into(),
721 upstream: None,
722 fedora_rawhide: None,
723 fedora_stable: None,
724 centos_stream: None,
725 hs9: Some(make_hs_result(HyperscaleSummary {
726 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
727 testing: None,
728 })),
729 hs10: None,
730 ref_version: Some("6.15".into()),
731 };
732 let rows = result_to_rows(&result);
733 assert_eq!(rows[0].status, "newest");
734 }
735
736 #[test]
737 fn test_result_to_rows_tracking_per_build() {
738 let result = CheckResult {
740 package: "systemd".into(),
741 upstream: None,
742 fedora_rawhide: None,
743 fedora_stable: None,
744 centos_stream: None,
745 hs9: Some(make_hs_result(HyperscaleSummary {
746 release: Some(make_build("258.5", "systemd-258.5-1.1.hs.el9")),
747 testing: Some(make_build("260", "systemd-260-1.hs.el9")),
748 })),
749 hs10: None,
750 ref_version: Some("260".into()),
751 };
752 let rows = result_to_rows(&result);
753 assert_eq!(rows[0].distro, "Hyperscale 9 (release)");
754 assert_eq!(rows[0].status, "outdated");
755 assert_eq!(rows[1].distro, "Hyperscale 9 (testing)");
756 assert_eq!(rows[1].status, "newest");
757 }
758
759 #[test]
760 fn test_compute_newest_version_matches() {
761 let summary = HyperscaleSummary {
762 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
763 testing: None,
764 };
765 assert_eq!(
766 compute_newest_version(&summary, &Some("6.15".into())),
767 Some(true)
768 );
769 }
770
771 #[test]
772 fn test_compute_newest_version_outdated() {
773 let summary = HyperscaleSummary {
774 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
775 testing: None,
776 };
777 assert_eq!(
778 compute_newest_version(&summary, &Some("6.19".into())),
779 Some(false)
780 );
781 }
782
783 #[test]
784 fn test_compute_newest_version_ahead() {
785 let summary = HyperscaleSummary {
786 release: Some(make_build("6.19", "pkg-6.19-1.hs.el9")),
787 testing: None,
788 };
789 assert_eq!(
790 compute_newest_version(&summary, &Some("6.18.16".into())),
791 Some(true)
792 );
793 }
794
795 #[test]
796 fn test_compute_newest_version_no_ref() {
797 let summary = HyperscaleSummary {
798 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
799 testing: None,
800 };
801 assert_eq!(compute_newest_version(&summary, &None), None);
802 }
803
804 #[test]
805 fn test_compute_newest_version_uses_testing_fallback() {
806 let summary = HyperscaleSummary {
807 release: None,
808 testing: Some(make_build("6.19", "ethtool-6.19-1.hs.el9")),
809 };
810 assert_eq!(
811 compute_newest_version(&summary, &Some("6.19".into())),
812 Some(true)
813 );
814 }
815
816 #[test]
817 fn test_compute_newest_version_no_builds() {
818 let summary = HyperscaleSummary {
819 release: None,
820 testing: None,
821 };
822 assert_eq!(
823 compute_newest_version(&summary, &Some("6.19".into())),
824 None
825 );
826 }
827
828 #[test]
829 fn test_json_serialization() {
830 let result = CheckResult {
831 package: "ethtool".into(),
832 upstream: Some("6.19".into()),
833 fedora_rawhide: None,
834 fedora_stable: None,
835 centos_stream: None,
836 hs9: None,
837 hs10: None,
838 ref_version: None,
839 };
840 let json = serde_json::to_value(&result).unwrap();
841 assert_eq!(json["package"], "ethtool");
842 assert_eq!(json["upstream"], "6.19");
843 assert!(json.get("fedora_rawhide").is_none());
845 assert!(json.get("hs9").is_none());
846 }
847
848 #[test]
849 fn test_json_serialization_with_newest_version() {
850 let result = CheckResult {
851 package: "ethtool".into(),
852 upstream: Some("6.19".into()),
853 fedora_rawhide: None,
854 fedora_stable: None,
855 centos_stream: None,
856 hs9: Some(HyperscaleResult {
857 summary: HyperscaleSummary {
858 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
859 testing: None,
860 },
861 newest_version: Some(false),
862 }),
863 hs10: None,
864 ref_version: Some("6.19".into()),
865 };
866 let json = serde_json::to_value(&result).unwrap();
867 assert_eq!(json["hs9"]["newest_version"], false);
868 assert_eq!(json["hs9"]["release"]["version"], "6.15");
869 assert!(json.get("ref_version").is_none());
871 }
872
873 #[test]
874 fn test_is_outdated_true() {
875 let result = CheckResult {
876 package: "pkg".into(),
877 upstream: None,
878 fedora_rawhide: None,
879 fedora_stable: None,
880 centos_stream: None,
881 hs9: Some(HyperscaleResult {
882 summary: HyperscaleSummary {
883 release: Some(make_build("1.0", "pkg-1.0-1.hs.el9")),
884 testing: None,
885 },
886 newest_version: Some(false),
887 }),
888 hs10: None,
889 ref_version: Some("2.0".into()),
890 };
891 assert!(result.is_outdated());
892 assert_eq!(result.ref_version(), Some("2.0"));
893 }
894
895 #[test]
896 fn test_is_outdated_false_when_newest() {
897 let result = CheckResult {
898 package: "pkg".into(),
899 upstream: None,
900 fedora_rawhide: None,
901 fedora_stable: None,
902 centos_stream: None,
903 hs9: Some(HyperscaleResult {
904 summary: HyperscaleSummary {
905 release: Some(make_build("2.0", "pkg-2.0-1.hs.el9")),
906 testing: None,
907 },
908 newest_version: Some(true),
909 }),
910 hs10: None,
911 ref_version: Some("2.0".into()),
912 };
913 assert!(!result.is_outdated());
914 }
915
916 #[test]
917 fn test_is_outdated_false_when_no_hs() {
918 let result = CheckResult {
919 package: "pkg".into(),
920 upstream: Some("2.0".into()),
921 fedora_rawhide: None,
922 fedora_stable: None,
923 centos_stream: None,
924 hs9: None,
925 hs10: None,
926 ref_version: None,
927 };
928 assert!(!result.is_outdated());
929 assert_eq!(result.ref_version(), None);
930 }
931
932 #[test]
933 fn test_is_outdated_mixed_hs9_hs10() {
934 let result = CheckResult {
935 package: "pkg".into(),
936 upstream: None,
937 fedora_rawhide: None,
938 fedora_stable: None,
939 centos_stream: None,
940 hs9: Some(HyperscaleResult {
941 summary: HyperscaleSummary {
942 release: Some(make_build("2.0", "pkg-2.0-1.hs.el9")),
943 testing: None,
944 },
945 newest_version: Some(true),
946 }),
947 hs10: Some(HyperscaleResult {
948 summary: HyperscaleSummary {
949 release: Some(make_build("1.0", "pkg-1.0-1.hs.el10")),
950 testing: None,
951 },
952 newest_version: Some(false),
953 }),
954 ref_version: Some("2.0".into()),
955 };
956 assert!(result.is_outdated());
957 }
958
959 #[test]
960 fn test_format_table() {
961 let result = CheckResult {
962 package: "ethtool".into(),
963 upstream: Some("6.19".into()),
964 fedora_rawhide: None,
965 fedora_stable: None,
966 centos_stream: None,
967 hs9: Some(make_hs_result(HyperscaleSummary {
968 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
969 testing: None,
970 })),
971 hs10: None,
972 ref_version: Some("6.19".into()),
973 };
974 let table = format_table(&result);
975 assert!(table.contains("ethtool"));
976 assert!(table.contains("Upstream"));
977 assert!(table.contains("outdated"));
978 }
979
980 #[test]
981 fn test_write_table_with_status() {
982 let result = CheckResult {
983 package: "ethtool".into(),
984 upstream: Some("6.19".into()),
985 fedora_rawhide: None,
986 fedora_stable: None,
987 centos_stream: None,
988 hs9: Some(make_hs_result(HyperscaleSummary {
989 release: Some(make_build("6.15", "ethtool-6.15-3.hs.el9")),
990 testing: None,
991 })),
992 hs10: None,
993 ref_version: Some("6.19".into()),
994 };
995 let mut buf = Vec::new();
996 write_table(&result, &mut buf).unwrap();
997 let output = String::from_utf8(buf).unwrap();
998 assert!(output.contains("ethtool"));
999 assert!(output.contains("Upstream"));
1000 assert!(output.contains("6.19"));
1001 assert!(output.contains("outdated"));
1002 assert!(output.contains("Status"));
1003 }
1004
1005 #[test]
1006 fn test_write_table_without_status() {
1007 let result = CheckResult {
1008 package: "pkg".into(),
1009 upstream: Some("1.0".into()),
1010 fedora_rawhide: None,
1011 fedora_stable: Some(VersionWithDetail {
1012 version: "1.0".into(),
1013 detail: "fedora_43".into(),
1014 }),
1015 centos_stream: None,
1016 hs9: None,
1017 hs10: None,
1018 ref_version: None,
1019 };
1020 let mut buf = Vec::new();
1021 write_table(&result, &mut buf).unwrap();
1022 let output = String::from_utf8(buf).unwrap();
1023 assert!(output.contains("pkg"));
1024 assert!(output.contains("Upstream"));
1025 assert!(output.contains("fedora_43"));
1026 assert!(!output.contains("Status"));
1027 }
1028
1029 #[test]
1030 fn test_write_table_empty() {
1031 let result = CheckResult {
1032 package: "pkg".into(),
1033 upstream: None,
1034 fedora_rawhide: None,
1035 fedora_stable: None,
1036 centos_stream: None,
1037 hs9: None,
1038 hs10: None,
1039 ref_version: None,
1040 };
1041 let mut buf = Vec::new();
1042 write_table(&result, &mut buf).unwrap();
1043 assert!(buf.is_empty());
1044 }
1045
1046 #[test]
1047 fn test_write_json() {
1048 let result = CheckResult {
1049 package: "ethtool".into(),
1050 upstream: Some("6.19".into()),
1051 fedora_rawhide: None,
1052 fedora_stable: None,
1053 centos_stream: None,
1054 hs9: None,
1055 hs10: None,
1056 ref_version: None,
1057 };
1058 let mut buf = Vec::new();
1059 write_json(&result, &mut buf).unwrap();
1060 let output = String::from_utf8(buf).unwrap();
1061 let json: serde_json::Value = serde_json::from_str(&output).unwrap();
1062 assert_eq!(json["package"], "ethtool");
1063 assert_eq!(json["upstream"], "6.19");
1064 }
1065
1066 #[test]
1067 fn test_json_serialization_without_tracking() {
1068 let result = CheckResult {
1069 package: "pkg".into(),
1070 upstream: None,
1071 fedora_rawhide: None,
1072 fedora_stable: None,
1073 centos_stream: None,
1074 hs9: Some(HyperscaleResult {
1075 summary: HyperscaleSummary {
1076 release: Some(make_build("1.0", "pkg-1.0-1.hs.el9")),
1077 testing: None,
1078 },
1079 newest_version: None,
1080 }),
1081 hs10: None,
1082 ref_version: None,
1083 };
1084 let json = serde_json::to_value(&result).unwrap();
1085 assert!(json["hs9"].get("newest_version").is_none());
1087 }
1088}