diff options
Diffstat (limited to 'crates/shirabe/src')
| -rw-r--r-- | crates/shirabe/src/command/archive_command.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/src/factory.rs | 124 | ||||
| -rw-r--r-- | crates/shirabe/src/installer/installation_manager.rs | 148 | ||||
| -rw-r--r-- | crates/shirabe/src/package/version/version_guesser.rs | 24 | ||||
| -rw-r--r-- | crates/shirabe/src/repository/installed_filesystem_repository.rs | 26 |
5 files changed, 313 insertions, 11 deletions
diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs index 91e8036..ce1d496 100644 --- a/crates/shirabe/src/command/archive_command.rs +++ b/crates/shirabe/src/command/archive_command.rs @@ -288,7 +288,7 @@ impl ArchiveCommand { composer_archive_manager_ref = composer_archive_manager.borrow_mut(); &mut *composer_archive_manager_ref } else { - let factory = Factory; + let factory = Factory::default(); let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))); let http_downloader = std::rc::Rc::new(std::cell::RefCell::new( Factory::create_http_downloader(io.clone(), config, indexmap::IndexMap::new())?, diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index b3d478c..dcbdf5d 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -85,7 +85,12 @@ impl DisablePlugins { } /// Creates a configured instance of composer. -pub struct Factory; +#[derive(Default)] +pub struct Factory { + /// For testing only: when true, the overridable `create_*`/`add_*` hooks behave like + /// `Composer\Test\Mock\FactoryMock`. `false` in production. + mock: bool, +} impl Factory { fn get_home_dir() -> anyhow::Result<String> { @@ -501,7 +506,11 @@ impl Factory { } // Load config and override with local config/auth config - let mut config = Self::create_config(Some(io.clone()), Some(&cwd))?; + let mut config = if self.mock { + Self::__create_config_mock(Some(&cwd))? + } else { + Self::create_config(Some(io.clone()), Some(&cwd))? + }; let is_global = local_config_source != Config::SOURCE_UNKNOWN && realpath(&config.get_str("home")?) == realpath(&dirname(&local_config_source)); config.merge(&local_config_data, &local_config_source); @@ -653,12 +662,22 @@ impl Factory { // load package let parser = VersionParser::new(); - let guesser = VersionGuesser::new( - config.clone(), - process.clone(), - parser.clone(), - Some(io.clone()), - ); + // FactoryMock::loadRootPackage swaps in a VersionGuesserMock (guessVersion returns null). + let guesser = if self.mock { + VersionGuesser::__new_mock( + config.clone(), + process.clone(), + parser.clone(), + Some(io.clone()), + ) + } else { + VersionGuesser::new( + config.clone(), + process.clone(), + parser.clone(), + Some(io.clone()), + ) + }; let mut loader = self.load_root_package( rm.clone(), config.clone(), @@ -890,7 +909,7 @@ impl Factory { disable_plugins: DisablePlugins, disable_scripts: bool, ) -> Option<PartialComposerHandle> { - let factory = Self; + let factory = Self::default(); let config = Self::create_config(Some(io.clone()), None).ok()?; factory.create_global_composer(io, &config, disable_plugins, disable_scripts, true) @@ -904,6 +923,15 @@ impl Factory { root_package: RootPackageInterfaceHandle, process: Option<&std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, ) { + // FactoryMock::addLocalRepository installs a bare InstalledArrayRepository instead. + if self.mock { + rm.set_local_repository(crate::repository::RepositoryInterfaceHandle::new( + crate::repository::InstalledArrayRepository::new() + .expect("InstalledArrayRepository::new should not fail"), + )); + return; + } + let fs = process .map(|p| std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(Some(p.clone()))))); @@ -1210,6 +1238,10 @@ impl Factory { io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, ) -> InstallationManager { + // FactoryMock::createInstallationManager returns a recording InstallationManagerMock. + if self.mock { + return InstallationManager::__new_mock(r#loop, io, event_dispatcher); + } InstallationManager::new(r#loop, io, event_dispatcher) } @@ -1220,6 +1252,11 @@ impl Factory { io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, process: Option<&std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>, ) { + // FactoryMock::createDefaultInstallers is a noop (no installers registered). + if self.mock { + return; + } + let fs = std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new( process.map(std::rc::Rc::clone), ))); @@ -1307,7 +1344,7 @@ impl Factory { disable_plugins: DisablePlugins, disable_scripts: bool, ) -> anyhow::Result<ComposerHandle> { - let factory = Self; + let factory = Self::default(); // for BC reasons, if a config is passed in either as array or a path that is not the default composer.json path // we disable local plugins as they really should not be loaded from CWD @@ -1338,6 +1375,73 @@ impl Factory { }) } + /// For testing only: equivalent of `Composer\Test\Mock\FactoryMock::create`. Builds the Composer + /// the same way as `create`, but with the FactoryMock overrides enabled (see the `self.mock` + /// branches in `create_composer`/`add_local_repository`/`create_installation_manager`/ + /// `create_default_installers`). + pub fn __create_mock( + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + config: Option<LocalConfigInput>, + disable_plugins: DisablePlugins, + disable_scripts: bool, + ) -> anyhow::Result<ComposerHandle> { + let factory = Self { mock: true }; + + let default_composer_file = Self::get_composer_file()?; + let config_is_default = matches!( + config.as_ref(), + Some(LocalConfigInput::Path(p)) if *p == default_composer_file + ); + let disable_plugins = if config.is_some() + && !config_is_default + && matches!(disable_plugins, DisablePlugins::None) + { + DisablePlugins::Local + } else { + disable_plugins + }; + + let composer = + factory.create_composer(io, config, disable_plugins, None, true, disable_scripts)?; + composer.as_full().ok_or_else(|| { + anyhow::anyhow!(RuntimeException { + message: "Composer expected with fullLoad=true".to_string(), + code: 0, + }) + }) + } + + /// For testing only: equivalent of `FactoryMock::createConfig`. Builds a `Config` with a unique + /// temp home and packagist disabled, without loading the global config/auth files. + fn __create_config_mock(cwd: Option<&str>) -> anyhow::Result<Config> { + use std::sync::atomic::{AtomicU64, Ordering}; + static NEXT: AtomicU64 = AtomicU64::new(0); + + let mut config = Config::new(true, cwd.map(|s| s.to_string())); + + let home = std::env::temp_dir().join(format!( + "shirabe-test-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + let _ = std::fs::create_dir_all(&home); + + let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new(); + config_section.insert( + "home".to_string(), + PhpMixed::String(home.to_string_lossy().into_owned()), + ); + let mut repositories: IndexMap<String, PhpMixed> = IndexMap::new(); + repositories.insert("packagist".to_string(), PhpMixed::Bool(false)); + + let mut merge: IndexMap<String, PhpMixed> = IndexMap::new(); + merge.insert("config".to_string(), PhpMixed::Array(config_section)); + merge.insert("repositories".to_string(), PhpMixed::Array(repositories)); + config.merge(&merge, Config::SOURCE_UNKNOWN); + + Ok(config) + } + /// If you are calling this in a plugin, you probably should instead use `$composer->getLoop()->getHttpDownloader()` pub fn create_http_downloader( io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, 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); } diff --git a/crates/shirabe/src/package/version/version_guesser.rs b/crates/shirabe/src/package/version/version_guesser.rs index 8e8cc32..6fba7c4 100644 --- a/crates/shirabe/src/package/version/version_guesser.rs +++ b/crates/shirabe/src/package/version/version_guesser.rs @@ -49,6 +49,10 @@ pub struct VersionGuesser { /// @var IOInterface|null io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>, + + /// For testing only: when true, `guess_version` always returns `None`, mirroring + /// `Composer\Test\Mock\VersionGuesserMock`. `false` in production. + mock: bool, } /// PHP: @phpstan-type Version array{version, commit, pretty_version, feature_version?, feature_pretty_version?} @@ -73,6 +77,21 @@ impl VersionGuesser { process, version_parser, io, + mock: false, + } + } + + /// For testing only: builds a guesser whose `guess_version` always returns `None`, mirroring + /// `Composer\Test\Mock\VersionGuesserMock`. + pub fn __new_mock( + config: std::rc::Rc<std::cell::RefCell<Config>>, + process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>, + version_parser: VersionParser, + io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>, + ) -> Self { + Self { + mock: true, + ..Self::new(config, process, version_parser, io) } } @@ -85,6 +104,11 @@ impl VersionGuesser { package_config: &IndexMap<String, PhpMixed>, path: &str, ) -> Result<Option<VersionData>> { + // For testing only (ref VersionGuesserMock::guessVersion returns null). + if self.mock { + return Ok(None); + } + if !function_exists("proc_open") { return Ok(None); } diff --git a/crates/shirabe/src/repository/installed_filesystem_repository.rs b/crates/shirabe/src/repository/installed_filesystem_repository.rs index 3cd21eb..8ce07e4 100644 --- a/crates/shirabe/src/repository/installed_filesystem_repository.rs +++ b/crates/shirabe/src/repository/installed_filesystem_repository.rs @@ -19,6 +19,9 @@ use shirabe_semver::constraint::AnyConstraint; #[derive(Debug)] pub struct InstalledFilesystemRepository { inner: FilesystemRepository, + /// For testing only: when true, `reload` and `write` are no-ops, mirroring + /// `Composer\Test\Mock\InstalledFilesystemRepositoryMock`. `false` in production. + mock: bool, } impl InstalledFilesystemRepository { @@ -35,6 +38,21 @@ impl InstalledFilesystemRepository { root_package, filesystem, )?, + mock: false, + }) + } + + /// For testing only: builds a repository whose `reload`/`write` are no-ops, mirroring + /// `Composer\Test\Mock\InstalledFilesystemRepositoryMock`. + pub fn __new_mock( + repository_file: JsonFile, + dump_versions: bool, + root_package: Option<RootPackageInterfaceHandle>, + filesystem: Option<std::rc::Rc<std::cell::RefCell<Filesystem>>>, + ) -> Result<Self> { + Ok(Self { + mock: true, + ..Self::new(repository_file, dump_versions, root_package, filesystem)? }) } @@ -59,6 +77,10 @@ impl WritableRepositoryInterface for InstalledFilesystemRepository { dev_mode: bool, installation_manager: &mut crate::installer::InstallationManager, ) -> anyhow::Result<()> { + // For testing only (ref InstalledFilesystemRepositoryMock::write is a noop). + if self.mock { + return Ok(()); + } self.inner.write(dev_mode, installation_manager) } @@ -75,6 +97,10 @@ impl WritableRepositoryInterface for InstalledFilesystemRepository { } fn reload(&mut self) -> anyhow::Result<()> { + // For testing only (ref InstalledFilesystemRepositoryMock::reload is a noop). + if self.mock { + return Ok(()); + } self.inner.reload() } |
