From d4cdccb8de8758bd46a12283f8df90e020327b99 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 25 Jun 2026 15:24:55 +0900 Subject: test: port 70 perforce/repository/downloader/installer/dispatcher tests Port perforce (36), locker (10), composer_repository (7), installation_manager (6), file_downloader (5), and event_dispatcher (6) tests via the mock infra. Fix production porting bugs surfaced en route: BufferIO::get_output look-behind regex, ComposerRepository list-form package iteration and initialize dispatch, gethostname and spl_autoload_functions shims; add EventDispatcher get_listeners test seam. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/event_dispatcher/event_dispatcher.rs | 30 ++++++++++ crates/shirabe/src/io/buffer_io.rs | 64 +++++++++++++-------- crates/shirabe/src/package/handle.rs | 10 ++++ .../shirabe/src/repository/composer_repository.rs | 66 ++++++++++++++++++---- 4 files changed, 137 insertions(+), 33 deletions(-) (limited to 'crates/shirabe/src') diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 3ef27f8..2441be6 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -70,6 +70,19 @@ pub struct EventDispatcher { skip_scripts: Vec, previous_hash: Option, previous_listeners: IndexMap, + /// For testing only. Mirrors PHPUnit's `getMockBuilder(EventDispatcher)->onlyMethods(['getListeners'])`: + /// when set, `get_listeners` returns this closure's result verbatim instead of resolving + /// registered listeners and package scripts. + get_listeners_override: Option, +} + +/// For testing only. Holds a closure standing in for an overridden `getListeners` method. +pub struct GetListenersOverride(pub Box Vec>); + +impl std::fmt::Debug for GetListenersOverride { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("GetListenersOverride(..)") + } } impl EventDispatcher { @@ -101,9 +114,20 @@ impl EventDispatcher { skip_scripts, previous_hash: None, previous_listeners: IndexMap::new(), + get_listeners_override: None, } } + /// For testing only. Installs a closure that overrides `get_listeners`, mirroring + /// PHPUnit's `onlyMethods(['getListeners'])->will($this->returnValue(...))` / + /// `->will($this->returnCallback(...))`. + pub fn __set_get_listeners_override( + &mut self, + callback: Box Vec>, + ) { + self.get_listeners_override = Some(GetListenersOverride(callback)); + } + /// Set whether script handlers are active or not pub fn set_run_scripts(&mut self, run_scripts: bool) -> &mut Self { self.run_scripts = run_scripts; @@ -923,6 +947,12 @@ impl EventDispatcher { /// Retrieves all listeners for a given event fn get_listeners(&mut self, event: &dyn EventInterface) -> Vec { + // For testing only: a test may override this method, mirroring PHPUnit's + // `onlyMethods(['getListeners'])`. + if let Some(override_cb) = &self.get_listeners_override { + return override_cb.0(event); + } + let script_listeners: Vec = if self.run_scripts { self.get_script_listeners(event) } else { diff --git a/crates/shirabe/src/io/buffer_io.rs b/crates/shirabe/src/io/buffer_io.rs index 16b97e4..75de4c7 100644 --- a/crates/shirabe/src/io/buffer_io.rs +++ b/crates/shirabe/src/io/buffer_io.rs @@ -67,31 +67,49 @@ impl BufferIO { let output = stream_get_contents(stream).unwrap_or_default(); - Preg::replace_callback( - r"{(?<=^|\n|\x08)(.+?)(\x08+)}", - |matches: &indexmap::IndexMap< - shirabe_external_packages::composer::pcre::CaptureKey, - String, - >| - -> String { - let empty = String::new(); - let g1 = matches - .get(&shirabe_external_packages::composer::pcre::CaptureKey::ByIndex(1)) - .unwrap_or(&empty); - let g2 = matches - .get(&shirabe_external_packages::composer::pcre::CaptureKey::ByIndex(2)) - .unwrap_or(&empty); - let pre = strip_tags(g1); + // Regex pattern compatibility: + // PHP uses `{(?<=^|\n|\x08)(.+?)(\x08+)}` to collapse backspace-overwritten spans (e.g. + // progress bars). The `regex` crate has no look-behind, so the `(?<=^|\n|\x08)` anchor is + // turned into a consuming optional leading group `(^|\n|\x08)` that is re-emitted in the + // replacement. Because PCRE's look-behind is zero-width, a `\x08` ending one match can also + // anchor the following match; consuming-and-restoring would break that chaining in a single + // pass, so the replacement is applied to a fixpoint (each pass strictly shrinks the string). + let mut output = output; + loop { + let next = Preg::replace_callback( + r"{(^|\n|\x08)(.+?)(\x08+)}", + |matches: &indexmap::IndexMap< + shirabe_external_packages::composer::pcre::CaptureKey, + String, + >| + -> String { + let empty = String::new(); + let g1 = matches + .get(&shirabe_external_packages::composer::pcre::CaptureKey::ByIndex(1)) + .unwrap_or(&empty); + let g2 = matches + .get(&shirabe_external_packages::composer::pcre::CaptureKey::ByIndex(2)) + .unwrap_or(&empty); + let g3 = matches + .get(&shirabe_external_packages::composer::pcre::CaptureKey::ByIndex(3)) + .unwrap_or(&empty); + let pre = strip_tags(g2); - if pre.len() == g2.len() { - return String::new(); - } + if pre.len() == g3.len() { + return g1.clone(); + } - // TODO reverse parse the string, skipping span tags and \033\[([0-9;]+)m(.*?)\033\[0m style blobs - format!("{}\n", g1.trim_end()) - }, - &output, - ) + // TODO reverse parse the string, skipping span tags and \033\[([0-9;]+)m(.*?)\033\[0m style blobs + format!("{}{}\n", g1, g2.trim_end()) + }, + &output, + ); + if next == output { + break; + } + output = next; + } + output } pub fn set_user_inputs(&mut self, inputs: Vec) -> Result<()> { diff --git a/crates/shirabe/src/package/handle.rs b/crates/shirabe/src/package/handle.rs index 665efb6..8a6ed83 100644 --- a/crates/shirabe/src/package/handle.rs +++ b/crates/shirabe/src/package/handle.rs @@ -1222,10 +1222,20 @@ macro_rules! impl_real_package_test_setters { .expect("real package handle invariant") .set_source_type(r#type); } + + /// For testing only: mirrors PHP `Package::setDistSha1Checksum`. + pub fn __set_dist_sha1_checksum(&self, sha1checksum: Option) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_dist_sha1_checksum(sha1checksum); + } } }; } +impl_real_package_test_setters!(PackageInterfaceHandle); impl_real_package_test_setters!(PackageHandle); impl_real_package_test_setters!(CompletePackageHandle); impl_real_package_test_setters!(RootPackageHandle); diff --git a/crates/shirabe/src/repository/composer_repository.rs b/crates/shirabe/src/repository/composer_repository.rs index 4428518..3314ebd 100644 --- a/crates/shirabe/src/repository/composer_repository.rs +++ b/crates/shirabe/src/repository/composer_repository.rs @@ -414,6 +414,13 @@ impl ComposerRepository { }.into()); } + // PHP relies on ArrayRepository::getPackages() invoking the virtual initialize(), + // which ComposerRepository overrides. Embedded inheritance does not dispatch back to + // the wrapper, so initialize the wrapper explicitly before delegating. + if !self.inner.is_initialized() { + self.initialize()?; + } + self.inner.get_packages() } @@ -1461,12 +1468,13 @@ impl ComposerRepository { let mut result: IndexMap = IndexMap::new(); let mut versions_to_load: IndexMap> = IndexMap::new(); - let packages_inner = packages - .get("packages") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - for (_pkg_key, versions_mixed) in packages_inner.iter() { + // $packages['packages'] can be either array or list + let packages_inner: Vec = match packages.get("packages") { + Some(PhpMixed::Array(a)) => a.values().cloned().collect(), + Some(PhpMixed::List(l)) => l.clone(), + _ => Vec::new(), + }; + for versions_mixed in packages_inner.iter() { // $versions can be either array or list let iter_versions: Vec = match versions_mixed { PhpMixed::Array(a) => a.values().map(|v| v.clone()).collect(), @@ -1633,6 +1641,42 @@ impl ComposerRepository { .map(RepositoryInterfaceHandle::from_rc) } + /// For testing only. Exposes the private `whatProvides` method, mirroring the + /// `ReflectionMethod` invocation in `ComposerRepositoryTest::testWhatProvides`. + pub fn __what_provides( + &mut self, + name: &str, + ) -> anyhow::Result> { + self.what_provides(name, None, None, IndexMap::new()) + } + + /// For testing only. Exposes the private `canonicalizeUrl` method, mirroring the + /// `ReflectionMethod` invocation in `ComposerRepositoryTest::testCanonicalizeUrl`. + pub fn __canonicalize_url(&self, url: &str) -> anyhow::Result { + self.canonicalize_url(url) + } + + /// For testing only. Sets the private `url` property, mirroring the + /// `ReflectionProperty` write in `ComposerRepositoryTest::testCanonicalizeUrl`. + pub fn __set_url(&mut self, url: impl Into) { + self.url = url.into(); + } + + /// For testing only. Sets the private `providerListing` property, mirroring the + /// `ReflectionProperty` write in `ComposerRepositoryTest::testWhatProvides`. + pub fn __set_provider_listing( + &mut self, + provider_listing: IndexMap, + ) { + self.provider_listing = Some(provider_listing); + } + + /// For testing only. Sets the private `providersUrl` property, mirroring the + /// `ReflectionProperty` write in `ComposerRepositoryTest::testWhatProvides`. + pub fn __set_providers_url(&mut self, providers_url: impl Into) { + self.providers_url = Some(providers_url.into()); + } + /// @param packageNames array of package name => ConstraintInterface|null - if a constraint is provided, only packages matching it will be loaded fn load_async_packages( &mut self, @@ -2474,11 +2518,13 @@ impl ComposerRepository { if let Some(pkgs) = data.get("packages").and_then(|v| v.as_array()).cloned() { for (package, versions_mixed) in pkgs.iter() { let package_name = strtolower(package); - let versions = match versions_mixed.as_array() { - Some(a) => a.clone(), - None => continue, + // $versions can be either array or list + let versions: Vec = match versions_mixed { + PhpMixed::Array(a) => a.values().cloned().collect(), + PhpMixed::List(l) => l.clone(), + _ => continue, }; - for (_version, metadata_mixed) in versions.iter() { + for metadata_mixed in versions.iter() { let metadata = match metadata_mixed.as_array() { Some(a) => a.clone(), None => continue, -- cgit v1.3.1