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

nordisk/holger

traits/src/test_support.rs

//! **Shared test support** — behind the `test-support` feature, pulled in only as a
//! dev-dependency (`traits = { ..., features = ["test-support"] }`).
//!
//! One home for what every test file used to copy:
//!
//! | helper | replaces |
//! |---|---|
//! | [`fstatus`] | 34 copies of the `nornir_testmatrix::functional_status` wrapper |
//! | [`MemReader`], [`CountingReader`] | the in-memory archive fixture of the 12 `znippy-*` crates |
//! | [`hold_port`], [`free_port`], [`bound_listener`] | the racy "bind :0, read the port, drop it" copies |
//! | [`wasm_guest`] | the hand-built guests (`cargo xtask build-wasm-plugins` needed podman) |
//! | [`lfs_bytes`], [`assert_not_lfs_pointer`] | a pointer file that failed with a misleading message |

use std::collections::HashMap;
use std::net::{IpAddr, SocketAddr, TcpListener};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, OnceLock};

use znippy_common::ZnippyReader;

/// Emit one functional-status row into the `nornir test` matrix.
///
/// The same function every test used to wrap. It records only when the build
/// turns on `nornir-testmatrix/testmatrix` (each crate's own `testmatrix`
/// feature does), and is an empty inline stub otherwise.
pub use nornir_testmatrix::functional_status as fstatus;

// ── in-memory archive fixtures ──────────────────────────────────────────────

/// An in-memory [`ZnippyReader`]: a path → bytes map standing in for an archive.
pub struct MemReader {
    files: HashMap<String, Vec<u8>>,
}

impl MemReader {
    /// A reader over `files` (`&str` or `String` paths).
    pub fn new<S: Into<String>>(files: impl IntoIterator<Item = (S, Vec<u8>)>) -> Arc<Self> {
        Arc::new(Self {
            files: files.into_iter().map(|(k, v)| (k.into(), v)).collect(),
        })
    }
}

impl ZnippyReader for MemReader {
    fn list_files(&self) -> anyhow::Result<Vec<String>> {
        Ok(self.files.keys().cloned().collect())
    }
    fn extract_file(&self, path: &str) -> anyhow::Result<Vec<u8>> {
        self.files
            .get(path)
            .cloned()
            .ok_or_else(|| anyhow::anyhow!("not found: {}", path))
    }
    fn contains(&self, path: &str) -> bool {
        self.files.contains_key(path)
    }
    fn file_size(&self, path: &str) -> Option<u64> {
        self.files.get(path).map(|v| v.len() as u64)
    }
}

/// A [`MemReader`] that counts the work a request caused.
///
/// * [`table_reads`](Self::table_reads): `list_files` calls (the archive file table).
/// * [`member_reads`](Self::member_reads): every `extract_file` call.
/// * [`hits`](Self::hits): the `extract_file` calls that found the member and
///   moved bytes (a probe for a missing path is a lookup, not work).
pub struct CountingReader {
    inner: Arc<MemReader>,
    table_reads: AtomicUsize,
    member_reads: AtomicUsize,
    hits: AtomicUsize,
}

impl CountingReader {
    /// Count the reads made through `inner`.
    pub fn new(inner: Arc<MemReader>) -> Arc<Self> {
        Arc::new(Self {
            inner,
            table_reads: AtomicUsize::new(0),
            member_reads: AtomicUsize::new(0),
            hits: AtomicUsize::new(0),
        })
    }
    /// How many times the file table was listed.
    pub fn table_reads(&self) -> usize {
        self.table_reads.load(Ordering::SeqCst)
    }
    /// How many members were asked for, found or not.
    pub fn member_reads(&self) -> usize {
        self.member_reads.load(Ordering::SeqCst)
    }
    /// How many members were found and extracted.
    pub fn hits(&self) -> usize {
        self.hits.load(Ordering::SeqCst)
    }
}

