use crate::progress::ProgressSender;
use deps_core::ConcreteVersion;
use deps_core::Deprecation;
use deps_core::FetchFailure;
use deps_core::PackageName;
use deps_core::PackageVersions;
use deps_core::Registry;
use deps_core::RemovalStatus;
use deps_core::Version;
use deps_core::VersionReq;
use std::collections::{HashMap, HashSet};
use std::num::NonZeroUsize;
use std::sync::Arc;
use std::time::Duration;
pub type DepSources = Vec<(PackageName, deps_core::parser::DependencySource)>;
pub fn dedup_dependencies_by_source(
parse_result: &dyn deps_core::ParseResult,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) -> (
HashMap<PackageName, deps_core::parser::DependencySource>,
HashSet<PackageName>,
) {
use std::collections::hash_map::Entry;
let mut by_name: HashMap<PackageName, deps_core::parser::DependencySource> = HashMap::new();
let mut collided: HashSet<PackageName> = HashSet::new();
for dep in parse_result
.dependencies()
.into_iter()
.filter(|dep| formatter.can_resolve_source(&dep.source()))
{
let name = dep.name().clone();
let source = dep.source();
match by_name.entry(name.clone()) {
Entry::Vacant(entry) => {
entry.insert(source);
}
Entry::Occupied(entry) => {
if *entry.get() != source && collided.insert(name.clone()) {
tracing::warn!(
package = %name.for_tracing(),
source_a = ?entry.get(),
source_b = ?source,
"dependency declared against two different resolved registries; \
skipping version resolution for all occurrences"
);
}
}
}
}
for name in &collided {
by_name.remove(name);
}
(by_name, collided)
}
#[non_exhaustive]
pub struct FetchPreparation {
pub dep_sources: DepSources,
pub in_use: HashMap<PackageName, Vec<ConcreteVersion>>,
pub selection_context: deps_core::SelectionContext,
pub collided_names: HashSet<PackageName>,
}
pub fn prepare_fetch(
parse_result: &dyn deps_core::ParseResult,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
ecosystem: deps_core::EcosystemId,
resolved_versions: &HashMap<PackageName, ConcreteVersion>,
resolved_version_candidates: &HashMap<PackageName, Vec<ConcreteVersion>>,
) -> FetchPreparation {
let (sources_map, collided_names) = dedup_dependencies_by_source(parse_result, formatter);
let dep_sources: DepSources = sources_map.into_iter().collect();
let in_use = crate::classify::resolved::collect_in_use_versions(
parse_result,
resolved_versions,
resolved_version_candidates,
formatter,
ecosystem,
);
FetchPreparation {
dep_sources,
in_use,
selection_context: parse_result.selection_context(),
collided_names,
}
}
#[non_exhaustive]
pub struct FetchResult {
pub versions: HashMap<PackageName, PackageVersions>,
pub yanked_versions: HashMap<PackageName, (ConcreteVersion, RemovalStatus)>,
pub deprecations: HashMap<PackageName, Deprecation>,
pub fetch_failed: HashMap<PackageName, FetchFailure>,
pub no_comparable_versions: HashSet<PackageName>,
pub failure_summary: Option<FailureSummary>,
pub licenses: HashMap<PackageName, Vec<String>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FailureSummary {
count: NonZeroUsize,
message: String,
}
impl FailureSummary {
#[must_use]
pub fn new(count: NonZeroUsize, message: String) -> Self {
Self { count, message }
}
#[must_use]
pub fn count(&self) -> usize {
self.count.get()
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
impl FetchResult {
#[must_use]
#[allow(clippy::too_many_arguments)]
pub fn new(
versions: HashMap<PackageName, PackageVersions>,
yanked_versions: HashMap<PackageName, (ConcreteVersion, RemovalStatus)>,
deprecations: HashMap<PackageName, Deprecation>,
fetch_failed: HashMap<PackageName, FetchFailure>,
no_comparable_versions: HashSet<PackageName>,
failure_summary: Option<FailureSummary>,
licenses: HashMap<PackageName, Vec<String>>,
) -> Self {
Self {
versions,
yanked_versions,
deprecations,
fetch_failed,
no_comparable_versions,
failure_summary,
licenses,
}
}
#[must_use]
pub fn failed_count(&self) -> usize {
self.failure_summary
.as_ref()
.map_or(0, FailureSummary::count)
}
#[must_use]
pub fn failure_message(&self) -> Option<&str> {
self.failure_summary.as_ref().map(FailureSummary::message)
}
}
#[allow(
clippy::too_many_arguments,
reason = "internal (non-pub) call-site-controlled fetch tuning + ecosystem-context \
parameters; grouping into a config struct would only move, not reduce, the \
per-call-site churn across this module's ~15 production and test call sites"
)]
pub async fn fetch_latest_versions_parallel(
registry: Arc<dyn Registry>,
package_sources: DepSources,
in_use: &HashMap<PackageName, Vec<ConcreteVersion>>,
progress_sender: Option<ProgressSender>,
freshness: deps_core::freshness::FreshnessSettings,
timeout_secs: u64,
max_concurrent: usize,
selection_context: &deps_core::SelectionContext,
gossip: Option<&HashMap<PackageName, deps_core::GossipFindings>>,
) -> FetchResult {
use futures::stream::{self, StreamExt};
use std::time::Duration;
let fetched = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let timeout = Duration::from_secs(timeout_secs);
let wildcard_req = deps_core::VersionReq::new("*");
let check_yanked = registry.reports_yanked();
let results: Vec<_> = stream::iter(package_sources)
.map(|(name, source)| {
let registry = Arc::clone(®istry);
let fetched = Arc::clone(&fetched);
let progress_sender = progress_sender.clone();
let wildcard_req = &wildcard_req;
let in_use_versions = in_use.get(&name).cloned().unwrap_or_default();
async move {
fetch_and_classify_package(
registry.as_ref(),
name,
source,
in_use_versions,
wildcard_req,
freshness,
timeout,
selection_context,
check_yanked,
gossip,
&fetched,
progress_sender.as_ref(),
)
.await
}
})
.buffer_unordered(max_concurrent.max(1))
.collect()
.await;
let mut versions = HashMap::with_capacity(results.len());
let mut yanked_versions = HashMap::new();
let mut fetch_failed = HashMap::new();
let mut deprecations = HashMap::new();
let mut no_comparable_versions = HashSet::new();
let mut licenses = HashMap::new();
let mut failed_count: usize = 0;
let mut priority_message: Option<String> = None;
let mut fallback_message: Option<String> = None;
for PackageOutcome {
name,
status,
yanked,
} in results
{
if let Some(y) = yanked {
yanked_versions.insert(name.clone(), y);
}
match status {
PackageStatus::Resolved {
versions: v,
deprecation,
license,
} => {
if let Some(d) = deprecation {
deprecations.insert(name.clone(), d);
}
if let Some(lic) = license {
licenses.insert(name.clone(), lic);
}
versions.insert(name, v);
}
PackageStatus::NoComparableVersions => {
no_comparable_versions.insert(name);
}
PackageStatus::NotFound { message } => {
failed_count += 1;
if fallback_message.is_none() {
fallback_message = Some(message);
}
}
PackageStatus::Failed { failure, message } => {
failed_count += 1;
fetch_failed.insert(name, failure);
if priority_message.is_none() {
priority_message = Some(message.clone());
}
if fallback_message.is_none() {
fallback_message = Some(message);
}
}
}
}
let failure_summary = NonZeroUsize::new(failed_count).map(|count| {
#[allow(clippy::expect_used)]
let message = priority_message.or(fallback_message).expect(
"every branch that increments failed_count also sets fallback_message \
in the same match arm",
);
FailureSummary::new(count, message)
});
FetchResult {
versions,
yanked_versions,
deprecations,
fetch_failed,
no_comparable_versions,
failure_summary,
licenses,
}
}
enum PackageStatus {
Resolved {
versions: PackageVersions,
deprecation: Option<Deprecation>,
license: Option<Vec<String>>,
},
NoComparableVersions,
NotFound { message: String },
Failed {
failure: FetchFailure,
message: String,
},
}
struct PackageOutcome {
name: PackageName,
status: PackageStatus,
yanked: Option<(ConcreteVersion, RemovalStatus)>,
}
struct ResolvedPick {
version: ConcreteVersion,
removal_status: RemovalStatus,
published_at: Option<deps_core::freshness::PublishTime>,
deprecation: Option<Deprecation>,
license: Vec<String>,
}
enum Pick {
Resolved(ResolvedPick),
Unresolved(PackageStatus),
}
impl Pick {
fn resolved(v: &dyn Version) -> Self {
Self::Resolved(ResolvedPick {
version: v.version_string().clone(),
removal_status: v.removal_status(),
published_at: v.published_at(),
deprecation: v.deprecation().cloned(),
license: v.license().to_vec(),
})
}
}
#[allow(
clippy::too_many_arguments,
reason = "mirrors the per-package async closure this was extracted from — every \
parameter is either call-site fetch tuning already threaded through \
fetch_latest_versions_parallel or a counter/sender shared across the \
whole stream; grouping into a struct would only move, not reduce, churn"
)]
async fn fetch_and_classify_package(
registry: &dyn Registry,
name: PackageName,
source: deps_core::parser::DependencySource,
in_use_versions: Vec<ConcreteVersion>,
wildcard_req: &VersionReq,
freshness: deps_core::freshness::FreshnessSettings,
timeout: Duration,
selection_context: &deps_core::SelectionContext,
check_yanked: bool,
gossip: Option<&HashMap<PackageName, deps_core::GossipFindings>>,
fetched: &std::sync::atomic::AtomicUsize,
progress_sender: Option<&ProgressSender>,
) -> PackageOutcome {
let result = tokio::time::timeout(
timeout,
registry.get_versions_from(&name, &source, freshness),
)
.await;
let mut yanked: Option<(ConcreteVersion, RemovalStatus)> = None;
let status = match result {
Ok(Ok(versions)) => {
let available: Arc<[ConcreteVersion]> = versions
.iter()
.map(|v| v.version_string().clone())
.collect();
let yanked_list: Arc<[(ConcreteVersion, RemovalStatus)]> = if check_yanked {
versions
.iter()
.filter_map(|v| {
let status = v.removal_status();
status
.is_flagged()
.then(|| (v.version_string().clone(), status))
})
.collect()
} else {
Arc::from([])
};
let in_use_yanked: Option<(ConcreteVersion, RemovalStatus)> = check_yanked
.then(|| {
in_use_versions.iter().find_map(|iv| {
versions
.iter()
.find(|v| v.version_string() == iv && v.removal_status().is_flagged())
.map(|v| (iv.clone(), v.removal_status()))
})
})
.flatten();
let unfiltered_pick_idx =
registry.select_latest_matching(&versions, wildcard_req, selection_context);
let unfiltered_pick_version: Option<ConcreteVersion> = unfiltered_pick_idx
.and_then(|idx| versions.get(idx))
.map(|v| v.version_string().clone());
let now = deps_core::freshness::PublishTime::now();
let unfiltered_pick_flagged = unfiltered_pick_version.as_ref().is_some_and(|version| {
deps_core::lsp_helpers::gossip_cooldown_for(gossip, &name, version.as_str(), now)
== deps_core::lsp_helpers::GossipCooldownLookup::Active
});
let cooldown_fallback = compute_cooldown_fallback(
registry,
&versions,
&name,
&in_use_versions,
wildcard_req,
*selection_context,
freshness,
gossip,
now,
unfiltered_pick_idx,
);
let (list_pick, gossip_excluded_version) = gossip_floor_protected_pick(
registry,
versions,
&in_use_versions,
wildcard_req,
*selection_context,
unfiltered_pick_idx,
unfiltered_pick_version,
unfiltered_pick_flagged,
gossip,
&name,
now,
);
let pick = if let Some(v) = list_pick.as_deref() {
tracing::debug!(
package = %name.for_tracing(),
version = %v.version_string(),
"fetched"
);
Pick::resolved(v)
} else {
get_latest_matching_fallback(
registry,
&name,
&source,
wildcard_req,
*selection_context,
timeout,
)
.await
};
let resolved: Option<&ResolvedPick> = match &pick {
Pick::Resolved(r) => Some(r),
Pick::Unresolved(_) => None,
};
if check_yanked {
if let Some(r) = resolved
&& r.removal_status.is_flagged()
{
yanked = Some((r.version.clone(), r.removal_status));
}
if let Some(iv_yanked) = in_use_yanked {
yanked = Some(iv_yanked);
}
}
let deprecation = resolved.and_then(|r| r.deprecation.clone());
let license = resolved
.map(|r| &r.license)
.filter(|lic| !lic.is_empty())
.cloned();
match pick {
Pick::Resolved(ResolvedPick {
version,
published_at,
..
}) => {
let mut versions =
PackageVersions::new(version, available).with_yanked(yanked_list);
if let Some(published_at) = published_at {
versions = versions.with_published_at(published_at);
}
if let Some(excluded) = gossip_excluded_version {
versions = versions.with_gossip_excluded_version(excluded);
}
if let Some(fallback) = cooldown_fallback {
versions = versions.with_cooldown_fallback(fallback);
}
PackageStatus::Resolved {
versions,
deprecation,
license,
}
}
Pick::Unresolved(status) => status,
}
}
Ok(Err(e)) => {
if e.is_offline() {
tracing::debug!(package = %name.for_tracing(), "fetch skipped: offline");
} else {
tracing::warn!(package = %name.for_tracing(), error = %e, "fetch failed");
}
if e.is_not_found() {
PackageStatus::NotFound {
message: e.to_string(),
}
} else {
PackageStatus::Failed {
failure: e.fetch_failure(),
message: e.to_string(),
}
}
}
Err(_) => {
tracing::warn!(package = %name.for_tracing(), "fetch timed out ({}s)", timeout.as_secs());
PackageStatus::Failed {
failure: FetchFailure::Transient,
message: format!(
"{}: registry request timed out after {}s",
name.for_tracing(),
timeout.as_secs()
),
}
}
};
let count = fetched.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + 1;
if let Some(sender) = progress_sender {
sender.send(count);
}
PackageOutcome {
name,
status,
yanked,
}
}
#[allow(
clippy::too_many_arguments,
reason = "every parameter is either data already owned by the caller (versions, the \
in-use floor inputs) or a piece of the unfiltered pick the caller computed \
once and must not recompute here — grouping into a struct would only move, \
not reduce, the parameter count"
)]
fn gossip_floor_protected_pick(
registry: &dyn Registry,
versions: Vec<Box<dyn Version>>,
in_use_versions: &[ConcreteVersion],
wildcard_req: &VersionReq,
selection_context: deps_core::SelectionContext,
unfiltered_pick_idx: Option<usize>,
unfiltered_pick_version: Option<ConcreteVersion>,
unfiltered_pick_flagged: bool,
gossip: Option<&HashMap<PackageName, deps_core::GossipFindings>>,
name: &PackageName,
now: deps_core::freshness::PublishTime,
) -> (Option<Box<dyn Version>>, Option<ConcreteVersion>) {
if !unfiltered_pick_flagged {
return (
unfiltered_pick_idx.and_then(|idx| versions.into_iter().nth(idx)),
None,
);
}
let is_gossip_cooldown = |version: &ConcreteVersion| {
deps_core::lsp_helpers::gossip_cooldown_for(gossip, name, version.as_str(), now)
== deps_core::lsp_helpers::GossipCooldownLookup::Active
};
let floor = match in_use_floor(in_use_versions, &versions) {
InUseFloor::Located(floor)
| InUseFloor::Unlocatable {
newest_located: Some(floor),
} => floor,
InUseFloor::Absent
| InUseFloor::Unlocatable {
newest_located: None,
} => {
return (
unfiltered_pick_idx.and_then(|idx| versions.into_iter().nth(idx)),
None,
);
}
};
type IndexedVersion = (usize, Box<dyn Version>);
let mut filtered_indices: Vec<usize> = Vec::new();
let mut filtered_versions: Vec<Box<dyn Version>> = Vec::new();
let mut dropped: Vec<IndexedVersion> = Vec::new();
for (idx, v) in versions.into_iter().enumerate() {
if idx >= floor || !is_gossip_cooldown(v.version_string()) {
filtered_indices.push(idx);
filtered_versions.push(v);
} else {
dropped.push((idx, v));
}
}
let filtered_pick_idx =
registry.select_latest_matching(&filtered_versions, wildcard_req, &selection_context);
let pick_at_or_above_floor = filtered_pick_idx
.and_then(|idx| filtered_indices.get(idx))
.is_some_and(|original_idx| *original_idx <= floor);
if pick_at_or_above_floor {
let filtered_pick_version = filtered_pick_idx
.and_then(|idx| filtered_versions.get(idx))
.map(|v| v.version_string().clone());
let excluded = (filtered_pick_version != unfiltered_pick_version)
.then(|| unfiltered_pick_version.clone())
.flatten();
(
filtered_pick_idx.and_then(|idx| filtered_versions.into_iter().nth(idx)),
excluded,
)
} else {
let recovered = unfiltered_pick_idx.and_then(|target| {
dropped
.iter()
.position(|(idx, _)| *idx == target)
.map(|i| dropped.swap_remove(i).1)
.or_else(|| {
filtered_indices
.iter()
.position(|idx| *idx == target)
.map(|i| filtered_versions.swap_remove(i))
})
});
(recovered, None)
}
}
async fn get_latest_matching_fallback(
registry: &dyn Registry,
name: &PackageName,
source: &deps_core::parser::DependencySource,
wildcard_req: &VersionReq,
selection_context: deps_core::SelectionContext,
timeout: Duration,
) -> Pick {
let fallback = tokio::time::timeout(
timeout,
registry.get_latest_matching_from(name, source, wildcard_req, &selection_context),
)
.await;
match fallback {
Ok(Ok(Some(v))) => {
tracing::debug!(
package = %name.for_tracing(),
version = %v.version_string(),
"fetched via get_latest_matching fallback"
);
Pick::resolved(v.as_ref())
}
Ok(Ok(None)) => {
tracing::debug!(package = %name.for_tracing(), "no version found");
Pick::Unresolved(PackageStatus::NoComparableVersions)
}
Ok(Err(e)) => {
tracing::warn!(
package = %name.for_tracing(),
error = %e,
"fetch fallback failed"
);
if e.is_not_found() {
Pick::Unresolved(PackageStatus::NotFound {
message: e.to_string(),
})
} else {
Pick::Unresolved(PackageStatus::Failed {
failure: e.fetch_failure(),
message: e.to_string(),
})
}
}
Err(_) => {
tracing::warn!(package = %name.for_tracing(), "fetch fallback timed out ({}s)", timeout.as_secs());
Pick::Unresolved(PackageStatus::Failed {
failure: FetchFailure::Transient,
message: format!(
"{}: registry request timed out after {}s",
name.for_tracing(),
timeout.as_secs()
),
})
}
}
}
#[derive(Debug, Clone)]
struct CooldownCandidate {
version: ConcreteVersion,
published_at: deps_core::freshness::PublishTime,
removal_status: RemovalStatus,
prerelease: bool,
}
impl Version for CooldownCandidate {
fn version_string(&self) -> &ConcreteVersion {
&self.version
}
fn published_at(&self) -> Option<deps_core::freshness::PublishTime> {
Some(self.published_at)
}
fn removal_status(&self) -> RemovalStatus {
self.removal_status
}
fn is_prerelease(&self) -> bool {
self.prerelease
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InUseFloor {
Absent,
Located(usize),
Unlocatable {
newest_located: Option<usize>,
},
}
fn in_use_floor(in_use_versions: &[ConcreteVersion], versions: &[Box<dyn Version>]) -> InUseFloor {
if in_use_versions.is_empty() {
return InUseFloor::Absent;
}
let mut newest_located: Option<usize> = None;
let mut all_located = true;
for iv in in_use_versions {
match versions.iter().position(|v| v.version_string() == iv) {
Some(idx) => newest_located = Some(newest_located.map_or(idx, |cur| cur.min(idx))),
None => all_located = false,
}
}
match (all_located, newest_located) {
(true, Some(idx)) => InUseFloor::Located(idx),
(true, None) | (false, _) => InUseFloor::Unlocatable { newest_located },
}
}
#[allow(
clippy::too_many_arguments,
reason = "mirrors fetch_and_classify_package's own identical rationale — every parameter \
is either registry/selection-rule plumbing already threaded through that \
function or a planning knob this helper alone needs"
)]
fn compute_cooldown_fallback(
registry: &dyn Registry,
versions: &[Box<dyn Version>],
name: &PackageName,
in_use_versions: &[ConcreteVersion],
wildcard_req: &VersionReq,
selection_context: deps_core::SelectionContext,
freshness: deps_core::freshness::FreshnessSettings,
gossip: Option<&HashMap<PackageName, deps_core::GossipFindings>>,
now: deps_core::freshness::PublishTime,
unfiltered_pick_idx: Option<usize>,
) -> Option<deps_core::lsp_helpers::CooldownFallback> {
let deps_core::freshness::FreshnessSettings::Enabled { cooldown } = freshness else {
return None;
};
let unfiltered_pick = unfiltered_pick_idx.and_then(|idx| versions.get(idx));
let latest_is_prerelease = unfiltered_pick.is_some_and(|v| v.is_prerelease());
if let Some(pick) = unfiltered_pick {
let cleared = matches!(
deps_core::lsp_helpers::cooldown_precedence(
gossip,
name,
pick.version_string().as_str(),
pick.published_at(),
cooldown,
now,
),
deps_core::lsp_helpers::CooldownPrecedence::Cleared
);
if cleared {
return None;
}
}
let floor: Option<usize> = match in_use_floor(in_use_versions, versions) {
InUseFloor::Located(idx) => Some(idx),
InUseFloor::Absent => None,
InUseFloor::Unlocatable { .. } => return None,
};
let (cleared_indices, cleared_versions): (Vec<usize>, Vec<Box<dyn Version>>) = versions
.iter()
.enumerate()
.filter_map(|(idx, v)| {
let published_at = v.published_at()?;
let cleared = matches!(
deps_core::lsp_helpers::cooldown_precedence(
gossip,
name,
v.version_string().as_str(),
Some(published_at),
cooldown,
now,
),
deps_core::lsp_helpers::CooldownPrecedence::Cleared
);
cleared.then(|| {
let candidate: Box<dyn Version> = Box::new(CooldownCandidate {
version: v.version_string().clone(),
published_at,
removal_status: v.removal_status(),
prerelease: v.is_prerelease(),
});
(idx, candidate)
})
})
.unzip();
let pick =
registry.select_latest_matching(&cleared_versions, wildcard_req, &selection_context)?;
let idx = *cleared_indices.get(pick)?;
let candidate = versions.get(idx)?;
let published_at = candidate.published_at()?;
let ecosystem_safe = !candidate.removal_status().blocks_resolution()
&& (!candidate.is_prerelease() || latest_is_prerelease);
let above_floor = floor.is_none_or(|floor| idx < floor);
(ecosystem_safe && above_floor).then(|| {
deps_core::lsp_helpers::CooldownFallback::new(
candidate.version_string().clone(),
published_at,
)
})
}
pub fn apply_fetch_outcomes(
outcomes: &mut deps_core::lsp_helpers::DependencyOutcomes,
yanked_versions: HashMap<PackageName, (ConcreteVersion, RemovalStatus)>,
fetch_failed: HashMap<PackageName, FetchFailure>,
collided_names: HashSet<PackageName>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) {
for (name, version) in yanked_versions {
outcomes.set_yanked(formatter.normalize_package_name(&name), version);
}
for (name, failure) in fetch_failed {
outcomes.set_fetch_failure(formatter.normalize_package_name(&name), failure);
}
for name in collided_names {
outcomes.set_fetch_failure_if_absent(
formatter.normalize_package_name(&name),
FetchFailure::NotAttempted,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use deps_core::SelectionContext;
use deps_core::parser::DependencySource;
fn with_registry_source(names: Vec<PackageName>) -> Vec<(PackageName, DependencySource)> {
names
.into_iter()
.map(|name| (name, DependencySource::Registry))
.collect()
}
#[cfg(feature = "cargo")]
mod apply_fetch_outcomes_tests {
use super::*;
use crate::setup::CargoFormatter;
use deps_core::lsp_helpers::DependencyOutcomes;
#[test]
fn apply_fetch_outcomes_sets_yanked_re_keyed_to_normalized_name() {
let mut outcomes = DependencyOutcomes::new();
let mut yanked_versions = HashMap::new();
yanked_versions.insert(
PackageName::new("time"),
("0.1.43".into(), RemovalStatus::Yanked),
);
apply_fetch_outcomes(
&mut outcomes,
yanked_versions,
HashMap::new(),
HashSet::new(),
&CargoFormatter,
);
assert_eq!(
outcomes.yanked("time"),
Some(&("0.1.43".into(), RemovalStatus::Yanked))
);
}
#[test]
fn apply_fetch_outcomes_collided_name_does_not_clobber_existing_fetch_failure() {
let mut outcomes = DependencyOutcomes::new();
let mut fetch_failed = HashMap::new();
fetch_failed.insert(PackageName::new("serde"), FetchFailure::Transient);
let mut collided_names = HashSet::new();
collided_names.insert(PackageName::new("serde"));
apply_fetch_outcomes(
&mut outcomes,
HashMap::new(),
fetch_failed,
collided_names,
&CargoFormatter,
);
assert_eq!(
outcomes.fetch_failure("serde"),
Some(&FetchFailure::Transient),
"a genuine fetch failure must survive a collided name normalizing to the same key"
);
}
#[test]
fn apply_fetch_outcomes_collided_name_alone_is_recorded_as_not_attempted() {
let mut outcomes = DependencyOutcomes::new();
let mut collided_names = HashSet::new();
collided_names.insert(PackageName::new("serde"));
apply_fetch_outcomes(
&mut outcomes,
HashMap::new(),
HashMap::new(),
collided_names,
&CargoFormatter,
);
assert_eq!(
outcomes.fetch_failure("serde"),
Some(&FetchFailure::NotAttempted)
);
}
}
mod dedup_by_source_collision_tests {
use super::*;
use deps_core::Dependency;
use deps_core::position::{Position, Range};
use deps_core::test_util::StubFormatter;
use std::any::Any;
const ALTERNATE_AWARE_FORMATTER: StubFormatter =
StubFormatter::new().with_alternate_registry_resolution();
struct MockDep {
name: PackageName,
source: DependencySource,
addr_tag: u32,
}
impl Dependency for MockDep {
fn name(&self) -> &PackageName {
&self.name
}
fn name_range(&self) -> Range {
Range::new(
Position::new(0, self.addr_tag),
Position::new(0, self.addr_tag + 1),
)
}
fn version_requirement(&self) -> Option<&VersionReq> {
None
}
fn version_range(&self) -> Option<Range> {
None
}
fn source(&self) -> DependencySource {
self.source.clone()
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockParseResult {
deps: Vec<MockDep>,
}
impl deps_core::ParseResult for MockParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
self.deps.iter().map(|d| d as &dyn Dependency).collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
static URI: std::sync::OnceLock<url::Url> = std::sync::OnceLock::new();
URI.get_or_init(|| deps_core::test_util::test_uri("/test/Cargo.toml"))
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[test]
fn test_two_different_resolvable_sources_collide_and_are_dropped() {
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: PackageName::new("shared-name"),
source: DependencySource::Registry,
addr_tag: 0,
},
MockDep {
name: PackageName::new("shared-name"),
source: DependencySource::AlternateRegistry {
index: "https://index.mycorp.dev".into(),
mirrors_crates_io: false,
},
addr_tag: 1,
},
],
};
let (sources, collided) =
dedup_dependencies_by_source(&parse_result, &ALTERNATE_AWARE_FORMATTER);
assert!(
!sources.contains_key(&PackageName::new("shared-name")),
"a colliding name must not be fetched under either source"
);
assert!(
collided.contains(&PackageName::new("shared-name")),
"the collision must be recorded so the caller can mark it fetch_failed"
);
}
#[test]
fn test_identical_sources_do_not_collide() {
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: PackageName::new("shared-name"),
source: DependencySource::Registry,
addr_tag: 0,
},
MockDep {
name: PackageName::new("shared-name"),
source: DependencySource::Registry,
addr_tag: 1,
},
],
};
let (sources, collided) =
dedup_dependencies_by_source(&parse_result, &ALTERNATE_AWARE_FORMATTER);
assert!(collided.is_empty());
assert_eq!(
sources.get(&PackageName::new("shared-name")),
Some(&DependencySource::Registry)
);
}
#[test]
fn test_non_resolvable_source_is_dropped_not_fetched() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("local-fork"),
source: DependencySource::Path {
path: "../local-fork".into(),
},
addr_tag: 0,
}],
};
let (sources, collided) =
dedup_dependencies_by_source(&parse_result, &ALTERNATE_AWARE_FORMATTER);
assert!(sources.is_empty());
assert!(collided.is_empty());
}
#[test]
fn test_collision_warning_redacts_credentials_in_alternate_registry_debug_output() {
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: PackageName::new("shared-name"),
source: DependencySource::AlternateRegistry {
index: "https://index-a.mycorp.dev/api?api_key=SECRET_A".into(),
mirrors_crates_io: false,
},
addr_tag: 0,
},
MockDep {
name: PackageName::new("shared-name"),
source: DependencySource::AlternateRegistry {
index: "https://index-b.mycorp.dev/api?api_key=SECRET_B".into(),
mirrors_crates_io: false,
},
addr_tag: 1,
},
],
};
let log = deps_core::test_util::capture_tracing_output(|| {
let (sources, collided) =
dedup_dependencies_by_source(&parse_result, &ALTERNATE_AWARE_FORMATTER);
assert!(!sources.contains_key(&PackageName::new("shared-name")));
assert!(collided.contains(&PackageName::new("shared-name")));
});
assert!(
log.contains("two different resolved registries"),
"expected the collision WARN to fire: {log:?}"
);
assert!(
!log.contains("SECRET_A") && !log.contains("SECRET_B"),
"tracing output leaked a query-string credential: {log:?}"
);
assert!(
log.contains("index-a.mycorp.dev") && log.contains("index-b.mycorp.dev"),
"host should survive redaction: {log:?}"
);
}
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_with_timeout() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::time::Duration;
struct TimeoutRegistry;
impl Registry for TimeoutRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(vec![])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(None)
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(TimeoutRegistry);
let packages = vec![PackageName::new("slow-package")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
1,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.versions.is_empty(), "Slow package should timeout");
assert_eq!(result.failed_count(), 1, "Should track 1 failed package");
assert_eq!(
result.fetch_failed,
HashMap::from([(PackageName::new("slow-package"), FetchFailure::Transient)]),
"timed-out package must be recorded in fetch_failed"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_fast_packages_not_blocked() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::time::Duration;
struct MixedRegistry;
impl Registry for MixedRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
if name == "slow-package" {
tokio::time::sleep(Duration::from_secs(10)).await;
}
Ok(vec![])
})
}
fn get_latest_matching<'a>(
&'a self,
name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
if name == "slow-package" {
tokio::time::sleep(Duration::from_secs(10)).await;
}
Ok(None)
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(MixedRegistry);
let packages = vec![
PackageName::new("slow-package"),
PackageName::new("fast-package"),
];
let start = std::time::Instant::now();
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
1,
10,
&SelectionContext::none(),
None,
)
.await;
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(3),
"Should not wait for slow package: {:?}",
elapsed
);
assert!(
result.versions.is_empty(),
"No versions returned (test registry returns empty)"
);
assert_eq!(
result.failed_count(),
1,
"Slow package should be marked as failed"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_concurrency_limit() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
struct ConcurrencyTrackingRegistry {
current: Arc<AtomicUsize>,
max_seen: Arc<AtomicUsize>,
}
impl Registry for ConcurrencyTrackingRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
let current = self.current.fetch_add(1, Ordering::SeqCst) + 1;
self.max_seen.fetch_max(current, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(50)).await;
self.current.fetch_sub(1, Ordering::SeqCst);
Ok(vec![])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
let current = self.current.fetch_add(1, Ordering::SeqCst) + 1;
self.max_seen.fetch_max(current, Ordering::SeqCst);
tokio::time::sleep(Duration::from_millis(50)).await;
self.current.fetch_sub(1, Ordering::SeqCst);
Ok(None)
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let current = Arc::new(AtomicUsize::new(0));
let max_seen = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(ConcurrencyTrackingRegistry {
current: Arc::clone(¤t),
max_seen: Arc::clone(&max_seen),
});
let packages: Vec<PackageName> = (0..50)
.map(|i| PackageName::new(format!("package-{}", i)))
.collect();
fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
20,
&SelectionContext::none(),
None,
)
.await;
let max = max_seen.load(Ordering::SeqCst);
assert!(
max <= 22,
"Concurrency limit violated: {} concurrent requests (limit: 20)",
max
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_zero_max_concurrent_still_completes() {
use deps_core::Registry;
let registry: Arc<dyn Registry> = Arc::new(deps_core::test_util::MockRegistry::new());
let packages = vec![PackageName::new("some-package")];
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
0,
&SelectionContext::none(),
None,
),
)
.await
.expect("fetch with max_concurrent=0 must not hang forever");
assert!(
result
.no_comparable_versions
.contains(&PackageName::new("some-package")),
"fetch must still run to completion when max_concurrent is 0"
);
}
#[tokio::test]
async fn test_fetch_partial_success_with_mixed_outcomes() {
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::time::Duration;
struct MixedOutcomeRegistry;
impl Registry for MixedOutcomeRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
match name.as_str() {
"package-fast" => {
Ok(vec![
Box::new(MockVersion::new("1.0.0").with_prerelease(false))
as Box<dyn Version>,
])
}
"package-slow" => {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(vec![])
}
"package-error" => Err(deps_core::error::DepsError::CacheError(
"Mock registry error".to_string(),
)),
_ => Ok(vec![]),
}
})
}
fn get_latest_matching<'a>(
&'a self,
name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
match name.as_str() {
"package-fast" => Ok(Some(Box::new(
MockVersion::new("1.0.0").with_prerelease(false),
) as Box<dyn Version>)),
"package-slow" => {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(None)
}
"package-error" => Err(deps_core::error::DepsError::CacheError(
"Mock registry error".to_string(),
)),
_ => Ok(None),
}
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &deps_core::VersionReq,
_selection_context: &deps_core::SelectionContext,
) -> Option<usize> {
if versions.is_empty() { None } else { Some(0) }
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(MixedOutcomeRegistry);
let packages = vec![
PackageName::new("package-fast"),
PackageName::new("package-slow"),
PackageName::new("package-error"),
];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
1,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.versions.len(),
1,
"Should have exactly 1 successful package"
);
assert_eq!(
result
.versions
.get("package-fast")
.map(|v| v.latest.as_str()),
Some("1.0.0"),
"Fast package should have correct version"
);
assert!(
!result.versions.contains_key("package-slow"),
"Slow package should not be in results (timeout)"
);
assert!(
!result.versions.contains_key("package-error"),
"Error package should not be in results"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_carries_yanked_flag_into_cache() {
use deps_core::Registry;
use deps_core::test_util::MockVersion;
let registry: Arc<dyn Registry> = Arc::new(
deps_core::test_util::MockRegistry::new().with_versions(vec![
MockVersion::new("1.0.214"),
MockVersion::new("1.0.213").yanked(true),
]),
);
let packages = vec![PackageName::new("serde")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
10,
10,
&SelectionContext::none(),
None,
)
.await;
let serde = result
.versions
.get("serde")
.expect("serde should be fetched");
assert_eq!(serde.latest, "1.0.214", "latest must skip the yanked entry");
assert_eq!(
&*serde.available,
&[
ConcreteVersion::new("1.0.214"),
ConcreteVersion::new("1.0.213")
],
"available must remain unfiltered"
);
assert_eq!(
&*serde.yanked,
&[(
ConcreteVersion::new("1.0.213"),
deps_core::RemovalStatus::Yanked
)],
"yanked must carry only the entries reported as yanked, paired with their status"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_carries_published_at_for_latest_only() {
use deps_core::Registry;
use deps_core::freshness::PublishTime;
use deps_core::test_util::MockVersion;
let registry: Arc<dyn Registry> = Arc::new(
deps_core::test_util::MockRegistry::new().with_versions(vec![
MockVersion::new("1.0.214").with_published_at(PublishTime::from_unix_secs(2_000)),
MockVersion::new("1.0.213")
.yanked(true)
.with_published_at(PublishTime::from_unix_secs(1_000)),
]),
);
let packages = vec![PackageName::new("serde")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
10,
10,
&SelectionContext::none(),
None,
)
.await;
let serde = result
.versions
.get("serde")
.expect("serde should be fetched");
assert_eq!(serde.latest, "1.0.214");
assert_eq!(
serde.published_at,
Some(PublishTime::from_unix_secs(2_000)),
"published_at must be 1.0.214's own timestamp, not the yanked 1.0.213 entry's"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_carries_license_into_fetch_result() {
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct LicensedRegistry;
impl Registry for LicensedRegistry {
fn get_versions<'a>(
&'a self,
name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
let license = if name.as_str() == "licensed-pkg" {
vec!["MIT".to_string()]
} else {
vec![]
};
Box::pin(async move {
Ok(vec![
Box::new(MockVersion::new("1.0.0").with_license(license))
as Box<dyn Version>,
])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &deps_core::VersionReq,
_selection_context: &deps_core::SelectionContext,
) -> Option<usize> {
(!versions.is_empty()).then_some(0)
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(LicensedRegistry);
let packages = vec![
PackageName::new("licensed-pkg"),
PackageName::new("unlicensed-pkg"),
];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
10,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.licenses.get(&PackageName::new("licensed-pkg")),
Some(&vec!["MIT".to_string()])
);
assert!(
!result
.licenses
.contains_key(&PackageName::new("unlicensed-pkg")),
"an empty Version::license() must produce no entry, not an empty-vec one"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_uses_get_versions_with_for_freshness() {
use deps_core::freshness::{FreshnessSettings, PublishTime};
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct FreshnessAwareRegistry;
impl Registry for FreshnessAwareRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Ok(vec![Box::new(MockVersion::new("1.0.0")) as Box<dyn Version>])
})
}
fn get_versions_with<'a>(
&'a self,
_name: &'a deps_core::PackageName,
freshness: FreshnessSettings,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
let version = MockVersion::new("1.0.0");
let version = match freshness
.is_enabled()
.then(|| PublishTime::from_unix_secs(5_000))
{
Some(published_at) => version.with_published_at(published_at),
None => version,
};
Box::pin(async move { Ok(vec![Box::new(version) as Box<dyn Version>]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &deps_core::VersionReq,
_selection_context: &deps_core::SelectionContext,
) -> Option<usize> {
if versions.is_empty() { None } else { Some(0) }
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(FreshnessAwareRegistry);
let packages = vec![PackageName::new("widget")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
FreshnessSettings::default(),
10,
10,
&SelectionContext::none(),
None,
)
.await;
let widget = result
.versions
.get("widget")
.expect("widget should be fetched");
assert_eq!(
widget.published_at,
Some(PublishTime::from_unix_secs(5_000)),
"published_at must come from get_versions_with, not the freshness-blind \
get_versions (#339)"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_threads_minimum_stability_into_select_latest_matching()
{
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Registry, StabilityFloor, Version};
use std::any::Any;
use std::sync::Mutex;
struct ContextAwareRegistry {
seen_minimum_stability: Mutex<Vec<Option<StabilityFloor>>>,
}
impl Registry for ContextAwareRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Ok(vec![Box::new(MockVersion::new("1.0.0")) as Box<dyn Version>])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &deps_core::VersionReq,
selection_context: &SelectionContext,
) -> Option<usize> {
self.seen_minimum_stability
.lock()
.unwrap_or_else(|p| p.into_inner())
.push(selection_context.minimum_stability());
if versions.is_empty() { None } else { Some(0) }
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry = Arc::new(ContextAwareRegistry {
seen_minimum_stability: Mutex::new(Vec::new()),
});
let packages = vec![PackageName::new("vendor/pkg")];
let result = fetch_latest_versions_parallel(
Arc::clone(®istry) as Arc<dyn Registry>,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
10,
10,
&SelectionContext::with_minimum_stability(StabilityFloor::Beta),
None,
)
.await;
assert_eq!(
*registry
.seen_minimum_stability
.lock()
.unwrap_or_else(|p| p.into_inner()),
vec![Some(StabilityFloor::Beta)],
"select_latest_matching must receive the caller's minimum_stability"
);
assert!(
result.versions.contains_key("vendor/pkg"),
"the pick must still succeed"
);
}
#[tokio::test]
async fn test_fetch_latest_versions_parallel_threads_minimum_stability_into_get_latest_matching_fallback()
{
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Registry, StabilityFloor, Version};
use std::any::Any;
use std::sync::Mutex;
struct FallbackContextAwareRegistry {
seen_minimum_stability: Mutex<Vec<Option<StabilityFloor>>>,
}
impl Registry for FallbackContextAwareRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
selection_context: &'a SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
self.seen_minimum_stability
.lock()
.unwrap_or_else(|p| p.into_inner())
.push(selection_context.minimum_stability());
Box::pin(async move {
Ok(Some(
Box::new(MockVersion::new("2.0.0-beta1")) as Box<dyn Version>
))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry = Arc::new(FallbackContextAwareRegistry {
seen_minimum_stability: Mutex::new(Vec::new()),
});
let packages = vec![PackageName::new("vendor/pkg")];
let result = fetch_latest_versions_parallel(
Arc::clone(®istry) as Arc<dyn Registry>,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
10,
10,
&SelectionContext::with_minimum_stability(StabilityFloor::Beta),
None,
)
.await;
assert_eq!(
*registry
.seen_minimum_stability
.lock()
.unwrap_or_else(|p| p.into_inner()),
vec![Some(StabilityFloor::Beta)],
"get_latest_matching must receive the caller's minimum_stability"
);
let widget = result
.versions
.get("vendor/pkg")
.expect("fallback pick should succeed");
assert_eq!(widget.latest, "2.0.0-beta1");
}
#[tokio::test]
async fn test_fetch_falls_back_to_get_latest_matching_when_list_based_pick_finds_nothing() {
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct UntaggedModuleRegistry;
impl Registry for UntaggedModuleRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
Ok(Some(
Box::new(MockVersion::new("v0.0.0-20191109021931-daa7c04131f5"))
as Box<dyn Version>,
))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(UntaggedModuleRegistry);
let packages = vec![PackageName::new("golang.org/x/exp")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result
.versions
.get("golang.org/x/exp")
.map(|v| v.latest.as_str()),
Some("v0.0.0-20191109021931-daa7c04131f5"),
"must fall back to get_latest_matching instead of reporting no version found"
);
}
#[tokio::test]
async fn test_fetch_registry_error_handled() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct ErrorRegistry;
impl Registry for ErrorRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::CacheError(format!(
"Failed to fetch package: {}",
name.as_str()
)))
})
}
fn get_latest_matching<'a>(
&'a self,
name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::CacheError(format!(
"Failed to fetch package: {}",
name.as_str()
)))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(ErrorRegistry);
let packages = vec![
PackageName::new("package-1"),
PackageName::new("package-2"),
PackageName::new("package-3"),
];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(
result.versions.is_empty(),
"All packages with errors should be omitted from results"
);
assert_eq!(
result.failed_count(),
3,
"All 3 packages should be marked as failed"
);
assert_eq!(
result.fetch_failed,
HashMap::from([
(PackageName::new("package-1"), FetchFailure::Transient),
(PackageName::new("package-2"), FetchFailure::Transient),
(PackageName::new("package-3"), FetchFailure::Transient),
]),
"every errored package must be recorded in fetch_failed"
);
}
#[tokio::test]
async fn test_fetch_failed_log_redacts_credential_shaped_package_name() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
#[tracing::instrument(skip_all, fields(package = %name.for_tracing()), level = "debug")]
async fn inner_fetch(name: &PackageName) -> deps_core::Result<Vec<Box<dyn Version>>> {
tracing::debug!("mock registry fetch invoked");
Err(deps_core::error::DepsError::CacheError(
"transient backend failure".to_string(),
))
}
struct AlwaysFailsRegistry;
impl Registry for AlwaysFailsRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(inner_fetch(name))
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::CacheError(
"transient backend failure".to_string(),
))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let sentinel_name =
PackageName::new("com.example:deploy:AUDITSENTINEL0000@git.internal.corp");
let registry: Arc<dyn Registry> = Arc::new(AlwaysFailsRegistry);
let packages = vec![sentinel_name.clone()];
let log =
deps_core::test_util::capture_tracing_output_async_at(tracing::Level::DEBUG, async {
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(result.failed_count(), 1);
})
.await;
assert!(
log.contains("fetch failed"),
"expected the fetch-failed WARN to fire: {log:?}"
);
assert!(
log.contains("mock registry fetch invoked"),
"expected the in-span event to fire — without it the span's fields never render, \
silently downgrading this test back to event-field-only coverage: {log:?}"
);
assert!(
!log.contains("AUDITSENTINEL0000"),
"tracing output leaked a credential-shaped package name: {log:?}"
);
assert!(
log.contains("git.internal.corp"),
"host should survive redaction: {log:?}"
);
}
#[tokio::test]
async fn test_fetch_not_found_is_not_recorded_as_fetch_failed() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct NotFoundRegistry;
impl Registry for NotFoundRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::PackageNotFound {
package: name.as_str().into(),
registry: "mock",
})
})
}
fn get_latest_matching<'a>(
&'a self,
name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::PackageNotFound {
package: name.as_str().into(),
registry: "mock",
})
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(NotFoundRegistry);
let packages = vec![PackageName::new("typo-pkg")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.versions.is_empty());
assert!(
result.fetch_failed.is_empty(),
"a genuine not-found must not be recorded in fetch_failed, or \
generate_diagnostics_from_cache would report it as a registry \
error instead of Unknown package"
);
}
#[tokio::test]
async fn test_fetch_success_with_zero_versions_is_recorded_as_no_comparable_versions() {
use deps_core::Registry;
let registry: Arc<dyn Registry> = Arc::new(deps_core::test_util::MockRegistry::new());
let packages = vec![PackageName::new("dtolnay/rust-toolchain")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.versions.is_empty());
assert!(
result.fetch_failed.is_empty(),
"a genuine empty-but-successful fetch must not be recorded as a fetch \
failure, or generate_diagnostics_from_cache would report a registry \
error instead of nothing"
);
assert!(
result
.no_comparable_versions
.contains(&PackageName::new("dtolnay/rust-toolchain")),
"a package whose fetch succeeded with zero comparable versions must be \
recorded in no_comparable_versions, or R5 would misreport it as Unknown \
package; got: {:?}",
result.no_comparable_versions
);
}
#[tokio::test]
async fn test_fetch_http_404_is_not_recorded_as_fetch_failed() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct Http404Registry;
impl Registry for Http404Registry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::HttpStatus {
url: format!("https://example.com/{}", name.as_str()).into(),
status: 404,
})
})
}
fn get_latest_matching<'a>(
&'a self,
name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::HttpStatus {
url: format!("https://example.com/{}", name.as_str()).into(),
status: 404,
})
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(Http404Registry);
let packages = vec![PackageName::new("typo-pkg")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.versions.is_empty());
assert!(
result.fetch_failed.is_empty(),
"a bare HTTP 404 must not be recorded in fetch_failed either"
);
}
#[tokio::test]
async fn test_fetch_fallback_error_recorded_as_fetch_failed_unless_not_found() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct FallbackErrorRegistry;
impl Registry for FallbackErrorRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
let name = name.clone();
Box::pin(async move {
if name.as_str() == "not-found" {
Err(deps_core::error::DepsError::PackageNotFound {
package: name.as_str().into(),
registry: "mock",
})
} else {
Err(deps_core::error::DepsError::CacheError(
"mock fallback failure".to_string(),
))
}
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(FallbackErrorRegistry);
let packages = vec![PackageName::new("flaky"), PackageName::new("not-found")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.versions.is_empty());
assert_eq!(
result.fetch_failed,
HashMap::from([(PackageName::new("flaky"), FetchFailure::Transient)]),
"the fallback's own non-not-found error must be recorded in fetch_failed, \
but its not-found error must not"
);
assert_eq!(
result.failed_count(),
2,
"both fallback failures count toward failed_count regardless of cause (S2)"
);
}
#[tokio::test]
async fn test_fetch_fallback_timeout_recorded_as_fetch_failed() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::time::Duration;
struct FallbackTimeoutRegistry;
impl Registry for FallbackTimeoutRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(None)
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(FallbackTimeoutRegistry);
let packages = vec![PackageName::new("slow-fallback")];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
1,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.versions.is_empty());
assert_eq!(
result.fetch_failed,
HashMap::from([(PackageName::new("slow-fallback"), FetchFailure::Transient)])
);
assert_eq!(result.failed_count(), 1);
}
#[tokio::test]
async fn test_first_error_prefers_actionable_error_over_not_found_regardless_of_race_order() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::time::Duration;
struct MixedErrorRegistry;
impl Registry for MixedErrorRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
if name.as_str() == "typo-pkg" {
Err(deps_core::error::DepsError::PackageNotFound {
package: name.as_str().into(),
registry: "mock",
})
} else {
tokio::time::sleep(Duration::from_millis(50)).await;
Err(deps_core::error::DepsError::CacheError(
"rate limit exceeded".to_string(),
))
}
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(MixedErrorRegistry);
let packages = vec![
PackageName::new("typo-pkg"),
PackageName::new("rate-limited"),
];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
let err = result
.failure_message()
.expect("an actionable failure occurred and must be reported");
assert!(
err.contains("rate limit exceeded"),
"the actionable error must win the toast over the faster-finishing not-found, \
got: {err}"
);
assert!(
!err.contains("not found"),
"a not-found error must never outrank an actionable error, got: {err}"
);
}
#[tokio::test]
async fn test_first_error_falls_back_to_not_found_when_no_actionable_error_occurred() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
struct AllNotFoundRegistry;
impl Registry for AllNotFoundRegistry {
fn get_versions<'a>(
&'a self,
name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::PackageNotFound {
package: name.as_str().into(),
registry: "mock",
})
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(AllNotFoundRegistry);
let packages = vec![
PackageName::new("typo-pkg-1"),
PackageName::new("typo-pkg-2"),
];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(
result.fetch_failed.is_empty(),
"not-found errors must never be recorded in fetch_failed"
);
let err = result
.failure_message()
.expect("a not-found-only batch must still fall back to reporting one via first_error");
assert!(err.contains("not found"), "got: {err}");
}
#[tokio::test]
async fn test_timeout_only_batch_reports_first_error_alongside_failed_count() {
use deps_core::{Metadata, Registry, Version};
use std::any::Any;
use std::time::Duration;
struct AlwaysTimesOutRegistry;
impl Registry for AlwaysTimesOutRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a deps_core::PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(vec![])
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a deps_core::PackageName,
_req: &'a deps_core::VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
tokio::time::sleep(Duration::from_secs(10)).await;
Ok(None)
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
let registry: Arc<dyn Registry> = Arc::new(AlwaysTimesOutRegistry);
let packages = vec![
PackageName::new("slow-1"),
PackageName::new("slow-2"),
PackageName::new("slow-3"),
];
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(packages),
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
1,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.failed_count(),
3,
"all 3 packages must count toward failed_count"
);
let err = result
.failure_message()
.expect("a timeout is actionable and must populate first_error, not just failed_count");
assert!(
err.contains("timed out"),
"first_error must be the actionable timeout message, got: {err}"
);
}
#[cfg(feature = "composer")]
mod composer_tests {
use super::*;
#[tokio::test]
async fn test_prepare_fetch_selection_context_extracts_from_real_parse_result() {
let json = r#"{
"minimum-stability": "beta",
"require": {
"symfony/console": "^6.0"
}
}"#;
let uri = deps_core::test_util::test_uri("/test/composer.json");
let parse_result = crate::setup::parse_composer_json(json, &uri).unwrap();
let formatter = deps_composer::ComposerFormatter;
let prep = prepare_fetch(
&parse_result as &dyn deps_core::ParseResult,
&formatter,
deps_core::EcosystemId::Composer,
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(
prep.selection_context.minimum_stability(),
Some(deps_core::StabilityFloor::Beta)
);
}
#[tokio::test]
async fn test_prepare_fetch_selection_context_none_when_absent() {
let json = r#"{"require": {"symfony/console": "^6.0"}}"#;
let uri = deps_core::test_util::test_uri("/test/composer.json");
let parse_result = crate::setup::parse_composer_json(json, &uri).unwrap();
let formatter = deps_composer::ComposerFormatter;
let prep = prepare_fetch(
&parse_result as &dyn deps_core::ParseResult,
&formatter,
deps_core::EcosystemId::Composer,
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(prep.selection_context.minimum_stability(), None);
}
}
mod yanked_check_tests {
use super::*;
use deps_core::test_util::MockVersion;
use deps_core::{Metadata, Version};
use std::any::Any;
use std::sync::atomic::{AtomicUsize, Ordering};
enum FetchOutcome {
Versions(Vec<(&'static str, bool)>),
Error,
Timeout,
}
struct MockRegistry {
reports_yanked: bool,
versions: HashMap<&'static str, FetchOutcome>,
latest_fallback: HashMap<&'static str, (&'static str, bool)>,
fetch_calls: Arc<AtomicUsize>,
}
impl Registry for MockRegistry {
fn get_versions<'a>(
&'a self,
name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
self.fetch_calls.fetch_add(1, Ordering::Relaxed);
let outcome = self.versions.get(name.as_str());
Box::pin(async move {
match outcome {
Some(FetchOutcome::Versions(vs)) => Ok(vs
.iter()
.map(|(v, y)| {
Box::new(MockVersion::new(*v).yanked(*y)) as Box<dyn Version>
})
.collect()),
Some(FetchOutcome::Error) => Err(deps_core::error::DepsError::CacheError(
"mock fetch error".to_string(),
)),
Some(FetchOutcome::Timeout) => {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
Ok(vec![])
}
None => Ok(vec![]),
}
})
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &deps_core::SelectionContext,
) -> Option<usize> {
versions
.iter()
.position(|v| !v.removal_status().blocks_resolution())
}
fn get_latest_matching<'a>(
&'a self,
name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
let outcome = self.latest_fallback.get(name.as_str()).copied();
Box::pin(async move {
Ok(outcome
.map(|(v, y)| Box::new(MockVersion::new(v).yanked(y)) as Box<dyn Version>))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn reports_yanked(&self) -> bool {
self.reports_yanked
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn reports_yanked_false_never_recorded() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: false,
versions: HashMap::from([(
"pkg",
FetchOutcome::Versions(vec![("2.0.0", false), ("1.0.0", true)]),
)]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::clone(&fetch_calls),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(fetch_calls.load(Ordering::Relaxed), 1);
assert!(result.yanked_versions.is_empty());
}
#[tokio::test]
async fn in_use_equal_to_latest_not_yanked() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Versions(vec![("1.0.0", false)]))]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::clone(&fetch_calls),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(fetch_calls.load(Ordering::Relaxed), 1);
assert!(result.yanked_versions.is_empty());
}
#[tokio::test]
async fn no_known_in_use_version_skips_the_check() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Versions(vec![("2.0.0", false)]))]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::clone(&fetch_calls),
});
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(fetch_calls.load(Ordering::Relaxed), 1);
assert!(result.yanked_versions.is_empty());
}
#[tokio::test]
async fn in_use_differs_and_yanked_is_recorded() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([(
"pkg",
FetchOutcome::Versions(vec![("2.0.0", false), ("1.0.0", true)]),
)]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::clone(&fetch_calls),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(fetch_calls.load(Ordering::Relaxed), 1);
assert_eq!(
result.yanked_versions.get(&PackageName::new("pkg")),
Some(&(ConcreteVersion::new("1.0.0"), RemovalStatus::Yanked))
);
}
#[tokio::test]
async fn in_use_differs_and_not_yanked_is_not_recorded() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([(
"pkg",
FetchOutcome::Versions(vec![("2.0.0", false), ("1.0.0", false)]),
)]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::clone(&fetch_calls),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(fetch_calls.load(Ordering::Relaxed), 1);
assert!(result.yanked_versions.is_empty());
}
#[tokio::test]
async fn every_version_yanked_still_checks_in_use() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Versions(vec![("1.0.0", true)]))]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::clone(&fetch_calls),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.yanked_versions.get(&PackageName::new("pkg")),
Some(&(ConcreteVersion::new("1.0.0"), RemovalStatus::Yanked))
);
assert!(result.versions.is_empty());
}
#[tokio::test]
async fn latest_pick_needs_fallback_in_use_yanked_still_found() {
let fetch_calls = Arc::new(AtomicUsize::new(0));
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Versions(vec![("1.0.0", true)]))]),
latest_fallback: HashMap::from([("pkg", ("2.0.0", false))]),
fetch_calls: Arc::clone(&fetch_calls),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(fetch_calls.load(Ordering::Relaxed), 1);
assert_eq!(
result
.versions
.get(&PackageName::new("pkg"))
.map(|v| v.latest.as_str()),
Some("2.0.0")
);
assert_eq!(
result.yanked_versions.get(&PackageName::new("pkg")),
Some(&(ConcreteVersion::new("1.0.0"), RemovalStatus::Yanked))
);
}
struct YankedThenFallbackErrorRegistry;
impl Registry for YankedThenFallbackErrorRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
Box::pin(async move {
Ok(vec![
Box::new(MockVersion::new("1.0.0").yanked(true)) as Box<dyn Version>
])
})
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &deps_core::SelectionContext,
) -> Option<usize> {
versions
.iter()
.position(|v| !v.removal_status().blocks_resolution())
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move {
Err(deps_core::error::DepsError::CacheError(
"mock fallback failure".to_string(),
))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn reports_yanked(&self) -> bool {
true
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn fallback_error_does_not_suppress_an_already_found_yanked_in_use_version() {
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
Arc::new(YankedThenFallbackErrorRegistry),
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(
result.versions.is_empty(),
"the fallback errored, so no version was resolved"
);
assert_eq!(
result.yanked_versions.get(&PackageName::new("pkg")),
Some(&(ConcreteVersion::new("1.0.0"), RemovalStatus::Yanked)),
"the yanked in-use finding must survive even though the fallback pick failed"
);
assert_eq!(
result.fetch_failed.get(&PackageName::new("pkg")),
Some(&FetchFailure::Transient)
);
assert_eq!(result.failed_count(), 1);
}
#[tokio::test]
async fn in_use_checks_every_occurrence_of_a_duplicate_name() {
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([(
"pkg",
FetchOutcome::Versions(vec![
("3.0.0", false),
("2.0.0", true),
("1.0.0", false),
]),
)]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::new(AtomicUsize::new(0)),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![
ConcreteVersion::from("1.0.0"),
ConcreteVersion::from("2.0.0"),
],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.yanked_versions.get(&PackageName::new("pkg")),
Some(&(ConcreteVersion::new("2.0.0"), RemovalStatus::Yanked)),
"the yanked occurrence must be found even though a name-keyed \
single-value map could have kept only the non-yanked \"1.0.0\" pin"
);
}
#[tokio::test]
async fn latest_is_yanked_recorded_as_defense_in_depth() {
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Versions(vec![("1.0.0", true)]))]),
latest_fallback: HashMap::from([("pkg", ("1.0.0", true))]),
fetch_calls: Arc::new(AtomicUsize::new(0)),
});
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.yanked_versions.get(&PackageName::new("pkg")),
Some(&(ConcreteVersion::new("1.0.0"), RemovalStatus::Yanked))
);
}
#[tokio::test]
async fn latest_is_yanked_not_recorded_when_reports_yanked_false() {
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: false,
versions: HashMap::from([("pkg", FetchOutcome::Versions(vec![("1.0.0", true)]))]),
latest_fallback: HashMap::from([("pkg", ("1.0.0", true))]),
fetch_calls: Arc::new(AtomicUsize::new(0)),
});
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(
result.yanked_versions.is_empty(),
"a `reports_yanked() == false` registry's `removal_status()` must never be \
trusted, even on the zero-cost row-1 path"
);
assert!(
result
.versions
.get(&PackageName::new("pkg"))
.expect("pkg was fetched")
.yanked
.is_empty(),
"`PackageVersions::yanked` must stay empty for a `reports_yanked() == false` \
registry, even though the fetched version is itself flagged"
);
}
#[tokio::test]
async fn primary_fetch_error_counts_as_failed_no_yanked_data() {
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Error)]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::new(AtomicUsize::new(0)),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.yanked_versions.is_empty());
assert_eq!(result.failed_count(), 1);
assert!(result.versions.is_empty());
}
#[tokio::test]
async fn primary_fetch_timeout_counts_as_failed_no_yanked_data() {
let registry: Arc<dyn Registry> = Arc::new(MockRegistry {
reports_yanked: true,
versions: HashMap::from([("pkg", FetchOutcome::Timeout)]),
latest_fallback: HashMap::new(),
fetch_calls: Arc::new(AtomicUsize::new(0)),
});
let mut in_use = HashMap::new();
in_use.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&in_use,
None,
deps_core::freshness::FreshnessSettings::default(),
1,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.yanked_versions.is_empty());
assert_eq!(result.failed_count(), 1);
assert!(result.versions.is_empty());
}
}
mod deprecation_derivation_tests {
use super::*;
use deps_core::{Metadata, Version};
use std::any::Any;
struct MockDeprecatedVersion {
version: ConcreteVersion,
deprecation: Option<Deprecation>,
}
impl Version for MockDeprecatedVersion {
fn version_string(&self) -> &ConcreteVersion {
&self.version
}
fn removal_status(&self) -> RemovalStatus {
RemovalStatus::from_advisory(self.deprecation.is_some())
}
fn deprecation(&self) -> Option<&Deprecation> {
self.deprecation.as_ref()
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct SingleVersionRegistry {
deprecation: Option<Deprecation>,
}
impl Registry for SingleVersionRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
let deprecation = self.deprecation.clone();
Box::pin(async move {
Ok(vec![Box::new(MockDeprecatedVersion {
version: "1.0.0".into(),
deprecation,
}) as Box<dyn Version>])
})
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &deps_core::SelectionContext,
) -> Option<usize> {
(!versions.is_empty()).then_some(0)
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a deps_core::SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn fetch_result_carries_deprecation_from_resolved_pick() {
let registry: Arc<dyn Registry> = Arc::new(SingleVersionRegistry {
deprecation: Some(Deprecation {
reason: Some("archived".to_string()),
replacement: Some("other/pkg".to_string()),
}),
});
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert_eq!(
result.deprecations.get(&PackageName::new("pkg")),
Some(&Deprecation {
reason: Some("archived".to_string()),
replacement: Some("other/pkg".to_string()),
})
);
}
#[tokio::test]
async fn fetch_result_has_no_deprecation_when_resolved_pick_is_clean() {
let registry: Arc<dyn Registry> = Arc::new(SingleVersionRegistry { deprecation: None });
let result = fetch_latest_versions_parallel(
registry,
vec![(PackageName::new("pkg"), DependencySource::Registry)],
&HashMap::new(),
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
None,
)
.await;
assert!(result.deprecations.is_empty());
}
}
mod gossip_cooldown_filter_tests {
use super::*;
use deps_core::test_util::{MockVersion, stub_gossip_findings};
use deps_core::{
GossipCooldown, GossipRiskLevel, Metadata, PublishTime, Registry, Version,
};
use std::any::Any;
struct FixedListRegistry {
versions: Vec<&'static str>,
fallback: Option<&'static str>,
reject_prerelease: bool,
fallback_calls: std::sync::atomic::AtomicUsize,
}
impl FixedListRegistry {
fn new(versions: Vec<&'static str>) -> Self {
Self {
versions,
fallback: None,
reject_prerelease: false,
fallback_calls: std::sync::atomic::AtomicUsize::new(0),
}
}
fn with_fallback(versions: Vec<&'static str>, fallback: &'static str) -> Self {
Self {
versions,
fallback: Some(fallback),
reject_prerelease: false,
fallback_calls: std::sync::atomic::AtomicUsize::new(0),
}
}
fn with_prerelease_rejection(versions: Vec<&'static str>) -> Self {
Self {
versions,
fallback: None,
reject_prerelease: true,
fallback_calls: std::sync::atomic::AtomicUsize::new(0),
}
}
fn fallback_call_count(&self) -> usize {
self.fallback_calls
.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl Registry for FixedListRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
let versions = self.versions.clone();
Box::pin(async move {
Ok(versions
.into_iter()
.map(|v| Box::new(MockVersion::new(v)) as Box<dyn Version>)
.collect())
})
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
self.fallback_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let fallback = self.fallback;
Box::pin(async move {
Ok(fallback.map(|v| Box::new(MockVersion::new(v)) as Box<dyn Version>))
})
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Metadata>>>>
{
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &SelectionContext,
) -> Option<usize> {
if self.reject_prerelease {
versions
.iter()
.position(|v| !v.version_string().as_str().contains('-'))
} else if versions.is_empty() {
None
} else {
Some(0)
}
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn active_cooldown() -> GossipCooldown {
GossipCooldown::new(
PublishTime::from_unix_secs(i64::MAX / 2),
GossipRiskLevel::High,
)
}
fn expired_cooldown() -> GossipCooldown {
GossipCooldown::new(PublishTime::from_unix_secs(1), GossipRiskLevel::High)
}
async fn fetch_pkg(
registry: Arc<FixedListRegistry>,
in_use: Vec<&'static str>,
gossip: HashMap<PackageName, deps_core::GossipFindings>,
) -> Option<PackageVersions> {
let registry: Arc<dyn Registry> = registry;
let mut in_use_map = HashMap::new();
if !in_use.is_empty() {
in_use_map.insert(
PackageName::new("pkg"),
in_use.into_iter().map(ConcreteVersion::from).collect(),
);
}
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(vec![PackageName::new("pkg")]),
&in_use_map,
None,
deps_core::freshness::FreshnessSettings::default(),
5,
10,
&SelectionContext::none(),
Some(&gossip),
)
.await;
result.versions.get(&PackageName::new("pkg")).cloned()
}
#[tokio::test]
async fn active_cooldown_on_the_latest_version_excludes_it() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("2.0.0", Some(active_cooldown())),
);
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["2.0.0", "1.0.0"])),
vec!["1.0.0"],
gossip,
)
.await
.expect("pkg must resolve");
assert_eq!(versions.latest.as_str(), "1.0.0");
assert_eq!(
versions
.gossip_excluded_version
.as_ref()
.map(ConcreteVersion::as_str),
Some("2.0.0")
);
}
#[tokio::test]
async fn no_finding_for_package_is_a_no_op() {
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["2.0.0", "1.0.0"])),
vec![],
HashMap::new(),
)
.await
.expect("pkg must resolve");
assert_eq!(versions.latest.as_str(), "2.0.0");
assert!(versions.gossip_excluded_version.is_none());
}
#[tokio::test]
async fn finding_without_a_cooldown_is_a_no_op() {
let mut gossip = HashMap::new();
gossip.insert(PackageName::new("pkg"), stub_gossip_findings("2.0.0", None));
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["2.0.0", "1.0.0"])),
vec![],
gossip,
)
.await
.expect("pkg must resolve");
assert_eq!(versions.latest.as_str(), "2.0.0");
assert!(versions.gossip_excluded_version.is_none());
}
#[tokio::test]
async fn expired_cooldown_is_a_no_op() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("2.0.0", Some(expired_cooldown())),
);
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["2.0.0", "1.0.0"])),
vec![],
gossip,
)
.await
.expect("pkg must resolve");
assert_eq!(versions.latest.as_str(), "2.0.0");
assert!(versions.gossip_excluded_version.is_none());
}
#[tokio::test]
async fn version_string_mismatch_is_a_no_op() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("3.0.0", Some(active_cooldown())),
);
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["2.0.0", "1.0.0"])),
vec![],
gossip,
)
.await
.expect("pkg must resolve");
assert_eq!(versions.latest.as_str(), "2.0.0");
assert!(versions.gossip_excluded_version.is_none());
}
#[tokio::test]
async fn in_use_version_itself_flagged_neutralizes_the_exclusion() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("2.0.0", Some(active_cooldown())),
);
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["2.0.0", "1.0.0"])),
vec!["2.0.0"],
gossip,
)
.await
.expect("pkg must resolve");
assert_eq!(
versions.latest.as_str(),
"2.0.0",
"must never regress below the already-declared/in-use version"
);
assert!(
versions.gossip_excluded_version.is_none(),
"the floor neutralized the exclusion — nothing was actually held back"
);
}
#[tokio::test]
async fn safe_intermediate_release_above_the_floor_is_picked() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("3.0.0", Some(active_cooldown())),
);
let versions = fetch_pkg(
Arc::new(FixedListRegistry::new(vec!["3.0.0", "2.0.0", "1.0.0"])),
vec!["1.0.0"],
gossip,
)
.await
.expect("pkg must resolve");
assert_eq!(
versions.latest.as_str(),
"2.0.0",
"the safe intermediate release must win, not the flagged 3.0.0 nor a regression to the in-use 1.0.0"
);
assert_eq!(
versions
.gossip_excluded_version
.as_ref()
.map(ConcreteVersion::as_str),
Some("3.0.0")
);
}
#[tokio::test]
async fn no_in_use_version_at_all_is_a_deliberate_no_op() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("2.0.0", Some(active_cooldown())),
);
let registry = Arc::new(FixedListRegistry::with_fallback(vec!["2.0.0"], "2.0.0"));
let versions = fetch_pkg(Arc::clone(®istry), vec![], gossip)
.await
.expect("pkg must resolve from the unfiltered list-based pick");
assert_eq!(
versions.latest.as_str(),
"2.0.0",
"no floor exists, so GOSSIP must not exclude anything this run"
);
assert!(
versions.gossip_excluded_version.is_none(),
"nothing was actually excluded — no attribution"
);
assert_eq!(
registry.fallback_call_count(),
0,
"the network fallback must never be invoked as a consequence of GOSSIP's own \
filtering when the unfiltered list-based pick already succeeded"
);
}
#[tokio::test]
async fn floor_exists_but_ecosystem_selection_rejects_the_remainder_is_a_no_op() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("1.0.0", Some(active_cooldown())),
);
let registry = Arc::new(FixedListRegistry::with_prerelease_rejection(vec![
"1.0.0",
"1.0.0-rc.1",
]));
let versions = fetch_pkg(Arc::clone(®istry), vec!["1.0.0-rc.1"], gossip)
.await
.expect("pkg must resolve via the recovered unfiltered pick");
assert_eq!(
versions.latest.as_str(),
"1.0.0",
"the ecosystem rejected the filtered remainder (an all-prerelease set), so the \
unfiltered pick must be used instead"
);
assert!(
versions.gossip_excluded_version.is_none(),
"nothing was actually excluded from the final pick — no attribution"
);
assert_eq!(
registry.fallback_call_count(),
0,
"the network fallback must never be invoked as a consequence of GOSSIP's own \
filtering — only when the unfiltered pick itself finds nothing, which it did not"
);
}
#[tokio::test]
async fn filtered_pick_below_the_floor_is_rejected_in_favor_of_the_unfiltered_pick() {
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
stub_gossip_findings("1.0.0", Some(active_cooldown())),
);
let registry = Arc::new(FixedListRegistry::with_prerelease_rejection(vec![
"1.0.0",
"1.0.0-rc.1",
"0.9.0",
]));
let versions = fetch_pkg(Arc::clone(®istry), vec!["1.0.0-rc.1"], gossip)
.await
.expect("pkg must resolve via the recovered unfiltered pick");
assert_eq!(
versions.latest.as_str(),
"1.0.0",
"must never regress to 0.9.0 — a downgrade below the floor (1.0.0-rc.1) — even \
though 0.9.0 is a legitimate, ecosystem-selectable filtered pick"
);
assert!(
versions.gossip_excluded_version.is_none(),
"no attribution — the recovered pick is identical to the unfiltered one"
);
assert_eq!(
registry.fallback_call_count(),
0,
"the network fallback must never be invoked as a consequence of GOSSIP's own filtering"
);
}
}
mod cooldown_fallback_tests {
use super::*;
use deps_core::test_util::MockVersion;
use deps_core::{PublishTime, Registry, Version};
use std::any::Any;
struct FixedRegistry(Vec<MockVersion>);
impl Registry for FixedRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
let versions: Vec<Box<dyn Version>> = self
.0
.iter()
.cloned()
.map(|v| Box::new(v) as Box<dyn Version>)
.collect();
Box::pin(async move { Ok(versions) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
Box::pin(async move { Ok(None) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<
'a,
deps_core::Result<Vec<Box<dyn deps_core::Metadata>>>,
> {
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &SelectionContext,
) -> Option<usize> {
versions
.iter()
.position(|v| !v.removal_status().blocks_resolution() && !v.is_prerelease())
}
fn as_any(&self) -> &dyn Any {
self
}
}
async fn fetch_pkg(
versions: Vec<MockVersion>,
in_use: Vec<&'static str>,
cooldown_secs: u64,
) -> Option<PackageVersions> {
fetch_pkg_with_registry(Arc::new(FixedRegistry(versions)), in_use, cooldown_secs).await
}
async fn fetch_pkg_with_registry(
registry: Arc<dyn Registry>,
in_use: Vec<&'static str>,
cooldown_secs: u64,
) -> Option<PackageVersions> {
let mut in_use_map = HashMap::new();
if !in_use.is_empty() {
in_use_map.insert(
PackageName::new("pkg"),
in_use.into_iter().map(ConcreteVersion::from).collect(),
);
}
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(vec![PackageName::new("pkg")]),
&in_use_map,
None,
deps_core::freshness::FreshnessSettings::Enabled {
cooldown: deps_core::CooldownWindow::from_secs(cooldown_secs),
},
5,
10,
&SelectionContext::none(),
None,
)
.await;
result.versions.get(&PackageName::new("pkg")).cloned()
}
struct NoListPickRegistry(Vec<MockVersion>);
impl Registry for NoListPickRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a PackageName,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Vec<Box<dyn Version>>>>
{
let versions: Vec<Box<dyn Version>> = self
.0
.iter()
.cloned()
.map(|v| Box::new(v) as Box<dyn Version>)
.collect();
Box::pin(async move { Ok(versions) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a PackageName,
_req: &'a VersionReq,
_selection_context: &'a SelectionContext,
) -> deps_core::ecosystem::BoxFuture<'a, deps_core::Result<Option<Box<dyn Version>>>>
{
let resolved = self.0.first().cloned();
Box::pin(async move { Ok(resolved.map(|v| Box::new(v) as Box<dyn Version>)) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> deps_core::ecosystem::BoxFuture<
'a,
deps_core::Result<Vec<Box<dyn deps_core::Metadata>>>,
> {
Box::pin(async move { Ok(vec![]) })
}
fn select_latest_matching(
&self,
versions: &[Box<dyn Version>],
_req: &VersionReq,
_selection_context: &SelectionContext,
) -> Option<usize> {
if versions
.iter()
.any(|v| v.as_any().downcast_ref::<MockVersion>().is_some())
{
return None;
}
versions
.iter()
.position(|v| !v.removal_status().blocks_resolution() && !v.is_prerelease())
}
fn as_any(&self) -> &dyn Any {
self
}
}
#[tokio::test]
async fn yanked_top_ranked_cleared_candidate_yields_no_fallback() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60); let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0")
.with_published_at(old)
.yanked(true),
MockVersion::new("1.0.0")
.with_published_at(old)
.yanked(true),
];
let package_versions = fetch_pkg(versions, vec!["1.0.0"], cooldown_secs)
.await
.expect("pkg must resolve");
assert!(
package_versions.cooldown_fallback.is_none(),
"a yanked top-ranked cooldown-cleared candidate must not be recovered by \
retrying further down the list: {:?}",
package_versions.cooldown_fallback
);
}
#[tokio::test]
async fn cleared_safe_candidate_above_the_floor_is_the_fallback() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0").with_published_at(old),
MockVersion::new("1.0.0").with_published_at(old),
];
let package_versions = fetch_pkg(versions, vec!["1.0.0"], cooldown_secs)
.await
.expect("pkg must resolve");
let fallback = package_versions
.cooldown_fallback
.expect("a safe, cooldown-cleared, above-floor candidate must be recovered");
assert_eq!(fallback.version.as_str(), "1.1.0");
}
#[tokio::test]
async fn permissive_range_repro_1564_still_computes_the_cleared_fallback() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60); let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("0.23.0-rc.1")
.with_published_at(recent)
.with_prerelease(true),
MockVersion::new("0.22.8").with_published_at(recent),
MockVersion::new("0.22.7").with_published_at(old),
MockVersion::new("0.22.6").with_published_at(old), ];
let package_versions = fetch_pkg(versions, vec!["0.22.6"], cooldown_secs)
.await
.expect("pkg must resolve");
let fallback = package_versions
.cooldown_fallback
.expect("a safe, cooldown-cleared, above-floor candidate must be recovered");
assert_eq!(fallback.version.as_str(), "0.22.7");
}
#[tokio::test]
async fn prerelease_top_ranked_by_date_is_skipped_for_the_next_cooled_stable_release() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let older = PublishTime::from_unix_secs(now.as_unix_secs() - 31 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("2.0.0").with_published_at(recent), MockVersion::new("2.0.0-rc.1")
.with_published_at(old)
.with_prerelease(true), MockVersion::new("1.9.0").with_published_at(older), MockVersion::new("1.8.0").with_published_at(older), ];
let package_versions = fetch_pkg(versions, vec!["1.8.0"], cooldown_secs)
.await
.expect("pkg must resolve");
let fallback = package_versions.cooldown_fallback.expect(
"the prerelease must be skipped in favor of the next cooled stable release",
);
assert_eq!(fallback.version.as_str(), "1.9.0");
}
#[tokio::test]
async fn no_in_use_version_yields_a_fallback_when_a_safe_cleared_candidate_exists() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0").with_published_at(old),
];
let package_versions = fetch_pkg(versions, Vec::new(), cooldown_secs)
.await
.expect("pkg must resolve");
let fallback = package_versions
.cooldown_fallback
.expect("Absent floor must no longer suppress a safe, cleared candidate");
assert_eq!(fallback.version.as_str(), "1.1.0");
}
#[tokio::test]
async fn partial_in_use_version_match_yields_no_fallback() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0").with_published_at(old),
];
let package_versions = fetch_pkg(
versions,
vec!["1.1.0", "not-a-resolvable-pseudo-version"],
cooldown_secs,
)
.await
.expect("pkg must resolve");
assert!(
package_versions.cooldown_fallback.is_none(),
"a partial in-use-version match (one locatable, one not) must fail closed, not \
silently floor at the locatable entry: {:?}",
package_versions.cooldown_fallback
);
}
#[tokio::test]
async fn non_normalized_in_use_pin_yields_no_fallback() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0").with_published_at(old),
MockVersion::new("1.0.0").with_published_at(old),
];
let package_versions = fetch_pkg(versions, vec!["1.0"], cooldown_secs)
.await
.expect("pkg must resolve");
assert!(
package_versions.cooldown_fallback.is_none(),
"a non-normalized in-use pin must fail closed (Unlocatable), never silently \
treated as Absent or matched loosely: {:?}",
package_versions.cooldown_fallback
);
}
#[tokio::test]
async fn unknown_list_based_pick_still_runs_the_full_fallback_search() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60); let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0").with_published_at(old),
MockVersion::new("1.0.0").with_published_at(old),
];
let package_versions = fetch_pkg_with_registry(
Arc::new(NoListPickRegistry(versions)),
vec!["1.0.0"],
cooldown_secs,
)
.await
.expect("pkg must resolve via the get_latest_matching network fallback");
assert_eq!(
package_versions.latest.as_str(),
"1.2.0",
"latest must come from the get_latest_matching fallback, not the list-based pick"
);
let fallback = package_versions.cooldown_fallback.expect(
"an unknown list-based pick must still run the full search and find the \
cooldown-cleared, above-floor candidate",
);
assert_eq!(fallback.version.as_str(), "1.1.0");
}
#[tokio::test]
async fn known_cleared_unfiltered_pick_yields_no_fallback() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(old),
MockVersion::new("1.1.0").with_published_at(old),
];
let package_versions = fetch_pkg(versions, vec!["1.1.0"], cooldown_secs)
.await
.expect("pkg must resolve");
assert!(
package_versions.cooldown_fallback.is_none(),
"a known, already-cleared unfiltered pick must skip the fallback scan: {:?}",
package_versions.cooldown_fallback
);
}
#[tokio::test]
async fn read_time_disposition_agrees_with_a_cleared_fetch_time_gate() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(old),
MockVersion::new("1.1.0").with_published_at(old),
];
let package_versions = fetch_pkg(versions, vec!["1.1.0"], cooldown_secs)
.await
.expect("pkg must resolve");
assert!(package_versions.cooldown_fallback.is_none());
let disposition = deps_core::lsp_helpers::cooldown_disposition(
&package_versions,
&PackageName::new("pkg"),
deps_core::freshness::FreshnessSettings::Enabled {
cooldown: deps_core::CooldownWindow::from_secs(cooldown_secs),
},
None,
now,
);
assert_eq!(
disposition,
deps_core::lsp_helpers::CooldownDisposition::Cleared,
"read time must agree with the fetch-time gate under unchanged inputs: {disposition:?}"
);
}
#[tokio::test]
async fn narrowed_cooldown_window_between_fetch_and_read_yields_a_safe_skip_not_a_write() {
let now = PublishTime::now();
let fetch_cooldown_secs = 3 * 24 * 60 * 60;
let one_day_ago = PublishTime::from_unix_secs(now.as_unix_secs() - 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(one_day_ago),
MockVersion::new("1.1.0").with_published_at(one_day_ago),
];
let package_versions = fetch_pkg(versions, vec!["1.1.0"], fetch_cooldown_secs)
.await
.expect("pkg must resolve");
assert!(
package_versions.cooldown_fallback.is_none(),
"the fetch-time gate saw latest as Cleared under the wider window, so no \
fallback was ever computed or stored"
);
let narrowed_cooldown_secs = 2 * 24 * 60 * 60;
let disposition = deps_core::lsp_helpers::cooldown_disposition(
&package_versions,
&PackageName::new("pkg"),
deps_core::freshness::FreshnessSettings::Enabled {
cooldown: deps_core::CooldownWindow::from_secs(narrowed_cooldown_secs),
},
None,
now,
);
match disposition {
deps_core::lsp_helpers::CooldownDisposition::Blocked { fallback, .. } => {
assert!(
fallback.is_none(),
"a narrowed window must never surface a fallback the fetch-time gate \
never computed — that would be an unsafe write, not a stricter skip"
);
}
other => panic!("expected a stricter read-time Blocked skip, got {other:?}"),
}
}
#[tokio::test]
async fn read_time_disposition_agrees_with_the_network_fallback_branch() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("1.2.0").with_published_at(recent),
MockVersion::new("1.1.0").with_published_at(old),
MockVersion::new("1.0.0").with_published_at(old),
];
let package_versions = fetch_pkg_with_registry(
Arc::new(NoListPickRegistry(versions)),
vec!["1.0.0"],
cooldown_secs,
)
.await
.expect("pkg must resolve via the get_latest_matching network fallback");
let disposition = deps_core::lsp_helpers::cooldown_disposition(
&package_versions,
&PackageName::new("pkg"),
deps_core::freshness::FreshnessSettings::Enabled {
cooldown: deps_core::CooldownWindow::from_secs(cooldown_secs),
},
None,
now,
);
match disposition {
deps_core::lsp_helpers::CooldownDisposition::Blocked {
fallback: Some(fallback),
..
} => {
assert_eq!(fallback.version.as_str(), "1.1.0");
}
other => panic!(
"expected read time to agree with the fetch-time-stored fallback, got {other:?}"
),
}
}
#[tokio::test]
async fn read_time_disposition_agrees_with_the_gossip_substituted_latest_branch() {
let now = PublishTime::now();
let cooldown_secs = 3 * 24 * 60 * 60;
let recent = PublishTime::from_unix_secs(now.as_unix_secs() - 60);
let old = PublishTime::from_unix_secs(now.as_unix_secs() - 30 * 24 * 60 * 60);
let versions = vec![
MockVersion::new("3.0.0").with_published_at(recent),
MockVersion::new("2.0.0").with_published_at(recent),
MockVersion::new("1.5.0").with_published_at(old),
MockVersion::new("1.0.0").with_published_at(old),
];
let mut gossip = HashMap::new();
gossip.insert(
PackageName::new("pkg"),
deps_core::test_util::stub_gossip_findings(
"3.0.0",
Some(deps_core::GossipCooldown::new(
PublishTime::from_unix_secs(i64::MAX / 2),
deps_core::GossipRiskLevel::High,
)),
),
);
let registry: Arc<dyn Registry> = Arc::new(FixedRegistry(versions));
let mut in_use_map = HashMap::new();
in_use_map.insert(
PackageName::new("pkg"),
vec![ConcreteVersion::from("1.0.0")],
);
let result = fetch_latest_versions_parallel(
registry,
with_registry_source(vec![PackageName::new("pkg")]),
&in_use_map,
None,
deps_core::freshness::FreshnessSettings::Enabled {
cooldown: deps_core::CooldownWindow::from_secs(cooldown_secs),
},
5,
10,
&SelectionContext::none(),
Some(&gossip),
)
.await;
let package_versions = result
.versions
.get(&PackageName::new("pkg"))
.cloned()
.expect("pkg must resolve");
assert_eq!(
package_versions.latest.as_str(),
"2.0.0",
"the GOSSIP floor-protected filter must substitute the filtered pick as latest"
);
assert_eq!(
package_versions
.gossip_excluded_version
.as_ref()
.map(ConcreteVersion::as_str),
Some("3.0.0")
);
let fetch_time_fallback = package_versions
.cooldown_fallback
.as_ref()
.expect("the full scan must have run (SC-012's short-circuit cannot fire here)");
assert_eq!(fetch_time_fallback.version.as_str(), "1.5.0");
let disposition = deps_core::lsp_helpers::cooldown_disposition(
&package_versions,
&PackageName::new("pkg"),
deps_core::freshness::FreshnessSettings::Enabled {
cooldown: deps_core::CooldownWindow::from_secs(cooldown_secs),
},
Some(&gossip),
now,
);
match disposition {
deps_core::lsp_helpers::CooldownDisposition::Blocked {
fallback: Some(fallback),
..
} => {
assert_eq!(
fallback.version.as_str(),
"1.5.0",
"read time, evaluated against the substituted latest, must agree with \
the fetch-time-computed fallback"
);
}
other => panic!(
"expected read time to agree with the fetch-time-stored fallback for the \
substituted latest, got {other:?}"
),
}
}
}
}