splicer 2.4.1

Plan and generate middleware splice operations for WebAssembly component composition graphs.
Documentation
//! Shared scaffolding for integration tests that drive a tier-1
//! builtin in wasmtime against a synthetic call-id.
//!
//! Each builtin smoke-test (`builtins_otel_bare_spans.rs`,
//! `builtins_otel_bare_metrics.rs`, …) supplies its own `Capture` type and
//! linker-side fake host implementation; everything else (engine
//! config, instantiation, `on-call` → `on-return` drive cycle, `Val`
//! extractors) lives here so the per-test files stay focused on the
//! assertions.

#![allow(dead_code)]

use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use anyhow::Result;
use wasmtime::component::{Component, Linker, ResourceTable, Val};
use wasmtime::{Config, Engine, Store};
use wasmtime_wasi::p2::pipe::MemoryOutputPipe;
use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, WasiView};

pub const SPLICER_BEFORE: &str = "splicer:tier1/before@0.3.0";
pub const SPLICER_AFTER: &str = "splicer:tier1/after@0.3.0";
pub const SPLICER_BUILTIN_CONFIG_GET: &str = "splicer:builtin-config/get@0.1.0";

pub const TARGET_IFACE: &str = "wasi:http/handler@0.3.0";
pub const TARGET_FN: &str = "handle";

pub struct Host<C: Send + 'static> {
    pub wasi: WasiCtx,
    pub table: ResourceTable,
    pub capture: Arc<Mutex<C>>,
}

impl<C: Send + 'static> WasiView for Host<C> {
    fn ctx(&mut self) -> WasiCtxView<'_> {
        WasiCtxView {
            ctx: &mut self.wasi,
            table: &mut self.table,
        }
    }
}

pub fn call_id_val(iface: &str, func: &str) -> Val {
    Val::Record(vec![
        ("interface-name".into(), Val::String(iface.into())),
        ("function-name".into(), Val::String(func.into())),
        ("id".into(), Val::U64(0)),
    ])
}

/// Empty `span-context` — all-zero ids, no flags, no state. Returned
/// by fake `outer-span-context` host fns so the builtin sees "no host
/// parent" and either mints a fresh trace-id (tracing) or leaves
/// trace-correlation fields unset on emitted records (logs).
pub fn empty_span_context() -> Val {
    Val::Record(vec![
        ("trace-id".into(), Val::String(String::new())),
        ("span-id".into(), Val::String(String::new())),
        ("trace-flags".into(), Val::Flags(vec![])),
        ("is-remote".into(), Val::Bool(false)),
        ("trace-state".into(), Val::List(vec![])),
    ])
}

/// Drive a single `on-call` → `on-return` cycle on the embedded
/// builtin against a synthetic call-id targeting `TARGET_IFACE` /
/// `TARGET_FN`.
///
/// `setup` runs against the linker before the host is built; it's the
/// hook for the test to install whatever fake host interfaces the
/// builtin imports (e.g. `wasi:otel/tracing`, `wasi:otel/metrics`).
/// Inside those host fns, capture state is reachable as
/// `store.data().capture.lock()`.
///
/// Returns the capture `Arc<Mutex<C>>` so the caller can inspect it
/// after the cycle completes.
pub fn drive_call_cycle<C, F>(bytes: &[u8], setup: F) -> Result<Arc<Mutex<C>>>
where
    C: Default + Send + 'static,
    F: FnOnce(&mut Linker<Host<C>>) -> Result<()>,
{
    let mut config = Config::new();
    config.wasm_component_model_async(true);
    config.wasm_component_model_async_stackful(true);
    let engine = Engine::new(&config)?;
    let component = Component::from_binary(&engine, bytes)?;

    let mut linker = Linker::new(&engine);
    wasmtime_wasi::p2::add_to_linker_async(&mut linker)?;
    setup(&mut linker)?;

    let capture = Arc::new(Mutex::new(C::default()));
    let stdout = MemoryOutputPipe::new(64 * 1024);
    let host = Host {
        wasi: WasiCtxBuilder::new().stdout(stdout).build(),
        table: ResourceTable::new(),
        capture: capture.clone(),
    };
    let mut store = Store::new(&engine, host);

    let rt = tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()?;

    rt.block_on(async {
        let instance = linker.instantiate_async(&mut store, &component).await?;

        // Tier-1 builtins may export any non-empty subset of
        // {before, after, blocking}. Drive whichever this builtin
        // actually exports; downstream assertions will catch a
        // degenerate component that exports nothing useful.
        let on_call = instance
            .get_export_index(&mut store, None, SPLICER_BEFORE)
            .and_then(|idx| instance.get_export_index(&mut store, Some(&idx), "on-call"))
            .and_then(|idx| instance.get_func(&mut store, idx));
        let on_return = instance
            .get_export_index(&mut store, None, SPLICER_AFTER)
            .and_then(|idx| instance.get_export_index(&mut store, Some(&idx), "on-return"))
            .and_then(|idx| instance.get_func(&mut store, idx));

        let cid = call_id_val(TARGET_IFACE, TARGET_FN);

        if let Some(on_call) = on_call {
            let mut results: Vec<Val> = vec![];
            on_call
                .call_async(&mut store, std::slice::from_ref(&cid), &mut results)
                .await?;
        }
        if let Some(on_return) = on_return {
            let mut results: Vec<Val> = vec![];
            on_return
                .call_async(&mut store, &[cid], &mut results)
                .await?;
        }

        Ok::<_, anyhow::Error>(())
    })?;

    Ok(capture)
}

