folk-runtime-embed 0.1.17

Embedded PHP runtime for Folk — PHP interpreter runs in-process via FFI
Documentation
//! Tests for fatal error and segfault protection.
//!
//! Verifies that:
//! - die()/exit() in PHP handler returns error, worker continues
//! - E_ERROR (undefined class) returns error, worker continues
//! - Uncaught exception returns error, worker continues
//! - Worker processes requests normally after fatal errors
//! - Signal handlers are restored after PHP init
//!
//! IMPORTANT: Requires PHP with embed SAPI (--enable-embed).

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

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

    // First request: die() causes fatal error
    php.request_startup().expect("startup failed");
    let result = php.eval("die('fatal');");
    // die() triggers zend_bailout → caught by zend_try → returns error
    assert!(result.is_err(), "die() should return error");
    php.request_shutdown();

    // Second request: worker should still work
    php.request_startup()
        .expect("startup after die should work");
    let result = php
        .eval(r#"echo "alive";"#)
        .expect("eval should work after die");
    assert_eq!(result.output, "alive");
    php.request_shutdown();
}

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

    php.request_startup().expect("startup failed");
    let result = php.eval("exit(1);");
    assert!(result.is_err(), "exit() should return error");
    php.request_shutdown();

    // Worker continues
    php.request_startup()
        .expect("startup after exit should work");
    let result = php.eval(r#"echo "still here";"#).expect("eval should work");
    assert_eq!(result.output, "still here");
    php.request_shutdown();
}

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

    php.request_startup().expect("startup failed");
    let result = php.eval("new ThisClassDoesNotExistAnywhere();");
    assert!(result.is_err(), "undefined class should return error");
    php.request_shutdown();

    // Worker continues
    php.request_startup().expect("startup should work");
    let result = php.call("strlen", &["test"]).expect("call should work");
    assert_eq!(result, ZvalValue::Long(4));
    php.request_shutdown();
}

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

    php.request_startup().expect("startup failed");
    let result = php.call("this_function_absolutely_does_not_exist", &[]);
    assert!(result.is_err(), "undefined function should return error");
    php.request_shutdown();

    // Worker continues
    php.request_startup().expect("startup should work");
    let result = php.eval(r#"echo "ok";"#).expect("eval should work");
    assert_eq!(result.output, "ok");
    php.request_shutdown();
}

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

    // Alternate between fatal errors and successful requests
    for i in 0..50 {
        php.request_startup().expect("startup failed");

        if i % 2 == 0 {
            // Fatal error
            let _ = php.eval("die('boom');");
        } else {
            // Success
            let result = php
                .eval(&format!(r#"echo "ok-{i}";"#))
                .expect("eval should work");
            assert_eq!(result.output, format!("ok-{i}"));
        }

        php.request_shutdown();
    }
}

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

    php.request_startup().expect("startup failed");
    let result = php.eval_protected("die('protected');");
    assert!(result.is_err());
    php.request_shutdown();

    // Worker continues
    php.request_startup().expect("startup should work");
    let result = php
        .eval_protected(r#"echo "protected ok";"#)
        .expect("eval should work");
    assert_eq!(result.output, "protected ok");
    php.request_shutdown();
}

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

    php.request_startup().expect("startup failed");
    let result = php.call_protected("nonexistent_function_xyz", &[]);
    assert!(result.is_err());
    php.request_shutdown();

    // Worker continues
    php.request_startup().expect("startup should work");
    let result = php
        .call_protected("strtoupper", &["hello"])
        .expect("call should work");
    assert_eq!(result, ZvalValue::String("HELLO".to_string()));
    php.request_shutdown();
}

#[test]
fn sapi_name_after_signal_restore() {
    // Verify PHP boots correctly with signal save/restore
    let mut php = PhpInstance::boot_custom_sapi().expect("boot with signals failed");

    let mut ctx = folk_runtime_embed::php::RequestContext::new("GET", "/");
    php.set_request_context(&mut ctx);
    php.request_startup().expect("startup failed");

    let result = php.eval(r#"echo php_sapi_name();"#).expect("eval failed");
    assert_eq!(result.output, "folk-embed");

    php.request_shutdown();
}