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

nordisk/znippy

plugins/wasm/znippy-plugin-maven-wasm/tests/maven_native_wasm_agree.rs

//! The guard that makes "the wasm plugin reuses the native code" checkable.
//!
//! It loads the REAL `.wasm` through the REAL `WasmPlugin` loader and asserts
//! that, over the same bytes, it answers with the same `ExtensionRow` as
//! `NativeMavenPlugin` — including the case where the two paths use *different*
//! decompressors (native fans DEFLATE across `ljar`/gatling, the wasm module
//! runs the single-threaded `linflate` scan).
//!
//! It is deliberately impossible to pass this file without a fresh `.wasm`:
//! a missing OR stale module panics with the build command rather than
//! skipping. A skip here would be a permanent green — the exact shape that let
//! a wasm example sit "documented but never built" since the loader landed.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::SystemTime;

use znippy_common::plugin::{ArchiveTypePlugin, ExtensionRow, ExtensionValue};
use znippy_common::plugins::wasm_loader::WasmPlugin;
use znippy_plugin_maven::NativeMavenPlugin;
use znippy_zip_fixtures::{build_zip_deflated, build_zip_stored, pom_path, pom_xml};

const BUILD_CMD: &str =
    "cargo build --target wasm32-unknown-unknown --release -p znippy-plugin-maven-wasm";
const WASM_NAME: &str = "znippy_plugin_maven_wasm.wasm";

/// Every place the module can legitimately land, most specific first.
fn wasm_candidates() -> Vec<PathBuf> {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let rel = Path::new("wasm32-unknown-unknown/release").join(WASM_NAME);
    let mut out = Vec::new();
    if let Ok(p) = std::env::var("MAVEN_WASM") {
        out.push(PathBuf::from(p));
    }
    if let Ok(t) = std::env::var("CARGO_TARGET_DIR") {
        out.push(PathBuf::from(t).join(&rel));
    }
    // **The target directory this very test binary was built into.**
    //
    // znippy sets `target-dir` in `.cargo/config.toml`, so the module lands in
    // neither `CARGO_TARGET_DIR` (unset) nor `<repo>/target` (does not exist) —
    // which is why this test could not find a module that WAS built, and why no
    // CI step could have made it pass. The honest way to find the target
    // directory is to ask where we are: this binary is
    // `<target>/debug/deps/<name>`.
    if let Ok(exe) = std::env::current_exe() {
        if let Some(target) = exe.ancestors().nth(3) {
            out.push(target.join(&rel));
        }
    }
    // plugins/wasm/znippy-plugin-maven-wasm → repo root
    out.push(manifest.join("../../../target").join(&rel));
    out
}

/// Newest mtime across the sources that go INTO the module — this crate's ABI
/// shim and the whole of `znippy-plugin-maven`, since the point of the design
/// is that the maven logic is the shared one.
fn newest_source_mtime() -> SystemTime {
    let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
    let roots = [manifest.join("src"), manifest.join("../../native/znippy-plugin-maven/src")];
    let mut newest = SystemTime::UNIX_EPOCH;
    for root in roots {
        let entries = std::fs::read_dir(&root)
            .unwrap_or_else(|e| panic!("cannot read wasm plugin source dir {}: {e}", root.display()));
        let mut seen = 0usize;
        for entry in entries.flatten() {
            if entry.path().extension().is_some_and(|e| e == "rs") {
                seen += 1;
                if let Ok(m) = entry.metadata().and_then(|m| m.modified()) {
                    newest = newest.max(m);
                }
            }
        }
        // A source root that yielded no .rs would silently make the staleness
        // check vacuous — the "guard that can never fire" shape.
        assert!(seen > 0, "no .rs sources found under {} — staleness check would be blind", root.display());
    }
    newest
}

/// Resolve the module, or fail with everything needed to fix it.
fn wasm_path() -> PathBuf {
    let candidates = wasm_candidates();
    let found = candidates.iter().find(|p| p.is_file()).unwrap_or_else(|| {
        panic!(
            "no {WASM_NAME} found. This test loads a real wasm module; it does not skip.\n\
             Build it with:\n    {BUILD_CMD}\n\
             Looked in:\n{}",
            candidates.iter().map(|p| format!("    {}", p.display())).collect::<Vec<_>>().join("\n")
        )
    });

    let built = found
        .metadata()
        .and_then(|m| m.modified())
        .unwrap_or_else(|e| panic!("cannot stat {}: {e}", found.display()));
    let newest = newest_source_mtime();
    assert!(
        built >= newest,
        "{} is STALE — it predates the maven sources it is built from.\n\
         Rebuild with:\n    {BUILD_CMD}",
        found.display()
    );
    found.clone()
}

fn load_wasm() -> WasmPlugin {
    let p = wasm_path();
    WasmPlugin::load(&p.to_string_lossy(), "maven", 3)
        .unwrap_or_else(|e| panic!("WasmPlugin::load({}) failed: {e}", p.display()))
}

/// Comparable projection of a row: column → value, with null columns dropped.
///
/// The two surfaces cannot be compared variant-for-variant, and that is a
/// property of the ABI rather than of this test: `wasm_loader::parse_json_to_row`
/// only ever produces `ExtensionValue::Str`, and its JSON has no null — which is
/// why `fields_to_json` OMITS a null column instead of writing one. Dropping
/// null on the native side is the matching half of that contract, not a
/// loosening of the assertion: a value present on one side and absent on the
/// other still fails.
fn normalize(row: &ExtensionRow) -> BTreeMap<String, String> {
    row.fields
        .iter()
        .filter_map(|(k, v)| match v {
            ExtensionValue::Str(s) => Some((k.clone(), s.clone())),
            ExtensionValue::OptStr(Some(s)) => Some((k.clone(), s.clone())),
            ExtensionValue::OptStr(None) => None,
            other => Some((k.clone(), format!("{other:?}"))),
        })
        .collect()
}

