aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/installer/installation_manager.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-28 22:22:01 +0900
committernsfisis <nsfisis@gmail.com>2026-06-28 22:22:01 +0900
commit1e72ee3ddaf581753e30b39dfc33b48d44a25ef7 (patch)
tree65f92b778f853bf25b6d20080b369f9b91773c52 /crates/shirabe/src/installer/installation_manager.rs
parent4728dbcddc1de54a2c8f2e90c8a1f80098485e22 (diff)
downloadphp-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.rs148
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);
}