From 9c6f614aa6ddb5d91340e34b0136be6a14712d63 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 25 Jun 2026 14:46:55 +0900 Subject: 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) --- .../shirabe/tests/common/process_executor_mock.rs | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 crates/shirabe/tests/common/process_executor_mock.rs (limited to 'crates/shirabe/tests/common/process_executor_mock.rs') 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, + stderr: impl Into, +) -> 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 { + fn into_mock_cmd(self) -> PhpMixed { + PhpMixed::List(self.into_iter().map(PhpMixed::String).collect()) + } +} + +impl 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>); + +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, + strict: bool, + default_handler: MockHandler, +) -> (Rc>, ProcessExecutorMockGuard) { + let process = Rc::new(RefCell::new(ProcessExecutor::new(None))); + process + .borrow_mut() + .__expects(expectations, strict, default_handler); + (process.clone(), ProcessExecutorMockGuard(process)) +} -- cgit v1.3.1