alef 0.84.0

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Rendering for the generated Rust tests/common.rs module.

use crate::core::hash::{self, CommentStyle};

/// Generate the `tests/common.rs` module for Rust e2e tests.
///
/// The module spawns the standalone mock-server binary once per test process
/// and exposes it via `mock_server_url()` which reads the `MOCK_SERVER_URL` env var.
/// This allows tests that use mock_url arguments to access the server dynamically
/// without panicking on unset env vars.
///
/// The module:
/// - Spawns the `mock-server` binary (resolved via `CARGO_BIN_EXE_mock-server`) with the fixtures directory as an argument
/// - Reads stdout lines looking for `MOCK_SERVER_URL=http://...` and `MOCK_SERVERS={...}`
/// - Sets environment variables: `MOCK_SERVER_URL` and `MOCK_SERVER_<FIXTURE_ID>` for each entry
/// - Drains remaining stdout in a background thread to prevent blocking
/// - Uses `OnceLock` to ensure the server is spawned exactly once
///
/// It also exposes `runtime()`, the single process-wide Tokio runtime every generated test
/// blocks on instead of each spinning up (and dropping) its own via `#[tokio::test]`.
pub fn render_common_module() -> String {
    // The module is included via `mod common;` in every integration-test
    // binary, but only fixtures that resolve `mock_url` arguments actually
    // call `mock_server_url()` / touch `MOCK_SERVER_URL`. Each integration
    // test compiles as a separate binary, so the unused-in-some symbols
    // would otherwise trip `-D dead_code` under `cargo test`/`cargo clippy`.
    // The crate-level `#![allow(dead_code)]` mirrors the pattern used in
    // `tests/mock_server.rs`.
    hash::header(CommentStyle::DoubleSlash)
        + r#"//
// Auto-spawned mock server setup for e2e tests.
// This module is auto-generated and should not be edited manually.

#![allow(dead_code)]

use std::sync::OnceLock;

static MOCK_SERVER_URL: OnceLock<String> = OnceLock::new();

/// Get the mock server URL, spawning the server if not already running.
///
/// The server is spawned once per test process and reused by all tests.
/// On first call, this function:
/// - Spawns the `mock-server` binary (resolved via `CARGO_BIN_EXE_mock-server`)
/// - Reads `MOCK_SERVER_URL=http://...` from its stdout
/// - Parses `MOCK_SERVERS={...}` JSON and sets env vars for per-fixture servers
/// - Sets `MOCK_SERVER_URL` env var globally
/// - Drains remaining stdout in a background thread
///
/// Subsequent calls return the cached URL without spawning again.
pub fn mock_server_url() -> &'static str {
    MOCK_SERVER_URL.get_or_init(|| {
        // `CARGO_BIN_EXE_mock-server` is injected by Cargo when compiling an
        // integration test in a crate that declares `[[bin]] name = "mock-server"`.
        // It resolves to the freshly built binary for whatever profile the test
        // is run under (debug or release) and guarantees the binary exists before
        // the test runs — unlike a hardcoded `target/release/mock-server` path,
        // which is absent under a plain `cargo test` (debug) run.
        let mock_server_bin = env!("CARGO_BIN_EXE_mock-server");
        let fixtures_dir = concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/../../fixtures"
        );

        // Spawn the mock-server binary with fixtures directory as argument.
        // The mock server is a per-test-process singleton: it is kept alive by the leaked
        // stdin below and reaped by the OS when the test binary exits, so it is deliberately
        // never wait()ed on.
        #[allow(clippy::zombie_processes)]
        let mut child = std::process::Command::new(mock_server_bin)
            .arg(fixtures_dir)
            .stdout(std::process::Stdio::piped())
            .stdin(std::process::Stdio::piped())
            .spawn()
            .expect("Failed to spawn mock-server binary");

        let stdout = child.stdout.take().expect("Failed to get stdout");
        let stdin = child.stdin.take().expect("Failed to get stdin");

        let mut url = String::new();
        let mut line_buffer = String::new();
        let mut line_count = 0;

        // Read startup lines from the mock server.
        // Expected: MOCK_SERVER_URL=http://... then MOCK_SERVERS={...json...}
        // The server prints one line per loaded fixture before the markers, so the
        // ceiling has to be high enough to clear hundreds of "loaded route" lines.
        // We bail on the first `MOCK_SERVERS=` line (always emitted last) rather than
        // relying on the line cap.
        use std::io::BufRead;
        let mut reader = std::io::BufReader::new(stdout);

        while line_count < 2048 {
            line_buffer.clear();
            match reader.read_line(&mut line_buffer) {
                Ok(0) => break,  // EOF
                Ok(_) => {
                    let line = line_buffer.trim();
                    if line.starts_with("MOCK_SERVER_URL=") {
                        url = line.strip_prefix("MOCK_SERVER_URL=")
                            .unwrap_or("")
                            .to_string();
                    } else if line.starts_with("MOCK_SERVERS=") {
                        let json_str = line.strip_prefix("MOCK_SERVERS=")
                            .unwrap_or("{}");
                        // Parse the JSON map and set env vars for each entry.
                        if let Ok(servers) = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(json_str) {
                            for (fid, furl) in servers {
                                if let serde_json::Value::String(url_str) = furl {
                                    let env_key = format!("MOCK_SERVER_{}", fid.to_uppercase());
                                    // SAFETY: runs inside the OnceLock initializer, before any
                                    // test thread has been spawned, so no concurrent env access.
                                    unsafe { std::env::set_var(&env_key, &url_str) };
                                }
                            }
                        }
                        // SAFETY: see above — single-threaded OnceLock initialization.
                        unsafe { std::env::set_var("MOCK_SERVERS", json_str) };
                        // We have seen both lines; stop reading.
                        break;
                    }
                    line_count += 1;
                }
                Err(_) => break,
            }
        }

        // Set the main URL env var globally.
        // SAFETY: see above — single-threaded OnceLock initialization.
        unsafe { std::env::set_var("MOCK_SERVER_URL", &url) };

        // Drain remaining stdout in a background thread to prevent the server from blocking.
        std::thread::spawn(move || {
            let _ = std::io::copy(&mut reader.into_inner(), &mut std::io::sink());
        });

        // Keep stdin alive for the test process lifetime — the mock-server treats
        // stdin EOF as the parent's shutdown signal, so dropping the handle would
        // make it exit before any test connects.
        Box::leak(Box::new(stdin));

        // Return the URL for this process.
        url
    }).as_str()
}