impl ZnippyReader for CountingReader {
    fn list_files(&self) -> anyhow::Result<Vec<String>> {
        self.table_reads.fetch_add(1, Ordering::SeqCst);
        self.inner.list_files()
    }
    fn extract_file(&self, path: &str) -> anyhow::Result<Vec<u8>> {
        self.member_reads.fetch_add(1, Ordering::SeqCst);
        let got = self.inner.extract_file(path);
        if got.is_ok() {
            self.hits.fetch_add(1, Ordering::SeqCst);
        }
        got
    }
    fn contains(&self, path: &str) -> bool {
        self.inner.contains(path)
    }
    fn file_size(&self, path: &str) -> Option<u64> {
        self.inner.file_size(path)
    }
}

// ── ports without a race ────────────────────────────────────────────────────

/// A loopback port held by a bound (not listening) socket for as long as the
/// lease lives.
///
/// The old copies bound `:0`, read the port and dropped the listener, so a
/// parallel test (or an outgoing connection's ephemeral port) could take it
/// before the server bound it. A lease keeps the port bound the whole time:
/// the kernel hands it to no other `bind(:0)` and to no outgoing connection.
/// The socket carries `SO_REUSEADDR` and never listens, so the server under
/// test (std and tokio listeners set `SO_REUSEADDR` too) binds and listens on
/// the same port while the lease still holds it. Keep the lease alive until the
/// test ends; restarts on the same port keep working.
///
/// It prints as the bare port number, so `format!("127.0.0.1:{port}")` reads
/// the same as before.
pub struct PortLease {
    _socket: socket2::Socket,
    addr: SocketAddr,
}

impl PortLease {
    /// The leased port.
    pub fn port(&self) -> u16 {
        self.addr.port()
    }
    /// The leased address (ip + port).
    pub fn addr(&self) -> SocketAddr {
        self.addr
    }
}

impl std::fmt::Display for PortLease {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.addr.port())
    }
}

impl std::fmt::Debug for PortLease {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "PortLease({})", self.addr)
    }
}

/// Lease a free port on `127.0.0.1`. See [`PortLease`].
pub fn free_port() -> PortLease {
    free_port_on("127.0.0.1")
}

/// Lease a free port on `ip` (for example `127.0.0.2` in the two-address tests).
pub fn free_port_on(ip: &str) -> PortLease {
    let ip: IpAddr = ip
        .parse()
        .unwrap_or_else(|e| panic!("free_port_on({ip:?}): {e}"));
    let addr = SocketAddr::new(ip, 0);
    let socket = socket2::Socket::new(
        socket2::Domain::for_address(addr),
        socket2::Type::STREAM,
        None,
    )
    .expect("a TCP socket for the port lease");
    socket
        .set_reuse_address(true)
        .expect("SO_REUSEADDR on the port lease");
    socket
        .bind(&addr.into())
        .unwrap_or_else(|e| panic!("bind {addr} for a port lease: {e}"));
    let addr = socket
        .local_addr()
        .expect("lease address")
        .as_socket()
        .expect("an inet address");
    PortLease {
        _socket: socket,
        addr,
    }
}

/// A listening socket on a free loopback port, for code that can take the
/// listener itself (a mock server, `serve_with_incoming`). Nothing to race.
pub fn bound_listener() -> TcpListener {
    TcpListener::bind("127.0.0.1:0").expect("bind 127.0.0.1:0")
}

/// Lease a loopback port and hold it until the test process exits; returns the
/// number. For a test that hands the port to a config, a helper or a child
/// process: no lease variable to keep alive, and a restart on the same port
/// stays safe. See [`PortLease`].
pub fn hold_port() -> u16 {
    hold_port_on("127.0.0.1")
}

/// [`hold_port`] on `ip` (for example `127.0.0.2`).
pub fn hold_port_on(ip: &str) -> u16 {
    static HELD: Mutex<Vec<PortLease>> = Mutex::new(Vec::new());
    let lease = free_port_on(ip);
    let port = lease.port();
    HELD.lock().unwrap_or_else(|p| p.into_inner()).push(lease);
    port
}

// ── wasm guests, built on demand ────────────────────────────────────────────

/// The wasm target every guest is built for.
pub const WASM_TARGET: &str = "wasm32-unknown-unknown";

/// The holger repo root (this crate is `<root>/traits`).
pub fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("traits/.. is the holger repo root")
        .to_path_buf()
}

