phper_test/cli.rs
1// Copyright (c) 2022 PHPER Framework Team
2// PHPER is licensed under Mulan PSL v2.
3// You can use this software according to the terms and conditions of the Mulan
4// PSL v2. You may obtain a copy of Mulan PSL v2 at:
5// http://license.coscl.org.cn/MulanPSL2
6// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY
7// KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO
8// NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
9// See the Mulan PSL v2 for more details.
10
11//! Test tools for php cli program.
12
13use crate::context::Context;
14use log::debug;
15use std::{
16 panic::{UnwindSafe, catch_unwind, resume_unwind},
17 path::Path,
18 process::{Child, Output},
19};
20
21/// Check your extension by executing the php script, if the all executing
22/// return success, than the test is pass.
23///
24/// - `lib_path` is the path of extension lib.
25///
26/// - `script` is the path of your php test script.
27pub fn test_php_script(lib_path: impl AsRef<Path>, script: impl AsRef<Path>) {
28 let condition = |output: Output| output.status.success();
29 let scripts = Some(script);
30 let scripts = scripts
31 .iter()
32 .map(|s| (s as _, &condition as _))
33 .collect::<Vec<_>>();
34 test_php_scripts_with_condition(lib_path, &scripts);
35}
36
37/// Check your extension by executing multiple php scripts with success
38/// condition.
39///
40/// This function executes multiple PHP scripts and checks if all of them return
41/// success status. It's a convenience wrapper around
42/// `test_php_scripts_with_condition` with a default success condition.
43///
44/// # Arguments
45///
46/// * `lib_path` - The path to the extension library file
47/// * `scripts` - A slice of references to PHP script paths to execute
48///
49/// # Panics
50///
51/// Panics if any script execution fails or returns non-success status.
52pub fn test_php_scripts(lib_path: impl AsRef<Path>, scripts: &[&dyn AsRef<Path>]) {
53 let condition = |output: Output| output.status.success();
54 let scripts = scripts
55 .iter()
56 .map(|s| (*s, &condition as _))
57 .collect::<Vec<_>>();
58 test_php_scripts_with_condition(lib_path, &scripts);
59}
60
61/// Check your extension by executing a single php script with custom condition.
62///
63/// This function allows you to specify a custom condition function to check the
64/// execution result of a PHP script, providing more flexibility than the
65/// default success-only check.
66///
67/// # Arguments
68///
69/// * `lib_path` - The path to the extension library file
70/// * `script` - The path to the PHP script to execute
71/// * `condition` - A function that takes the command output and returns true if
72/// the test passes
73///
74/// # Panics
75///
76/// Panics if the script execution fails or the condition function returns
77/// false.
78///
79/// # Examples
80///
81/// ```rust,no_run
82/// use phper_test::cli::test_php_script_with_condition;
83/// use std::process::Output;
84///
85/// // Test that script outputs specific text
86/// let condition =
87/// |output: Output| String::from_utf8_lossy(&output.stdout).contains("expected output");
88/// test_php_script_with_condition("/path/to/extension.so", "test.php", condition);
89/// ```
90pub fn test_php_script_with_condition(
91 lib_path: impl AsRef<Path>, script: impl AsRef<Path>, condition: impl Fn(Output) -> bool,
92) {
93 let scripts = Some(script);
94 let scripts = scripts
95 .iter()
96 .map(|s| (s as _, &condition as _))
97 .collect::<Vec<_>>();
98 test_php_scripts_with_condition(lib_path, &scripts);
99}
100
101/// Type alias for script and condition pair used in batch testing.
102///
103/// The first element is a reference to a path-like object representing the PHP
104/// script, and the second element is a function that validates the execution
105/// output.
106pub type ScriptCondition<'a> = (&'a dyn AsRef<Path>, &'a dyn Fn(Output) -> bool);
107
108/// Check your extension by executing multiple php scripts with custom
109/// conditions.
110///
111/// This is the most flexible testing function that allows you to specify
112/// different validation conditions for each script. It executes each script
113/// with the extension loaded and validates the results using the provided
114/// condition functions.
115///
116/// # Arguments
117///
118/// * `lib_path` - The path to the extension library file
119/// * `scripts` - A slice of tuples containing script paths and their validation
120/// conditions
121///
122/// # Panics
123///
124/// Panics if any script execution fails or if any condition function returns
125/// false. The panic message will include the path of the failing script.
126///
127/// # Examples
128///
129/// ```rust,no_run
130/// use phper_test::cli::{ScriptCondition, test_php_scripts_with_condition};
131/// use std::process::Output;
132///
133/// let success_condition = |output: Output| output.status.success();
134/// let custom_condition =
135/// |output: Output| String::from_utf8_lossy(&output.stdout).contains("custom check");
136///
137/// let scripts: &[ScriptCondition] = &[
138/// (&"test1.php", &success_condition),
139/// (&"test2.php", &custom_condition),
140/// ];
141/// test_php_scripts_with_condition("/path/to/extension.so", scripts);
142/// ```
143pub fn test_php_scripts_with_condition(
144 lib_path: impl AsRef<Path>, scripts: &[ScriptCondition<'_>],
145) {
146 let context = Context::get_global();
147
148 for (script, condition) in scripts {
149 let mut cmd = context.create_command_with_lib(&lib_path, script);
150
151 let output = cmd.output().unwrap();
152 let path = script.as_ref().to_str().unwrap();
153
154 let mut stdout = String::from_utf8_lossy(&output.stdout).to_string();
155 if stdout.is_empty() {
156 stdout.push_str("<empty>");
157 }
158
159 let mut stderr = String::from_utf8_lossy(&output.stderr).to_string();
160 if stderr.is_empty() {
161 stderr.push_str("<empty>");
162 };
163
164 debug!(command:% = cmd.get_command().join(" ".as_ref()).to_string_lossy(),
165 status:? = output.status.code(),
166 stdout = &*stdout,
167 stderr:%,
168 signal:? = {
169 #[cfg(unix)]
170 {
171 use std::os::unix::process::ExitStatusExt as _;
172 output.status.signal()
173 }
174 #[cfg(not(unix))]
175 {
176 None
177 }
178 };
179 "execute php test command");
180
181 if !condition(output) {
182 panic!("test php file `{}` failed", path);
183 }
184 }
185}
186
187/// Check your extension by executing the long term php script such as http
188/// server, if the all your specified checkers are pass, than the test is pass.
189#[allow(clippy::zombie_processes)]
190pub fn test_long_term_php_script_with_condition(
191 lib_path: impl AsRef<Path>, script: impl AsRef<Path>,
192 condition: impl FnOnce(&Child) + UnwindSafe,
193) {
194 let context = Context::get_global();
195 let mut command = context.create_command_with_lib(lib_path, script);
196 let mut child = command.spawn().unwrap();
197 let r = catch_unwind(|| condition(&child));
198 child.kill().unwrap();
199 if let Err(e) = r {
200 resume_unwind(e);
201 }
202}