blob: 332b70b9fb33ef47eaf629288e2ec4869d73af7f [file] [log] [blame]
mod utils;
use http::header;
use tame_index::{IndexLocation, IndexUrl, SparseIndex};
#[inline]
fn crates_io(path: impl AsRef<tame_index::Path>) -> SparseIndex {
SparseIndex::new(
IndexLocation::new(IndexUrl::CratesIoSparse).with_root(Some(path.as_ref().to_owned())),
)
.unwrap()
}
/// Validates the we get a valid root and krate url
#[test]
fn opens_crates_io() {
let index = crates_io(env!("CARGO_MANIFEST_DIR"));
assert_eq!(index.url(), "https://index.crates.io/");
assert_eq!(
index.crate_url("autocfg".try_into().unwrap()),
"https://index.crates.io/au/to/autocfg"
);
}
/// Validates a request can be made for a crate that doesn't have a local cache entry
#[test]
fn make_request_without_cache() {
let index = crates_io(env!("CARGO_MANIFEST_DIR"));
let lock = &utils::unlocked();
let req = index
.make_remote_request("serde".try_into().unwrap(), None, lock)
.unwrap();
let hdrs = req.headers();
// Ensure neither of the possible cache headers are set
assert!(
hdrs.get(header::IF_MODIFIED_SINCE).is_none() && hdrs.get(header::IF_NONE_MATCH).is_none()
);
assert_eq!(hdrs.get("cargo-protocol").unwrap(), "version=1");
assert_eq!(hdrs.get(header::ACCEPT).unwrap(), "text/plain");
assert_eq!(hdrs.get(header::ACCEPT_ENCODING).unwrap(), "gzip");
}
const ETAG: &str = "W/\"fa62f662c9aae1f21cab393950d4ae23\"";
const DATE: &str = "Thu, 22 Oct 2023 09:40:03 GMT";
/// Validates appropriate headers are set when a cache entry does exist
#[test]
fn make_request_with_cache() {
let td = utils::tempdir();
let index = crates_io(&td);
let lock = &utils::unlocked();
{
let etag_krate = utils::fake_krate("etag-krate", 2);
index
.cache()
.write_to_cache(&etag_krate, &format!("{}: {ETAG}", header::ETAG), lock)
.unwrap();
let req = index
.make_remote_request("etag-krate".try_into().unwrap(), None, lock)
.unwrap();
assert_eq!(req.headers().get(header::IF_NONE_MATCH).unwrap(), ETAG);
}
{
let req = index
.make_remote_request("etag-specified-krate".try_into().unwrap(), Some(ETAG), lock)
.unwrap();
assert_eq!(req.headers().get(header::IF_NONE_MATCH).unwrap(), ETAG);
}
{
let modified_krate = utils::fake_krate("modified-krate", 2);
index
.cache()
.write_to_cache(
&modified_krate,
&format!("{}: {DATE}", header::LAST_MODIFIED),
lock,
)
.unwrap();
let req = index
.make_remote_request("modified-krate".try_into().unwrap(), None, lock)
.unwrap();
assert_eq!(req.headers().get(header::IF_MODIFIED_SINCE).unwrap(), DATE);
}
}
/// Validates we can parse a response where the local cache version is up to date
#[test]
fn parse_unmodified_response() {
let td = utils::tempdir();
let index = crates_io(&td);
let lock = &utils::unlocked();
let etag_krate = utils::fake_krate("etag-krate", 2);
index
.cache()
.write_to_cache(&etag_krate, &format!("{}: {ETAG}", header::ETAG), lock)
.unwrap();
let response = http::Response::builder()
.status(http::StatusCode::NOT_MODIFIED)
.header(header::ETAG, ETAG)
.body(Vec::new())
.unwrap();
let cached_krate = index
.parse_remote_response("etag-krate".try_into().unwrap(), response, true, lock)
.unwrap()
.expect("cached krate");
assert_eq!(etag_krate, cached_krate);
}
/// Validates we can parse a modified response
#[test]
fn parse_modified_response() {
let td = utils::tempdir();
let index = crates_io(&td);
let lock = &utils::unlocked();
{
let etag_krate = utils::fake_krate("etag-krate", 3);
let mut serialized = Vec::new();
etag_krate.write_json_lines(&mut serialized).unwrap();
let response = http::Response::builder()
.status(http::StatusCode::OK)
.header(header::ETAG, ETAG)
.body(serialized)
.unwrap();
let new_krate = index
.parse_remote_response("etag-krate".try_into().unwrap(), response, true, lock)
.unwrap()
.expect("new response");
assert_eq!(etag_krate, new_krate);
let cached_krate = index
.cache()
.cached_krate(
"etag-krate".try_into().unwrap(),
Some(&format!("{}: {ETAG}", header::ETAG)),
lock,
)
.unwrap()
.expect("cached krate");
assert_eq!(etag_krate, cached_krate);
}
{
let modified_krate = utils::fake_krate("modified-krate", 3);
let mut serialized = Vec::new();
modified_krate.write_json_lines(&mut serialized).unwrap();
let response = http::Response::builder()
.status(http::StatusCode::OK)
.header(header::LAST_MODIFIED, DATE)
.body(serialized)
.unwrap();
let new_krate = index
.parse_remote_response("modified-krate".try_into().unwrap(), response, true, lock)
.unwrap()
.expect("new response");
assert_eq!(modified_krate, new_krate);
let cached_krate = index
.cache()
.cached_krate(
"modified-krate".try_into().unwrap(),
Some(&format!("{}: {DATE}", header::LAST_MODIFIED)),
lock,
)
.unwrap()
.expect("cached krate");
assert_eq!(modified_krate, cached_krate);
}
}
#[cfg(feature = "sparse")]
mod remote {
use super::*;
/// Ensure we can actually send a request to crates.io and parse the response
#[test]
fn end_to_end() {
let td = utils::tempdir();
let index = crates_io(&td);
let lock = &utils::unlocked();
let client = reqwest::blocking::Client::builder().build().unwrap();
let rsi = tame_index::index::RemoteSparseIndex::new(index, client);
let spdx_krate = rsi
.krate("spdx".try_into().unwrap(), true, lock)
.expect("failed to retrieve spdx")
.expect("failed to find spdx");
spdx_krate
.versions
.iter()
.find(|iv| iv.version == "0.10.1")
.expect("failed to find expected version");
}
// cargo metadata --format-version=1 | jq -r '.packages[].name | "\"\(.)\","'
const KRATES: &[&str] = &[
"addr2line",
"adler",
"async-compression",
"autocfg",
"backtrace",
"base64",
"bitflags",
"bitflags",
"bumpalo",
"bytes",
"camino",
"cargo-platform",
"cargo_metadata",
"cc",
"cfg-if",
"core-foundation",
"core-foundation-sys",
"crc32fast",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-utils",
"either",
"encoding_rs",
"equivalent",
"errno",
"fastrand",
"flate2",
"fnv",
"form_urlencoded",
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
"getrandom",
"gimli",
"h2",
"hashbrown",
"hermit-abi",
"home",
"http",
"http-body",
"httparse",
"httpdate",
"hyper",
"hyper-rustls",
"idna",
"indexmap",
"ipnet",
"itoa",
"js-sys",
"libc",
"linux-raw-sys",
"log",
"memchr",
"mime",
"miniz_oxide",
"mio",
"num_cpus",
"object",
"once_cell",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"proc-macro2",
"quote",
"rayon",
"rayon-core",
"reqwest",
"ring",
"rustc-demangle",
"rustix",
"rustls",
"rustls-pemfile",
"rustls-webpki",
"ryu",
"sct",
"semver",
"serde",
"serde_derive",
"serde_json",
"serde_spanned",
"serde_urlencoded",
"slab",
"smol_str",
"socket2",
"spin",
"static_assertions",
"syn",
"sync_wrapper",
"system-configuration",
"system-configuration-sys",
"tame-index",
"tempfile",
"thiserror",
"thiserror-impl",
"tiny-bench",
"tinyvec",
"tinyvec_macros",
"tokio",
"tokio-rustls",
"tokio-util",
"toml",
"toml_datetime",
"toml_edit",
"tower-service",
"tracing",
"tracing-core",
"try-lock",
"twox-hash",
"unicode-bidi",
"unicode-ident",
"unicode-normalization",
"untrusted",
"url",
"want",
"wasi",
"wasm-bindgen",
"wasm-bindgen-backend",
"wasm-bindgen-futures",
"wasm-bindgen-macro",
"wasm-bindgen-macro-support",
"wasm-bindgen-shared",
"web-sys",
"webpki-roots",
"windows-sys",
"windows-sys",
"windows-targets",
"windows-targets",
"windows_aarch64_gnullvm",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
"windows_x86_64_msvc",
"winnow",
"winreg",
];
fn ensure_no_errors(
results: std::collections::BTreeMap<
String,
Result<Option<tame_index::IndexKrate>, tame_index::Error>,
>,
) {
use std::fmt::Write;
let mut errors = String::new();
for (name, res) in results {
match res {
Ok(Some(_)) => continue,
Ok(None) => writeln!(&mut errors, "{name}:\tfailed to locate").unwrap(),
Err(err) => writeln!(&mut errors, "{name}:\t{err}").unwrap(),
}
}
assert!(errors.is_empty(), "{errors}");
}
/// Reuses connections. This test is intended to be run under strace to
/// validate that <n> connections are not being created
/// <https://github.com/EmbarkStudios/tame-index/issues/46>
#[test]
fn reuses_connection() {
let td = utils::tempdir();
let index = crates_io(&td);
let lock = &utils::unlocked();
let client = reqwest::blocking::Client::builder().build().unwrap();
let rsi = tame_index::index::RemoteSparseIndex::new(index, client);
let results = rsi.krates(
KRATES.iter().map(|s| (*s).to_string()).collect(),
false,
lock,
);
ensure_no_errors(results);
}
// Ditto, but for async
#[test]
fn async_reuses_connection() {
let rt = tokio::runtime::Runtime::new().unwrap();
let _guard = rt.enter();
let td = utils::tempdir();
let index = crates_io(&td);
let lock = &utils::unlocked();
let client = reqwest::Client::builder().build().unwrap();
let rsi = tame_index::index::AsyncRemoteSparseIndex::new(index, client);
let results = rsi
.krates_blocking(
KRATES.iter().map(|s| (*s).to_string()).collect(),
false,
None,
lock,
)
.unwrap();
ensure_no_errors(results);
}
}