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/factory.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/factory.rs')
| -rw-r--r-- | crates/shirabe/src/factory.rs | 124 |
1 files changed, 114 insertions, 10 deletions
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>>, |
