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