1use crate::util::data_structures::{HashMap, HashSet};
5use std::cmp::Ordering;
6use std::time::Duration;
7
8use cargo_util_schemas::core::PartialVersion;
9
10use crate::context::CargoResolverConfig;
11use crate::context::GlobalRegistryConfig;
12use crate::context::IncompatiblePublishAge;
13use crate::context::RegistryConfig;
14use crate::sources::CRATES_IO_REGISTRY;
15use crate::util::CargoResult;
16use crate::util::GlobalContext;
17use crate::util::interning::InternedString;
18use crate::util::time_span::parse_time_span;
19use crate::workspace::Dependency;
20use crate::workspace::PackageId;
21use crate::workspace::SourceId;
22use crate::workspace::Summary;
23
24#[derive(Default)]
32pub struct VersionPreferences {
33 try_to_use: HashSet<PackageId>,
34 prefer_patch_deps: HashMap<InternedString, HashSet<Dependency>>,
35 version_ordering: VersionOrdering,
36 rust_versions: Vec<PartialVersion>,
37 publish_time: Option<jiff::Timestamp>,
38 publish_age: Option<PublishAgePolicy>,
39}
40
41#[derive(Copy, Clone, Default, PartialEq, Eq, Hash, Debug)]
42pub enum VersionOrdering {
43 #[default]
44 MaximumVersionsFirst,
45 MinimumVersionsFirst,
46}
47
48impl VersionPreferences {
49 pub fn prefer_package_id(&mut self, pkg_id: PackageId) {
51 self.try_to_use.insert(pkg_id);
52 }
53
54 pub fn prefer_dependency(&mut self, dep: Dependency) {
56 self.prefer_patch_deps
57 .entry(dep.package_name())
58 .or_insert_with(HashSet::default)
59 .insert(dep);
60 }
61
62 pub fn version_ordering(&mut self, ordering: VersionOrdering) {
63 self.version_ordering = ordering;
64 }
65
66 pub fn rust_versions(&mut self, vers: Vec<PartialVersion>) {
67 self.rust_versions = vers;
68 }
69
70 pub fn publish_time(&mut self, publish_time: jiff::Timestamp) {
71 self.publish_time = Some(publish_time);
72 }
73
74 pub fn publish_age(&mut self, policy: PublishAgePolicy) {
75 self.publish_age = Some(policy);
76 }
77
78 pub fn too_new(&self, summary: &Summary) -> Option<PublishAgeViolation> {
81 self.publish_age.as_ref()?.too_new(summary)
82 }
83
84 pub fn should_prefer(&self, pkg_id: &PackageId) -> bool {
86 self.try_to_use.contains(pkg_id)
87 || self
88 .prefer_patch_deps
89 .get(&pkg_id.name())
90 .map(|deps| deps.iter().any(|d| d.matches_id(*pkg_id)))
91 .unwrap_or(false)
92 }
93
94 pub fn sort_summaries(
107 &self,
108 summaries: &mut Vec<Summary>,
109 first_version: Option<VersionOrdering>,
110 ) {
111 if let Some(max_publish_time) = self.publish_time {
112 summaries.retain(|s| {
113 if let Some(summary_publish_time) = s.pubtime() {
114 summary_publish_time <= max_publish_time
115 } else {
116 true
117 }
118 });
119 }
120 summaries.sort_unstable_by(|a, b| {
121 let prefer_a = self.should_prefer(&a.package_id());
122 let prefer_b = self.should_prefer(&b.package_id());
123 let previous_cmp = prefer_a.cmp(&prefer_b).reverse();
124 if previous_cmp != Ordering::Equal {
125 return previous_cmp;
126 }
127
128 if !self.rust_versions.is_empty() {
129 let a_compat_count = self.msrv_compat_count(a);
130 let b_compat_count = self.msrv_compat_count(b);
131 if b_compat_count != a_compat_count {
132 return b_compat_count.cmp(&a_compat_count);
133 }
134 }
135
136 let cmp = a.version().cmp(b.version());
137 match first_version.unwrap_or(self.version_ordering) {
138 VersionOrdering::MaximumVersionsFirst => cmp.reverse(),
139 VersionOrdering::MinimumVersionsFirst => cmp,
140 }
141 });
142 if first_version.is_some() && !summaries.is_empty() {
143 let _ = summaries.split_off(1);
144 }
145 }
146
147 fn msrv_compat_count(&self, summary: &Summary) -> usize {
148 let Some(rust_version) = summary.rust_version() else {
149 return self.rust_versions.len();
150 };
151
152 self.rust_versions
153 .iter()
154 .filter(|max| rust_version.is_compatible_with(max))
155 .count()
156 }
157}
158
159#[derive(Debug)]
161pub struct PublishAgePolicy {
162 invocation_time: jiff::Timestamp,
164 global: MinPublishAge,
166 per_registry: HashMap<String, MinPublishAge>,
168}
169
170impl PublishAgePolicy {
171 pub fn new(now: Option<jiff::Timestamp>, gctx: &GlobalContext) -> CargoResult<Option<Self>> {
178 let resolver_config = gctx.get::<Option<CargoResolverConfig>>("resolver")?;
179 if resolver_config
180 .and_then(|c| c.incompatible_publish_age)
181 .is_some_and(|v| v == IncompatiblePublishAge::Allow)
182 {
183 return Ok(None);
184 }
185
186 Self::for_report(now, gctx)
187 }
188
189 pub fn for_report(
192 now: Option<jiff::Timestamp>,
193 gctx: &GlobalContext,
194 ) -> CargoResult<Option<Self>> {
195 let parse = |key: &str, config: Option<String>| -> CargoResult<MinPublishAge> {
196 let Some(config) = config else {
197 return Ok(MinPublishAge::Unset);
198 };
199 if config == "0" {
200 return Ok(MinPublishAge::None);
201 }
202 let duration = parse_time_span(&config)
203 .map_err(|e| anyhow::format_err!("invalid value for `{key}`: {e}"))?;
204 Ok(MinPublishAge::Age(duration, config))
205 };
206
207 let registry = gctx.get::<Option<GlobalRegistryConfig>>("registry")?;
208 let global = parse(
209 "registry.global-min-publish-age",
210 registry.and_then(|r| r.global_min_publish_age),
211 )?;
212 let mut per_registry = HashMap::default();
213 if let Some(registries) =
214 gctx.get::<Option<HashMap<String, RegistryConfig>>>("registries")?
215 {
216 for (name, config) in registries {
217 let limit = parse(
218 &format!("registries.{name}.min-publish-age"),
219 config.min_publish_age,
220 )?;
221 if limit.is_set() {
222 per_registry.insert(name, limit);
223 }
224 }
225 }
226
227 let nothing_configured = !global.is_set() && per_registry.is_empty();
228 if nothing_configured {
229 return Ok(None);
230 }
231
232 Ok(Some(Self {
233 invocation_time: now.unwrap_or_else(|| gctx.invocation_time()),
234 global,
235 per_registry,
236 }))
237 }
238
239 pub fn too_new(&self, summary: &Summary) -> Option<PublishAgeViolation> {
243 let pubtime = summary.pubtime()?;
244 let MinPublishAge::Age(min_age, config) = self.min_age(summary.source_id()) else {
245 return None;
246 };
247
248 let max_pubtime = jiff::SignedDuration::try_from(*min_age)
249 .ok()
250 .and_then(|min_age| self.invocation_time.checked_sub(min_age).ok());
251
252 let age = self.invocation_time.duration_since(pubtime);
253 let publish_age = || PublishAgeViolation {
254 age,
255 config: config.clone(),
256 };
257
258 match max_pubtime {
259 Some(max_pubtime) => (pubtime > max_pubtime).then(publish_age),
260 None => Some(publish_age()),
261 }
262 }
263
264 fn min_age(&self, source_id: SourceId) -> &MinPublishAge {
271 let name = source_id.alt_registry_key().or_else(|| {
272 source_id.is_crates_io().then_some(CRATES_IO_REGISTRY)
278 });
279 if let Some(min_age) = name
280 .and_then(|name| self.per_registry.get(name))
281 .filter(|min_age| min_age.is_set())
282 {
283 return min_age;
284 }
285
286 &self.global
287 }
288
289 pub fn common_min_publish_age(&self) -> Option<PublishAgeViolation> {
291 if !self.per_registry.is_empty() {
292 return None;
293 }
294
295 if let MinPublishAge::Age(age, config) = &self.global {
296 jiff::SignedDuration::try_from(*age).ok().and_then(|age| {
297 Some(PublishAgeViolation {
298 age,
299 config: config.clone(),
300 })
301 })
302 } else {
303 None
304 }
305 }
306}
307
308#[derive(Debug, Clone)]
310enum MinPublishAge {
311 Unset,
313 None,
315 Age(Duration, String),
317}
318
319impl MinPublishAge {
320 fn is_set(&self) -> bool {
322 !matches!(self, MinPublishAge::Unset)
323 }
324}
325
326#[derive(Debug, Clone, PartialEq)]
328pub struct PublishAgeViolation {
329 age: jiff::SignedDuration,
331 config: String,
333}
334
335impl PublishAgeViolation {
336 pub fn age_label(&self) -> String {
339 format_age_as_single_unit(self.age)
340 }
341
342 pub fn config(&self) -> &str {
344 &self.config
345 }
346
347 pub fn note(&self) -> String {
350 let age = self.age_label();
351 let config = self.config();
352 format!("published {age} ago, minimum age {config}",)
353 }
354}
355
356fn format_age_as_single_unit(age: jiff::SignedDuration) -> String {
358 use jiff::Unit;
359 use jiff::fmt::friendly::Designator;
360 use jiff::fmt::friendly::Spacing;
361 use jiff::fmt::friendly::SpanPrinter;
362
363 if age <= jiff::SignedDuration::ZERO {
365 return "moments".to_string();
366 }
367
368 let rounded = jiff::Span::try_from(age).and_then(|span| {
369 let unit = if age >= jiff::SignedDuration::from_hours(48) {
370 Unit::Day
371 } else if age >= jiff::SignedDuration::from_hours(1) {
372 Unit::Hour
373 } else if age >= jiff::SignedDuration::from_mins(1) {
374 Unit::Minute
375 } else {
376 Unit::Second
377 };
378 let opts = jiff::SpanRound::new()
379 .largest(unit)
380 .smallest(unit)
381 .relative(jiff::SpanRelativeTo::days_are_24_hours());
382 span.round(opts)
383 });
384
385 let printer = SpanPrinter::new()
386 .designator(Designator::Verbose)
387 .spacing(Spacing::BetweenUnitsAndDesignators);
388
389 match rounded {
390 Ok(span) => printer.span_to_string(&span).to_string(),
391 Err(e) => {
392 tracing::warn!("failed to round `{age}`: {e}");
393 format!("{} seconds", age.as_secs())
394 }
395 }
396}
397
398#[cfg(test)]
399mod test {
400 use super::*;
401 use crate::sources::CRATES_IO_INDEX;
402 use crate::sources::CRATES_IO_REGISTRY;
403 use crate::util::IntoUrl as _;
404 use crate::workspace::SourceId;
405
406 use std::collections::BTreeMap;
407
408 fn pkgid(name: &str, version: &str) -> PackageId {
409 let src_id =
410 SourceId::from_url("registry+https://github.com/rust-lang/crates.io-index").unwrap();
411 PackageId::try_new(name, version, src_id).unwrap()
412 }
413
414 fn dep(name: &str, version: &str) -> Dependency {
415 let src_id =
416 SourceId::from_url("registry+https://github.com/rust-lang/crates.io-index").unwrap();
417 Dependency::parse(name, Some(version), src_id).unwrap()
418 }
419
420 fn summ(name: &str, version: &str, msrv: Option<&str>) -> Summary {
421 let pkg_id = pkgid(name, version);
422 let features = BTreeMap::new();
423 Summary::new(
424 pkg_id,
425 Vec::new(),
426 &features,
427 None::<&String>,
428 msrv.map(|m| m.parse().unwrap()),
429 )
430 .unwrap()
431 }
432
433 fn describe(summaries: &Vec<Summary>) -> String {
434 let strs: Vec<String> = summaries
435 .iter()
436 .map(|summary| format!("{}/{}", summary.name(), summary.version()))
437 .collect();
438 strs.join(", ")
439 }
440
441 #[test]
442 fn test_prefer_package_id() {
443 let mut vp = VersionPreferences::default();
444 vp.prefer_package_id(pkgid("foo", "1.2.3"));
445
446 let mut summaries = vec![
447 summ("foo", "1.2.4", None),
448 summ("foo", "1.2.3", None),
449 summ("foo", "1.1.0", None),
450 summ("foo", "1.0.9", None),
451 ];
452
453 vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
454 vp.sort_summaries(&mut summaries, None);
455 assert_eq!(
456 describe(&summaries),
457 "foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string()
458 );
459
460 vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
461 vp.sort_summaries(&mut summaries, None);
462 assert_eq!(
463 describe(&summaries),
464 "foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string()
465 );
466 }
467
468 #[test]
469 fn test_prefer_dependency() {
470 let mut vp = VersionPreferences::default();
471 vp.prefer_dependency(dep("foo", "=1.2.3"));
472
473 let mut summaries = vec![
474 summ("foo", "1.2.4", None),
475 summ("foo", "1.2.3", None),
476 summ("foo", "1.1.0", None),
477 summ("foo", "1.0.9", None),
478 ];
479
480 vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
481 vp.sort_summaries(&mut summaries, None);
482 assert_eq!(
483 describe(&summaries),
484 "foo/1.2.3, foo/1.2.4, foo/1.1.0, foo/1.0.9".to_string()
485 );
486
487 vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
488 vp.sort_summaries(&mut summaries, None);
489 assert_eq!(
490 describe(&summaries),
491 "foo/1.2.3, foo/1.0.9, foo/1.1.0, foo/1.2.4".to_string()
492 );
493 }
494
495 #[test]
496 fn test_prefer_both() {
497 let mut vp = VersionPreferences::default();
498 vp.prefer_package_id(pkgid("foo", "1.2.3"));
499 vp.prefer_dependency(dep("foo", "=1.1.0"));
500
501 let mut summaries = vec![
502 summ("foo", "1.2.4", None),
503 summ("foo", "1.2.3", None),
504 summ("foo", "1.1.0", None),
505 summ("foo", "1.0.9", None),
506 ];
507
508 vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
509 vp.sort_summaries(&mut summaries, None);
510 assert_eq!(
511 describe(&summaries),
512 "foo/1.2.3, foo/1.1.0, foo/1.2.4, foo/1.0.9".to_string()
513 );
514
515 vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
516 vp.sort_summaries(&mut summaries, None);
517 assert_eq!(
518 describe(&summaries),
519 "foo/1.1.0, foo/1.2.3, foo/1.0.9, foo/1.2.4".to_string()
520 );
521 }
522
523 #[test]
524 fn test_single_rust_version() {
525 let mut vp = VersionPreferences::default();
526 vp.rust_versions(vec!["1.50".parse().unwrap()]);
527
528 let mut summaries = vec![
529 summ("foo", "1.2.4", None),
530 summ("foo", "1.2.3", Some("1.60")),
531 summ("foo", "1.2.2", None),
532 summ("foo", "1.2.1", Some("1.50")),
533 summ("foo", "1.2.0", None),
534 summ("foo", "1.1.0", Some("1.40")),
535 summ("foo", "1.0.9", None),
536 ];
537
538 vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
539 vp.sort_summaries(&mut summaries, None);
540 assert_eq!(
541 describe(&summaries),
542 "foo/1.2.4, foo/1.2.2, foo/1.2.1, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.3"
543 .to_string()
544 );
545
546 vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
547 vp.sort_summaries(&mut summaries, None);
548 assert_eq!(
549 describe(&summaries),
550 "foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.1, foo/1.2.2, foo/1.2.4, foo/1.2.3"
551 .to_string()
552 );
553 }
554
555 #[test]
556 fn test_multiple_rust_versions() {
557 let mut vp = VersionPreferences::default();
558 vp.rust_versions(vec!["1.45".parse().unwrap(), "1.55".parse().unwrap()]);
559
560 let mut summaries = vec![
561 summ("foo", "1.2.4", None),
562 summ("foo", "1.2.3", Some("1.60")),
563 summ("foo", "1.2.2", None),
564 summ("foo", "1.2.1", Some("1.50")),
565 summ("foo", "1.2.0", None),
566 summ("foo", "1.1.0", Some("1.40")),
567 summ("foo", "1.0.9", None),
568 ];
569
570 vp.version_ordering(VersionOrdering::MaximumVersionsFirst);
571 vp.sort_summaries(&mut summaries, None);
572 assert_eq!(
573 describe(&summaries),
574 "foo/1.2.4, foo/1.2.2, foo/1.2.0, foo/1.1.0, foo/1.0.9, foo/1.2.1, foo/1.2.3"
575 .to_string()
576 );
577
578 vp.version_ordering(VersionOrdering::MinimumVersionsFirst);
579 vp.sort_summaries(&mut summaries, None);
580 assert_eq!(
581 describe(&summaries),
582 "foo/1.0.9, foo/1.1.0, foo/1.2.0, foo/1.2.2, foo/1.2.4, foo/1.2.1, foo/1.2.3"
583 .to_string()
584 );
585 }
586
587 #[test]
588 fn test_empty_summaries() {
589 let vp = VersionPreferences::default();
590 let mut summaries = vec![];
591
592 vp.sort_summaries(&mut summaries, Some(VersionOrdering::MaximumVersionsFirst));
593 assert_eq!(summaries, vec![]);
594 }
595
596 const NOW: &str = "2006-08-08T00:00:00Z";
597
598 fn age(raw: &str) -> MinPublishAge {
599 MinPublishAge::Age(parse_time_span(raw).unwrap(), raw.to_string())
600 }
601
602 fn hours(n: i64) -> jiff::SignedDuration {
603 jiff::SignedDuration::from_hours(n)
604 }
605
606 fn policy(global: MinPublishAge, per_registry: &[(&str, MinPublishAge)]) -> PublishAgePolicy {
607 PublishAgePolicy {
608 invocation_time: NOW.parse().unwrap(),
609 global,
610 per_registry: per_registry
611 .iter()
612 .map(|(name, age)| (name.to_string(), age.clone()))
613 .collect(),
614 }
615 }
616
617 fn crates_io_source() -> SourceId {
618 let url = CRATES_IO_INDEX.into_url().unwrap();
619 SourceId::for_alt_registry(&url, CRATES_IO_REGISTRY).unwrap()
620 }
621
622 fn alt_source() -> SourceId {
623 let url = "https://example.com/index".into_url().unwrap();
624 SourceId::for_alt_registry(&url, "alt").unwrap()
625 }
626
627 fn published(source: SourceId, age: jiff::SignedDuration) -> Summary {
630 let pkg_id = PackageId::try_new("foo", "1.0.0", source).unwrap();
631 let mut summary =
632 Summary::new(pkg_id, Vec::new(), &BTreeMap::new(), None::<&String>, None).unwrap();
633 let now: jiff::Timestamp = NOW.parse().unwrap();
634 summary.set_pubtime(now - age);
635 summary
636 }
637
638 #[test]
639 fn publish_age_reports_exact_age() {
640 let p = policy(age("7 days"), &[]);
641 let violation = p.too_new(&published(crates_io_source(), hours(50)));
642 assert_eq!(
643 violation,
644 Some(PublishAgeViolation {
645 age: hours(50),
646 config: "7 days".to_string(),
647 })
648 );
649 }
650
651 #[test]
652 fn publish_age_older_than_threshold_is_acceptable() {
653 let p = policy(age("7 days"), &[]);
654 let violation = p.too_new(&published(crates_io_source(), hours(10 * 24)));
655 assert_eq!(violation, None);
656 }
657
658 #[test]
659 fn publish_age_at_threshold_boundary_is_acceptable() {
660 let p = policy(age("7 days"), &[]);
661 let violation = p.too_new(&published(crates_io_source(), hours(7 * 24)));
662 assert_eq!(violation, None);
663 }
664
665 #[test]
666 fn publish_age_just_inside_threshold_is_too_new() {
667 let p = policy(age("7 days"), &[]);
668 let violation = p.too_new(&published(crates_io_source(), hours(7 * 24 - 1)));
669 assert_eq!(
670 violation,
671 Some(PublishAgeViolation {
672 age: hours(7 * 24 - 1),
673 config: "7 days".to_string(),
674 })
675 );
676 }
677
678 #[test]
679 fn publish_age_per_registry_overrides_global() {
680 let p = policy(age("30 days"), &[("alt", age("1 day"))]);
681 let violation = p.too_new(&published(alt_source(), hours(2 * 24)));
682 assert_eq!(violation, None);
683 }
684
685 #[test]
686 fn publish_age_crates_io_scope_excludes_alt_registry() {
687 let p = policy(age("1 day"), &[(CRATES_IO_REGISTRY, age("30 days"))]);
688 let crates_io = p.too_new(&published(crates_io_source(), hours(2 * 24)));
689 let alt = p.too_new(&published(alt_source(), hours(2 * 24)));
690 assert_eq!(
691 crates_io,
692 Some(PublishAgeViolation {
693 age: hours(2 * 24),
694 config: "30 days".to_string(),
695 })
696 );
697 assert_eq!(alt, None);
698 }
699
700 #[test]
701 fn publish_age_alt_registry_falls_through_to_global() {
702 let p = policy(age("7 days"), &[]);
703 let violation = p.too_new(&published(alt_source(), hours(2 * 24)));
704 assert_eq!(
705 violation,
706 Some(PublishAgeViolation {
707 age: hours(2 * 24),
708 config: "7 days".to_string(),
709 })
710 );
711 }
712
713 #[test]
714 fn publish_age_per_registry_too_new() {
715 let p = policy(MinPublishAge::Unset, &[("alt", age("7 days"))]);
716 let violation = p.too_new(&published(alt_source(), hours(2 * 24)));
717 assert_eq!(
718 violation,
719 Some(PublishAgeViolation {
720 age: hours(2 * 24),
721 config: "7 days".to_string(),
722 })
723 );
724 }
725
726 #[test]
727 fn publish_age_per_registry_zero_overrides_global() {
728 let p = policy(age("30 days"), &[("alt", MinPublishAge::None)]);
729 let violation = p.too_new(&published(alt_source(), hours(0)));
730 assert_eq!(violation, None);
731 }
732
733 #[test]
734 fn publish_age_no_applicable_scope_is_acceptable() {
735 let p = policy(MinPublishAge::Unset, &[(CRATES_IO_REGISTRY, age("7 days"))]);
736 let violation = p.too_new(&published(alt_source(), hours(0)));
737 assert_eq!(violation, None);
738 }
739
740 #[test]
741 fn publish_age_zero_disables_threshold() {
742 let p = policy(MinPublishAge::None, &[]);
743 let violation = p.too_new(&published(crates_io_source(), hours(0)));
744 assert_eq!(violation, None);
745 }
746
747 #[test]
748 fn publish_age_zero_stops_scope_fallthrough() {
749 let p = policy(age("30 days"), &[(CRATES_IO_REGISTRY, MinPublishAge::None)]);
750 let violation = p.too_new(&published(crates_io_source(), hours(0)));
751 assert_eq!(violation, None);
752 }
753
754 #[test]
755 fn publish_age_missing_pubtime_is_acceptable() {
756 let p = policy(age("7 days"), &[]);
757 let pkg_id = PackageId::try_new("foo", "1.0.0", crates_io_source()).unwrap();
758 let summary =
759 Summary::new(pkg_id, Vec::new(), &BTreeMap::new(), None::<&String>, None).unwrap();
760 let violation = p.too_new(&summary);
761 assert_eq!(violation, None);
762 }
763
764 #[test]
765 fn publish_age_future_pubtime_is_too_new() {
766 let p = policy(age("7 days"), &[]);
767 let violation = p.too_new(&published(crates_io_source(), hours(-24)));
768 assert_eq!(
769 violation,
770 Some(PublishAgeViolation {
771 age: hours(-24),
772 config: "7 days".to_string(),
773 })
774 );
775 }
776
777 #[test]
778 fn publish_age_out_of_range_threshold_is_too_new() {
779 let p = policy(age("18446744073709551615 seconds"), &[]);
781 let violation = p.too_new(&published(crates_io_source(), hours(24)));
782 assert_eq!(
783 violation,
784 Some(PublishAgeViolation {
785 age: hours(24),
786 config: "18446744073709551615 seconds".to_string(),
787 })
788 );
789 }
790
791 #[track_caller]
792 fn assert_age(secs: i64, expected: &str) {
793 assert_eq!(
794 format_age_as_single_unit(jiff::SignedDuration::from_secs(secs)),
795 expected
796 );
797 }
798
799 const MIN: i64 = 60;
800 const HOUR: i64 = 60 * MIN;
801 const DAY: i64 = 24 * HOUR;
802
803 #[test]
804 fn rounds_to_a_single_unit() {
805 assert_age(2 * DAY, "2 days");
807 assert_age(2 * DAY + 8 * HOUR + 23 * MIN, "2 days");
808 assert_age(2 * DAY + 13 * HOUR, "3 days");
809 assert_age(540 * DAY, "540 days");
810
811 assert_age(47 * HOUR, "47 hours");
813 assert_age(24 * HOUR, "24 hours");
814 assert_age(11 * HOUR + 40 * MIN, "12 hours");
815 assert_age(11 * HOUR + 20 * MIN, "11 hours");
816 assert_age(HOUR, "1 hour");
817
818 assert_age(40 * MIN, "40 minutes");
820 assert_age(MIN, "1 minute");
821
822 assert_age(40, "40 seconds");
824 assert_age(1, "1 second");
825
826 assert_age(0, "moments");
828 assert_age(-20, "moments");
829 assert_age(-2 * DAY, "moments");
830 }
831}