folk-runtime-embed 0.1.17

Embedded PHP runtime for Folk — PHP interpreter runs in-process via FFI
Documentation
//! POC integration test: Rust ↔ PHP FFI via embed SAPI.
//!
//! These tests verify the basic FFI bridge works:
//! 1. Boot PHP, eval code, capture output
//! 2. Call a PHP function, get return value
//! 3. Request lifecycle (startup/shutdown) doesn't leak
//! 4. Fatal errors are caught (zend_try/zend_catch)
//!
//! IMPORTANT: PHP embed SAPI is NOT thread-safe for init/shutdown.
//! All tests in this file run sequentially (single-threaded).
//! Each test boots a fresh PHP instance.

use folk_runtime_embed::php::{PhpInstance, ZvalValue};

#[test]
fn test_boot_and_eval_echo() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    let result = php.eval(r#"echo "hello from PHP";"#).expect("eval failed");
    assert_eq!(result.output, "hello from PHP");

    php.request_shutdown();
}

#[test]
fn test_eval_return_value() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    let result = php.eval("return 42;").expect("eval failed");
    // Note: zend_eval_string with "return X;" may or may not set the retval
    // depending on PHP version. We test string returns via function calls.

    php.request_shutdown();
}

#[test]
fn test_call_function() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    // Define a PHP function
    php.eval(r#"function folk_test_add($a, $b) { return (int)$a + (int)$b; }"#)
        .expect("define function failed");

    let result = php.call("folk_test_add", &["3", "4"]).expect("call failed");
    assert_eq!(result, ZvalValue::Long(7));

    php.request_shutdown();
}

#[test]
fn test_call_builtin_strlen() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    let result = php.call("strlen", &["hello world"]).expect("call failed");
    assert_eq!(result, ZvalValue::Long(11));

    php.request_shutdown();
}

#[test]
fn test_call_strtoupper() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    let result = php.call("strtoupper", &["hello"]).expect("call failed");
    assert_eq!(result, ZvalValue::String("HELLO".to_string()));

    php.request_shutdown();
}

#[test]
fn test_multiple_request_cycles() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");

    for i in 0..100 {
        php.request_startup().expect("request startup failed");

        let code = format!(r#"echo "request {i}";"#);
        let result = php.eval(&code).expect("eval failed");
        assert_eq!(result.output, format!("request {i}"));

        php.request_shutdown();
    }
}

#[test]
fn test_no_leaks_10k_requests() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");

    for _ in 0..10_000 {
        php.request_startup().expect("request startup failed");

        php.eval(r#"$x = str_repeat("A", 1024); echo strlen($x);"#)
            .expect("eval failed");

        php.request_shutdown();
    }

    // If we get here without OOM or crash, cleanup works.
}

#[test]
fn test_fatal_error_caught() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    // Calling an undefined function should cause an error
    let result = php.call("this_function_does_not_exist_at_all", &[]);
    assert!(result.is_err(), "calling undefined function should fail");

    // PHP should still be usable after the error
    php.request_shutdown();
    php.request_startup()
        .expect("request startup should work after error");

    let result = php
        .call("strlen", &["test"])
        .expect("call should work after error");
    assert_eq!(result, ZvalValue::Long(4));

    php.request_shutdown();
}

#[test]
fn test_output_capture_reset_between_requests() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");

    php.request_startup().expect("request startup failed");
    php.eval(r#"echo "first";"#).expect("eval failed");
    let output1 = php.take_output();
    php.request_shutdown();

    php.request_startup().expect("request startup failed");
    php.eval(r#"echo "second";"#).expect("eval failed");
    let output2 = php.take_output();
    php.request_shutdown();

    // Output should NOT accumulate between requests
    assert!(!output2.contains("first"), "output leaked between requests");
    assert_eq!(output2, "second");
}

#[test]
fn test_json_encode_decode() {
    let mut php = PhpInstance::boot().expect("PHP boot failed");
    php.request_startup().expect("request startup failed");

    php.eval(
        r#"
        $data = ['status' => 'ok', 'count' => 42];
        echo json_encode($data);
    "#,
    )
    .expect("eval failed");

    let output = php.take_output();
    assert_eq!(output, r#"{"status":"ok","count":42}"#);

    php.request_shutdown();
}