fn both(path: &str, data: &[u8]) -> (BTreeMap<String, String>, BTreeMap<String, String>) {
    let native = NativeMavenPlugin
        .extract_metadata(path, data)
        .unwrap_or_else(|| panic!("native plugin returned None for {path}"));
    let wasm = load_wasm()
        .extract_metadata(path, data)
        .unwrap_or_else(|| panic!("wasm plugin returned None for {path}"));
    (normalize(&native), normalize(&wasm))
}

#[test]
fn a_bare_pom_gives_both_paths_the_same_row() {
    let pom = pom_xml("com.example", "mylib", "1.2.3");
    let (native, wasm) = both("com/example/mylib/1.2.3/mylib-1.2.3.pom", pom.as_bytes());

    // Assert the CONTENT, not just that the two agree — two identically broken
    // paths would agree on an empty map.
    assert_eq!(native.get("group_id").map(String::as_str), Some("com.example"));
    assert_eq!(native.get("artifact_id").map(String::as_str), Some("mylib"));
    assert_eq!(native.get("version").map(String::as_str), Some("1.2.3"));
    assert_eq!(native.len(), 4, "primary artifact must carry no classifier: {native:?}");
    assert_eq!(native, wasm, "native and wasm rows differ for a .pom");
}

#[test]
fn a_stored_jar_gives_both_paths_the_same_row() {
    let jar = build_zip_stored(
        &pom_path("org.apache.commons", "commons-lang3"),
        pom_xml("org.apache.commons", "commons-lang3", "3.14.0").as_bytes(),
    );
    let (native, wasm) = both("commons-lang3-3.14.0.jar", &jar);

    assert_eq!(native.get("group_id").map(String::as_str), Some("org.apache.commons"));
    assert_eq!(native.get("version").map(String::as_str), Some("3.14.0"));
    assert_eq!(native, wasm, "native and wasm rows differ for a stored JAR");
}

/// The interesting one: the two paths run DIFFERENT decompressors here — native
/// goes through `ljar` (gatling fan-out), wasm through the in-module `linflate`
/// scan — and must still land on the same row.
#[test]
fn a_deflated_jar_agrees_across_two_different_decompressors() {
    let jar = build_zip_deflated(
        &pom_path("io.grpc", "grpc-core"),
        pom_xml("io.grpc", "grpc-core", "1.63.0").as_bytes(),
    );
    let (native, wasm) = both("grpc-core-1.63.0.jar", &jar);

    assert_eq!(native.get("artifact_id").map(String::as_str), Some("grpc-core"));
    assert_eq!(native.get("version").map(String::as_str), Some("1.63.0"));
    assert_eq!(native, wasm, "native (ljar) and wasm (linflate) rows differ for a DEFLATE JAR");
}

/// A classifier exists only in the filename, so this proves the shared
/// `classifier_from_path` really is reached from both surfaces — and that the
/// column travels the JSON ABI rather than being dropped by it.
#[test]
fn a_classified_artifact_carries_the_classifier_on_both_paths() {
    let jar = build_zip_stored(
        &pom_path("com.example", "mylib"),
        pom_xml("com.example", "mylib", "1.2.3").as_bytes(),
    );
    let (native, wasm) = both("com/example/mylib/1.2.3/mylib-1.2.3-sources.jar", &jar);

    assert_eq!(native.get("classifier").map(String::as_str), Some("sources"));
    assert_eq!(native.len(), 5, "classified artifact must carry all five columns");
    assert_eq!(native, wasm, "classifier disagrees between native and wasm");
}

#[test]
fn both_surfaces_publish_the_same_schema_and_extensions() {
    let wasm = load_wasm();

    let native_cols: Vec<_> = NativeMavenPlugin
        .schema_fields()
        .iter()
        .map(|f| (f.name().clone(), f.data_type().to_string()))
        .collect();
    let wasm_cols: Vec<_> = wasm
        .schema_fields()
        .iter()
        .map(|f| (f.name().clone(), f.data_type().to_string()))
        .collect();
    assert!(!native_cols.is_empty(), "native schema is empty — nothing to compare");
    assert_eq!(native_cols, wasm_cols, "schema_fields() differ");

    for p in [".jar", ".war", ".ear", ".pom"] {
        let path = format!("x-1.0{p}");
        assert!(NativeMavenPlugin.matches_path(&path), "native missed {p}");
        assert!(wasm.matches_path(&path), "wasm missed {p}");
    }
    assert!(!NativeMavenPlugin.matches_path("x-1.0.whl"));
    assert!(!wasm.matches_path("x-1.0.whl"), "wasm claims a wheel");
}

/// Not every input has a pom; both surfaces must say so the same way.
#[test]
fn both_surfaces_return_none_for_a_jar_with_no_pom() {
    let jar = build_zip_stored("com/example/App.class", b"\xca\xfe\xba\xbe");
    assert!(NativeMavenPlugin.extract_metadata("app-1.0.jar", &jar).is_none());
    assert!(load_wasm().extract_metadata("app-1.0.jar", &jar).is_none());
}