From a525784f70c77e0ab4edad47698fb514dcc890a9 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sat, 21 Jun 2025 14:20:28 +0800 Subject: [PATCH 01/10] feat: Improve integration tests --- Cargo.lock | 180 +++++++++++++ examples/http-server/tests/integration.rs | 2 +- examples/http-server/tests/php/test.php | 4 +- phper-test/Cargo.toml | 3 +- phper-test/src/cargo.rs | 101 +++++++ phper-test/src/cli.rs | 18 +- phper-test/src/fpm.rs | 307 +++++++++++++--------- phper-test/src/lib.rs | 1 + tests/integration/Cargo.toml | 3 +- tests/integration/tests/cli.rs | 89 +++++++ tests/integration/tests/common/mod.rs | 25 ++ tests/integration/tests/fpm.rs | 108 ++++++++ tests/integration/tests/integration.rs | 75 ------ 13 files changed, 712 insertions(+), 204 deletions(-) create mode 100644 phper-test/src/cargo.rs create mode 100644 tests/integration/tests/cli.rs create mode 100644 tests/integration/tests/common/mod.rs create mode 100644 tests/integration/tests/fpm.rs delete mode 100644 tests/integration/tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 1b551ea5..d5151631 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,55 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" +[[package]] +name = "camino" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84982c6c0ae343635a3a4ee6dedef965513735c8b183caa7289fa6e27399ebd4" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-util-schemas" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63d2780ac94487eb9f1fea7b0d56300abc9eb488800854ca217f102f5caccca" +dependencies = [ + "semver", + "serde", + "serde-untagged", + "serde-value", + "thiserror 1.0.69", + "toml", + "unicode-xid", + "url", +] + +[[package]] +name = "cargo_metadata" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7835cfc6135093070e95eb2b53e5d9b5c403dc3a6be6040ee026270aa82502" +dependencies = [ + "camino", + "cargo-platform", + "cargo-util-schemas", + "semver", + "serde", + "serde_json", + "thiserror 2.0.11", +] + [[package]] name = "cc" version = "1.2.15" @@ -301,6 +350,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e004d887f51fcb9fef17317a2f3525c887d8aa3f4f50fed920816a688284a5b7" +dependencies = [ + "serde", + "typeid", +] + [[package]] name = "errno" version = "0.3.10" @@ -744,6 +803,7 @@ dependencies = [ "phper", "phper-build", "phper-test", + "tokio", ] [[package]] @@ -904,6 +964,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -963,6 +1032,15 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + [[package]] name = "parking_lot" version = "0.12.3" @@ -1102,6 +1180,7 @@ dependencies = [ name = "phper-test" version = "0.15.1" dependencies = [ + "cargo_metadata", "fastcgi-client", "libc", "phper-macros", @@ -1401,6 +1480,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.218" @@ -1410,6 +1498,27 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-untagged" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299d9c19d7d466db4ab10addd5703e4c615dec2a5a16dbbafe191045e87ee66e" +dependencies = [ + "erased-serde", + "serde", + "typeid", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.218" @@ -1443,6 +1552,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1716,6 +1834,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tower" version = "0.5.2" @@ -1782,12 +1941,24 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "unicode-ident" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2046,6 +2217,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.33.0" diff --git a/examples/http-server/tests/integration.rs b/examples/http-server/tests/integration.rs index 607796ca..341466b8 100644 --- a/examples/http-server/tests/integration.rs +++ b/examples/http-server/tests/integration.rs @@ -39,7 +39,7 @@ fn test_php() { let client = Client::new(); for _ in 0..5 { - let response = client.get("http://127.0.0.1:9000/").send().unwrap(); + let response = client.get("http://127.0.0.1:9010/").send().unwrap(); assert_eq!(response.status(), StatusCode::OK); let content_type = response.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(content_type, "text/plain"); diff --git a/examples/http-server/tests/php/test.php b/examples/http-server/tests/php/test.php index 09edc95c..d62c34e0 100644 --- a/examples/http-server/tests/php/test.php +++ b/examples/http-server/tests/php/test.php @@ -17,7 +17,7 @@ ini_set("display_startup_errors", "On"); error_reporting(E_ALL); -$server = new HttpServer("127.0.0.1", 9000); +$server = new HttpServer("127.0.0.1", 9010); $server->onRequest(function ($request, $response) { echo "HEADERS:\n"; foreach ($request->headers as $key => $value) { @@ -30,6 +30,6 @@ $response->end("Hello World\n"); }); -echo "Listening http://127.0.0.1:9000\n\n"; +echo "Listening http://127.0.0.1:9010\n\n"; $server->start(); diff --git a/phper-test/Cargo.toml b/phper-test/Cargo.toml index 673af74d..80f6cc24 100644 --- a/phper-test/Cargo.toml +++ b/phper-test/Cargo.toml @@ -20,11 +20,12 @@ repository = { workspace = true } license = { workspace = true } [dependencies] +cargo_metadata = "0.20.0" fastcgi-client = "0.9.0" libc = "0.2.169" phper-macros = { workspace = true } tempfile = "3.17.1" -tokio = { version = "1.43.0", features = ["full"] } +tokio = { version = "1.43.0", features = ["net"] } [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/phper-test/src/cargo.rs b/phper-test/src/cargo.rs new file mode 100644 index 00000000..616fc900 --- /dev/null +++ b/phper-test/src/cargo.rs @@ -0,0 +1,101 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +//! Cargo build utilities for building and analyzing Rust libraries. + +use cargo_metadata::Message; +use std::{ + io::{self, BufReader}, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +/// Builder for running cargo build commands with JSON output +pub struct CargoBuilder { + command: Command, +} + +/// Result of a cargo build operation +pub struct CargoBuildResult { + messages: Vec, +} + +impl CargoBuilder { + /// Create a new CargoBuilder instance + pub fn new() -> Self { + let mut command = Command::new(env!("CARGO")); + command + .args(["build", "--lib", "--message-format", "json"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + Self { command } + } + + /// Add additional arguments to the cargo command + pub fn arg>(&mut self, arg: S) -> &mut Self { + self.command.arg(arg); + self + } + + /// Set the current directory for the cargo command + pub fn current_dir>(&mut self, dir: P) -> &mut Self { + self.command.current_dir(dir); + self + } + + /// Execute the cargo build command and return the result + pub fn build(&mut self) -> io::Result { + let mut child = self.command.spawn()?; + let stdout = child + .stdout + .take() + .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to capture stdout"))?; + let reader = BufReader::new(stdout); + let mut messages = Vec::new(); + for message in cargo_metadata::Message::parse_stream(reader) { + let message = message?; + messages.push(message); + } + let exit_status = child.wait()?; + if !exit_status.success() { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Cargo build failed with exit status: {}", exit_status), + )); + } + Ok(CargoBuildResult { messages }) + } +} + +impl Default for CargoBuilder { + fn default() -> Self { + Self::new() + } +} + +impl CargoBuildResult { + /// Get the cdylib file path from the last compiler-artifact message + pub fn get_cdylib(&self) -> Option { + self.messages.iter().rev().find_map(|msg| { + if let Message::CompilerArtifact(artifact) = msg { + artifact.filenames.iter().find_map(|filename| { + let ext = filename.extension(); + if matches!(ext, Some("so") | Some("dylib") | Some("dll")) { + Some(PathBuf::from(filename.as_std_path())) + } else { + None + } + }) + } else { + None + } + }) + } +} diff --git a/phper-test/src/cli.rs b/phper-test/src/cli.rs index e3207b55..8368a49f 100644 --- a/phper-test/src/cli.rs +++ b/phper-test/src/cli.rs @@ -22,9 +22,23 @@ use std::{ /// /// - `lib_path` is the path of extension lib. /// -/// - `scripts` is the path of your php test scripts. +/// - `script` is the path of your php test script. +pub fn test_php_script(lib_path: impl AsRef, scripts: impl AsRef) { + let condition = |output: Output| output.status.success(); + let scripts = Some(scripts); + let scripts = scripts + .iter() + .map(|s| (s as _, &condition as _)) + .collect::>(); + test_php_scripts_with_condition(lib_path, &scripts); +} + +/// Check your extension by executing the php script, if the all executing +/// return success, than the test is pass. /// -/// See [example hello integration test](https://github.com/phper-framework/phper/blob/master/examples/hello/tests/integration.rs). +/// - `lib_path` is the path of extension lib. +/// +/// - `scripts` is the path of your php test scripts. pub fn test_php_scripts(lib_path: impl AsRef, scripts: &[&dyn AsRef]) { let condition = |output: Output| output.status.success(); let scripts = scripts diff --git a/phper-test/src/fpm.rs b/phper-test/src/fpm.rs index 8da8ea2d..1396b2a9 100644 --- a/phper-test/src/fpm.rs +++ b/phper-test/src/fpm.rs @@ -14,42 +14,59 @@ use fastcgi_client::{Client, Params, Request}; use libc::{SIGTERM, atexit, kill, pid_t}; use std::{ fs, - mem::{ManuallyDrop, forget}, - path::{Path, PathBuf}, + path::Path, process::Child, - sync::{Mutex, OnceLock}, + sync::{Mutex, Once, OnceLock}, time::Duration, }; use tempfile::NamedTempFile; -use tokio::{io, net::TcpStream, runtime::Handle, task::block_in_place}; - -static FPM_HANDLE: OnceLock> = OnceLock::new(); - -struct FpmHandle { - lib_path: PathBuf, +use tokio::{io, net::TcpStream}; + +static FPM_HANDLE: OnceLock = OnceLock::new(); + +/// A handle for managing a PHP-FPM (FastCGI Process Manager) instance. +/// +/// This struct provides functionality to start, manage, and interact with a +/// PHP-FPM process for testing purposes. It maintains the FPM process lifecycle +/// and provides methods to send FastCGI requests to the running FPM instance. +/// +/// The FpmHandle is designed as a singleton - only one instance can exist at a +/// time, and it's automatically cleaned up when the program exits. +pub struct FpmHandle { + /// The running PHP-FPM child process fpm_child: Child, - fpm_conf_file: ManuallyDrop, + /// Temporary configuration file for PHP-FPM + fpm_conf_file: Mutex>, } -/// Start php-fpm process and tokio runtime. -pub fn setup(lib_path: impl AsRef) { - let lib_path = lib_path.as_ref().to_owned(); - - let handle = FPM_HANDLE.get_or_init(|| { - // shutdown hook. - unsafe { - atexit(teardown); +impl FpmHandle { + /// Sets up and starts a PHP-FPM process for testing. + /// + /// This method creates a singleton FpmHandle instance that manages a + /// PHP-FPM process with the specified PHP extension loaded. The FPM + /// process is configured to listen on port 9000 and uses a temporary + /// configuration file. + /// + /// # Arguments + /// + /// * `lib_path` - Path to the PHP extension library file (.so) to be loaded + /// + /// # Returns + /// + /// A static reference to the FpmHandle instance + /// + /// # Panics + /// + /// Panics if: + /// - PHP-FPM binary cannot be found + /// - FPM process fails to start + /// - FpmHandle has already been initialized + pub fn setup(lib_path: impl AsRef) -> &'static FpmHandle { + if FPM_HANDLE.get().is_some() { + panic!("FPM_HANDLE has set"); } - // Run tokio runtime. - let rt = tokio::runtime::Builder::new_multi_thread() - .worker_threads(3) - .enable_all() - .build() - .unwrap(); - let guard = rt.enter(); - forget(guard); - forget(rt); + let lib_path = lib_path.as_ref().to_owned(); // Run php-fpm. let context = Context::get_global(); @@ -63,115 +80,161 @@ pub fn setup(lib_path: impl AsRef) { "-d", &format!("extension={}", lib_path.display()), "-y", - fpm_conf_file.path().to_str().unwrap(), + &fpm_conf_file.path().display().to_string(), ]; - eprintln!("===== setup php-fpm =====\n{}", argv.join(" ")); + eprintln!("===== setup php-fpm ====="); + eprintln!("{}", argv.join(" ")); let child = spawn_command(&argv, Some(Duration::from_secs(3))); let log = fs::read_to_string("/tmp/.php-fpm.log").unwrap(); - eprintln!("===== php-fpm log =====\n{}", log); + eprintln!("===== php-fpm log ====="); + eprintln!("{}", log); // fs::remove_file("/tmp/.php-fpm.log").unwrap(); - Mutex::new(FpmHandle { - lib_path: lib_path.clone(), + let handle = FpmHandle { fpm_child: child, - fpm_conf_file: ManuallyDrop::new(fpm_conf_file), - }) - }); + fpm_conf_file: Mutex::new(Some(fpm_conf_file)), + }; + + // shutdown hook. + static TEARDOWN: Once = Once::new(); + TEARDOWN.call_once(|| unsafe { + atexit(teardown); + }); + + if FPM_HANDLE.set(handle).is_err() { + panic!("FPM_HANDLE has set"); + } - assert_eq!(handle.lock().unwrap().lib_path, &*lib_path); + FPM_HANDLE.get().unwrap() + } + + /// Sends a FastCGI request to the PHP-FPM process and validates the + /// response. + /// + /// This method executes a FastCGI request to the running PHP-FPM instance + /// using the specified parameters. It establishes a TCP connection to + /// the FPM process and sends the request with the provided HTTP method, + /// script path, and optional content. + /// + /// The method automatically constructs the necessary FastCGI parameters + /// including script filename, server information, and remote address + /// details. After receiving the response, it validates that no errors + /// occurred during processing. + /// + /// # Arguments + /// + /// * `method` - HTTP method for the request (e.g., "GET", "POST", "PUT") + /// * `root` - Document root directory where PHP scripts are located + /// * `request_uri` - The URI being requested (e.g., + /// "/test.php?param=value") + /// * `content_type` - Optional Content-Type header for the request + /// * `body` - Optional request body as bytes + /// + /// # Panics + /// + /// Panics if: + /// - FpmHandle has not been initialized via `setup()` first + /// - Cannot connect to the FPM process on port 9000 + /// - The PHP script execution results in errors (stderr is not empty) + pub async fn test_fpm_request( + &self, method: &str, root: impl AsRef, request_uri: &str, + content_type: Option, body: Option>, + ) { + let root = root.as_ref(); + let script_name = request_uri.split('?').next().unwrap(); + + let mut tmp = root.to_path_buf(); + tmp.push(script_name.trim_start_matches('/')); + let script_filename = tmp.as_path().to_str().unwrap(); + + let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); + let local_addr = stream.local_addr().unwrap(); + let peer_addr = stream.peer_addr().unwrap(); + let local_ip = local_addr.ip().to_string(); + let local_port = local_addr.port(); + let peer_ip = peer_addr.ip().to_string(); + let peer_port = peer_addr.port(); + + let client = Client::new(stream); + let mut params = Params::default() + .request_method(method) + .script_name(request_uri) + .script_filename(script_filename) + .request_uri(request_uri) + .document_uri(script_name) + .remote_addr(&local_ip) + .remote_port(local_port) + .server_addr(&peer_ip) + .server_port(peer_port) + .server_name("phper-test"); + if let Some(content_type) = &content_type { + params = params.content_type(content_type); + } + if let Some(body) = &body { + params = params.content_length(body.len()); + } + + let response = if let Some(body) = body { + client + .execute_once(Request::new(params, body.as_ref())) + .await + } else { + client + .execute_once(Request::new(params, &mut io::empty())) + .await + }; + + let output = response.unwrap(); + let stdout = output.stdout.unwrap_or_default(); + let stderr = output.stderr.unwrap_or_default(); + + let no_error = stderr.is_empty(); + + let f = |out: Vec| { + String::from_utf8(out) + .map(|out| { + if out.is_empty() { + "".to_owned() + } else { + out + } + }) + .unwrap_or_else(|_| "".to_owned()) + }; + + eprintln!("===== request ====="); + eprintln!("{}", request_uri); + eprintln!("===== stdout ======"); + eprintln!("{}", f(stdout)); + eprintln!("===== stderr ======"); + eprintln!("{}", f(stderr)); + + assert!(no_error, "request not success: {}", request_uri); + } } +/// Cleanup function called on program exit to properly shutdown the PHP-FPM +/// process. +/// +/// This function is automatically registered as an exit handler and is +/// responsible for: +/// - Cleaning up the temporary FPM configuration file +/// - Sending a SIGTERM signal to the FPM process to gracefully shutdown +/// +/// # Safety +/// +/// This function is marked as `unsafe` because it: +/// - Directly manipulates the global FPM_HANDLE singleton +/// - Uses raw system calls to send signals to processes +/// - Is called from an exit handler context where normal safety guarantees may +/// not apply extern "C" fn teardown() { - let mut fpm_handle = FPM_HANDLE.get().unwrap().lock().unwrap(); - unsafe { - ManuallyDrop::drop(&mut fpm_handle.fpm_conf_file); + let fpm_handle = FPM_HANDLE.get().unwrap(); + drop(fpm_handle.fpm_conf_file.lock().unwrap().take()); let id = fpm_handle.fpm_child.id(); kill(id as pid_t, SIGTERM); } } - -/// Start php-fpm and test the url request. -pub fn test_fpm_request( - method: &str, root: impl AsRef, request_uri: &str, content_type: Option, - body: Option>, -) { - assert!(FPM_HANDLE.get().is_some(), "must call `setup()` first"); - - block_in_place(move || { - Handle::current().block_on(async move { - let root = root.as_ref(); - let script_name = request_uri.split('?').next().unwrap(); - - let mut tmp = root.to_path_buf(); - tmp.push(script_name.trim_start_matches('/')); - let script_filename = tmp.as_path().to_str().unwrap(); - - let stream = TcpStream::connect(("127.0.0.1", 9000)).await.unwrap(); - let local_addr = stream.local_addr().unwrap(); - let peer_addr = stream.peer_addr().unwrap(); - let local_ip = local_addr.ip().to_string(); - let local_port = local_addr.port(); - let peer_ip = peer_addr.ip().to_string(); - let peer_port = peer_addr.port(); - - let client = Client::new(stream); - let mut params = Params::default() - .request_method(method) - .script_name(request_uri) - .script_filename(script_filename) - .request_uri(request_uri) - .document_uri(script_name) - .remote_addr(&local_ip) - .remote_port(local_port) - .server_addr(&peer_ip) - .server_port(peer_port) - .server_name("phper-test"); - if let Some(content_type) = &content_type { - params = params.content_type(content_type); - } - if let Some(body) = &body { - params = params.content_length(body.len()); - } - - let response = if let Some(body) = body { - client - .execute_once(Request::new(params, body.as_ref())) - .await - } else { - client - .execute_once(Request::new(params, &mut io::empty())) - .await - }; - - let output = response.unwrap(); - let stdout = output.stdout.unwrap_or_default(); - let stderr = output.stderr.unwrap_or_default(); - - let no_error = stderr.is_empty(); - - let f = |out: Vec| { - String::from_utf8(out) - .map(|out| { - if out.is_empty() { - "".to_owned() - } else { - out - } - }) - .unwrap_or_else(|_| "".to_owned()) - }; - - eprintln!( - "===== request =====\n{}\n===== stdout ======\n{}\n===== stderr ======\n{}", - request_uri, - f(stdout), - f(stderr), - ); - - assert!(no_error, "request not success: {}", request_uri); - }); - }); -} diff --git a/phper-test/src/lib.rs b/phper-test/src/lib.rs index 61717d0d..9b98bbab 100644 --- a/phper-test/src/lib.rs +++ b/phper-test/src/lib.rs @@ -14,6 +14,7 @@ #![doc = include_str!("../README.md")] #![doc(html_logo_url = "https://avatars.githubusercontent.com/u/112468984?s=200&v=4")] +pub mod cargo; pub mod cli; mod context; pub mod fpm; diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index bbc0a29b..8df53fb1 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -18,7 +18,7 @@ publish = false license = { workspace = true } [lib] -crate-type = ["lib", "cdylib"] +crate-type = ["cdylib"] [dependencies] indexmap = "2.7.1" @@ -26,6 +26,7 @@ phper = { workspace = true } [dev-dependencies] phper-test = { workspace = true } +tokio = { version = "1.43.0", features = ["full"] } [build-dependencies] phper-build = { workspace = true } diff --git a/tests/integration/tests/cli.rs b/tests/integration/tests/cli.rs new file mode 100644 index 00000000..a8c0c94d --- /dev/null +++ b/tests/integration/tests/cli.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + +mod common; + +use crate::common::{DYLIB_PATH, TESTS_PHP_DIR}; +use phper_test::cli::test_php_script; + +#[test] +fn test_phpinfo() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("phpinfo.php")); +} + +#[test] +fn test_arguments() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("arguments.php")); +} + +#[test] +fn test_arrays() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("arrays.php")); +} + +#[test] +fn test_classes() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("classes.php")); +} + +#[test] +fn test_functions() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("functions.php")); +} + +#[test] +fn test_objects() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("objects.php")); +} + +#[test] +fn test_strings() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("strings.php")); +} + +#[test] +fn test_values() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("values.php")); +} + +#[test] +fn test_constants() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("constants.php")); +} + +#[test] +fn test_ini() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("ini.php")); +} + +#[test] +fn test_references() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("references.php")); +} + +#[test] +fn test_errors() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("errors.php")); +} + +#[test] +fn test_reflection() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("reflection.php")); +} + +#[test] +fn test_typehints() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("typehints.php")); +} + +#[test] +fn test_enums() { + test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("enums.php")); +} diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs new file mode 100644 index 00000000..b9f78685 --- /dev/null +++ b/tests/integration/tests/common/mod.rs @@ -0,0 +1,25 @@ +use phper_test::{cargo::CargoBuilder, fpm::FpmHandle}; +use std::{ + path::{Path, PathBuf}, + sync::LazyLock, +}; + +#[allow(dead_code)] +pub(crate) static DYLIB_PATH: LazyLock = LazyLock::new(|| { + let result = CargoBuilder::new() + .current_dir(env!("CARGO_MANIFEST_DIR")) + .build() + .unwrap(); + result.get_cdylib().unwrap() +}); + +#[allow(dead_code)] +pub(crate) static TESTS_PHP_DIR: LazyLock = LazyLock::new(|| { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("php") +}); + +#[allow(dead_code)] +pub(crate) static FPM_HANDLE: LazyLock<&FpmHandle> = + LazyLock::new(|| FpmHandle::setup(&*DYLIB_PATH)); diff --git a/tests/integration/tests/fpm.rs b/tests/integration/tests/fpm.rs new file mode 100644 index 00000000..5392d4ad --- /dev/null +++ b/tests/integration/tests/fpm.rs @@ -0,0 +1,108 @@ +mod common; + +use crate::common::{FPM_HANDLE, TESTS_PHP_DIR}; + +#[tokio::test] +async fn test_phpinfo() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/phpinfo.php", None, None) + .await; +} + +#[tokio::test] +async fn test_arguments() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/arguments.php", None, None) + .await; +} + +#[tokio::test] +async fn test_arrays() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/arrays.php", None, None) + .await; +} + +#[tokio::test] +async fn test_classes() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/classes.php", None, None) + .await; +} + +#[tokio::test] +async fn test_functions() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/functions.php", None, None) + .await; +} + +#[tokio::test] +async fn test_objects() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/objects.php", None, None) + .await; +} + +#[tokio::test] +async fn test_strings() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/strings.php", None, None) + .await; +} + +#[tokio::test] +async fn test_values() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/values.php", None, None) + .await; +} + +#[tokio::test] +async fn test_constants() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/constants.php", None, None) + .await; +} + +#[tokio::test] +async fn test_ini() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/ini.php", None, None) + .await; +} + +#[tokio::test] +async fn test_references() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/references.php", None, None) + .await; +} + +#[tokio::test] +async fn test_errors() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/errors.php", None, None) + .await; +} + +#[tokio::test] +async fn test_reflection() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/reflection.php", None, None) + .await; +} + +#[tokio::test] +async fn test_typehints() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/typehints.php", None, None) + .await; +} + +#[tokio::test] +async fn test_enums() { + FPM_HANDLE + .test_fpm_request("GET", &*TESTS_PHP_DIR, "/enums.php", None, None) + .await; +} diff --git a/tests/integration/tests/integration.rs b/tests/integration/tests/integration.rs deleted file mode 100644 index 7abe2a66..00000000 --- a/tests/integration/tests/integration.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) 2022 PHPER Framework Team -// PHPER is licensed under Mulan PSL v2. -// You can use this software according to the terms and conditions of the Mulan -// PSL v2. You may obtain a copy of Mulan PSL v2 at: -// http://license.coscl.org.cn/MulanPSL2 -// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY -// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -// See the Mulan PSL v2 for more details. - -use phper_test::{cli::test_php_scripts, fpm, fpm::test_fpm_request, utils::get_lib_path}; -use std::{ - env, - path::{Path, PathBuf}, -}; - -#[test] -fn test_cli() { - let tests_php_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("php"); - - test_php_scripts( - get_lib_path( - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("target"), - "integration", - ), - &[ - &tests_php_dir.join("phpinfo.php"), - &tests_php_dir.join("arguments.php"), - &tests_php_dir.join("arrays.php"), - &tests_php_dir.join("classes.php"), - &tests_php_dir.join("functions.php"), - &tests_php_dir.join("objects.php"), - &tests_php_dir.join("strings.php"), - &tests_php_dir.join("values.php"), - &tests_php_dir.join("constants.php"), - &tests_php_dir.join("ini.php"), - &tests_php_dir.join("references.php"), - &tests_php_dir.join("errors.php"), - &tests_php_dir.join("reflection.php"), - &tests_php_dir.join("typehints.php"), - &tests_php_dir.join("enums.php"), - ], - ); -} - -#[test] -fn test_fpm() { - let tests_php_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("php"); - - fpm::setup(get_lib_path( - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("..") - .join("..") - .join("target"), - "integration", - )); - - test_fpm_request("GET", &tests_php_dir, "/arguments.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/arrays.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/classes.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/functions.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/objects.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/strings.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/values.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/constants.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/ini.php", None, None); - test_fpm_request("GET", &tests_php_dir, "/enums.php", None, None); -} From 8265ad45114e6fd141215d0ad92aae97c9e16dcb Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sat, 21 Jun 2025 23:11:41 +0800 Subject: [PATCH 02/10] fix: Update copyright headers in mod.rs and fpm.rs for consistency --- tests/integration/tests/common/mod.rs | 10 ++++++++++ tests/integration/tests/fpm.rs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index b9f78685..ec66713a 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -1,3 +1,13 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + use phper_test::{cargo::CargoBuilder, fpm::FpmHandle}; use std::{ path::{Path, PathBuf}, diff --git a/tests/integration/tests/fpm.rs b/tests/integration/tests/fpm.rs index 5392d4ad..8c7afa39 100644 --- a/tests/integration/tests/fpm.rs +++ b/tests/integration/tests/fpm.rs @@ -1,3 +1,13 @@ +// Copyright (c) 2022 PHPER Framework Team +// PHPER is licensed under Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan +// PSL v2. You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +// See the Mulan PSL v2 for more details. + mod common; use crate::common::{FPM_HANDLE, TESTS_PHP_DIR}; From faf303bb4ddae07c6a845a1a3aff7b09faaaa633 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 22 Jun 2025 13:58:52 +0800 Subject: [PATCH 03/10] feat: Add logging and setup functions for improved debugging in tests --- Cargo.lock | 143 +++++++++++++++++++++++++- phper-test/Cargo.toml | 1 + phper-test/src/cargo.rs | 2 + phper-test/src/cli.rs | 38 +++---- phper-test/src/context.rs | 9 ++ phper-test/src/fpm.rs | 32 +++--- tests/integration/Cargo.toml | 2 + tests/integration/tests/cli.rs | 17 ++- tests/integration/tests/common/mod.rs | 39 +++++-- tests/integration/tests/fpm.rs | 17 ++- 10 files changed, 251 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5151631..51a6e496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,56 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -235,6 +285,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "cookie" version = "0.18.1" @@ -344,6 +400,29 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -799,7 +878,9 @@ dependencies = [ name = "integration" version = "0.0.0" dependencies = [ + "env_logger", "indexmap", + "log", "phper", "phper-build", "phper-test", @@ -812,6 +893,12 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.13.0" @@ -827,6 +914,30 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "js-sys" version = "0.3.77" @@ -883,9 +994,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "matchit" @@ -988,6 +1099,12 @@ version = "1.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" version = "0.10.71" @@ -1183,6 +1300,7 @@ dependencies = [ "cargo_metadata", "fastcgi-client", "libc", + "log", "phper-macros", "tempfile", "tokio", @@ -1206,6 +1324,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1988,6 +2121,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/phper-test/Cargo.toml b/phper-test/Cargo.toml index 80f6cc24..0f945320 100644 --- a/phper-test/Cargo.toml +++ b/phper-test/Cargo.toml @@ -23,6 +23,7 @@ license = { workspace = true } cargo_metadata = "0.20.0" fastcgi-client = "0.9.0" libc = "0.2.169" +log = { version = "0.4.27", features = ["kv"] } phper-macros = { workspace = true } tempfile = "3.17.1" tokio = { version = "1.43.0", features = ["net"] } diff --git a/phper-test/src/cargo.rs b/phper-test/src/cargo.rs index 616fc900..072f54cd 100644 --- a/phper-test/src/cargo.rs +++ b/phper-test/src/cargo.rs @@ -11,6 +11,7 @@ //! Cargo build utilities for building and analyzing Rust libraries. use cargo_metadata::Message; +use log::debug; use std::{ io::{self, BufReader}, path::{Path, PathBuf}, @@ -60,6 +61,7 @@ impl CargoBuilder { let reader = BufReader::new(stdout); let mut messages = Vec::new(); for message in cargo_metadata::Message::parse_stream(reader) { + debug!(message:?; "cargo build message"); let message = message?; messages.push(message); } diff --git a/phper-test/src/cli.rs b/phper-test/src/cli.rs index 8368a49f..b4757bce 100644 --- a/phper-test/src/cli.rs +++ b/phper-test/src/cli.rs @@ -11,6 +11,7 @@ //! Test tools for php cli program. use crate::context::Context; +use log::debug; use std::{ panic::{UnwindSafe, catch_unwind, resume_unwind}, path::Path, @@ -72,31 +73,32 @@ pub fn test_php_scripts_with_condition( let output = cmd.output().unwrap(); let path = script.as_ref().to_str().unwrap(); - let mut stdout = String::from_utf8(output.stdout.clone()).unwrap(); + let mut stdout = String::from_utf8_lossy(&output.stdout).to_string(); if stdout.is_empty() { stdout.push_str(""); } - let mut stderr = String::from_utf8(output.stderr.clone()).unwrap(); + let mut stderr = String::from_utf8_lossy(&output.stderr).to_string(); if stderr.is_empty() { stderr.push_str(""); - } + }; - eprintln!( - "===== command =====\n{} {}\n===== stdout ======\n{}\n===== stderr ======\n{}", - &context.php_bin, - cmd.get_args().join(" "), - stdout, - stderr, - ); - #[cfg(target_os = "linux")] - if output.status.code().is_none() { - use std::os::unix::process::ExitStatusExt; - eprintln!( - "===== signal ======\nExitStatusExt is None, the signal is: {:?}", - output.status.signal() - ); - } + debug!(command:% = cmd.get_command().join(" ".as_ref()).to_string_lossy(), + status:? = output.status.code(), + stdout = &*stdout, + stderr:%, + signal:? = { + #[cfg(unix)] + { + use std::os::unix::process::ExitStatusExt as _; + output.status.signal() + } + #[cfg(not(unix))] + { + None + } + }; + "execute php test command"); if !condition(output) { panic!("test php file `{}` failed", path); diff --git a/phper-test/src/context.rs b/phper-test/src/context.rs index 97ad26a8..10c61a2d 100644 --- a/phper-test/src/context.rs +++ b/phper-test/src/context.rs @@ -11,6 +11,7 @@ use crate::utils; use std::{ env, + ffi::OsStr, fs::read_to_string, io::Write, ops::{Deref, DerefMut}, @@ -125,6 +126,14 @@ pub struct ContextCommand { } impl ContextCommand { + pub fn get_command(&self) -> Vec<&OsStr> { + let program = self.cmd.get_program(); + let args = self.cmd.get_args(); + let mut command = vec![program]; + command.extend(args); + command + } + pub fn get_args(&self) -> &[String] { &self.args } diff --git a/phper-test/src/fpm.rs b/phper-test/src/fpm.rs index 1396b2a9..96360373 100644 --- a/phper-test/src/fpm.rs +++ b/phper-test/src/fpm.rs @@ -12,7 +12,9 @@ use crate::{context::Context, utils::spawn_command}; use fastcgi_client::{Client, Params, Request}; use libc::{SIGTERM, atexit, kill, pid_t}; +use log::debug; use std::{ + borrow::Cow, fs, path::Path, process::Child, @@ -82,13 +84,11 @@ impl FpmHandle { "-y", &fpm_conf_file.path().display().to_string(), ]; - eprintln!("===== setup php-fpm ====="); - eprintln!("{}", argv.join(" ")); + debug!(argv:% = argv.join(" "); "setup php-fpm"); let child = spawn_command(&argv, Some(Duration::from_secs(3))); let log = fs::read_to_string("/tmp/.php-fpm.log").unwrap(); - eprintln!("===== php-fpm log ====="); - eprintln!("{}", log); + debug!(log:%; "php-fpm log"); // fs::remove_file("/tmp/.php-fpm.log").unwrap(); let handle = FpmHandle { @@ -191,24 +191,16 @@ impl FpmHandle { let no_error = stderr.is_empty(); - let f = |out: Vec| { - String::from_utf8(out) - .map(|out| { - if out.is_empty() { - "".to_owned() - } else { - out - } - }) - .unwrap_or_else(|_| "".to_owned()) + let f = |out| { + let out = String::from_utf8_lossy(out); + if out.is_empty() { + Cow::Borrowed("") + } else { + out + } }; - eprintln!("===== request ====="); - eprintln!("{}", request_uri); - eprintln!("===== stdout ======"); - eprintln!("{}", f(stdout)); - eprintln!("===== stderr ======"); - eprintln!("{}", f(stderr)); + debug!(uri:% = request_uri, stdout:% = f(&stdout), stderr:% = f(&stderr); "test php request"); assert!(no_error, "request not success: {}", request_uri); } diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 8df53fb1..7122875b 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -25,6 +25,8 @@ indexmap = "2.7.1" phper = { workspace = true } [dev-dependencies] +env_logger = { version = "0.11.8", features = ["kv"] } +log = { version = "0.4.27", features = ["kv"] } phper-test = { workspace = true } tokio = { version = "1.43.0", features = ["full"] } diff --git a/tests/integration/tests/cli.rs b/tests/integration/tests/cli.rs index a8c0c94d..81507e39 100644 --- a/tests/integration/tests/cli.rs +++ b/tests/integration/tests/cli.rs @@ -10,80 +10,95 @@ mod common; -use crate::common::{DYLIB_PATH, TESTS_PHP_DIR}; +use crate::common::{DYLIB_PATH, TESTS_PHP_DIR, setup}; use phper_test::cli::test_php_script; #[test] fn test_phpinfo() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("phpinfo.php")); } #[test] fn test_arguments() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("arguments.php")); } #[test] fn test_arrays() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("arrays.php")); } #[test] fn test_classes() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("classes.php")); } #[test] fn test_functions() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("functions.php")); } #[test] fn test_objects() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("objects.php")); } #[test] fn test_strings() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("strings.php")); } #[test] fn test_values() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("values.php")); } #[test] fn test_constants() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("constants.php")); } #[test] fn test_ini() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("ini.php")); } #[test] fn test_references() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("references.php")); } #[test] fn test_errors() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("errors.php")); } #[test] fn test_reflection() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("reflection.php")); } #[test] fn test_typehints() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("typehints.php")); } #[test] fn test_enums() { + setup(); test_php_script(&*DYLIB_PATH, TESTS_PHP_DIR.join("enums.php")); } diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index ec66713a..edf31f40 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -8,14 +8,15 @@ // NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. +use env_logger::fmt::Formatter; +use log::kv::{self, Key, Value}; use phper_test::{cargo::CargoBuilder, fpm::FpmHandle}; use std::{ path::{Path, PathBuf}, - sync::LazyLock, + sync::{LazyLock, Once}, }; -#[allow(dead_code)] -pub(crate) static DYLIB_PATH: LazyLock = LazyLock::new(|| { +pub static DYLIB_PATH: LazyLock = LazyLock::new(|| { let result = CargoBuilder::new() .current_dir(env!("CARGO_MANIFEST_DIR")) .build() @@ -23,13 +24,37 @@ pub(crate) static DYLIB_PATH: LazyLock = LazyLock::new(|| { result.get_cdylib().unwrap() }); -#[allow(dead_code)] -pub(crate) static TESTS_PHP_DIR: LazyLock = LazyLock::new(|| { +pub static TESTS_PHP_DIR: LazyLock = LazyLock::new(|| { Path::new(env!("CARGO_MANIFEST_DIR")) .join("tests") .join("php") }); #[allow(dead_code)] -pub(crate) static FPM_HANDLE: LazyLock<&FpmHandle> = - LazyLock::new(|| FpmHandle::setup(&*DYLIB_PATH)); +pub static FPM_HANDLE: LazyLock<&FpmHandle> = LazyLock::new(|| FpmHandle::setup(&*DYLIB_PATH)); + +pub fn setup() { + static ONCE: Once = Once::new(); + ONCE.call_once(|| { + env_logger::Builder::from_default_env() + .default_format() + .is_test(true) + .format_key_values(|buf, args| { + use std::io::Write as _; + struct Visitor<'a>(&'a mut Formatter); + impl<'kvs> kv::VisitSource<'kvs> for Visitor<'kvs> { + fn visit_pair( + &mut self, key: Key<'kvs>, value: Value<'kvs>, + ) -> Result<(), kv::Error> { + writeln!(self.0).unwrap(); + writeln!(self.0, "===== {} =====", key).unwrap(); + writeln!(self.0, "{}", value).unwrap(); + Ok(()) + } + } + args.visit(&mut Visitor(buf)).unwrap(); + Ok(()) + }) + .init(); + }); +} diff --git a/tests/integration/tests/fpm.rs b/tests/integration/tests/fpm.rs index 8c7afa39..b36b97a1 100644 --- a/tests/integration/tests/fpm.rs +++ b/tests/integration/tests/fpm.rs @@ -10,10 +10,11 @@ mod common; -use crate::common::{FPM_HANDLE, TESTS_PHP_DIR}; +use crate::common::{FPM_HANDLE, TESTS_PHP_DIR, setup}; #[tokio::test] async fn test_phpinfo() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/phpinfo.php", None, None) .await; @@ -21,6 +22,7 @@ async fn test_phpinfo() { #[tokio::test] async fn test_arguments() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/arguments.php", None, None) .await; @@ -28,6 +30,7 @@ async fn test_arguments() { #[tokio::test] async fn test_arrays() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/arrays.php", None, None) .await; @@ -35,6 +38,7 @@ async fn test_arrays() { #[tokio::test] async fn test_classes() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/classes.php", None, None) .await; @@ -42,6 +46,7 @@ async fn test_classes() { #[tokio::test] async fn test_functions() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/functions.php", None, None) .await; @@ -49,6 +54,7 @@ async fn test_functions() { #[tokio::test] async fn test_objects() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/objects.php", None, None) .await; @@ -56,6 +62,7 @@ async fn test_objects() { #[tokio::test] async fn test_strings() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/strings.php", None, None) .await; @@ -63,6 +70,7 @@ async fn test_strings() { #[tokio::test] async fn test_values() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/values.php", None, None) .await; @@ -70,6 +78,7 @@ async fn test_values() { #[tokio::test] async fn test_constants() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/constants.php", None, None) .await; @@ -77,6 +86,7 @@ async fn test_constants() { #[tokio::test] async fn test_ini() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/ini.php", None, None) .await; @@ -84,6 +94,7 @@ async fn test_ini() { #[tokio::test] async fn test_references() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/references.php", None, None) .await; @@ -91,6 +102,7 @@ async fn test_references() { #[tokio::test] async fn test_errors() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/errors.php", None, None) .await; @@ -98,6 +110,7 @@ async fn test_errors() { #[tokio::test] async fn test_reflection() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/reflection.php", None, None) .await; @@ -105,6 +118,7 @@ async fn test_reflection() { #[tokio::test] async fn test_typehints() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/typehints.php", None, None) .await; @@ -112,6 +126,7 @@ async fn test_typehints() { #[tokio::test] async fn test_enums() { + setup(); FPM_HANDLE .test_fpm_request("GET", &*TESTS_PHP_DIR, "/enums.php", None, None) .await; From 5924c09d160e380a008d533d60f0ba507e50eed8 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 22 Jun 2025 14:04:25 +0800 Subject: [PATCH 04/10] refactor: Remove unused args field from ContextCommand struct --- phper-test/src/context.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/phper-test/src/context.rs b/phper-test/src/context.rs index 10c61a2d..f0941ece 100644 --- a/phper-test/src/context.rs +++ b/phper-test/src/context.rs @@ -80,7 +80,7 @@ impl Context { script.as_ref().display().to_string(), ]; cmd.args(&args); - ContextCommand { cmd, args } + ContextCommand { cmd } } pub fn find_php_fpm(&self) -> Option { @@ -122,7 +122,6 @@ impl Context { pub struct ContextCommand { cmd: Command, - args: Vec, } impl ContextCommand { @@ -133,10 +132,6 @@ impl ContextCommand { command.extend(args); command } - - pub fn get_args(&self) -> &[String] { - &self.args - } } impl Deref for ContextCommand { From a4144927986044aa4465bacc7346d7904db00381 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Sun, 22 Jun 2025 14:18:38 +0800 Subject: [PATCH 05/10] feat: Add argument for parallel build in DYLIB_PATH initialization --- tests/integration/tests/common/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index edf31f40..c9980925 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -18,6 +18,7 @@ use std::{ pub static DYLIB_PATH: LazyLock = LazyLock::new(|| { let result = CargoBuilder::new() + .arg("-j1") .current_dir(env!("CARGO_MANIFEST_DIR")) .build() .unwrap(); From 0a177c9b8939372fc226f20f6b8e69ab7f3faac1 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Mon, 23 Jun 2025 10:13:00 +0800 Subject: [PATCH 06/10] fix: Limit test threads to 1 for consistent test results --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bf41238..4d2dac2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --release -- --nocapture + args: --release -- --nocapture --test-threads=1 - name: Cargo doc uses: actions-rs/cargo@v1 From 3c5317d696a253534aa8ec1fe7dabd84f753cbc1 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Mon, 23 Jun 2025 15:30:29 +0800 Subject: [PATCH 07/10] fix: Update macOS matrix to use 'macos-14-large' for CI jobs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d2dac2b..7fc2c5d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: matrix: os: - ubuntu-24.04 - - macos-14 + - macos-14-large php-version: - "7.0" - "7.1" From 9bb1e094ce6e75a0b565d8358bdd5e8bcbb35c1c Mon Sep 17 00:00:00 2001 From: jmjoy Date: Mon, 23 Jun 2025 16:02:37 +0800 Subject: [PATCH 08/10] fix: Update macOS matrix to use 'macos-15' for CI jobs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fc2c5d5..1a7f6b30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: matrix: os: - ubuntu-24.04 - - macos-14-large + - macos-15 php-version: - "7.0" - "7.1" From aa288dc62eb018b5be5d075a7387cc57155f2a8c Mon Sep 17 00:00:00 2001 From: jmjoy Date: Tue, 24 Jun 2025 18:27:44 +0800 Subject: [PATCH 09/10] fix: Update macOS matrix to use 'macos-14' for CI jobs fix: Suppress stderr output in CargoBuilder for cleaner build logs --- .github/workflows/ci.yml | 2 +- phper-test/src/cargo.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a7f6b30..4d2dac2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: matrix: os: - ubuntu-24.04 - - macos-15 + - macos-14 php-version: - "7.0" - "7.1" diff --git a/phper-test/src/cargo.rs b/phper-test/src/cargo.rs index 072f54cd..920240f8 100644 --- a/phper-test/src/cargo.rs +++ b/phper-test/src/cargo.rs @@ -34,8 +34,9 @@ impl CargoBuilder { let mut command = Command::new(env!("CARGO")); command .args(["build", "--lib", "--message-format", "json"]) + .stdin(Stdio::null()) .stdout(Stdio::piped()) - .stderr(Stdio::piped()); + .stderr(Stdio::null()); Self { command } } From a60ce2313a4b43f022314abef7f0f2b1fcb0ded7 Mon Sep 17 00:00:00 2001 From: jmjoy Date: Tue, 24 Jun 2025 23:22:18 +0800 Subject: [PATCH 10/10] fix: Remove test thread argument for consistent build behavior --- .github/workflows/ci.yml | 2 +- phper-test/src/cargo.rs | 18 +++++++++++++++--- tests/integration/tests/common/mod.rs | 1 - 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d2dac2b..9bf41238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -135,7 +135,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --release -- --nocapture --test-threads=1 + args: --release -- --nocapture - name: Cargo doc uses: actions-rs/cargo@v1 diff --git a/phper-test/src/cargo.rs b/phper-test/src/cargo.rs index 920240f8..ef426bc3 100644 --- a/phper-test/src/cargo.rs +++ b/phper-test/src/cargo.rs @@ -11,7 +11,7 @@ //! Cargo build utilities for building and analyzing Rust libraries. use cargo_metadata::Message; -use log::debug; +use log::{debug, trace}; use std::{ io::{self, BufReader}, path::{Path, PathBuf}, @@ -31,9 +31,13 @@ pub struct CargoBuildResult { impl CargoBuilder { /// Create a new CargoBuilder instance pub fn new() -> Self { + let mut args = vec!["build", "--lib", "--message-format", "json"]; + if !cfg!(debug_assertions) { + args.push("--release"); + } let mut command = Command::new(env!("CARGO")); command - .args(["build", "--lib", "--message-format", "json"]) + .args(&args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()); @@ -54,6 +58,14 @@ impl CargoBuilder { /// Execute the cargo build command and return the result pub fn build(&mut self) -> io::Result { + debug!(command:% = { + let program = self.command.get_program(); + let args = self.command.get_args(); + let mut command = vec![program]; + command.extend(args); + command.join(" ".as_ref()).to_string_lossy().to_string() + }; "run cargo build command"); + let mut child = self.command.spawn()?; let stdout = child .stdout @@ -62,7 +74,7 @@ impl CargoBuilder { let reader = BufReader::new(stdout); let mut messages = Vec::new(); for message in cargo_metadata::Message::parse_stream(reader) { - debug!(message:?; "cargo build message"); + trace!(message:?; "cargo build message"); let message = message?; messages.push(message); } diff --git a/tests/integration/tests/common/mod.rs b/tests/integration/tests/common/mod.rs index c9980925..edf31f40 100644 --- a/tests/integration/tests/common/mod.rs +++ b/tests/integration/tests/common/mod.rs @@ -18,7 +18,6 @@ use std::{ pub static DYLIB_PATH: LazyLock = LazyLock::new(|| { let result = CargoBuilder::new() - .arg("-j1") .current_dir(env!("CARGO_MANIFEST_DIR")) .build() .unwrap();