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}