aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 14:46:55 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:19:42 +0900
commit9c6f614aa6ddb5d91340e34b0136be6a14712d63 (patch)
tree35973a35532ada33fe05805995b2e5b5e7bee1bc /crates/shirabe/tests
parentd0336078c5b63b174e7313d54d973a1832228928 (diff)
downloadphp-shirabe-9c6f614aa6ddb5d91340e34b0136be6a14712d63.tar.gz
php-shirabe-9c6f614aa6ddb5d91340e34b0136be6a14712d63.tar.zst
php-shirabe-9c6f614aa6ddb5d91340e34b0136be6a14712d63.zip
feat(util): add ProcessExecutorMock test infra; port SymfonyStyle message methods
Add an internal mock hook to ProcessExecutor (None in production) so tests can stub command execution without spawning processes, mirroring Composer's ProcessExecutorMock subclass. Add get_process_executor_mock helper and two verification tests. Implement SymfonyStyle's message-handling methods. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests')
-rw-r--r--crates/shirabe/tests/common/process_executor_mock.rs101
-rw-r--r--crates/shirabe/tests/util/main.rs3
-rw-r--r--crates/shirabe/tests/util/process_executor_test.rs61
3 files changed, 165 insertions, 0 deletions
diff --git a/crates/shirabe/tests/common/process_executor_mock.rs b/crates/shirabe/tests/common/process_executor_mock.rs
new file mode 100644
index 0000000..b81c8bb
--- /dev/null
+++ b/crates/shirabe/tests/common/process_executor_mock.rs
@@ -0,0 +1,101 @@
+//! ref: composer/tests/Composer/Test/Mock/ProcessExecutorMock.php
+
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor};
+use shirabe_php_shim::PhpMixed;
+
+// A command expectation as written in the PHP tests: either a bare command
+// (`'git command'` / `['git', '--version']`) or the full
+// `{cmd, return?, stdout?, stderr?}` associative form.
+pub fn cmd(command: impl IntoMockCmd) -> MockExpectation {
+ MockExpectation::from_cmd(command.into_mock_cmd())
+}
+
+pub fn cmd_full(
+ command: impl IntoMockCmd,
+ r#return: i64,
+ stdout: impl Into<String>,
+ stderr: impl Into<String>,
+) -> MockExpectation {
+ MockExpectation {
+ cmd: command.into_mock_cmd(),
+ r#return,
+ stdout: stdout.into(),
+ stderr: stderr.into(),
+ callback: None,
+ }
+}
+
+// Accepts the same command shapes the tests use: a string command or a list of
+// string args. Comparison against the executed command is exact (PHP `===`), so
+// the form here must match the form the code under test passes to `execute`.
+pub trait IntoMockCmd {
+ fn into_mock_cmd(self) -> PhpMixed;
+}
+
+impl IntoMockCmd for &str {
+ fn into_mock_cmd(self) -> PhpMixed {
+ PhpMixed::String(self.to_string())
+ }
+}
+
+impl IntoMockCmd for String {
+ fn into_mock_cmd(self) -> PhpMixed {
+ PhpMixed::String(self)
+ }
+}
+
+impl IntoMockCmd for Vec<&str> {
+ fn into_mock_cmd(self) -> PhpMixed {
+ PhpMixed::List(
+ self.into_iter()
+ .map(|s| PhpMixed::String(s.to_string()))
+ .collect(),
+ )
+ }
+}
+
+impl IntoMockCmd for Vec<String> {
+ fn into_mock_cmd(self) -> PhpMixed {
+ PhpMixed::List(self.into_iter().map(PhpMixed::String).collect())
+ }
+}
+
+impl<const N: usize> IntoMockCmd for [&str; N] {
+ fn into_mock_cmd(self) -> PhpMixed {
+ PhpMixed::List(
+ self.iter()
+ .map(|s| PhpMixed::String(s.to_string()))
+ .collect(),
+ )
+ }
+}
+
+pub struct ProcessExecutorMockGuard(Rc<RefCell<ProcessExecutor>>);
+
+impl Drop for ProcessExecutorMockGuard {
+ fn drop(&mut self) {
+ // Avoid aborting on a double panic when a test assertion is already unwinding.
+ if std::thread::panicking() {
+ return;
+ }
+ self.0.borrow().__assert_complete();
+ }
+}
+
+// For testing only. Mirrors TestCase::getProcessExecutorMock: returns a shared
+// ProcessExecutor handle configured with the given expectations, plus a guard
+// that runs `__assert_complete` when it drops at the end of the test scope.
+pub fn get_process_executor_mock(
+ expectations: Vec<MockExpectation>,
+ strict: bool,
+ default_handler: MockHandler,
+) -> (Rc<RefCell<ProcessExecutor>>, ProcessExecutorMockGuard) {
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+ process
+ .borrow_mut()
+ .__expects(expectations, strict, default_handler);
+ (process.clone(), ProcessExecutorMockGuard(process))
+}
diff --git a/crates/shirabe/tests/util/main.rs b/crates/shirabe/tests/util/main.rs
index e779fe0..bf23c6b 100644
--- a/crates/shirabe/tests/util/main.rs
+++ b/crates/shirabe/tests/util/main.rs
@@ -1,3 +1,6 @@
+#[path = "../common/process_executor_mock.rs"]
+mod process_executor_mock;
+
mod auth_helper_test;
mod bitbucket_test;
mod config_validator_test;
diff --git a/crates/shirabe/tests/util/process_executor_test.rs b/crates/shirabe/tests/util/process_executor_test.rs
index ba3177f..342c7ff 100644
--- a/crates/shirabe/tests/util/process_executor_test.rs
+++ b/crates/shirabe/tests/util/process_executor_test.rs
@@ -204,3 +204,64 @@ fn test_escape_argument() {
assert_eq!(unix, ProcessExecutor::escape(argument));
}
}
+
+// Exercises the ProcessExecutorMock infrastructure (cf.
+// composer/tests/Composer/Test/Mock/ProcessExecutorMock.php): expectations are matched in order,
+// stdout is captured back, stderr surfaces via get_error_output, and assert_complete (via the
+// guard) verifies the queue was fully consumed.
+#[test]
+fn test_mock_consumes_expectations_in_order() {
+ use crate::process_executor_mock::{cmd_full, get_process_executor_mock};
+ use shirabe::util::process_executor::MockHandler;
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd_full("git command", 1, "out one", ""),
+ cmd_full(["git", "--version"], 0, "git version 2.0.0", "warn"),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut output = String::new();
+ let rc = process
+ .borrow_mut()
+ .execute("git command", &mut output, None)
+ .unwrap();
+ assert_eq!(1, rc);
+ assert_eq!("out one", output);
+
+ let mut output2 = String::new();
+ let rc2 = process
+ .borrow_mut()
+ .execute(&["git", "--version"], &mut output2, None)
+ .unwrap();
+ assert_eq!(0, rc2);
+ assert_eq!("git version 2.0.0", output2);
+ assert_eq!("warn", process.borrow().get_error_output());
+}
+
+// Non-strict mode falls back to the default handler for unexpected commands.
+#[test]
+fn test_mock_default_handler_for_unexpected_command() {
+ use crate::process_executor_mock::get_process_executor_mock;
+ use shirabe::util::process_executor::MockHandler;
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![],
+ false,
+ MockHandler {
+ r#return: 7,
+ stdout: "fallback".to_string(),
+ stderr: String::new(),
+ },
+ );
+
+ let mut output = String::new();
+ let rc = process
+ .borrow_mut()
+ .execute("anything goes", &mut output, None)
+ .unwrap();
+ assert_eq!(7, rc);
+ assert_eq!("fallback", output);
+}