/// The single process-wide Tokio runtime every generated test blocks on.
///
/// ~keep Every test used to be its own `#[tokio::test]`, which builds and drops a fresh
/// `current_thread` runtime per test. The extraction code under test reaches a crawler
/// whose HTTP client pool is a process-global cache keyed on config alone, with no runtime
/// identity. Hyper spawns a connection's driver task and the pool's idle reaper on whichever
/// runtime is current when the connection is created; when that runtime is dropped at the end
/// of the test, those tasks die but the now-dead connection stays in the global pool, ready
/// for a later test on a different runtime to check it out. That produced intermittent
/// "error sending request" and "error decoding response body" failures. Routing every test
/// through one shared runtime that lives for the whole process keeps every pooled connection's
/// driver task alive for as long as the pool can hand the connection out.
pub fn runtime() -> &'static tokio::runtime::Runtime {
    static RUNTIME: OnceLock<tokio::runtime::Runtime> = OnceLock::new();
    RUNTIME.get_or_init(|| {
        // 16 MiB: tokio's ~2 MB default worker stack can overflow on a deep extraction
        // future (a nested archive member, a multi-stage OCR pipeline), and a stack overflow
        // aborts the process with SIGBUS instead of raising a catchable panic.
        const TEST_RUNTIME_STACK_SIZE_BYTES: usize = 16 * 1024 * 1024;
        tokio::runtime::Builder::new_multi_thread()
            .enable_all()
            .thread_stack_size(TEST_RUNTIME_STACK_SIZE_BYTES)
            .build()
            .expect("failed to build the shared test runtime")
    })
}
"#
}