/// Where the guests are built: `<target>/wasm-guests`, its own target dir, so
/// the nested cargo never waits on the lock of the build that runs the test.
pub fn wasm_guest_target_dir() -> PathBuf {
    let target = std::env::var_os("CARGO_TARGET_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| repo_root().join("target"));
    target.join("wasm-guests")
}

/// The module file name cargo writes for `package`.
pub fn wasm_file_name(package: &str) -> String {
    format!("{}.wasm", package.replace('-', "_"))
}

/// **Build the wasm guest `package` and return its module.**
///
/// Runs `cargo build --release --target wasm32-unknown-unknown -p <package>` in
/// [`wasm_guest_target_dir`]. Cargo's own freshness check decides whether
/// anything is rebuilt, so the module is never stale. No container and no
/// podman. Built once per test process; parallel processes queue on cargo's
/// lock of that directory.
///
/// Panics with the fix when the wasm32 target is missing
/// (`rustup target add wasm32-unknown-unknown`) or the build fails. It never
/// skips: a test that skipped over a missing module would be a permanent green.
pub fn wasm_guest(package: &str) -> PathBuf {
    static BUILT: OnceLock<Mutex<HashMap<String, PathBuf>>> = OnceLock::new();
    let built = BUILT.get_or_init(|| Mutex::new(HashMap::new()));
    let mut built = built.lock().unwrap_or_else(|p| p.into_inner());
    if let Some(p) = built.get(package) {
        return p.clone();
    }
    assert_wasm_target_installed();

    let target = wasm_guest_target_dir();
    let cargo = std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
    let build = |manifest: Option<&str>| {
        let mut cmd = std::process::Command::new(&cargo);
        cmd.current_dir(repo_root())
            .env("CARGO_TARGET_DIR", &target)
            // The outer build's flags are for the host target, not for a guest.
            .env_remove("RUSTFLAGS")
            .env_remove("CARGO_ENCODED_RUSTFLAGS")
            .args(["build", "--release", "--target", WASM_TARGET, "-p", package]);
        if let Some(m) = manifest {
            cmd.args(["--manifest-path", m]);
        }
        cmd.output().unwrap_or_else(|e| {
            panic!(
                "run {} build for the wasm guest {package}: {e}",
                Path::new(&cargo).display()
            )
        })
    };
    // A guest is a member of the root workspace or of `estate/` (H1: holger-build-thing).
    let mut out = build(None);
    if !out.status.success()
        && String::from_utf8_lossy(&out.stderr).contains("did not match any packages")
    {
        out = build(Some("estate/Cargo.toml"));
    }
    if !out.status.success() {
        let stderr = String::from_utf8_lossy(&out.stderr);
        let hint = if stderr.contains("target may not be installed")
            || stderr.contains("can't find crate for `core`")
        {
            "\nThe wasm32 target is missing. Install it with:\n    rustup target add wasm32-unknown-unknown\n"
        } else {
            ""
        };
        panic!("building the wasm guest {package} failed{hint}\n{stderr}");
    }
    let module = target
        .join(WASM_TARGET)
        .join("release")
        .join(wasm_file_name(package));
    let len = std::fs::metadata(&module).map(|m| m.len()).unwrap_or(0);
    assert!(
        len > 0,
        "the build of {package} succeeded but wrote no module at {} (does the crate declare `crate-type = [\"cdylib\"]`?)",
        module.display()
    );
    built.insert(package.to_string(), module.clone());
    module
}

/// Panic with `rustup target add wasm32-unknown-unknown` when the active
/// toolchain has no wasm32 standard library.
pub fn assert_wasm_target_installed() {
    let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into());
    let out = std::process::Command::new(&rustc)
        .args(["--print", "target-libdir", "--target", WASM_TARGET])
        .output()
        .unwrap_or_else(|e| {
            panic!(
                "run {} --print target-libdir: {e}",
                Path::new(&rustc).display()
            )
        });
    let libdir = PathBuf::from(String::from_utf8_lossy(&out.stdout).trim());
    let has_core = std::fs::read_dir(&libdir)
        .map(|d| {
            d.flatten()
                .any(|e| e.file_name().to_string_lossy().starts_with("libcore-"))
        })
        .unwrap_or(false);
    assert!(
        out.status.success() && has_core,
        "the wasm32 target is not installed for this toolchain (no core library under {}).\n\
         Install it with:\n    rustup target add wasm32-unknown-unknown",
        libdir.display()
    );
}

