folk-runtime-embed 0.1.17

Embedded PHP runtime for Folk — PHP interpreter runs in-process via FFI
Documentation
//! Integration tests for the EmbedRuntime worker thread pool.
//!
//! Tests verify:
//! - Workers boot and send control.ready
//! - Task dispatch works (echo via fallback)
//! - Worker termination is clean
//! - Multiple workers can run concurrently
//!
//! IMPORTANT: These tests require PHP with embed SAPI (--enable-embed).
//! They will fail on systems without libphp.

use folk_core::runtime::{Runtime, WorkerHandle};
use folk_protocol::RpcMessage;
use folk_runtime_embed::{EmbedConfig, EmbedRuntime};
use rmpv::Value;

fn make_runtime(script: Option<&str>) -> EmbedRuntime {
    EmbedRuntime::new(EmbedConfig {
        script: script.map(String::from),
    })
    .expect("failed to init embed runtime")
}

#[tokio::test]
async fn worker_boots_and_sends_ready() {
    let runtime = make_runtime(None);
    let mut handle = runtime.spawn().await.expect("spawn failed");

    let msg = handle
        .recv_control()
        .await
        .expect("recv_control failed")
        .expect("expected control.ready");

    match &msg {
        RpcMessage::Notify { method, .. } => {
            assert_eq!(method, "control.ready");
        },
        _ => panic!("expected Notify, got {msg:?}"),
    }

    handle.terminate().await.expect("terminate failed");
}

#[tokio::test]
async fn worker_echo_dispatch() {
    let runtime = make_runtime(None);
    let mut handle = runtime.spawn().await.expect("spawn failed");

    // Wait for ready
    let _ = handle.recv_control().await.unwrap().unwrap();

    // Send a request — without folk_embed_dispatch defined, worker echoes params
    let params = Value::Map(vec![(
        Value::String("key".into()),
        Value::String("value".into()),
    )]);
    let request = RpcMessage::request(1, "test.echo", params.clone());
    handle.send_task(request).await.expect("send failed");

    let response = handle
        .recv_task()
        .await
        .expect("recv failed")
        .expect("expected response");

    match response {
        RpcMessage::Response {
            msgid,
            error,
            result,
        } => {
            assert_eq!(msgid, 1);
            assert!(error.is_nil(), "unexpected error: {error:?}");
            assert_eq!(result, params);
        },
        _ => panic!("expected Response, got {response:?}"),
    }

    handle.terminate().await.expect("terminate failed");
}

#[tokio::test]
async fn multiple_workers_concurrent() {
    let runtime = make_runtime(None);

    let mut handles: Vec<Box<dyn WorkerHandle>> = Vec::new();
    for _ in 0..4 {
        let mut h = runtime.spawn().await.expect("spawn failed");
        let _ = h.recv_control().await.unwrap().unwrap();
        handles.push(h);
    }

    // Send requests to all workers
    for (i, h) in handles.iter_mut().enumerate() {
        let request = RpcMessage::request(
            i as u32 + 1,
            "test",
            Value::String(format!("worker-{i}").into()),
        );
        h.send_task(request).await.expect("send failed");
    }

    // Receive responses
    for (i, h) in handles.iter_mut().enumerate() {
        let resp = h.recv_task().await.unwrap().unwrap();
        match resp {
            RpcMessage::Response { msgid, .. } => {
                assert_eq!(msgid, i as u32 + 1);
            },
            _ => panic!("expected Response"),
        }
    }

    // Clean termination
    for h in &mut handles {
        h.terminate().await.expect("terminate failed");
    }
}

#[tokio::test]
async fn worker_handles_multiple_requests() {
    let runtime = make_runtime(None);
    let mut handle = runtime.spawn().await.expect("spawn failed");
    let _ = handle.recv_control().await.unwrap().unwrap();

    for i in 0..100 {
        let request = RpcMessage::request(i, "test", Value::Integer(i.into()));
        handle.send_task(request).await.expect("send failed");

        let resp = handle.recv_task().await.unwrap().unwrap();
        match resp {
            RpcMessage::Response { msgid, .. } => assert_eq!(msgid, i),
            _ => panic!("expected Response"),
        }
    }

    handle.terminate().await.expect("terminate failed");
}