aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/downloader/zip_downloader.rs6
-rw-r--r--crates/shirabe/src/package/version/version_guesser.rs5
-rw-r--r--crates/shirabe/src/util/filesystem.rs2
-rw-r--r--crates/shirabe/src/util/git.rs6
-rw-r--r--crates/shirabe/src/util/github.rs13
-rw-r--r--crates/shirabe/src/util/gitlab.rs20
-rw-r--r--crates/shirabe/src/util/process_executor.rs167
-rw-r--r--crates/shirabe/tests/command/init_command_test.rs23
-rw-r--r--crates/shirabe/tests/common/test_case.rs41
-rw-r--r--crates/shirabe/tests/downloader/zip_downloader_test.rs163
-rw-r--r--crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs32
-rw-r--r--crates/shirabe/tests/package/loader/root_package_loader_test.rs12
-rw-r--r--crates/shirabe/tests/package/version/version_guesser_test.rs3
-rw-r--r--crates/shirabe/tests/repository/vcs/github_driver_test.rs13
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());