Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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"
            );
        }
    }
}