server/repository/znippy-conda/tests/plugin_read_view.rs
//! Holger conda backend serve test, through a REAL compressed archive built with
//! the conda plugin (so the authoritative name/version/build/subdir columns from
//! `info/index.json` are present). Proves the backend serves the package via the
//! typed view AND that R4 holds: `repodata.json` is a REAL index listing the
//! package with its real name/version/build, not the old dummy
//! `{"packages":{},"packages.conda":{}}` stub.
use std::fs;
use std::path::Path;
use holger_znippy_conda_repository::CondaRepoZnippy;
use traits::{ArtifactId, RepositoryBackendTrait};
use znippy_common::plugin::PluginRegistry;
use znippy_common::plugins::conda_native::CondaPlugin;
use znippy_compress::compress_dir;
/// A real classic conda `.tar.bz2`: a bzip2-compressed tar whose `info/index.json`
/// carries the authoritative name/version/build/subdir. These live ONLY there.
fn make_conda_tar_bz2(
name: &str,
version: &str,
build: &str,
subdir: &str,
payload: &[u8],
) -> Vec<u8> {
let index_json = format!(
"{{\"name\":\"{name}\",\"version\":\"{version}\",\"build\":\"{build}\",\"subdir\":\"{subdir}\"}}"
);
let mut tar_bytes = Vec::new();
{
let mut tar = tar::Builder::new(&mut tar_bytes);
let ij = index_json.as_bytes();
let mut h = tar::Header::new_gnu();
h.set_path("info/index.json").unwrap();
h.set_size(ij.len() as u64);
h.set_mode(0o644);
h.set_cksum();
tar.append(&h, ij).unwrap();
let body = payload;
let mut h2 = tar::Header::new_gnu();
h2.set_path("lib/thing.py").unwrap();
h2.set_size(body.len() as u64);
h2.set_mode(0o644);
h2.set_cksum();
tar.append(&h2, body).unwrap();
tar.finish().unwrap();
}
use std::io::Write;
let mut enc = bzip2::write::BzEncoder::new(Vec::new(), bzip2::Compression::fast());
enc.write_all(&tar_bytes).unwrap();
enc.finish().unwrap()
}
fn build_archive(dir: &Path, out: &Path) {
compress_dir(
&dir.to_path_buf(),
&out.to_path_buf(),
false,
Some(&PluginRegistry::with_plugin(Box::new(CondaPlugin))),
None,
None,
)
.expect("compress_dir with conda plugin");
}
#[test]
fn conda_backend_serves_package_via_view_and_real_repodata() {
let src = tempfile::tempdir().unwrap();
let out = tempfile::tempdir().unwrap();
let pkg_bytes = make_conda_tar_bz2(
"numpy",
"1.26.0",
"py311h1234567_0",
"linux-64",
b"payload\n",
);
fs::create_dir_all(src.path().join("linux-64")).unwrap();
fs::write(
src.path()
.join("linux-64/numpy-1.26.0-py311h1234567_0.tar.bz2"),
&pkg_bytes,
)
.unwrap();
let archive = out.path().join("conda.znippy");
build_archive(src.path(), &archive);
let repo = CondaRepoZnippy::with_archive("conda".into(), archive).unwrap();
// fetch() resolves via the view. subdir rides in the namespace; version is the
// authoritative `1.26.0` from info/index.json.
let id = ArtifactId {
namespace: Some("linux-64".into()),
name: "numpy".into(),
version: "1.26.0".into(),
};
let got = repo.fetch(&id).unwrap();
assert_eq!(
got.as_deref(),
Some(pkg_bytes.as_slice()),
"conda package must serve via the view"
);
// Package wire path resolves via the view (filename rebuilt from coords).
let url = "/conda/linux-64/numpy-1.26.0-py311h1234567_0.tar.bz2";
let (status, _h, body) = repo.handle_http2_request("GET", url, &[]).unwrap();
assert_eq!(status, 200, "package wire path must serve 200");
assert_eq!(body, pkg_bytes, "package serve must return the exact bytes");
// R4: repodata.json is a REAL index built from the view's coords — NOT the
// old empty dummy.
let (rstatus, _rh, rbody) = repo
.handle_http2_request("GET", "/conda/linux-64/repodata.json", &[])
.unwrap();
assert_eq!(rstatus, 200, "repodata.json must resolve");
let repodata = String::from_utf8(rbody).unwrap();
assert!(
!repodata.contains(r#""packages":{}"#),
"R4: repodata.json packages map must NOT be the empty dummy: {repodata}"
);
assert!(
repodata.contains(r#""numpy-1.26.0-py311h1234567_0.tar.bz2""#),
"repodata must key the package by its filename (R4): {repodata}"
);
assert!(
repodata.contains(r#""name":"numpy""#)
&& repodata.contains(r#""version":"1.26.0""#)
&& repodata.contains(r#""build":"py311h1234567_0""#)
&& repodata.contains(r#""subdir":"linux-64""#),
"repodata entry must carry the authoritative name/version/build/subdir (R4): {repodata}"
);
// The `sha256` field must be a REAL SHA-256 (bare hex) of the served bytes.
let expected_sha256 = {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(&pkg_bytes);
format!(
r#""sha256":"{}""#,
h.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect::<String>()
)
};
assert!(
repodata.contains(&expected_sha256),
"repodata sha256 must be the real SHA-256 of the served bytes (R4): want {expected_sha256} in {repodata}"
);
// blake3 is retained under its own non-standard key.
let expected_blake3 = format!(r#""blake3":"{}""#, blake3::hash(&pkg_bytes).to_hex());
assert!(
repodata.contains(&expected_blake3),
"repodata blake3 key carries the blake3 hex: want {expected_blake3} in {repodata}"
);
// list() reports the authoritative coords with the subdir as namespace.
let listed = repo.list(None, 0).unwrap();
assert!(
listed.iter().any(|e| e.id.name == "numpy"
&& e.id.version == "1.26.0"
&& e.id.namespace.as_deref() == Some("linux-64")),
"list must carry the authoritative coords, got: {:?}",
listed
.iter()
.map(|e| (&e.id.namespace, &e.id.name, &e.id.version))
.collect::<Vec<_>>()
);
// Absent coord → None (not a wrong file).
let absent = ArtifactId {
namespace: Some("linux-64".into()),
name: "numpy".into(),
version: "9.9.9".into(),
};
assert_eq!(repo.fetch(&absent).unwrap(), None);
}
// ───────────────────────── the change token ─────────────────────────
/// `coordinate -> change_token` for one listing, refusing a row without one.
fn token_map(entries: &[traits::ArtifactEntry]) -> std::collections::BTreeMap<String, String> {
entries
.iter()
.map(|e| {
let key = format!(
"{}/{}@{}",
e.id.namespace.clone().unwrap_or_default(),
e.id.name,
e.id.version
);
let tok = e
.change_token
.clone()
.unwrap_or_else(|| panic!("{key}: no change token"));
(key, tok)
})
.collect()
}
/// **Every conda row carries a change token, and it moves only when the bytes do.**
///
/// Two independent seals of the same three packages, the second with ONE
/// rewritten (same name, version, build and subdir, same archive path, a
/// different payload). The token is folded from the per-chunk checksums the
/// archive index carries for that row, so the two unchanged packages fold to
/// the same token across the seals and the rewritten one does not. Before
/// `CondaPackage::relative_path` this backend listed through the typed view and
/// could not name its row, so it stamped nothing.
#[test]
fn conda_entries_carry_a_change_token_that_moves_only_when_the_bytes_do() {
let seal = |rewrite: bool| -> (tempfile::TempDir, std::path::PathBuf) {
let src = tempfile::tempdir().unwrap();
let out = tempfile::tempdir().unwrap();
fs::create_dir_all(src.path().join("linux-64")).unwrap();
let lay = |file: &str, bytes: &[u8]| {
fs::write(src.path().join("linux-64").join(file), bytes).unwrap()
};
lay(
"numpy-1.26.0-py311h1234567_0.tar.bz2",
&make_conda_tar_bz2(
"numpy",
"1.26.0",
"py311h1234567_0",
"linux-64",
b"payload\n",
),
);
lay(
"scipy-1.11.4-py311h0000000_0.tar.bz2",
&make_conda_tar_bz2(
"scipy",
"1.11.4",
"py311h0000000_0",
"linux-64",
b"payload\n",
),
);
let payload: &[u8] = if rewrite {
b"rewritten payload\n"
} else {
b"payload\n"
};
lay(
"pandas-2.1.4-py311hffffff_0.tar.bz2",
&make_conda_tar_bz2("pandas", "2.1.4", "py311hffffff_0", "linux-64", payload),
);
let archive = out.path().join("conda.znippy");
build_archive(src.path(), &archive);
(out, archive)
};
let (_a, first) = seal(false);
let (_b, second) = seal(true);
let before = token_map(
&CondaRepoZnippy::with_archive("conda".into(), first)
.unwrap()
.list(None, 0)
.unwrap(),
);
let after = token_map(
&CondaRepoZnippy::with_archive("conda".into(), second)
.unwrap()
.list(None, 0)
.unwrap(),
);
assert_eq!(
before.len(),
3,
"three rows, every one with a token: {before:?}"
);
assert_eq!(
before.keys().collect::<Vec<_>>(),
after.keys().collect::<Vec<_>>(),
"the same coordinates on both sides"
);
let moved = "linux-64/pandas@2.1.4";
assert!(
before.contains_key(moved),
"the rewritten row is listed: {before:?}"
);
for (k, t) in &before {
if k == moved {
assert_ne!(
t, &after[k],
"{k}: the bytes changed, so the token must move"
);
} else {
assert_eq!(
t, &after[k],
"{k}: the bytes did not change, so the token must not move"
);
}
}
}