#![allow(clippy::unwrap_used)]
#![expect(
clippy::expect_used,
reason = "this entire module is test-only fixture/helper infrastructure (most of it gated \
behind the test-util feature); every .expect() here is on a fixture Mutex/UTF-8 \
conversion that cannot fail in a single-threaded test harness"
)]
mod stub_formatter;
#[cfg_attr(docsrs, doc(cfg(feature = "test-util")))]
pub use stub_formatter::StubFormatter;
#[must_use]
pub fn test_uri(unix_path: &str) -> url::Url {
#[cfg(windows)]
let owned;
#[cfg(windows)]
let path: &str = {
owned = format!("C:{unix_path}");
&owned
};
#[cfg(not(windows))]
let path: &str = unix_path;
url::Url::from_file_path(path).expect("test_uri: fixture path must be a valid file URL")
}
#[cfg(feature = "test-util")]
pub async fn fallback_edit_outcome(
ecosystem: &dyn crate::Ecosystem,
formatter: &dyn crate::lsp_helpers::EcosystemFormatter,
uri: &url::Url,
content: &str,
dep_name: &str,
fallback: &str,
available: &[&str],
) -> crate::lsp_helpers::FallbackEditVerdict {
let parsed = ecosystem
.parse_manifest(content, uri)
.await
.expect("manifest must parse");
let dep = parsed
.dependencies()
.into_iter()
.find(|d| d.name().as_str() == dep_name)
.expect("dependency present");
let fallback = crate::ConcreteVersion::new(fallback);
let new_text = crate::edit::replacement_text(
formatter,
dep,
&fallback,
dep.version_requirement()
.expect("declared requirement")
.as_str(),
)
.expect("replacement text");
let candidate = crate::edit::ManifestEdit {
range: dep.version_range().expect("version range"),
new_text,
};
let reparse = crate::edit::EcosystemReparse { ecosystem, uri };
let available: Vec<crate::ConcreteVersion> = available
.iter()
.map(|v| crate::ConcreteVersion::new(*v))
.collect();
crate::lsp_helpers::fallback_edit_excludes_newer(
formatter, &reparse, content, dep, &candidate, &fallback, &available,
)
}
#[must_use]
pub fn vuln_key(name: &str) -> crate::osv::VulnKey {
crate::osv::VulnKey::from_name(name.to_string())
}
#[derive(Clone)]
pub struct MockMetadata {
name: crate::PackageName,
latest_version: crate::ConcreteVersion,
}
impl MockMetadata {
#[must_use]
pub fn new(
name: impl Into<crate::PackageName>,
latest_version: impl Into<crate::ConcreteVersion>,
) -> Self {
Self {
name: name.into(),
latest_version: latest_version.into(),
}
}
}
impl crate::Metadata for MockMetadata {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn description(&self) -> Option<&str> {
None
}
fn repository(&self) -> Option<&str> {
None
}
fn documentation(&self) -> Option<&str> {
None
}
fn latest_version(&self) -> &crate::ConcreteVersion {
&self.latest_version
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Prerelease {
MarkerDefault,
Forced(bool),
}
#[derive(Debug, Clone)]
pub struct MockVersion {
version: crate::ConcreteVersion,
published_at: Option<crate::freshness::PublishTime>,
removal_status: crate::RemovalStatus,
prerelease: Prerelease,
license: Vec<String>,
}
impl MockVersion {
#[must_use]
pub fn new(version: impl Into<crate::ConcreteVersion>) -> Self {
Self {
version: version.into(),
published_at: None,
removal_status: crate::RemovalStatus::Available,
prerelease: Prerelease::MarkerDefault,
license: Vec::new(),
}
}
#[must_use]
pub const fn with_published_at(mut self, published_at: crate::freshness::PublishTime) -> Self {
self.published_at = Some(published_at);
self
}
#[must_use]
pub const fn with_removal_status(mut self, removal_status: crate::RemovalStatus) -> Self {
self.removal_status = removal_status;
self
}
#[must_use]
pub fn yanked(mut self, yanked: bool) -> Self {
self.removal_status = crate::RemovalStatus::from_yanked(yanked);
self
}
#[must_use]
pub const fn with_prerelease(mut self, prerelease: bool) -> Self {
self.prerelease = Prerelease::Forced(prerelease);
self
}
#[must_use]
pub fn with_license(mut self, license: Vec<String>) -> Self {
self.license = license;
self
}
}
impl crate::Version for MockVersion {
fn version_string(&self) -> &crate::ConcreteVersion {
&self.version
}
fn removal_status(&self) -> crate::RemovalStatus {
self.removal_status
}
fn is_prerelease(&self) -> bool {
match self.prerelease {
Prerelease::MarkerDefault => {
crate::has_default_prerelease_marker(self.version.as_str())
}
Prerelease::Forced(forced) => forced,
}
}
fn published_at(&self) -> Option<crate::freshness::PublishTime> {
self.published_at
}
fn license(&self) -> &[String] {
&self.license
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub enum SearchBehavior {
Empty,
Results(Vec<MockMetadata>),
Panic(&'static str),
}
#[derive(Default)]
pub struct MockRegistry {
versions: Vec<MockVersion>,
search: Option<SearchBehavior>,
}
impl MockRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_versions(mut self, versions: Vec<MockVersion>) -> Self {
self.versions = versions;
self
}
#[must_use]
pub fn with_search(mut self, search: SearchBehavior) -> Self {
self.search = Some(search);
self
}
}
impl crate::Registry for MockRegistry {
fn get_versions<'a>(
&'a self,
_name: &'a crate::PackageName,
) -> crate::ecosystem::BoxFuture<'a, crate::Result<Vec<Box<dyn crate::Version>>>> {
let versions: Vec<Box<dyn crate::Version>> = self
.versions
.iter()
.cloned()
.map(|v| Box::new(v) as Box<dyn crate::Version>)
.collect();
Box::pin(async move { Ok(versions) })
}
fn get_latest_matching<'a>(
&'a self,
_name: &'a crate::PackageName,
_req: &'a crate::VersionReq,
_selection_context: &'a crate::SelectionContext,
) -> crate::ecosystem::BoxFuture<'a, crate::Result<Option<Box<dyn crate::Version>>>> {
let picked: Option<Box<dyn crate::Version>> = self
.versions
.iter()
.find(|v| !v.removal_status.blocks_resolution())
.cloned()
.map(|v| Box::new(v) as Box<dyn crate::Version>);
Box::pin(async move { Ok(picked) })
}
fn search_raw<'a>(
&'a self,
_query: &'a str,
_limit: usize,
) -> crate::ecosystem::BoxFuture<'a, crate::Result<Vec<Box<dyn crate::Metadata>>>> {
match self.search.as_ref().unwrap_or(&SearchBehavior::Empty) {
SearchBehavior::Empty => Box::pin(async move { Ok(vec![]) }),
SearchBehavior::Results(results) => {
let results: Vec<Box<dyn crate::Metadata>> = results
.iter()
.cloned()
.map(|m| Box::new(m) as Box<dyn crate::Metadata>)
.collect();
Box::pin(async move { Ok(results) })
}
SearchBehavior::Panic(msg) => panic!("{msg}"),
}
}
fn select_latest_matching(
&self,
versions: &[Box<dyn crate::Version>],
_req: &crate::VersionReq,
_selection_context: &crate::SelectionContext,
) -> Option<usize> {
versions
.iter()
.position(|v| !v.removal_status().blocks_resolution())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
pub const ADVERSARIAL_URL_SEGMENTS: &[&str] = &[
".",
"..",
"../../etc/passwd",
"a b",
"a?b=1",
"a#frag",
"%2e%2e",
];
pub fn assert_dot_segment_gated_or_contained(
resolve: impl Fn(&str) -> Option<String>,
expected_host: &str,
expected_path_prefix: &str,
) {
assert_dot_segment_gated_or_contained_transformed(
resolve,
str::to_string,
expected_host,
expected_path_prefix,
);
}
pub fn assert_dot_segment_gated_or_contained_transformed(
resolve: impl Fn(&str) -> Option<String>,
transform: impl Fn(&str) -> String,
expected_host: &str,
expected_path_prefix: &str,
) {
for segment in ADVERSARIAL_URL_SEGMENTS {
let Some(built) = resolve(segment) else {
continue;
};
let parsed = url::Url::parse(&built).unwrap_or_else(|e| {
panic!("adversarial segment {segment:?} produced an unparsable URL {built:?}: {e}")
});
assert_eq!(
parsed.host_str(),
Some(expected_host),
"adversarial segment {segment:?} escaped host: {built}"
);
assert!(
parsed.path().starts_with(expected_path_prefix),
"adversarial segment {segment:?} escaped path prefix {expected_path_prefix:?}: {built}"
);
let expected_fragment = transform(segment);
assert!(
!expected_fragment.is_empty(),
"adversarial segment {segment:?} transformed to an empty fragment but `resolve` \
still returned Some(url) — an empty identifier must be rejected (return `None`) \
before reaching the sink, since an empty survival check would trivially pass \
(\"\".contains(\"\") is always true) and hide a real gate deletion"
);
if expected_fragment == "." || expected_fragment == ".." {
let matched = parsed.path_segments().is_some_and(|mut segs| {
segs.any(|seg| {
urlencoding::decode(seg)
.is_ok_and(|decoded| decoded.starts_with(expected_fragment.as_str()))
})
});
assert!(
matched,
"adversarial segment {segment:?} (expected to survive as {expected_fragment:?}) \
did not survive as its own path segment (silently dropped/collapsed by \
dot-segment normalization?): built url {built}"
);
} else {
let decoded_path = urlencoding::decode(parsed.path()).unwrap_or_else(|e| {
panic!(
"adversarial segment {segment:?}'s URL path failed to percent-decode: {built:?}: {e}"
)
});
assert!(
decoded_path.contains(&expected_fragment),
"adversarial segment {segment:?} (expected to survive as {expected_fragment:?}) \
did not survive intact in the decoded path (silently dropped/collapsed by \
dot-segment normalization?): decoded path {decoded_path:?}, built url {built}"
);
}
}
}
#[cfg(feature = "test-util")]
mod tracing_capture {
use std::cell::{Cell, RefCell};
use std::sync::Once;
use tracing_subscriber::Layer;
use tracing_subscriber::layer::{Context, Filter, SubscriberExt};
thread_local! {
static BUFFER: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
static MAX_LEVEL: Cell<Option<tracing::Level>> = const { Cell::new(None) };
}
#[derive(Clone, Default)]
struct ThreadLocalWriter;
impl std::io::Write for ThreadLocalWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
BUFFER.with(|b| b.borrow_mut().extend_from_slice(buf));
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for ThreadLocalWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
struct ThreadLocalLevelFilter;
impl<S> Filter<S> for ThreadLocalLevelFilter {
fn enabled(&self, meta: &tracing::Metadata<'_>, _cx: &Context<'_, S>) -> bool {
MAX_LEVEL.with(|cell| cell.get().is_some_and(|max| *meta.level() <= max))
}
}
static INIT: Once = Once::new();
fn ensure_installed() {
INIT.call_once(|| {
let fmt_layer = tracing_subscriber::fmt::layer()
.with_writer(ThreadLocalWriter)
.without_time()
.with_target(false)
.with_filter(ThreadLocalLevelFilter);
let subscriber = tracing_subscriber::registry().with(fmt_layer);
tracing::subscriber::set_global_default(subscriber)
.expect("test-capture tracing subscriber must install exactly once per process");
tracing::callsite::rebuild_interest_cache();
});
}
struct CaptureGuard;
impl Drop for CaptureGuard {
fn drop(&mut self) {
MAX_LEVEL.with(|cell| cell.set(None));
}
}
fn begin_capture(max_level: tracing::Level) -> CaptureGuard {
ensure_installed();
assert!(
MAX_LEVEL.with(Cell::get).is_none(),
"nested capture_tracing_output* is not supported: the inner call's buffer/level \
would silently clobber the outer call's"
);
BUFFER.with(|b| b.borrow_mut().clear());
MAX_LEVEL.with(|cell| cell.set(Some(max_level)));
CaptureGuard
}
fn take_captured() -> String {
BUFFER.with(|b| {
String::from_utf8(std::mem::take(&mut *b.borrow_mut()))
.expect("tracing output is valid utf8")
})
}
pub(super) fn capture_at(max_level: tracing::Level, f: impl FnOnce()) -> String {
let _guard = begin_capture(max_level);
f();
take_captured()
}
pub(super) async fn capture_async_at(
max_level: tracing::Level,
fut: impl std::future::Future<Output = ()>,
) -> String {
let _guard = begin_capture(max_level);
fut.await;
take_captured()
}
}
#[cfg(feature = "test-util")]
#[must_use]
pub fn capture_tracing_output(f: impl FnOnce()) -> String {
tracing_capture::capture_at(tracing::Level::INFO, f)
}
#[cfg(feature = "test-util")]
#[must_use]
pub fn capture_tracing_output_at(max_level: tracing::Level, f: impl FnOnce()) -> String {
tracing_capture::capture_at(max_level, f)
}
#[cfg(feature = "test-util")]
pub async fn capture_tracing_output_async(fut: impl std::future::Future<Output = ()>) -> String {
tracing_capture::capture_async_at(tracing::Level::INFO, fut).await
}
#[cfg(feature = "test-util")]
pub async fn capture_tracing_output_async_at(
max_level: tracing::Level,
fut: impl std::future::Future<Output = ()>,
) -> String {
tracing_capture::capture_async_at(max_level, fut).await
}
#[cfg(feature = "test-util")]
pub async fn capture_tracing_span_fields_async(
fut: impl std::future::Future<Output = ()>,
) -> String {
use tracing_subscriber::fmt::format::FmtSpan;
#[derive(Clone)]
struct BufWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
impl std::io::Write for BufWriter {
fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for BufWriter {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::<u8>::new()));
let subscriber = tracing_subscriber::fmt()
.with_writer(BufWriter(std::sync::Arc::clone(&buf)))
.with_span_events(FmtSpan::NEW)
.without_time()
.with_target(false)
.finish();
let guard = tracing::subscriber::set_default(subscriber);
fut.await;
drop(guard);
let captured = buf.lock().unwrap().clone();
String::from_utf8(captured).expect("tracing output is valid utf8")
}
struct StubDependency {
name: crate::PackageName,
}
impl crate::Dependency for StubDependency {
fn name(&self) -> &crate::PackageName {
&self.name
}
fn name_range(&self) -> crate::position::Range {
crate::position::Range::default()
}
fn version_requirement(&self) -> Option<&crate::VersionReq> {
None
}
fn version_range(&self) -> Option<crate::position::Range> {
None
}
fn source(&self) -> crate::parser::DependencySource {
crate::parser::DependencySource::Registry
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
struct StubParseResult {
dependencies: Vec<StubDependency>,
uri: url::Url,
}
impl crate::ParseResult for StubParseResult {
fn dependencies(&self) -> Vec<&dyn crate::Dependency> {
self.dependencies
.iter()
.map(|d| d as &dyn crate::Dependency)
.collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
&self.uri
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
#[must_use]
pub fn github_token_configured() -> bool {
is_configured_token(std::env::var("GITHUB_TOKEN").ok())
}
fn is_configured_token(token: Option<String>) -> bool {
token.is_some_and(|t| !t.is_empty())
}
pub fn unwrap_or_skip_github_rate_limit<T>(
result: Result<T, crate::DepsError>,
test_name: &str,
) -> Option<T> {
rate_limit_skip_decision(result, test_name, github_token_configured())
}
fn rate_limit_skip_decision<T>(
result: Result<T, crate::DepsError>,
test_name: &str,
token_configured: bool,
) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(crate::DepsError::RateLimited {
message,
verified: crate::RateLimitEvidence::Confirmed,
..
}) if !token_configured => {
eprintln!(
"{test_name}: skipping — GitHub API rate-limited and no GITHUB_TOKEN is \
configured: {message}"
);
None
}
Err(e) => panic!(
"{test_name}: unexpected error (GITHUB_TOKEN configured: {token_configured}): {e:?}"
),
}
}
pub async fn should_skip_on_empty_result<T>(
primary_is_empty: bool,
test_name: &str,
probe: impl std::future::Future<Output = Result<T, crate::DepsError>>,
) -> bool {
primary_is_empty
&& rate_limit_skip_decision(probe.await, test_name, github_token_configured()).is_none()
}
#[derive(Debug)]
pub struct SelectionContextCapture;
thread_local! {
static CAPTURED_SELECTION_CONTEXT: std::cell::Cell<Option<crate::SelectionContext>> =
const { std::cell::Cell::new(None) };
}
impl SelectionContextCapture {
pub fn record(ctx: &crate::SelectionContext) {
CAPTURED_SELECTION_CONTEXT.with(|c| c.set(Some(*ctx)));
}
pub fn reset() {
CAPTURED_SELECTION_CONTEXT.with(|c| c.set(None));
}
#[must_use]
pub fn last() -> Option<crate::SelectionContext> {
CAPTURED_SELECTION_CONTEXT.with(std::cell::Cell::get)
}
}
#[must_use]
pub fn stub_parse_result_with_dependencies(count: usize) -> Box<dyn crate::ParseResult> {
Box::new(StubParseResult {
dependencies: (0..count)
.map(|i| StubDependency {
name: crate::PackageName::new(format!("dep-{i}")),
})
.collect(),
uri: test_uri("/project/manifest.toml"),
})
}
#[must_use]
pub fn stub_gossip_findings(
version: impl Into<String>,
cooldown: Option<crate::deps_dev::GossipCooldown>,
) -> crate::GossipFindings {
crate::GossipFindings {
version: version.into(),
cooldown,
low_usage: None,
}
}
#[cfg(all(test, feature = "test-util"))]
mod tests {
use super::capture_tracing_output;
use super::{is_configured_token, rate_limit_skip_decision};
use crate::RateLimitEvidence;
#[test]
fn is_configured_token_rejects_missing_and_empty() {
assert!(!is_configured_token(None));
assert!(!is_configured_token(Some(String::new())));
assert!(is_configured_token(Some("ghp_x".to_string())));
}
#[test]
fn rate_limit_skip_decision_skips_on_verified_rate_limit_without_token() {
let result: Result<u32, crate::DepsError> = Err(crate::DepsError::RateLimited {
message: "rate limited".to_string(),
verified: RateLimitEvidence::Confirmed,
source_status: Some(403),
});
assert_eq!(rate_limit_skip_decision(result, "unit-test", false), None);
}
#[test]
#[should_panic(expected = "unexpected error")]
fn rate_limit_skip_decision_panics_on_unverified_rate_limit_without_token() {
let result: Result<u32, crate::DepsError> = Err(crate::DepsError::RateLimited {
message: "rate limited".to_string(),
verified: RateLimitEvidence::Inferred,
source_status: None,
});
let _ = rate_limit_skip_decision(result, "unit-test", false);
}
#[test]
fn rate_limit_skip_decision_passes_through_ok() {
let result: Result<u32, crate::DepsError> = Ok(7);
assert_eq!(
rate_limit_skip_decision(result, "unit-test", false),
Some(7)
);
}
#[test]
#[should_panic(expected = "unexpected error")]
fn rate_limit_skip_decision_panics_on_rate_limit_with_token_configured() {
let result: Result<u32, crate::DepsError> = Err(crate::DepsError::RateLimited {
message: "rate limited".to_string(),
verified: RateLimitEvidence::Confirmed,
source_status: Some(403),
});
let _ = rate_limit_skip_decision(result, "unit-test", true);
}
#[test]
#[should_panic(expected = "unexpected error")]
fn rate_limit_skip_decision_panics_on_non_rate_limit_error_even_without_token() {
let result: Result<u32, crate::DepsError> =
Err(crate::DepsError::InvalidUri("bad uri".to_string()));
let _ = rate_limit_skip_decision(result, "unit-test", false);
}
#[test]
fn concurrent_untraced_touch_does_not_poison_active_capture() {
fn emit_marker() {
tracing::warn!("deps-core-1006-concurrent-touch-regression-marker");
}
let output = capture_tracing_output(|| {
let (untraced_touched_tx, untraced_touched_rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
emit_marker();
untraced_touched_tx.send(()).unwrap();
});
untraced_touched_rx.recv().unwrap();
emit_marker();
handle.join().unwrap();
});
assert!(
output.contains("deps-core-1006-concurrent-touch-regression-marker"),
"capture was empty after a concurrent sibling-thread untraced touch — \
tracing-core's per-callsite Interest cache regressed (#1006): {output:?}"
);
}
#[test]
fn capture_filters_by_level_and_does_not_leak_across_calls() {
let output = capture_tracing_output(|| {
tracing::debug!("deps-core-1006-level-filter-marker-should-not-appear");
});
assert!(
!output.contains("deps-core-1006-level-filter-marker-should-not-appear"),
"capture_tracing_output's default INFO filter captured a DEBUG emission — \
level filtering regressed: {output:?}"
);
tracing::warn!("deps-core-1006-stale-marker-should-not-leak");
let output = capture_tracing_output(|| {
tracing::warn!("deps-core-1006-fresh-marker");
});
assert!(
!output.contains("deps-core-1006-stale-marker-should-not-leak"),
"a warn! emitted outside any capture window leaked into the next capture's \
result — cross-call isolation regressed: {output:?}"
);
assert!(
output.contains("deps-core-1006-fresh-marker"),
"the current capture's own emission is missing: {output:?}"
);
}
}