facett-ops/tests/wheel_verify.rs
//! **RED FIRST for row 1.** `facett_ops::pip::verify_wheel` is the only thing standing
//! between "podman exited 0" and "the wheel we published actually carries the
//! components". A verifier that has only ever been run on a good wheel is a
//! verifier nobody has seen fail — LAW 2 — so every fault it can report is
//! constructed here, as a real zip on disk, and the test asserts THAT SPECIFIC
//! fault comes back.
//!
//! The wheels below are built by hand rather than by python on purpose: the point
//! is to hand the checker bytes that a build tool would never produce, which is
//! precisely the shape a corrupted or half-copied artifact has.
use std::io::Write;
use std::path::{Path, PathBuf};
use base64::Engine as _;
use sha2::{Digest, Sha256};
use facett_ops::pip::{self, WheelFault};
/// A minimal but VALID wasm module: the 4-byte `\0asm` magic + version 1. Small
/// enough to inline, real enough that the magic check passes for the right reason.
const TINY_WASM: &[u8] = b"\0asm\x01\x00\x00\x00";
/// A scratch dir under the session scratchpad — never `/tmp` (that is RAM on oden).
fn scratch(tag: &str) -> PathBuf {
let base = std::env::var("CARGO_TARGET_TMPDIR")
.map(PathBuf::from)
.unwrap_or_else(|_| std::env::temp_dir());
let dir = base.join(format!("wheel-verify-{tag}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("create scratch dir");
dir
}
fn record_line(path: &str, bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
format!("{path},sha256={b64},{}\n", bytes.len())
}
/// The members a HEALTHY `facett` (components) wheel has. Mutating this list is how
/// each red case is built, so every fault differs from the green case in exactly
/// one way.
fn healthy_members() -> Vec<(String, Vec<u8>)> {
let mut m: Vec<(String, Vec<u8>)> = vec![
("facett/__init__.py".into(), b"ASSETS = 'assets'\n".to_vec()),
("facett/streamlit.py".into(), b"import streamlit\n".to_vec()),
("facet_wrapped_web/__init__.py".into(), b"from facett import *\n".to_vec()),
("facett/assets/facet_wrapped_bg.wasm".into(), TINY_WASM.to_vec()),
("facett/assets/facet_wrapped.js".into(), b"export function init(){}\n".to_vec()),
("facett/assets/manifest.json".into(), b"{\"components\":[]}\n".to_vec()),
("facett/assets/index.html".into(), b"<canvas id=facet></canvas>\n".to_vec()),
("facett-0.2.1.dist-info/METADATA".into(), b"Metadata-Version: 2.4\nName: facett\nVersion: 0.2.1\n".to_vec()),
("facett-0.2.1.dist-info/WHEEL".into(), b"Wheel-Version: 1.0\n".to_vec()),
];
// Every OTHER required asset (the non-core bundle groups and the routing table,
// all from the recipe): a tiny real module for a `.wasm`, a line of text else.
for asset in pip::required_assets() {
let path = format!("facett/assets/{asset}");
if !m.iter().any(|(n, _)| *n == path) {
let bytes = if asset.ends_with(".wasm") { TINY_WASM.to_vec() } else { b"{}\n".to_vec() };
m.push((path, bytes));
}
}
m
}
/// How a wheel should be broken, if at all.
#[derive(Default)]
struct Damage {
/// Write the RECORD for these bytes, then store DIFFERENT bytes for this path.
tamper: Option<(String, Vec<u8>)>,
/// Store this member but leave it out of RECORD entirely.
unrecord: Option<String>,
/// Record this member but do not store it.
drop_member: Option<String>,
/// Do not emit a RECORD at all.
no_record: bool,
/// Emit a second RECORD under another dist-info.
second_record: bool,
/// Lie about a member's size in RECORD (hash stays correct).
lie_size: Option<String>,
}
fn build_wheel(dir: &Path, file_name: &str, members: Vec<(String, Vec<u8>)>, d: Damage) -> PathBuf {
let path = dir.join(file_name);
let f = std::fs::File::create(&path).expect("create wheel");
let mut zw = zip::ZipWriter::new(f);
let opts: zip::write::FileOptions<'_, '_, ()> =
zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
let mut record = String::new();
for (name, bytes) in &members {
if d.drop_member.as_deref() == Some(name.as_str()) {
// Recorded below, but never written into the archive.
record.push_str(&record_line(name, bytes));
continue;
}
let stored: &[u8] = match &d.tamper {
Some((t, replacement)) if t == name => replacement,
_ => bytes,
};
zw.start_file(name.clone(), opts).expect("start member");
zw.write_all(stored).expect("write member");
if d.unrecord.as_deref() == Some(name.as_str()) {
continue;
}
if d.lie_size.as_deref() == Some(name.as_str()) {
let digest = Sha256::digest(bytes);
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(digest);
record.push_str(&format!("{name},sha256={b64},{}\n", bytes.len() + 999));
continue;
}
// NOTE: the record is written for the ORIGINAL bytes even when `tamper`
// stored something else. That is the whole tampered-wheel scenario.
record.push_str(&record_line(name, bytes));
}
if !d.no_record {
let rec_path = "facett-0.2.1.dist-info/RECORD";
record.push_str(&format!("{rec_path},,\n"));
zw.start_file(rec_path, opts).expect("start RECORD");
zw.write_all(record.as_bytes()).expect("write RECORD");
if d.second_record {
let rec2 = "facett-0.1.99.dist-info/RECORD";
let mut r2 = record.clone();
r2.push_str(&format!("{rec2},,\n"));
zw.start_file(rec2, opts).expect("start RECORD 2");
zw.write_all(r2.as_bytes()).expect("write RECORD 2");
}
}
zw.finish().expect("finish zip");
path
}
const GOOD_NAME: &str = "facett-0.2.1-py3-none-any.whl";
// ── the GREEN case, so every red below differs by exactly one thing ─────────
#[test]
fn a_healthy_wheel_verifies_and_reports_its_assets() {
let dir = scratch("healthy");
let whl = build_wheel(&dir, GOOD_NAME, healthy_members(), Damage::default());
let report = pip::verify_wheel(&whl).expect("healthy wheel must verify");
assert_eq!(report.distribution, "facett");
assert_eq!(report.version, "0.2.1");
// Every member carries a hash; RECORD itself carries none, by spec.
let n = healthy_members().len();
assert_eq!(report.recorded, n, "every non-RECORD member must be hash-checked");
assert_eq!(report.files.len(), n + 1, "{n} members + RECORD");
// The report carries SIZES, not a bool — a shrinking wasm is visible.
assert_eq!(report.asset_len("facet_wrapped_bg.wasm"), Some(TINY_WASM.len() as u64));
for asset in pip::required_assets() {
assert!(report.asset_len(&asset).is_some_and(|n| n > 0), "{asset} must be non-empty");
}
}
// ── RED: the bytes changed after the record was written ────────────────────
#[test]
fn tampered_bytes_are_caught_by_the_record_hash() {
let dir = scratch("tampered");
let target = "facett/assets/facet_wrapped_bg.wasm";
// Same LENGTH, one byte different: a size check alone would pass this.
let mut evil = TINY_WASM.to_vec();
let last = evil.len() - 1;
evil[last] ^= 0xff;
let whl = build_wheel(
&dir,
GOOD_NAME,
healthy_members(),
Damage { tamper: Some((target.into(), evil)), ..Default::default() },
);
match pip::verify_wheel(&whl) {
Err(WheelFault::RecordHashMismatch { path, recorded, actual }) => {
assert_eq!(path, target);
assert_ne!(recorded, actual, "the fault must carry BOTH hashes to be diagnosable");
}
other => panic!("expected RecordHashMismatch, got {other:?}"),
}
}
// ── RED: a file rides along that RECORD does not list ──────────────────────
#[test]
fn an_unrecorded_member_is_refused() {
let dir = scratch("unrecorded");
let target = "facett/assets/manifest.json";
let whl = build_wheel(
&dir,
GOOD_NAME,
healthy_members(),
Damage { unrecord: Some(target.into()), ..Default::default() },
);
assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::UnrecordedFile(target.into())));
}
// ── RED: RECORD promises a file the archive does not hold ──────────────────
#[test]
fn a_recorded_but_absent_member_is_refused() {
let dir = scratch("absent");
let target = "facett/assets/facet_wrapped.js";
let whl = build_wheel(
&dir,
GOOD_NAME,
healthy_members(),
Damage { drop_member: Some(target.into()), ..Default::default() },
);
assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::MissingFromArchive(target.into())));
}
// ── RED: the size is a lie even though the hash is right ───────────────────
#[test]
fn a_lied_size_is_refused_even_with_a_correct_hash() {
let dir = scratch("size");
let target = "facett/__init__.py";
let whl = build_wheel(
&dir,
GOOD_NAME,
healthy_members(),
Damage { lie_size: Some(target.into()), ..Default::default() },
);
match pip::verify_wheel(&whl) {
Err(WheelFault::RecordSizeMismatch { path, recorded, actual }) => {
assert_eq!(path, target);
assert_eq!(recorded, actual + 999);
}
other => panic!("expected RecordSizeMismatch, got {other:?}"),
}
}
// ── RED: no manifest at all ────────────────────────────────────────────────
#[test]
fn a_wheel_without_a_record_is_refused() {
let dir = scratch("norecord");
let whl = build_wheel(
&dir,
GOOD_NAME,
healthy_members(),
Damage { no_record: true, ..Default::default() },
);
assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::NoRecord));
}
#[test]
fn two_records_are_ambiguous_not_a_pass() {
let dir = scratch("tworecords");
let whl = build_wheel(
&dir,
GOOD_NAME,
healthy_members(),
Damage { second_record: true, ..Default::default() },
);
match pip::verify_wheel(&whl) {
Err(WheelFault::AmbiguousRecord(v)) => assert_eq!(v.len(), 2),
other => panic!("expected AmbiguousRecord, got {other:?}"),
}
}
// ── RED: the payload is missing, which is the failure this lane exists for ─
#[test]
fn a_wheel_with_no_wasm_asset_is_refused() {
let dir = scratch("nowasm");
let target = "facett/assets/facet_wrapped_bg.wasm";
let members: Vec<_> =
healthy_members().into_iter().filter(|(n, _)| n != target).collect();
let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::MissingAsset(target.into())));
}
#[test]
fn each_required_asset_is_individually_required() {
// Not one asset — EVERY ONE (each bundle group's three, the loader, the routing table). A guard that only ever checks the wasm would let a
// wheel ship without its loader page and still call itself green.
for asset in pip::required_assets() {
let dir = scratch(&format!("missing-{}", asset.replace('/', "_")));
let target = format!("facett/assets/{asset}");
let members: Vec<_> =
healthy_members().into_iter().filter(|(n, _)| n != &target).collect();
let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
assert_eq!(
pip::verify_wheel(&whl),
Err(WheelFault::MissingAsset(target.clone())),
"dropping {asset} must be caught by name"
);
}
}
// ── RED: present, recorded, hashing perfectly — and still not a component ──
#[test]
fn a_zero_byte_asset_records_perfectly_and_is_still_refused() {
// THE identity-value case. An empty file has a valid sha256, a truthful size of
// 0, and a RECORD line that verifies. "The path exists" cannot tell this from a
// working bundle; the length can.
let dir = scratch("empty");
let target = "facett/assets/facet_wrapped.js";
let members: Vec<_> = healthy_members()
.into_iter()
.map(|(n, b)| if n == target { (n, Vec::new()) } else { (n, b) })
.collect();
let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::EmptyAsset(target.into())));
}
#[test]
fn a_non_wasm_file_wearing_the_wasm_name_is_refused() {
// The other half of the identity value: non-empty, correctly recorded, and not
// a wasm module. This is what a failed build that wrote an error page into
// dist/ looks like from the outside.
let dir = scratch("notwasm");
let target = "facett/assets/facet_wrapped_bg.wasm";
let members: Vec<_> = healthy_members()
.into_iter()
.map(|(n, b)| if n == target { (n, b"<!doctype html>\n".to_vec()) } else { (n, b) })
.collect();
let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
match pip::verify_wheel(&whl) {
Err(WheelFault::NotWasm { first_bytes }) => assert_eq!(first_bytes, b"<!do".to_vec()),
other => panic!("expected NotWasm, got {other:?}"),
}
}
// ── RED: not a wheel at all ────────────────────────────────────────────────
#[test]
fn a_non_zip_is_refused_as_not_a_zip() {
let dir = scratch("notzip");
let whl = dir.join(GOOD_NAME);
std::fs::write(&whl, b"this is not a zip archive").unwrap();
assert!(matches!(pip::verify_wheel(&whl), Err(WheelFault::NotAZip(_))));
}
#[test]
fn a_missing_file_is_refused_as_unreadable() {
let dir = scratch("missing");
assert!(matches!(
pip::verify_wheel(&dir.join(GOOD_NAME)),
Err(WheelFault::Unreadable(_))
));
}
#[test]
fn wheel_names_that_are_not_pep427_are_refused() {
for bad in ["facet_wrapped_web.whl", "facet_wrapped_web-0.1.19.tar.gz", "-0.1.19-py3-none-any.whl"] {
assert!(
matches!(pip::parse_wheel_name(bad), Err(WheelFault::BadWheelName(_))),
"`{bad}` must not parse as a wheel name"
);
}
assert_eq!(
pip::parse_wheel_name(GOOD_NAME).unwrap(),
("facett".to_string(), "0.2.1".to_string())
);
}
// ── 0.2.0: the modules, the version, and the DEMO wheel ─────────────────────
#[test]
fn a_components_wheel_without_its_streamlit_glue_or_alias_is_refused() {
for gone in ["facett/streamlit.py", "facet_wrapped_web/__init__.py", "facett/__init__.py"] {
let dir = scratch(&format!("nomodule-{}", gone.replace('/', "_")));
let members: Vec<_> = healthy_members().into_iter().filter(|(n, _)| n != gone).collect();
let whl = build_wheel(&dir, GOOD_NAME, members, Damage::default());
assert_eq!(pip::verify_wheel(&whl), Err(WheelFault::MissingModule(gone.into())), "{gone}");
}
}
#[test]
fn a_wheel_of_another_version_than_the_recipe_releases_is_refused() {
let dir = scratch("version");
let whl = build_wheel(&dir, "facett-0.1.19-py3-none-any.whl", healthy_members(), Damage::default());
match pip::verify_wheel(&whl) {
Err(WheelFault::VersionMismatch { wheel, declared }) => {
assert_eq!(wheel, "0.1.19");
assert_eq!(declared, facett_build_thing::PYPI_VERSION);
}
other => panic!("expected VersionMismatch, got {other:?}"),
}
}
const DEMO_NAME: &str = "facett_demo-0.2.1-py3-none-any.whl";
/// A healthy `facett-demo` wheel: the required files, the console script, the
/// requirement on `facett[streamlit]` and the optional `korp` extra.
fn healthy_demo(metadata: &str, entry_points: &str) -> Vec<(String, Vec<u8>)> {
let mut m: Vec<(String, Vec<u8>)> = pip::required_demo_files()
.into_iter()
.map(|f| {
let body = if f.ends_with(".json") { b"{}\n".to_vec() } else { b"x = 1\n".to_vec() };
(f, body)
})
.collect();
m.push(("facett_demo-0.2.1.dist-info/METADATA".into(), metadata.as_bytes().to_vec()));
m.push(("facett_demo-0.2.1.dist-info/entry_points.txt".into(), entry_points.as_bytes().to_vec()));
m
}
const DEMO_METADATA: &str = "Metadata-Version: 2.4\nName: facett-demo\nVersion: 0.2.1\n\
Requires-Dist: facett[streamlit]<0.3,>=0.2.1\nProvides-Extra: korp\n\
Requires-Dist: grpcio>=1.60; extra == \"korp\"\n\n# facett-demo\n";
const DEMO_EPS: &str = "[console_scripts]\nfacett-demo = facett_demo.cli:main\n";
fn demo_wheel(tag: &str, members: Vec<(String, Vec<u8>)>) -> PathBuf {
// `build_wheel` writes its RECORD under the components' dist-info; the demo needs
// its own, so the RECORD line is re-pointed by building with a renamed record path.
let dir = scratch(tag);
let path = dir.join(DEMO_NAME);
let f = std::fs::File::create(&path).unwrap();
let mut zw = zip::ZipWriter::new(f);
let opts: zip::write::FileOptions<'_, '_, ()> = zip::write::FileOptions::default();
let mut record = String::new();
for (name, bytes) in &members {
zw.start_file(name.clone(), opts).unwrap();
zw.write_all(bytes).unwrap();
record.push_str(&record_line(name, bytes));
}
let rec = "facett_demo-0.2.1.dist-info/RECORD";
record.push_str(&format!("{rec},,\n"));
zw.start_file(rec, opts).unwrap();
zw.write_all(record.as_bytes()).unwrap();
zw.finish().unwrap();
path
}
#[test]
fn a_healthy_demo_wheel_verifies() {
let whl = demo_wheel("demo-ok", healthy_demo(DEMO_METADATA, DEMO_EPS));
let r = pip::verify_demo_wheel(&whl).expect("healthy demo wheel");
assert_eq!(r.distribution, "facett_demo");
assert_eq!(r.assets.len(), pip::required_demo_files().len());
assert_eq!(pip::Release::of_wheel(DEMO_NAME), Some(pip::Release::Demo));
assert_eq!(pip::Release::of_wheel(GOOD_NAME), Some(pip::Release::Components));
}
#[test]
fn a_demo_wheel_missing_its_run_time_data_is_refused() {
// setuptools leaves non-.py files out unless package-data names them — this is the
// wheel that installs, starts, and dies on the first gallery read.
for gone in ["facett_demo/demo_payloads.json", "facett_demo/errcodes.json", "facett_demo/_buildinfo.py"] {
let members: Vec<_> = healthy_demo(DEMO_METADATA, DEMO_EPS).into_iter().filter(|(n, _)| n != gone).collect();
let whl = demo_wheel(&format!("demo-{}", gone.replace('/', "_")), members);
assert_eq!(pip::verify_demo_wheel(&whl), Err(WheelFault::MissingModule(gone.into())), "{gone}");
}
}
#[test]
fn a_demo_wheel_without_the_console_script_is_refused() {
let whl = demo_wheel("demo-noep", healthy_demo(DEMO_METADATA, "[console_scripts]\nfacett-demo-python = facett_demo.cli:main\n"));
match pip::verify_demo_wheel(&whl) {
Err(WheelFault::MissingEntryPoint { want, found }) => {
assert_eq!(want, "facett-demo = facett_demo.cli:main");
assert!(found.contains("facett-demo-python"), "{found}");
}
other => panic!("expected MissingEntryPoint, got {other:?}"),
}
}
#[test]
fn a_demo_wheel_whose_components_requirement_is_only_an_extra_is_refused() {
// `facett[streamlit]` behind `extra == ...` installs a demo with no components.
let md = DEMO_METADATA.replace(
"Requires-Dist: facett[streamlit]<0.3,>=0.2.1\n",
"Requires-Dist: facett[streamlit]<0.3,>=0.2.1; extra == \"full\"\n",
);
let whl = demo_wheel("demo-noreq", healthy_demo(&md, DEMO_EPS));
assert!(matches!(pip::verify_demo_wheel(&whl), Err(WheelFault::MissingRequirement(r)) if r.contains("facett[streamlit]")));
}
#[test]
fn a_demo_wheel_without_the_korp_extra_is_refused() {
let md = DEMO_METADATA.replace("Provides-Extra: korp\n", "");
let whl = demo_wheel("demo-nokorp", healthy_demo(&md, DEMO_EPS));
assert!(matches!(pip::verify_demo_wheel(&whl), Err(WheelFault::MissingRequirement(r)) if r.contains("korp")));
}