diff options
Diffstat (limited to 'crates/shirabe/tests')
| -rw-r--r-- | crates/shirabe/tests/common/process_executor_mock.rs | 101 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/main.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/process_executor_test.rs | 61 |
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); +} |
