diff options
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/src/downloader/zip_downloader.rs | 6 | ||||
| -rw-r--r-- | crates/shirabe/src/package/version/version_guesser.rs | 5 | ||||
| -rw-r--r-- | crates/shirabe/src/util/filesystem.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/util/git.rs | 6 | ||||
| -rw-r--r-- | crates/shirabe/src/util/github.rs | 13 | ||||
| -rw-r--r-- | crates/shirabe/src/util/gitlab.rs | 20 | ||||
| -rw-r--r-- | crates/shirabe/src/util/process_executor.rs | 167 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/init_command_test.rs | 23 | ||||
| -rw-r--r-- | crates/shirabe/tests/common/test_case.rs | 41 | ||||
| -rw-r--r-- | crates/shirabe/tests/downloader/zip_downloader_test.rs | 163 | ||||
| -rw-r--r-- | crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs | 32 | ||||
| -rw-r--r-- | crates/shirabe/tests/package/loader/root_package_loader_test.rs | 12 | ||||
| -rw-r--r-- | crates/shirabe/tests/package/version/version_guesser_test.rs | 3 | ||||
| -rw-r--r-- | crates/shirabe/tests/repository/vcs/github_driver_test.rs | 13 |
14 files changed, 382 insertions, 124 deletions
diff --git a/crates/shirabe/src/downloader/zip_downloader.rs b/crates/shirabe/src/downloader/zip_downloader.rs index 9fa68c53..f70846e4 100644 --- a/crates/shirabe/src/downloader/zip_downloader.rs +++ b/crates/shirabe/src/downloader/zip_downloader.rs @@ -130,7 +130,11 @@ impl ZipDownloader { // Build the future first so the executor borrow is released before awaiting; a borrow // held across the await would collide with sibling extracts or sync execute() calls. - let process_future = self.inner.process.borrow().execute_async(&command, None); + let process_future = self + .inner + .process + .borrow_mut() + .execute_async(&command, None); let process_result = process_future.await; match process_result { Ok(mut process) => { diff --git a/crates/shirabe/src/package/version/version_guesser.rs b/crates/shirabe/src/package/version/version_guesser.rs index 185616ac..2f98e339 100644 --- a/crates/shirabe/src/package/version/version_guesser.rs +++ b/crates/shirabe/src/package/version/version_guesser.rs @@ -576,7 +576,10 @@ impl VersionGuesser { }, &scm_cmdline, ); - let process_future = self.process.borrow().execute_async(&cmd_line, Some(path)); + let process_future = self + .process + .borrow_mut() + .execute_async(&cmd_line, Some(path)); let mut process = sync_executor::block_on(process_future)?; if !process.is_successful() { continue; diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs index 70bf69f7..3516b284 100644 --- a/crates/shirabe/src/util/filesystem.rs +++ b/crates/shirabe/src/util/filesystem.rs @@ -200,7 +200,7 @@ impl Filesystem { (fs.get_process_handle(), cmd) }; - let process_future = process_executor.borrow().execute_async( + let process_future = process_executor.borrow_mut().execute_async( PhpMixed::List(cmd.iter().map(|s| PhpMixed::String(s.clone())).collect()), None, ); diff --git a/crates/shirabe/src/util/git.rs b/crates/shirabe/src/util/git.rs index 1e6768b1..6a6aaeb1 100644 --- a/crates/shirabe/src/util/git.rs +++ b/crates/shirabe/src/util/git.rs @@ -229,11 +229,11 @@ impl Git { if !initial_clone { // capture username/password from URL if there is one and we have no auth configured yet let mut output = String::new(); - self.process.borrow_mut().execute_args( - &["git".to_string(), "remote".to_string(), "-v".to_string()], + self.process.borrow_mut().execute( + &["git".to_string(), "remote".to_string(), "-v".to_string()][..], &mut output, cwd, - ); + )?; let mut m: IndexMap<CaptureKey, String> = IndexMap::new(); if Preg::is_match3( php_regex!(r"{^(?:composer|origin)\s+https?://(.+):(.+)@([^/]+)}im"), diff --git a/crates/shirabe/src/util/github.rs b/crates/shirabe/src/util/github.rs index e8e83ddd..e6bac7ec 100644 --- a/crates/shirabe/src/util/github.rs +++ b/crates/shirabe/src/util/github.rs @@ -9,7 +9,7 @@ use crate::util::HttpDownloader; use crate::util::ProcessExecutor; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; -use shirabe_php_shim::{PhpMixed, date, php_regex, stripos, strtolower}; +use shirabe_php_shim::{PhpMixed, date, in_array, php_regex, stripos, strtolower}; #[derive(Debug)] pub struct GitHub { @@ -52,12 +52,11 @@ impl GitHub { pub fn authorize_oauth(&mut self, origin_url: &str) -> bool { let github_domains = self.config.borrow_mut().get("github-domains"); - let domains = match github_domains.as_array() { - Some(arr) => arr.clone(), - None => return false, - }; - let origin_in_domains = domains.values().any(|v| v.as_string() == Some(origin_url)); - if !origin_in_domains { + if !in_array( + PhpMixed::String(origin_url.to_string()), + &github_domains, + false, + ) { return false; } diff --git a/crates/shirabe/src/util/gitlab.rs b/crates/shirabe/src/util/gitlab.rs index 4a636efa..485d4126 100644 --- a/crates/shirabe/src/util/gitlab.rs +++ b/crates/shirabe/src/util/gitlab.rs @@ -11,7 +11,7 @@ use crate::util::ProcessExecutor; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{ - PhpMixed, RuntimeException, http_build_query, json_decode, php_regex, time, + PhpMixed, RuntimeException, http_build_query, in_array, json_decode, php_regex, time, }; #[derive(Debug)] @@ -55,15 +55,15 @@ impl GitLab { let bc_origin_url = Preg::replace(php_regex!("{:\\d+}"), "", origin_url); let gitlab_domains = self.config.borrow_mut().get("gitlab-domains"); - let domains = match gitlab_domains.as_array() { - Some(arr) => arr.clone(), - None => return false, - }; - let origin_in_domains = domains.values().any(|v| v.as_string() == Some(origin_url)); - let bc_in_domains = domains - .values() - .any(|v| v.as_string() == Some(bc_origin_url.as_str())); - if !origin_in_domains && !bc_in_domains { + if !in_array( + PhpMixed::String(origin_url.to_string()), + &gitlab_domains, + true, + ) && !in_array( + PhpMixed::String(bc_origin_url.clone()), + &gitlab_domains, + true, + ) { return false; } diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index ab4768a4..1f00eaeb 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -10,12 +10,14 @@ use shirabe_external_packages::composer::pcre::{CaptureKey, Preg}; use shirabe_external_packages::seld::signal::SignalHandler; use shirabe_external_packages::symfony::process::ExecutableFinder; use shirabe_external_packages::symfony::process::Process; +use shirabe_external_packages::symfony::process::ProcessMock; use shirabe_external_packages::symfony::process::exception::ProcessSignaledException; use shirabe_external_packages::symfony::process::exception::RuntimeException as SymfonyProcessRuntimeException; use shirabe_php_shim::{ - LogicException, PHP_EOL, PhpMixed, array_intersect, array_map, call_user_func, escapeshellarg, - explode, implode, in_array, is_array, is_dir, is_numeric, is_string, php_regex, rtrim, sprintf, - str_replace, strcspn, strlen, strpbrk, strtolower, strtr_array, substr_replace, trim, + LogicException, PHP_EOL, PhpMixed, RuntimeException, array_intersect, array_map, + call_user_func, escapeshellarg, explode, implode, in_array, is_array, is_dir, is_numeric, + is_string, php_regex, rtrim, sprintf, str_replace, strcspn, strlen, strpbrk, strtolower, + strtr_array, substr_replace, trim, }; use std::sync::{LazyLock, Mutex}; @@ -41,8 +43,9 @@ pub struct ProcessExecutor { /// @var bool allow_async: bool, /// Test-only mock state. `None` in production; set via [`ProcessExecutor::__expects`] in tests. - /// Mirrors `composer/tests/Composer/Test/Mock/ProcessExecutorMock.php`. - mock: Option<ProcessExecutorMockState>, + /// Mirrors `composer/tests/Composer/Test/Mock/ProcessExecutorMock.php`. Wrapped in a `RefCell` + /// so `execute_async`'s `&self` receiver can still consume the expectation queue. + mock: Option<std::cell::RefCell<ProcessExecutorMockState>>, } /// Test-only state for the ProcessExecutorMock behaviour (cf. @@ -154,7 +157,16 @@ impl ProcessExecutor { /// Convenience wrapper used by phase-A code that calls /// `process.execute(&[String], &mut String, Option<&str>) == 0`. - /// Forwards to `execute`, returning the status code (0 on Err for compatibility). + /// Forwards to `execute`, returning the status code (1 on Err for compatibility) — this + /// mirrors PHP call sites that check the `int` return of `execute()` without a surrounding + /// `try`/`catch`, where an uncaught mock-mismatch exception would otherwise propagate. + // TODO(phase-d): under a strict `ProcessExecutorMock`, an incomplete expectation list now + // surfaces here as a swallowed "exit code 1" instead of the old `panic!`, so a future test + // ported through this call site could silently take a wrong branch instead of failing loudly. + // `ProcessExecutorMockGuard::__assert_complete` still catches unconsumed expectations at + // scope exit, but not a mismatch that happened to consume nothing. Distinguishing "expectation + // mismatch" from "real process failure" here would need a marker type incompatible with + // `RuntimeException` (see `mock_match`'s doc comment) — deferred until a concrete test needs it. pub fn execute_args( &mut self, command: &[String], @@ -372,26 +384,22 @@ impl ProcessExecutor { .unwrap_or(0)) } - /// Mock replacement for `do_execute` when [`Self::mock`] is set (cf. - /// `ProcessExecutorMock::doExecute`). Logs the command, matches it against the head of the - /// expectation queue (exact `===`), pops on match (firing the optional callback), falls back to - /// the default handler in non-strict mode, or panics in strict mode. Emits stdout/stderr through - /// the output target and records `error_output`. - fn mock_do_execute<'o, O>( - &mut self, - command: PhpMixed, + /// Shared expectation-matching logic behind the mock branches of `do_execute` and + /// `execute_async` (cf. `ProcessExecutorMock::doExecute`). Logs the command, matches it + /// against the head of the expectation queue (exact `===`), pops on match (firing the + /// optional callback), falls back to the default handler in non-strict mode, or returns an + /// error in strict mode (cf. PHPUnit's `AssertionFailedError`, itself a catchable + /// `\RuntimeException` — callers such as `Git::get_mirror_default_branch` rely on being able + /// to catch a strict-mode mismatch rather than have it abort the process). Returns `(stdout, + /// stderr, return)`. Takes `&self`: the expectation queue is wrapped in a `RefCell` so + /// `execute_async`'s `&self` receiver can still consume it. + fn mock_match( + &self, + command: &PhpMixed, cwd: Option<&str>, - output: O, - ) -> anyhow::Result<i64> - where - O: IntoExecOutput<'o>, - { - let capture_output = output.capture_output(); - self.capture_output = capture_output; - self.error_output = String::new(); - - let command_string = if is_array(&command) { - match &command { + ) -> anyhow::Result<(String, String, i64)> { + let command_string = if is_array(command) { + match command { PhpMixed::List(l) => implode( " ", &l.iter() @@ -410,24 +418,23 @@ impl ProcessExecutor { command.as_string().unwrap_or("").to_string() }; - let mock = self.mock.as_mut().unwrap(); + let mut mock = self.mock.as_ref().unwrap().borrow_mut(); mock.log.push(command_string.clone()); let matched = mock .expectations .as_ref() - .map(|exps| !exps.is_empty() && exps[0].cmd == command) + .map(|exps| !exps.is_empty() && exps[0].cmd == *command) .unwrap_or(false); let (stdout, stderr, r#return); + let mut callback = None; if matched { let mut expect = mock.expectations.as_mut().unwrap().remove(0); stdout = expect.stdout.clone(); stderr = expect.stderr.clone(); r#return = expect.r#return; - if let Some(callback) = expect.callback.as_mut() { - callback(); - } + callback = expect.callback.take(); } else if !mock.strict { stdout = mock.default_handler.stdout.clone(); stderr = mock.default_handler.stderr.clone(); @@ -440,18 +447,57 @@ impl ProcessExecutor { .map(|exps| format!("Expected {:?} at this point.", exps[0].cmd)) .unwrap_or_else(|| "Expected no more calls at this point.".to_string()); let received = mock.log[..mock.log.len().saturating_sub(1)].join(PHP_EOL); - panic!( - "Received unexpected command {:?} in \"{}\"{}{}{}Received calls:{}{}", - command, - cwd.unwrap_or(""), - PHP_EOL, - expected, - PHP_EOL, - PHP_EOL, - received - ); + // PHPUnit's `AssertionFailedError` (thrown by `ProcessExecutorMock::doExecute` on a + // strict-mode mismatch) extends `\RuntimeException`, so PHP call sites that + // `catch (\RuntimeException $e)` around a mock-driven git/hg/svn call (e.g. + // `GitDriver::supports`) treat a mismatch as an ordinary recoverable failure. Using + // the same `RuntimeException` type here keeps `downcast_ref::<RuntimeException>()` + // checks working the same way against a mismatch. + return Err(RuntimeException { + message: format!( + "Received unexpected command {:?} in \"{}\"{}{}{}Received calls:{}{}", + command, + cwd.unwrap_or(""), + PHP_EOL, + expected, + PHP_EOL, + PHP_EOL, + received + ), + code: 0, + } + .into()); + } + + // Release the RefMut before firing the callback: a callback that re-enters the same + // executor (e.g. via a cloned `Rc<RefCell<ProcessExecutor>>`) would otherwise hit + // `already mutably borrowed` here even before reaching the outer RefCell. + drop(mock); + if let Some(mut callback) = callback { + callback(); } + Ok((stdout, stderr, r#return)) + } + + /// Mock replacement for `do_execute` when [`Self::mock`] is set (cf. + /// `ProcessExecutorMock::doExecute`). Delegates the matching to [`Self::mock_match`], then + /// emits stdout/stderr through the output target and records `error_output`. + fn mock_do_execute<'o, O>( + &mut self, + command: PhpMixed, + cwd: Option<&str>, + output: O, + ) -> anyhow::Result<i64> + where + O: IntoExecOutput<'o>, + { + let capture_output = output.capture_output(); + self.capture_output = capture_output; + self.error_output = String::new(); + + let (stdout, stderr, r#return) = self.mock_match(&command, cwd)?; + // Feed stdout/stderr through the output target, mirroring the PHP `$callback(...)` calls. match output.to_callback() { Ok(mut callback) => { @@ -489,12 +535,12 @@ impl ProcessExecutor { strict: bool, default_handler: MockHandler, ) { - self.mock = Some(ProcessExecutorMockState { + self.mock = Some(std::cell::RefCell::new(ProcessExecutorMockState { expectations: Some(expectations), strict, default_handler, log: Vec::new(), - }); + })); } /// For testing only. Asserts all configured expectations were consumed (cf. @@ -503,6 +549,7 @@ impl ProcessExecutor { let Some(mock) = self.mock.as_ref() else { return; }; + let mock = mock.borrow(); // Not configured to expect anything, so no need to react here. let Some(expectations) = mock.expectations.as_ref() else { return; @@ -548,13 +595,18 @@ impl ProcessExecutor { /// starts a process on the commandline in async mode /// - /// Returns a future that does NOT borrow the executor: everything it needs is captured up + /// The returned future does NOT borrow the executor: everything it needs is captured up /// front, so callers can drop their `Ref`/`RefMut` on the shared `Rc<RefCell<ProcessExecutor>>` - /// before awaiting (`let fut = pe.borrow().execute_async(...); fut.await`). Holding a borrow - /// across the await would panic as soon as a sibling future or a sync `execute()` call touches - /// the same executor. The max_jobs throttle is enforced by the semaphore. + /// before awaiting (`let fut = pe.borrow_mut().execute_async(...); fut.await`). Holding a + /// borrow across the await would panic as soon as a sibling future or a sync `execute()` call + /// touches the same executor. The max_jobs throttle is enforced by the semaphore. + /// + /// Takes `&mut self` (unlike the `&self` used while this only read `self.mock`) so the mock + /// branch can update `error_output`/`capture_output` before returning, mirroring + /// `ProcessExecutorMock::executeAsync` — which resolves through the same `doExecute` the sync + /// path uses, so it updates the executor's cached error output too (cf. `mock_do_execute`). pub fn execute_async<C>( - &self, + &mut self, command: C, cwd: Option<&str>, ) -> std::pin::Pin<Box<dyn std::future::Future<Output = anyhow::Result<Process>>>> @@ -564,11 +616,22 @@ impl ProcessExecutor { let command = command.into_exec_command(); if self.mock.is_some() { // PHP resolves the promise with a Process mock whose getOutput/isSuccessful/getExitCode - // reflect the doExecute result. We cannot fabricate a Process mock here: Process has no - // test seam in the external-packages crate (out of scope to modify). No portable test - // currently exercises the async mock path, so leave it unimplemented rather than - // returning a misleading Process. - todo!("ProcessExecutorMock async path needs a Process mock seam in external-packages"); + // reflect the doExecute result; reuse the same expectation matching as the sync mock + // branch and fabricate the resolved Process via `Process::__mock`, a test seam mirroring + // `ZipArchive::__mock`. + let matched = self.mock_match(&command, cwd); + if let Ok((_, ref stderr, _)) = matched { + self.capture_output = true; + self.error_output = stderr.clone(); + } + return Box::pin(async move { + let (stdout, stderr, r#return) = matched?; + Ok(Process::__mock(ProcessMock { + exit_code: r#return, + stdout, + stderr, + })) + }); } let allow_async = self.allow_async; let semaphore = self.semaphore.clone(); diff --git a/crates/shirabe/tests/command/init_command_test.rs b/crates/shirabe/tests/command/init_command_test.rs index 9c52dddd..57777e35 100644 --- a/crates/shirabe/tests/command/init_command_test.rs +++ b/crates/shirabe/tests/command/init_command_test.rs @@ -1,9 +1,10 @@ //! ref: composer/tests/Composer/Test/Command/InitCommandTest.php -use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use crate::test_case::{RestoreEnv, RunOptions, get_application_tester, init_temp_composer}; use serial_test::serial; use shirabe::command::init_command::InitCommand; use shirabe::json::JsonFile; +use shirabe::util::platform::Platform; use shirabe_php_shim::{PHP_SERVER, PhpMixed}; use tempfile::TempDir; @@ -597,13 +598,27 @@ fn test_format_authors() { assert_eq!(expected, authors[0]); } +/// ref: InitCommandTest::testGetGitConfig. +/// +/// Composer's own CI runs `git config --global user.name/user.email` before the test suite so a +/// global config is guaranteed to exist; here `HOME` is pointed at a throwaway directory carrying +/// its own `.gitconfig` with those keys so the test doesn't depend on (or mutate) the real user's +/// global git config. #[test] -#[ignore = "requires the host's global git config to have user.name/user.email set (Composer's \ - own CI runs `git config --global user.name/user.email` before the test suite); \ - fails in environments without that global config"] +#[serial] fn test_get_git_config() { set_up(); + let home = TempDir::new().unwrap(); + std::fs::write( + home.path().join(".gitconfig"), + "[user]\n\tname = Test User\n\temail = test-user@example.com\n", + ) + .unwrap(); + let original_home = Platform::get_env("HOME"); + Platform::put_env("HOME", &home.path().to_string_lossy()); + let _restore_home = RestoreEnv::new("HOME", original_home); + let command = InitCommand::new(); let git_config = command.__get_git_config(); assert!(git_config.contains_key("user.name")); diff --git a/crates/shirabe/tests/common/test_case.rs b/crates/shirabe/tests/common/test_case.rs index 6c3bb5ad..7f8e1d54 100644 --- a/crates/shirabe/tests/common/test_case.rs +++ b/crates/shirabe/tests/common/test_case.rs @@ -16,6 +16,7 @@ use shirabe::package::handle::{ CompleteAliasPackageHandle, CompletePackageHandle, PackageInterfaceHandle, }; use shirabe::repository::{InstalledFilesystemRepository, WritableRepositoryInterface}; +use shirabe::util::Git as GitUtil; use shirabe::util::http_downloader::HttpDownloader; use shirabe::util::r#loop::Loop; use shirabe::util::platform::Platform; @@ -168,6 +169,46 @@ pub fn init_temp_composer( } } +/// For testing only. Resets the cached `GitUtil` version static on drop, mirroring the +/// `ReflectionProperty(GitUtil::class, 'version')->setValue(null, false)` reset done in +/// `VersionGuesserTest`'s setUp/tearDown. `GitUtil::VERSION` is shared process-wide, so any test +/// that seeds it via `GitUtil::__set_version` should hold one of these (and be `#[serial]`, since +/// `#[serial]` only excludes other `#[serial]` tests from running concurrently). +pub struct GitVersionGuard; + +impl Drop for GitVersionGuard { + fn drop(&mut self) { + GitUtil::__reset_version(); + } +} + +/// Restores an environment variable to its prior value (or clears it if it was unset) once +/// dropped. Pair with `Platform::get_env(name)` captured before the override: +/// ```ignore +/// let original = Platform::get_env("HOME"); +/// Platform::put_env("HOME", "/tmp/fake-home"); +/// let _restore = RestoreEnv::new("HOME", original); +/// ``` +pub struct RestoreEnv { + name: &'static str, + original: Option<String>, +} + +impl RestoreEnv { + pub fn new(name: &'static str, original: Option<String>) -> Self { + Self { name, original } + } +} + +impl Drop for RestoreEnv { + fn drop(&mut self) { + match &self.original { + Some(value) => Platform::put_env(self.name, value), + None => Platform::clear_env(self.name), + } + } +} + fn null_io() -> std::rc::Rc<std::cell::RefCell<dyn IOInterface>> { std::rc::Rc::new(std::cell::RefCell::new(NullIO::new())) } diff --git a/crates/shirabe/tests/downloader/zip_downloader_test.rs b/crates/shirabe/tests/downloader/zip_downloader_test.rs index 593f3e76..338b2b54 100644 --- a/crates/shirabe/tests/downloader/zip_downloader_test.rs +++ b/crates/shirabe/tests/downloader/zip_downloader_test.rs @@ -14,6 +14,7 @@ use shirabe::util::HttpDownloader; use shirabe::util::ProcessExecutor; use shirabe::util::filesystem::Filesystem; use shirabe::util::r#loop::Loop; +use shirabe::util::process_executor::MockHandler; use shirabe_php_shim::{PhpMixed, ZipArchive, ZipArchiveMock}; use shirabe_semver::VersionParser; use tempfile::TempDir; @@ -87,10 +88,17 @@ impl Drop for TearDown { } fn make_downloader(set_up: &SetUp) -> ZipDownloader { - let filesystem = std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None))); let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some( set_up.io.clone(), )))); + make_downloader_with_process(set_up, process) +} + +fn make_downloader_with_process( + set_up: &SetUp, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, +) -> ZipDownloader { + let filesystem = std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(None))); ZipDownloader::new( set_up.io.clone(), set_up.config.clone(), @@ -102,11 +110,6 @@ fn make_downloader(set_up: &SetUp) -> ZipDownloader { ) } -// The system-unzip / non-windows-fallback paths route through ProcessExecutor::execute_async, whose -// mock branch is an unimplemented todo!() (no Process mock seam exists in the external-packages -// crate). The PHP tests below mock Process/ProcessExecutor::executeAsync, which is not reproducible -// here, so they remain ignored. -// // testErrorMessages drives a real HttpDownloader + Loop, but RemoteFilesystem::get_remote_contents // is a phase-c stub returning None, so the file:// dist download fails before the ZipArchive path. @@ -266,38 +269,146 @@ fn test_zip_archive_only_good() { result.expect("extract should succeed"); } -#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] +// setPrivateProperty('unzipCommands', [['unzip', 'unzip -qq %s -d %s']]) in PHP: a single +// two-element commandSpec (executable name, then one literal arg string that contains %s +// placeholders rather than %file%/%path%, so it is passed through to executeAsync verbatim). The +// PHPUnit test fully replaces $processExecutor, so the exact command content is never asserted on; +// this only needs to be non-empty so extractWithSystemUnzip proceeds past the "no commands" +// short-circuit into ZipDownloader::extract_with_zip_archive. +fn unzip_command_spec() -> Vec<Vec<String>> { + vec![vec!["unzip".to_string(), "unzip -qq %s -d %s".to_string()]] +} + #[test] +#[serial] fn test_system_unzip_only_failed() { - let _ = set_up(); - // TODO(phase-d): routes through ProcessExecutor::execute_async, whose mock branch is - // todo!() (no Process mock seam exists in the external-packages crate). - todo!() + let set_up = set_up(); + let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); + + ZipDownloader::__set_is_windows(Some(false)); + ZipDownloader::__set_has_zip_archive(Some(false)); + ZipDownloader::__set_unzip_commands(Some(unzip_command_spec())); + + let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))); + process.borrow_mut().__expects( + vec![], + false, + MockHandler { + r#return: 1, + stdout: String::new(), + stderr: "output".to_string(), + }, + ); + let downloader = make_downloader_with_process(&set_up, process); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + let e = result.expect_err("expected RuntimeException"); + assert!( + e.to_string() + .contains("Failed to extract test/pkg: (1) unzip"), + "got: {e}" + ); } -#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] +#[serial] fn test_system_unzip_only_good() { - let _ = set_up(); - // TODO(phase-d): routes through ProcessExecutor::execute_async, whose mock branch is - // todo!() (no Process mock seam exists in the external-packages crate). - todo!() + let set_up = set_up(); + let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); + + ZipDownloader::__set_is_windows(Some(false)); + ZipDownloader::__set_has_zip_archive(Some(false)); + ZipDownloader::__set_unzip_commands(Some(unzip_command_spec())); + + let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))); + process.borrow_mut().__expects( + vec![], + false, + MockHandler { + r#return: 0, + stdout: String::new(), + stderr: "output".to_string(), + }, + ); + let downloader = make_downloader_with_process(&set_up, process); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + result.expect("extract should succeed"); } -#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] +#[serial] fn test_non_windows_fallback_good() { - let _ = set_up(); - // TODO(phase-d): routes through ProcessExecutor::execute_async, whose mock branch is - // todo!() (no Process mock seam exists in the external-packages crate). - todo!() + let set_up = set_up(); + let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); + + ZipDownloader::__set_is_windows(Some(false)); + ZipDownloader::__set_has_zip_archive(Some(true)); + ZipDownloader::__set_unzip_commands(Some(unzip_command_spec())); + + let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))); + process.borrow_mut().__expects( + vec![], + false, + MockHandler { + r#return: 1, + stdout: String::new(), + stderr: "output".to_string(), + }, + ); + let downloader = make_downloader_with_process(&set_up, process); + let zip_archive = ZipArchive::__mock(ZipArchiveMock { + open: Ok(()), + count: 0, + extract_to: Ok(true), + }); + downloader.__set_zip_archive_object(Some(zip_archive)); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + result.expect("extract should succeed"); } -#[ignore = "routes through ProcessExecutor::execute_async whose mock branch is todo!() (no Process mock seam in external-packages)"] #[test] +#[serial] fn test_non_windows_fallback_failed() { - let _ = set_up(); - // TODO(phase-d): routes through ProcessExecutor::execute_async, whose mock branch is - // todo!() (no Process mock seam exists in the external-packages crate). - todo!() + let set_up = set_up(); + let _tear_down = TearDown::new(set_up.test_dir.path().to_path_buf()); + + ZipDownloader::__set_is_windows(Some(false)); + ZipDownloader::__set_has_zip_archive(Some(true)); + ZipDownloader::__set_unzip_commands(Some(unzip_command_spec())); + + let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))); + process.borrow_mut().__expects( + vec![], + false, + MockHandler { + r#return: 1, + stdout: String::new(), + stderr: "output".to_string(), + }, + ); + let downloader = make_downloader_with_process(&set_up, process); + let zip_archive = ZipArchive::__mock(ZipArchiveMock { + open: Ok(()), + count: 0, + extract_to: Ok(false), + }); + downloader.__set_zip_archive_object(Some(zip_archive)); + + let filename = set_up.filename.to_string_lossy().into_owned(); + let result = run(downloader.extract(set_up.package.clone(), &filename, "vendor/dir")); + + let e = result.expect_err("expected RuntimeException"); + assert!( + e.to_string() + .contains("There was an error extracting the ZIP file"), + "got: {e}" + ); } diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs index 2c38ae6a..0f392d7e 100644 --- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs +++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs @@ -405,10 +405,34 @@ fn test_dispatcher_outputs_command() { } #[test] -#[ignore = "uses an unmocked ProcessExecutor running a real `exit 1`; depends on real shell execution"] +#[serial] fn test_dispatcher_outputs_error_on_failed_command() { let _tear_down = TearDown; - // TODO(phase-d): uses an unmocked ProcessExecutor running a real `exit 1`; depends on real - // shell execution - todo!() + + let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))); + let composer = create_composer_instance(); + let io = std::rc::Rc::new(std::cell::RefCell::new( + BufferIO::new(String::new(), output_interface::VERBOSITY_NORMAL, None).unwrap(), + )); + let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone(); + + let code = "exit 1"; + let mut dispatcher = + dispatcher_with_listeners(&composer, io_dyn, process, listeners_const(vec![code])); + + let result = dispatcher.dispatch_script( + ScriptEvents::POST_INSTALL_CMD, + false, + vec![], + IndexMap::new(), + ); + + let e = result.expect_err("expected ScriptExecutionException"); + assert!(e.to_string().contains("Error Output: "), "got: {e}"); + + let expected = format!( + "> exit 1{eol}Script exit 1 handling the post-install-cmd event returned with error code 1{eol}", + eol = PHP_EOL + ); + assert_eq!(expected, io.borrow().get_output()); } diff --git a/crates/shirabe/tests/package/loader/root_package_loader_test.rs b/crates/shirabe/tests/package/loader/root_package_loader_test.rs index 069cf5a7..248c4e6a 100644 --- a/crates/shirabe/tests/package/loader/root_package_loader_test.rs +++ b/crates/shirabe/tests/package/loader/root_package_loader_test.rs @@ -5,6 +5,7 @@ // look-around regex the regex crate cannot compile. use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock}; +use crate::test_case::GitVersionGuard; use indexmap::IndexMap; use serial_test::serial; use shirabe::config::Config; @@ -72,16 +73,6 @@ fn require_map(entries: &[(&str, &str)]) -> PhpMixed { PhpMixed::Array(m) } -// Resets the cached git `version` static on drop so a seeded value does not leak into other -// tests in this binary (VersionGuesserTest seeds/resets the same static). -struct GitVersionGuard; - -impl Drop for GitVersionGuard { - fn drop(&mut self) { - GitUtil::__reset_version(); - } -} - // A test double for the concrete VersionGuesser, supplied through the VersionGuesserInterface seam. mockall::mock! { #[derive(Debug)] @@ -271,7 +262,6 @@ fn test_pretty_version_for_root_package_in_version_branch() { } #[test] -#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"] #[serial] fn test_feature_branch_pretty_version() { // proc_open() is always available; the PHP markTestSkipped guard does not apply here. diff --git a/crates/shirabe/tests/package/version/version_guesser_test.rs b/crates/shirabe/tests/package/version/version_guesser_test.rs index 92d5e90b..dd71d8ab 100644 --- a/crates/shirabe/tests/package/version/version_guesser_test.rs +++ b/crates/shirabe/tests/package/version/version_guesser_test.rs @@ -185,7 +185,6 @@ fn test_guess_version_does_not_see_custom_default_branch_as_non_feature_branch() assert_eq!(another_commit_hash, version_data.commit.unwrap()); } -#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"] #[test] #[serial] fn test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming() { @@ -239,7 +238,6 @@ fn test_guess_version_reads_and_respects_non_feature_branches_configuration_for_ ); } -#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"] #[test] #[serial] fn test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming_regex() @@ -560,7 +558,6 @@ fn test_numeric_branches_show_nicely() { assert_eq!("1.5.9999999.9999999-dev", version_data.version.unwrap()); } -#[ignore = "remote-branch feature guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"] #[test] #[serial] fn test_remote_branches_are_selected() { diff --git a/crates/shirabe/tests/repository/vcs/github_driver_test.rs b/crates/shirabe/tests/repository/vcs/github_driver_test.rs index 5b8367c7..8b30a796 100644 --- a/crates/shirabe/tests/repository/vcs/github_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/github_driver_test.rs @@ -5,12 +5,15 @@ use crate::io_stub::IOStub; use crate::process_executor_mock::{ ProcessExecutorMockGuard, cmd, cmd_full, get_process_executor_mock, }; +use crate::test_case::GitVersionGuard; use indexmap::IndexMap; +use serial_test::serial; use shirabe::config::Config; use shirabe::config::ConfigSourceInterface; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::repository::vcs::GitHubDriver; +use shirabe::util::Git as GitUtil; use shirabe::util::filesystem::Filesystem; use shirabe::util::http_downloader::HttpDownloaderMockHandler; use shirabe::util::process_executor::{MockHandler, ProcessExecutor}; @@ -681,9 +684,17 @@ fn test_public_repository_archived() { assert_eq!(Some(true), data.get("abandoned").and_then(|v| v.as_bool())); } +// GitDriver::initialize calls GitUtil::cleanEnv, which calls GitUtil::getVersion; PHP's +// `GitUtil::$version` is a class-level static that persists for the whole PHPUnit run, so by the +// time this test runs it has already been populated (as a side effect of some earlier-run test +// invoking the real `git --version`) and the mock expectation list below never needs to include it. +// Rust test execution order isn't guaranteed the same way, so the cache is seeded explicitly here. #[test] -#[ignore = "GitDriver clone-fallback path runs an unexpected `git --version` (Git::get_version) not in the PHP mock expectation list; needs the version static seeded and the Rust sync_mirror command sequence to match"] +#[serial] fn test_private_repository_no_interaction() { + GitUtil::__set_version(Some("2.52.0".to_string())); + let _git_guard = GitVersionGuard; + let SetUp { home, config } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); |