// ── Git LFS ─────────────────────────────────────────────────────────────────

/// The first line of every Git LFS pointer file.
pub const LFS_POINTER_PREFIX: &[u8] = b"version https://git-lfs.github.com/spec/";

/// True when `bytes` is a Git LFS pointer, not the real file.
pub fn is_lfs_pointer(bytes: &[u8]) -> bool {
    bytes.starts_with(LFS_POINTER_PREFIX)
}

/// Panic with "run git lfs pull" when `bytes` (read from `what`) is an LFS pointer.
pub fn assert_not_lfs_pointer(what: &str, bytes: &[u8]) {
    assert!(
        !is_lfs_pointer(bytes),
        "{what} is a Git LFS pointer, not the real file. Fetch the LFS files first:\n    \
         git lfs install --local && git lfs pull"
    );
}

/// Read an LFS-tracked file, failing with "run git lfs pull" if it is a pointer.
pub fn lfs_bytes(path: impl AsRef<Path>) -> Vec<u8> {
    let path = path.as_ref();
    let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
    assert_not_lfs_pointer(&path.display().to_string(), &bytes);
    bytes
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_leased_port_is_not_handed_out_again_and_the_server_can_still_bind_it() {
        let lease = free_port();
        // While the lease lives, no other lease gets the same port.
        let others: Vec<PortLease> = (0..64).map(|_| free_port()).collect();
        assert!(
            others.iter().all(|o| o.port() != lease.port()),
            "a held port was handed out twice"
        );
        // The server under test binds and listens on the leased port.
        let server = TcpListener::bind(("127.0.0.1", lease.port()))
            .expect("the server binds the leased port");
        assert_eq!(server.local_addr().unwrap().port(), lease.port());
        let _client =
            std::net::TcpStream::connect(lease.addr()).expect("a client reaches the server");
        assert_eq!(
            lease.to_string(),
            lease.port().to_string(),
            "prints as the bare port"
        );
    }

    #[test]
    fn a_held_port_stays_held_and_the_server_can_bind_it_twice() {
        let port = hold_port();
        let others: Vec<PortLease> = (0..64).map(|_| free_port()).collect();
        assert!(
            others.iter().all(|o| o.port() != port),
            "a held port was handed out again"
        );
        // A server binds it, stops, and binds it again (a restart).
        drop(TcpListener::bind(("127.0.0.1", port)).expect("first bind"));
        let again =
            TcpListener::bind(("127.0.0.1", port)).expect("the restart binds the same port");
        assert_eq!(again.local_addr().unwrap().port(), port);
    }

    #[test]
    fn counting_reader_counts_tables_members_and_hits() {
        let r = CountingReader::new(MemReader::new([("a", b"1".to_vec())]));
        r.list_files().unwrap();
        assert_eq!(r.extract_file("a").unwrap(), b"1");
        assert!(r.extract_file("missing").is_err());
        assert_eq!((r.table_reads(), r.member_reads(), r.hits()), (1, 2, 1));
        assert!(r.contains("a") && !r.contains("b"));
        assert_eq!(r.file_size("a"), Some(1));
    }

    #[test]
    fn an_lfs_pointer_is_named_with_the_fix() {
        let pointer = b"version https://git-lfs.github.com/spec/v1\noid sha256:00\nsize 1\n";
        assert!(is_lfs_pointer(pointer));
        assert!(!is_lfs_pointer(b"RIFF....WEBP"));
        let msg =
            std::panic::catch_unwind(|| assert_not_lfs_pointer("x.webp", pointer)).unwrap_err();
        let msg = msg.downcast_ref::<String>().cloned().unwrap_or_default();
        assert!(
            msg.contains("git lfs pull"),
            "the message names the fix: {msg}"
        );
    }

    #[test]
    fn the_module_name_follows_the_package() {
        assert_eq!(
            wasm_file_name("holger-handler-skidbladnir-wasm"),
            "holger_handler_skidbladnir_wasm.wasm"
        );
        assert!(wasm_guest_target_dir().ends_with("wasm-guests"));
    }
}