diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-28 22:22:01 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-28 22:22:01 +0900 |
| commit | 1e72ee3ddaf581753e30b39dfc33b48d44a25ef7 (patch) | |
| tree | 65f92b778f853bf25b6d20080b369f9b91773c52 /crates/shirabe/src/installer/installation_manager.rs | |
| parent | 4728dbcddc1de54a2c8f2e90c8a1f80098485e22 (diff) | |
| download | php-shirabe-1e72ee3ddaf581753e30b39dfc33b48d44a25ef7.tar.gz php-shirabe-1e72ee3ddaf581753e30b39dfc33b48d44a25ef7.tar.zst php-shirabe-1e72ee3ddaf581753e30b39dfc33b48d44a25ef7.zip | |
test(installer): port InstallerTest unit and integration harness
Port composer/tests/Composer/Test/InstallerTest.php. testInstaller (the
provideInstaller cases) is fully ported and passes; the three integration
tests port doTestIntegration in full (the .test fixture loader, FactoryMock,
the in-process console Application with install/update commands, and the
PHPUnit assertStringMatchesFormat matcher) and remain #[ignore]'d since the
install pipeline is not yet executable end-to-end.
Add test-only `__`-seams to the concrete types the test depends on, since
their consumers (e.g. Locker takes the concrete InstallationManager) and the
subclass-style mocks have no trait to mock: InstallationManager (recording
mock + as_any), Factory (__create_mock), VersionGuesser, and
InstalledFilesystemRepository. The production path (mock: false) is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src/installer/installation_manager.rs')
| -rw-r--r-- | crates/shirabe/src/installer/installation_manager.rs | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index a2cc228..ea6f668 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -40,6 +40,19 @@ pub struct InstallationManager { io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, output_progress: bool, + /// For testing only: present iff this manager behaves like + /// `Composer\Test\Mock\InstallationManagerMock`, recording operations instead of executing + /// them. `None` in production. + mock: Option<InstallationManagerMockState>, +} + +/// For testing only: recorded operations for the `InstallationManagerMock` behavior. +#[derive(Debug, Default)] +struct InstallationManagerMockState { + installed: Vec<PackageInterfaceHandle>, + updated: Vec<(PackageInterfaceHandle, PackageInterfaceHandle)>, + uninstalled: Vec<PackageInterfaceHandle>, + trace: Vec<String>, } impl InstallationManager { @@ -56,9 +69,55 @@ impl InstallationManager { io, event_dispatcher, output_progress: false, + mock: None, } } + /// For testing only: builds a manager that records operations instead of executing them, + /// mirroring `Composer\Test\Mock\InstallationManagerMock`. + pub fn __new_mock( + loop_: std::rc::Rc<std::cell::RefCell<Loop>>, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, + ) -> Self { + Self { + mock: Some(InstallationManagerMockState::default()), + ..Self::new(loop_, io, event_dispatcher) + } + } + + /// For testing only: the trace of stringified operations recorded by the mock. + pub fn __get_trace(&self) -> Vec<String> { + self.mock + .as_ref() + .map(|m| m.trace.clone()) + .unwrap_or_default() + } + + /// For testing only: packages passed to install (and markAliasInstalled) operations. + pub fn __get_installed_packages(&self) -> Vec<PackageInterfaceHandle> { + self.mock + .as_ref() + .map(|m| m.installed.clone()) + .unwrap_or_default() + } + + /// For testing only: (initial, target) package pairs passed to update operations. + pub fn __get_updated_packages(&self) -> Vec<(PackageInterfaceHandle, PackageInterfaceHandle)> { + self.mock + .as_ref() + .map(|m| m.updated.clone()) + .unwrap_or_default() + } + + /// For testing only: packages passed to uninstall (and markAliasUninstalled) operations. + pub fn __get_uninstalled_packages(&self) -> Vec<PackageInterfaceHandle> { + self.mock + .as_ref() + .map(|m| m.uninstalled.clone()) + .unwrap_or_default() + } + pub fn reset(&mut self) { self.notifiable_packages = IndexMap::new(); FileDownloader::reset_download_metadata(); @@ -126,6 +185,11 @@ impl InstallationManager { repo: &dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> Result<bool> { + // For testing only (ref InstallationManagerMock::isPackageInstalled). + if self.mock.is_some() { + return Ok(repo.has_package(package)); + } + if let Some(alias) = package.as_alias() { let alias_of: PackageInterfaceHandle = alias.get_alias_of().into(); return Ok( @@ -165,6 +229,70 @@ impl InstallationManager { run_scripts: bool, download_only: bool, ) -> Result<()> { + // For testing only: the mock records each operation and mutates the repo directly, + // skipping the download step (ref InstallationManagerMock::execute). The alias operations' + // repo mutation is inlined (rather than calling mark_alias_*) so `self.mock` can stay + // borrowed across the loop without also borrowing `&self`. + if let Some(mock) = self.mock.as_mut() { + let _ = (dev_mode, run_scripts, download_only); + for operation in operations { + let trace = shirabe_php_shim::strip_tags(&operation.to_string()); + match operation.get_operation_type().as_str() { + "install" => { + let op = operation.as_install_operation().expect("install operation"); + let package = op.get_package(); + mock.installed.push(package.clone()); + mock.trace.push(trace); + repo.add_package(PackageInterfaceHandle::dup(&package)); + } + "update" => { + let op = operation.as_update_operation().expect("update operation"); + let initial = op.get_initial_package().clone(); + let target = op.get_target_package().clone(); + mock.updated.push((initial.clone(), target.clone())); + mock.trace.push(trace); + repo.remove_package(initial); + if !repo.has_package(target.clone()) { + repo.add_package(PackageInterfaceHandle::dup(&target)); + } + } + "uninstall" => { + let op = operation + .as_uninstall_operation() + .expect("uninstall operation"); + let package = op.get_package(); + mock.uninstalled.push(package.clone()); + mock.trace.push(trace); + repo.remove_package(package); + } + "markAliasInstalled" => { + let op = operation + .as_any() + .downcast_ref::<MarkAliasInstalledOperation>() + .expect("markAliasInstalled operation"); + let package = op.get_package(); + mock.installed.push(package.clone().into()); + mock.trace.push(trace); + if !repo.has_package(package.clone().into()) { + repo.add_package(PackageInterfaceHandle::dup(&package.into())); + } + } + "markAliasUninstalled" => { + let op = operation + .as_any() + .downcast_ref::<MarkAliasUninstalledOperation>() + .expect("markAliasUninstalled operation"); + let package = op.get_package(); + mock.uninstalled.push(package.clone().into()); + mock.trace.push(trace); + repo.remove_package(package.into()); + } + other => panic!("unknown operation type: {}", other), + } + } + return Ok(()); + } + // @var array<callable(): ?PromiseInterface<void|null>> $cleanupPromises let mut cleanup_promises: IndexMap< i64, @@ -623,6 +751,11 @@ impl InstallationManager { /// Returns the installation path of a package pub fn get_install_path(&mut self, package: PackageInterfaceHandle) -> Option<String> { + // For testing only (ref InstallationManagerMock::getInstallPath). + if self.mock.is_some() { + return Some(format!("vendor/{}", package.get_name())); + } + let installer = self.get_installer(&package.get_type()).ok()?; installer.get_install_path(package) @@ -633,6 +766,11 @@ impl InstallationManager { } pub fn notify_installs(&mut self, _io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) { + // For testing only (ref InstallationManagerMock::notifyInstalls is a noop). + if self.mock.is_some() { + return; + } + // TODO(phase-c-promise): PHP collects every http_downloader.add() promise and runs them via // Loop::wait; the single-threaded sync bridge block_on's each notification serially instead. let result: Result<()> = (|| -> Result<()> { @@ -792,6 +930,12 @@ impl InstallationManager { // plugins may swap in a replacement. The interface captures the methods reached through Composer's // accessor and through the `&mut dyn InstallationManagerInterface` references fed from it. pub trait InstallationManagerInterface: std::fmt::Debug { + /// For testing only: lets a test recover the concrete manager (e.g. the recording mock) from a + /// trait object returned by `Composer::get_installation_manager`. + fn as_any(&self) -> &dyn std::any::Any { + unimplemented!("as_any is only implemented for the concrete InstallationManager") + } + fn add_installer(&mut self, installer: Box<dyn InstallerInterface>); fn remove_installer(&mut self, installer: &dyn InstallerInterface); fn disable_plugins(&mut self); @@ -815,6 +959,10 @@ pub trait InstallationManagerInterface: std::fmt::Debug { } impl InstallationManagerInterface for InstallationManager { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn add_installer(&mut self, installer: Box<dyn InstallerInterface>) { self.add_installer(installer); } |