/// Register a no-op stub for `splicer:builtin-config/get`: every key
/// returns `none`, so the builtin falls back to its hardcoded defaults
/// across the test cycle. Tests that want non-default config should
/// register their own `get` function instead.
pub fn add_builtin_config_stub<C: Send + 'static>(linker: &mut Linker<Host<C>>) -> Result<()> {
    let mut iface = linker.instance(SPLICER_BUILTIN_CONFIG_GET)?;
    iface.func_new("get", |_store, _ty, _params, results| {
        results[0] = Val::Option(None);
        Ok(())
    })?;
    Ok(())
}

/// Assert an attribute list carries `code.namespace` / `code.function`
/// entries matching `TARGET_IFACE` / `TARGET_FN`, both JSON-encoded as
/// quoted strings (the `wasi:otel/types.value` `AnyValue` wire format).
pub fn assert_call_attrs(attrs: &[Val]) {
    let attr_map: HashMap<String, String> = attrs
        .iter()
        .map(|kv| {
            let r = expect_record(kv);
            (
                expect_string(field(r, "key")).to_string(),
                expect_string(field(r, "value")).to_string(),
            )
        })
        .collect();
    assert_eq!(
        attr_map.get("code.namespace").map(String::as_str),
        Some(format!("\"{TARGET_IFACE}\"").as_str()),
        "code.namespace JSON-encoded; got {attr_map:?}"
    );
    assert_eq!(
        attr_map.get("code.function").map(String::as_str),
        Some(format!("\"{TARGET_FN}\"").as_str()),
        "code.function JSON-encoded; got {attr_map:?}"
    );
}

// ─── Val extractors ────────────────────────────────────────────────

pub fn field<'a>(record: &'a [(String, Val)], name: &str) -> &'a Val {
    record
        .iter()
        .find(|(k, _)| k == name)
        .map(|(_, v)| v)
        .unwrap_or_else(|| panic!("field {name:?} not found in record {record:?}"))
}

pub fn expect_record(v: &Val) -> &[(String, Val)] {
    if let Val::Record(fields) = v {
        fields
    } else {
        panic!("expected record, got {v:?}")
    }
}
pub fn expect_string(v: &Val) -> &str {
    if let Val::String(s) = v {
        s
    } else {
        panic!("expected string, got {v:?}")
    }
}
pub fn expect_u64(v: &Val) -> u64 {
    if let Val::U64(n) = v {
        *n
    } else {
        panic!("expected u64, got {v:?}")
    }
}
pub fn expect_u32(v: &Val) -> u32 {
    if let Val::U32(n) = v {
        *n
    } else {
        panic!("expected u32, got {v:?}")
    }
}
pub fn expect_bool(v: &Val) -> bool {
    if let Val::Bool(b) = v {
        *b
    } else {
        panic!("expected bool, got {v:?}")
    }
}
pub fn expect_list(v: &Val) -> &[Val] {
    if let Val::List(items) = v {
        items
    } else {
        panic!("expected list, got {v:?}")
    }
}
pub fn expect_variant(v: &Val) -> (&str, Option<&Val>) {
    if let Val::Variant(case, payload) = v {
        (case.as_str(), payload.as_deref())
    } else {
        panic!("expected variant, got {v:?}")
    }
}
pub fn expect_enum(v: &Val) -> &str {
    if let Val::Enum(case) = v {
        case.as_str()
    } else {
        panic!("expected enum, got {v:?}")
    }
}

/// Read a built builtin's bytes from disk. Honors `SPLICER_BUILTINS_DIR`
/// (the same env var splicer's runtime fetch reads); otherwise looks in
/// `<crate-root>/assets/builtins/`. Panics with a `make build-builtins`
/// hint if the file is missing — these tests instantiate real
/// components and have no useful behavior to fall back on.
pub fn read_builtin(name: &str) -> Vec<u8> {
    let dir = std::env::var_os("SPLICER_BUILTINS_DIR")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/builtins"));
    let path = dir.join(format!("{name}.wasm"));
    std::fs::read(&path).unwrap_or_else(|e| {
        panic!(
            "couldn't read {}: {e}\n\
             run `make build-builtins`, or set SPLICER_BUILTINS_DIR=<dir>",
            path.display()
        )
    })
}