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