diff --git a/tests/sapi.rs b/tests/sapi.rs index d74accb89..3e16a67d6 100644 --- a/tests/sapi.rs +++ b/tests/sapi.rs @@ -9,7 +9,9 @@ extern crate ext_php_rs; use ext_php_rs::builders::SapiBuilder; -use ext_php_rs::embed::{ext_php_rs_sapi_startup, Embed}; +use ext_php_rs::embed::{ + ext_php_rs_sapi_per_thread_init, ext_php_rs_sapi_shutdown, ext_php_rs_sapi_startup, Embed, +}; use ext_php_rs::ffi::{ php_module_shutdown, php_module_startup, php_request_shutdown, php_request_startup, sapi_shutdown, sapi_startup, ZEND_RESULT_CODE_SUCCESS, @@ -17,9 +19,15 @@ use ext_php_rs::ffi::{ use ext_php_rs::prelude::*; use ext_php_rs::zend::try_catch_first; use std::ffi::c_char; +use std::sync::Mutex; static mut LAST_OUTPUT: String = String::new(); +// Global mutex to ensure SAPI tests don't run concurrently. PHP does not allow +// multiple SAPIs to exist at the same time. This prevents the tests from +// overwriting each other's state. +static SAPI_TEST_MUTEX: Mutex<()> = Mutex::new(()); + extern "C" fn output_tester(str: *const c_char, str_length: usize) -> usize { let char = unsafe { std::slice::from_raw_parts(str.cast::(), str_length) }; let string = String::from_utf8_lossy(char); @@ -35,6 +43,8 @@ extern "C" fn output_tester(str: *const c_char, str_length: usize) -> usize { #[test] fn test_sapi() { + let _guard = SAPI_TEST_MUTEX.lock().unwrap(); + let mut builder = SapiBuilder::new("test", "Test"); builder = builder.ub_write_function(output_tester); @@ -86,6 +96,10 @@ fn test_sapi() { unsafe { sapi_shutdown(); } + + unsafe { + ext_php_rs_sapi_shutdown(); + } } /// Gives you a nice greeting! @@ -102,3 +116,92 @@ pub fn hello_world(name: String) -> String { pub fn module(module: ModuleBuilder) -> ModuleBuilder { module.function(wrap_function!(hello_world)) } + +#[test] +fn test_sapi_multithread() { + let _guard = SAPI_TEST_MUTEX.lock().unwrap(); + + use std::sync::{Arc, Mutex}; + use std::thread; + + let mut builder = SapiBuilder::new("test-mt", "Test Multi-threaded"); + builder = builder.ub_write_function(output_tester); + + let sapi = builder.build().unwrap().into_raw(); + let module = get_module(); + + unsafe { + ext_php_rs_sapi_startup(); + } + + unsafe { + sapi_startup(sapi); + } + + unsafe { + php_module_startup(sapi, module); + } + + let results = Arc::new(Mutex::new(Vec::new())); + let mut handles = vec![]; + + for i in 0..4 { + let results = Arc::clone(&results); + + let handle = thread::spawn(move || { + unsafe { + ext_php_rs_sapi_per_thread_init(); + } + + let result = unsafe { php_request_startup() }; + assert_eq!(result, ZEND_RESULT_CODE_SUCCESS); + + let _ = try_catch_first(|| { + let eval_result = Embed::eval(&format!("hello_world('thread-{}');", i)); + + match eval_result { + Ok(zval) => { + assert!(zval.is_string()); + let string = zval.string().unwrap(); + let output = string.to_string(); + assert_eq!(output, format!("Hello, thread-{}!", i)); + + results.lock().unwrap().push((i, output)); + } + Err(_) => panic!("Evaluation failed in thread {}", i), + } + }); + + unsafe { + php_request_shutdown(std::ptr::null_mut()); + } + }); + + handles.push(handle); + } + + for handle in handles { + handle.join().expect("Thread panicked"); + } + + let results = results.lock().unwrap(); + assert_eq!(results.len(), 4); + + for i in 0..4 { + assert!(results + .iter() + .any(|(idx, output)| { *idx == i && output == &format!("Hello, thread-{}!", i) })); + } + + unsafe { + php_module_shutdown(); + } + + unsafe { + sapi_shutdown(); + } + + unsafe { + ext_php_rs_sapi_shutdown(); + } +}