diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-07 07:01:50 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-07 07:01:50 +0900 |
| commit | 759b2980e70dfb8960238f75d68bb6dddce25414 (patch) | |
| tree | e37a0af6423f8ae8dd26da309b278d49d45b3451 | |
| parent | 9a393adc0ace86cac788723b524e83c63dfc91c1 (diff) | |
| download | php-shirabe-759b2980e70dfb8960238f75d68bb6dddce25414.tar.gz php-shirabe-759b2980e70dfb8960238f75d68bb6dddce25414.tar.zst php-shirabe-759b2980e70dfb8960238f75d68bb6dddce25414.zip | |
test: port the tests left as todo!() stubs
Replace the todo!() bodies with real ports. Four autoload-generator
tests now run for real; the rest stay #[ignore]d, but each ignore reason
now names the concrete missing symbol instead of a vague subsystem.
Production additions the ports need: the deprecated
AuthHelper::addAuthenticationHeader wrapper,
EventDispatcher::__set_dispatch_script_override as the seam for PHPUnit
onlyMethods(['dispatchScript']), and a define() stub in the shim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| -rw-r--r-- | crates/shirabe-php-shim/src/runtime.rs | 6 | ||||
| -rw-r--r-- | crates/shirabe/src/event_dispatcher/event_dispatcher.rs | 31 | ||||
| -rw-r--r-- | crates/shirabe/src/util/auth_helper.rs | 33 | ||||
| -rw-r--r-- | crates/shirabe/tests/application_test.rs | 38 | ||||
| -rw-r--r-- | crates/shirabe/tests/autoload/autoload_generator_test.rs | 329 | ||||
| -rw-r--r-- | crates/shirabe/tests/autoload/class_loader_test.rs | 39 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/run_script_command_test.rs | 239 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/self_update_command_test.rs | 138 | ||||
| -rw-r--r-- | crates/shirabe/tests/downloader/file_downloader_test.rs | 20 | ||||
| -rw-r--r-- | crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs | 261 | ||||
| -rw-r--r-- | crates/shirabe/tests/event_dispatcher/main.rs | 2 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/auth_helper_test.rs | 50 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/error_handler_test.rs | 9 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/process_executor_test.rs | 48 |
14 files changed, 1108 insertions, 135 deletions
diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs index c529c263..f7fb3b86 100644 --- a/crates/shirabe-php-shim/src/runtime.rs +++ b/crates/shirabe-php-shim/src/runtime.rs @@ -49,6 +49,12 @@ pub fn constant(_name: &str) -> PhpMixed { todo!() } +pub fn define(_constant_name: &str, _value: PhpMixed) -> bool { + // TODO(php-runtime): defining a constant at runtime needs the same registry `constant` and + // `defined` would read from; the shim has none. + todo!() +} + // Models the constants defined in a standard modern PHP CLI environment on a // non-Windows platform with the common extensions loaded (curl, openssl, json). // Windows-only, HHVM and Composer-bootstrap constants are reported undefined. diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 4aff50c9..5043610b 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -101,6 +101,10 @@ pub struct EventDispatcher { /// when set, `get_listeners` returns this closure's result verbatim instead of resolving /// registered listeners and package scripts. get_listeners_override: Option<GetListenersOverride>, + /// For testing only. Mirrors PHPUnit's `getMockBuilder(EventDispatcher)->onlyMethods(['dispatchScript'])`: + /// when set, `dispatch_script` returns this closure's result instead of building and + /// dispatching a script event. + dispatch_script_override: Option<DispatchScriptOverride>, } /// For testing only. Holds a closure standing in for an overridden `getListeners` method. @@ -112,6 +116,17 @@ impl std::fmt::Debug for GetListenersOverride { } } +/// For testing only. Holds a closure standing in for an overridden `dispatchScript` method. +pub struct DispatchScriptOverride( + pub Box<dyn Fn(&str, bool, &[String], &IndexMap<String, PhpMixed>) -> anyhow::Result<i64>>, +); + +impl std::fmt::Debug for DispatchScriptOverride { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("DispatchScriptOverride(..)") + } +} + impl EventDispatcher { pub fn new( composer: PartialComposerWeakHandle, @@ -142,6 +157,7 @@ impl EventDispatcher { previous_hash: None, previous_listeners: IndexMap::new(), get_listeners_override: None, + dispatch_script_override: None, } } @@ -155,6 +171,17 @@ impl EventDispatcher { self.get_listeners_override = Some(GetListenersOverride(callback)); } + /// For testing only. Installs a closure that overrides `dispatch_script`, mirroring PHPUnit's + /// `onlyMethods(['dispatchScript'])->willReturnCallback(...)`. + pub fn __set_dispatch_script_override( + &mut self, + callback: Box< + dyn Fn(&str, bool, &[String], &IndexMap<String, PhpMixed>) -> anyhow::Result<i64>, + >, + ) { + self.dispatch_script_override = Some(DispatchScriptOverride(callback)); + } + /// For testing only. Exposes the protected `getPhpExecCommand`, mirroring the PHP tests' /// `new \ReflectionMethod($dispatcher, 'getPhpExecCommand')`. pub fn __get_php_exec_command(&self) -> anyhow::Result<String> { @@ -197,6 +224,10 @@ impl EventDispatcher { additional_args: Vec<String>, flags: IndexMap<String, PhpMixed>, ) -> anyhow::Result<i64> { + if let Some(over) = &self.dispatch_script_override { + return (over.0)(event_name, dev_mode, &additional_args, &flags); + } + let composer = self.composer(); assert!( composer.is_full(), diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs index c779a1bb..a323c37e 100644 --- a/crates/shirabe/src/util/auth_helper.rs +++ b/crates/shirabe/src/util/auth_helper.rs @@ -436,6 +436,39 @@ impl AuthHelper { }) } + /// @param string[] $headers + /// + /// @return string[] updated headers array + pub fn add_authentication_header( + &mut self, + headers: Vec<String>, + origin: &str, + url: &str, + ) -> anyhow::Result<Vec<String>> { + shirabe_php_shim::trigger_error( + "AuthHelper::addAuthenticationHeader is deprecated since Composer 2.9 use addAuthenticationOptions instead.", + shirabe_php_shim::E_USER_DEPRECATED, + ); + + let mut http: IndexMap<String, PhpMixed> = IndexMap::new(); + http.insert( + "header".to_string(), + PhpMixed::List(headers.into_iter().map(PhpMixed::String).collect()), + ); + let mut options: IndexMap<String, PhpMixed> = IndexMap::new(); + options.insert("http".to_string(), PhpMixed::Array(http)); + let options = self.add_authentication_options(options, origin, url)?; + + Ok(options["http"] + .as_array() + .and_then(|http| http.get("header")) + .and_then(|header| header.as_list()) + .expect("addAuthenticationOptions always leaves http.header a list") + .iter() + .map(|v| v.as_string().unwrap_or("").to_string()) + .collect()) + } + /// @return array<string, mixed> updated options pub fn add_authentication_options( &mut self, diff --git a/crates/shirabe/tests/application_test.rs b/crates/shirabe/tests/application_test.rs index c7ec6cfd..6ce9166b 100644 --- a/crates/shirabe/tests/application_test.rs +++ b/crates/shirabe/tests/application_test.rs @@ -22,7 +22,7 @@ use shirabe_external_packages::symfony::console::input::array_input::ArrayInput; use shirabe_external_packages::symfony::console::input::input_interface::InputInterface; use shirabe_external_packages::symfony::console::output::buffered_output::BufferedOutput; use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface; -use shirabe_php_shim::PhpMixed; +use shirabe_php_shim::{PHP_EOL, PHP_SERVER, PhpMixed, define, defined, time}; fn set_up() { Platform::put_env("COMPOSER_DISABLE_XDEBUG_WARN", "1"); @@ -40,15 +40,43 @@ impl Drop for TearDown { } } -#[ignore = "no define() setter exists for the COMPOSER_DEV_WARNING_TIME constant (shim defined() is a fixed matches!)"] +#[ignore = "shirabe_php_shim::define is a todo!() (there is no runtime constant registry), so COMPOSER_DEV_WARNING_TIME cannot be defined and defined() — a fixed matches! that omits it — keeps the warning branch unreachable"] #[test] fn test_dev_warning() { let _tear_down = TearDown; set_up(); - // TODO(phase-d): no define() setter exists for the COMPOSER_DEV_WARNING_TIME constant (the - // shim's defined() is a fixed matches!), so this test's runtime define() cannot be reproduced. - todo!() + let application = ApplicationHandle::new("Composer".to_string(), "".to_string()).unwrap(); + + if !defined("COMPOSER_DEV_WARNING_TIME") { + define("COMPOSER_DEV_WARNING_TIME", PhpMixed::Int(time() - 1)); + } + + let output = std::rc::Rc::new(std::cell::RefCell::new(BufferedOutput::new( + None, false, None, + ))); + let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = + std::rc::Rc::new(std::cell::RefCell::new( + ArrayInput::new( + vec![(PhpMixed::from("command"), PhpMixed::from("about"))], + None, + ) + .unwrap(), + )); + let output_trait: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = output.clone(); + application.do_run(input, output_trait).unwrap(); + + let expected_output = format!( + "<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>{}", + PHP_SERVER + .lock() + .unwrap() + .get("PHP_SELF") + .unwrap_or_default() + .to_string_lossy(), + PHP_EOL + ); + assert!(output.borrow().fetch().contains(&expected_output)); } #[ignore = "SelfUpdateCommand::execute is intentionally stubbed with a Shirabe-specific \"not available\" message instead of the original Composer wording this test expects"] diff --git a/crates/shirabe/tests/autoload/autoload_generator_test.rs b/crates/shirabe/tests/autoload/autoload_generator_test.rs index 81f508cc..92baadcd 100644 --- a/crates/shirabe/tests/autoload/autoload_generator_test.rs +++ b/crates/shirabe/tests/autoload/autoload_generator_test.rs @@ -10,15 +10,18 @@ use shirabe::event_dispatcher::EventDispatcher; use shirabe::filter::platform_requirement_filter::PlatformRequirementFilterFactory; use shirabe::installer::{InstallationManager, InstallerInterface}; use shirabe::io::{BufferIO, IOInterface}; -use shirabe::package::handle::{AliasPackageHandle, PackageHandle, RootPackageHandle}; +use shirabe::package::handle::{ + AliasPackageHandle, CompletePackageHandle, PackageHandle, RootPackageHandle, +}; use shirabe::package::{Link, PackageInterfaceHandle, RootPackageInterfaceHandle}; use shirabe::repository::{ InstalledArrayRepository, InstalledRepositoryInterfaceHandle, WritableRepositoryInterface, }; +use shirabe::script::ScriptEvents; use shirabe::util::http_downloader::HttpDownloader; use shirabe::util::r#loop::Loop; use shirabe_external_packages::symfony::console::output::output_interface; -use shirabe_php_shim::PhpMixed; +use shirabe_php_shim::{PhpMixed, dirname, preg_quote, realpath, strtr}; use shirabe_semver::VersionParser; use shirabe_semver::constraint::{AnyConstraint, MatchAllConstraint, SimpleConstraint}; use tempfile::TempDir; @@ -125,6 +128,8 @@ struct SetUp { working_dir: String, vendor_dir: String, repository: InstalledArrayRepository, + /// ref: `$this->configValueMap['use-include-path']`, which testUseGlobalIncludePath mutates. + use_include_path: bool, im: InstallationManager, io: std::rc::Rc<std::cell::RefCell<BufferIO>>, generator: AutoloadGenerator, @@ -199,6 +204,7 @@ fn set_up() -> SetUp { working_dir, vendor_dir, repository, + use_include_path: false, im, io, generator, @@ -213,7 +219,7 @@ impl SetUp { ConfigStubBuilder::new() .with("vendor-dir", PhpMixed::String(self.vendor_dir.clone())) .with("platform-check", PhpMixed::Bool(true)) - .with("use-include-path", PhpMixed::Bool(false)) + .with("use-include-path", PhpMixed::Bool(self.use_include_path)) .build() } @@ -1740,11 +1746,164 @@ fn test_files_autoload_order_by_dependencies() { } #[test] -#[ignore = "needs getCanonicalPackages consecutive-call return values (different package set per dump)"] +#[serial] fn test_files_autoload_generation_remove_extra_entities_from_autoload_files() { - // TODO(phase-d): needs a repository mock returning a different package set on each of several - // consecutive dump() calls (PHPUnit consecutive-call mock); not modeled by InstalledArrayRepository. - todo!() + let mut s = set_up(); + let autoload_package = new_root_pkg("root/a"); + autoload_package.set_autoload(autoload(vec![("files", str_list(&["root.php"]))])); + autoload_package.__set_include_paths(vec!["/lib".to_string(), "/src".to_string()]); + + let not_autoload_package = new_root_pkg("root/a"); + + let requires_ = || { + requires(vec![ + ("a/a", link("a", "a/a", match_all(), None)), + ("b/b", link("a", "b/b", match_all(), None)), + ("c/c", link("a", "c/c", match_all(), None)), + ]) + }; + autoload_package.set_requires(requires_()); + not_autoload_package.set_requires(requires_()); + + let a = new_pkg("a/a"); + let b = new_pkg("b/b"); + let c = new_pkg("c/c"); + a.__set_autoload(autoload(vec![("files", str_list(&["test.php"]))])); + a.__set_include_paths(vec!["lib1".to_string(), "src1".to_string()]); + b.__set_autoload(autoload(vec![("files", str_list(&["test2.php"]))])); + b.__set_include_paths(vec!["lib2".to_string()]); + c.__set_autoload(autoload(vec![( + "files", + str_list(&["test3.php", "foo/bar/test4.php"]), + )])); + c.__set_include_paths(vec!["lib3".to_string()]); + c.__set_target_dir(Some("foo/bar".to_string())); + let autoload_packages: Vec<PackageInterfaceHandle> = vec![a.into(), b.into(), c.into()]; + + // PHP re-creates the three packages without autoload/include-path settings for the second and + // third `getCanonicalPackages` calls; the repository contents are swapped between dumps here. + let not_autoload_packages = || -> Vec<PackageInterfaceHandle> { + vec![ + new_pkg("a/a").into(), + new_pkg("b/b").into(), + new_pkg("c/c").into(), + ] + }; + + s.ensure_dir(&format!("{}/a/a", s.vendor_dir)); + s.ensure_dir(&format!("{}/b/b", s.vendor_dir)); + s.ensure_dir(&format!("{}/c/c/foo/bar", s.vendor_dir)); + s.put( + &format!("{}/a/a/test.php", s.vendor_dir), + "<?php function testFilesAutoloadGeneration1() {}", + ); + s.put( + &format!("{}/b/b/test2.php", s.vendor_dir), + "<?php function testFilesAutoloadGeneration2() {}", + ); + s.put( + &format!("{}/c/c/foo/bar/test3.php", s.vendor_dir), + "<?php function testFilesAutoloadGeneration3() {}", + ); + s.put( + &format!("{}/c/c/foo/bar/test4.php", s.vendor_dir), + "<?php function testFilesAutoloadGeneration4() {}", + ); + s.put( + &format!("{}/root.php", s.working_dir), + "<?php function testFilesAutoloadGenerationRoot() {}", + ); + + let vendor = s.vendor_dir.clone(); + let composer_out = format!("{}/composer", vendor); + let fx = fixtures_dir(); + + s.set_canonical_packages(autoload_packages); + dump( + &mut s, + autoload_package.clone().into(), + false, + "FilesAutoload", + ) + .unwrap(); + assert_file_content_equals( + fx.join("autoload_functions.php").to_str().unwrap(), + &format!("{}/autoload.php", vendor), + ); + assert_file_content_equals( + fx.join("autoload_real_functions_with_include_paths.php") + .to_str() + .unwrap(), + &format!("{}/autoload_real.php", composer_out), + ); + assert_file_content_equals( + fx.join("autoload_static_functions_with_include_paths.php") + .to_str() + .unwrap(), + &format!("{}/autoload_static.php", composer_out), + ); + assert_file_content_equals( + fx.join("autoload_files_functions.php").to_str().unwrap(), + &format!("{}/autoload_files.php", composer_out), + ); + assert_file_content_equals( + fx.join("include_paths_functions.php").to_str().unwrap(), + &format!("{}/include_paths.php", composer_out), + ); + + s.repository = InstalledArrayRepository::new().unwrap(); + s.set_canonical_packages(not_autoload_packages()); + dump( + &mut s, + autoload_package.clone().into(), + false, + "FilesAutoload", + ) + .unwrap(); + assert_file_content_equals( + fx.join("autoload_functions.php").to_str().unwrap(), + &format!("{}/autoload.php", vendor), + ); + assert_file_content_equals( + fx.join("autoload_real_functions_with_include_paths.php") + .to_str() + .unwrap(), + &format!("{}/autoload_real.php", composer_out), + ); + assert_file_content_equals( + fx.join("autoload_files_functions_with_removed_extra.php") + .to_str() + .unwrap(), + &format!("{}/autoload_files.php", composer_out), + ); + assert_file_content_equals( + fx.join("include_paths_functions_with_removed_extra.php") + .to_str() + .unwrap(), + &format!("{}/include_paths.php", composer_out), + ); + + s.repository = InstalledArrayRepository::new().unwrap(); + s.set_canonical_packages(not_autoload_packages()); + dump(&mut s, not_autoload_package.into(), false, "FilesAutoload").unwrap(); + assert_file_content_equals( + fx.join("autoload_functions.php").to_str().unwrap(), + &format!("{}/autoload.php", vendor), + ); + assert_file_content_equals( + fx.join("autoload_real_functions_with_removed_include_paths_and_autolad_files.php") + .to_str() + .unwrap(), + &format!("{}/autoload_real.php", composer_out), + ); + assert_file_content_equals( + fx.join("autoload_static_functions_with_removed_include_paths_and_autolad_files.php") + .to_str() + .unwrap(), + &format!("{}/autoload_static.php", composer_out), + ); + assert!(!std::path::Path::new(&format!("{}/autoload_files.php", composer_out)).exists()); + assert!(!std::path::Path::new(&format!("{}/include_paths.php", composer_out)).exists()); } #[test] @@ -1764,19 +1923,67 @@ fn test_include_paths_in_root_package() { } #[test] -#[ignore = "EventDispatcher::dispatchScript spy not modeled"] +#[serial] fn test_pre_and_post_events_are_dispatched_during_autoload_dump() { - // TODO(phase-d): requires spying on EventDispatcher::dispatchScript to record the events - // dispatched around the dump; no dispatcher spy/mock hook is modeled yet. - todo!() + let mut s = set_up(); + + let series = std::rc::Rc::new(std::cell::RefCell::new(vec![ + (ScriptEvents::PRE_AUTOLOAD_DUMP, false), + (ScriptEvents::POST_AUTOLOAD_DUMP, false), + ])); + let dispatched = std::rc::Rc::new(std::cell::Cell::new(0)); + let series_for_cb = series.clone(); + let dispatched_for_cb = dispatched.clone(); + s.event_dispatcher + .borrow_mut() + .__set_dispatch_script_override(Box::new(move |r#type, dev, _args, _flags| { + dispatched_for_cb.set(dispatched_for_cb.get() + 1); + let expected = series_for_cb.borrow_mut().remove(0); + assert_eq!(expected, (r#type, dev)); + + Ok(0) + })); + + let package = new_root_pkg("root/a"); + package.set_autoload(autoload(vec![( + "psr-0", + str_map(&[("Prefix", pstr("foo/bar/non/existing/"))]), + )])); + + s.generator.set_run_scripts(true); + dump(&mut s, package.into(), true, "_8").unwrap(); + + assert_eq!(2, dispatched.get()); } #[test] -#[ignore = "asserts PHP get_include_path()/require behavior with use-include-path"] +#[serial] fn test_use_global_include_path() { - // TODO(phase-d): asserts PHP get_include_path()/require() behavior driven by the - // use-include-path setting; no Rust equivalent. - todo!() + let mut s = set_up(); + let package = new_root_pkg("root/a"); + package.set_autoload(autoload(vec![( + "psr-0", + str_map(&[("Main\\Foo", pstr("")), ("Main\\Bar", pstr(""))]), + )])); + package.__set_target_dir(Some("Main/Foo/".to_string())); + + s.use_include_path = true; + + s.ensure_dir(&format!("{}/a", s.vendor_dir)); + + let composer_out = format!("{}/composer", s.vendor_dir); + dump(&mut s, package.into(), false, "IncludePath").unwrap(); + let fx = fixtures_dir(); + assert_file_content_equals( + fx.join("autoload_real_include_path.php").to_str().unwrap(), + &format!("{}/autoload_real.php", composer_out), + ); + assert_file_content_equals( + fx.join("autoload_static_include_path.php") + .to_str() + .unwrap(), + &format!("{}/autoload_static.php", composer_out), + ); } #[test] @@ -1973,12 +2180,94 @@ fn test_up_level_relative_paths() { } #[test] -#[ignore = "exercises buildPackageMap/parseAutoloads directly with multi-dump mutation"] +#[serial] fn test_autoload_rules_in_package_that_does_not_exist_on_disk() { - // TODO(phase-d): exercises AutoloadGenerator::buildPackageMap/parseAutoloads directly across - // multiple dump() calls with the package list mutated between calls; needs those internals - // exposed to tests. - todo!() + let mut s = set_up(); + let package = new_root_pkg("root/a"); + package.set_requires(requires(vec![( + "dep/a", + link("root/a", "dep/a", match_all(), Some("requires")), + )])); + let dep = CompletePackageHandle::new("dep/a".to_string(), "1.0".to_string(), "1.0".to_string()); + + s.set_canonical_packages(vec![dep.clone().into()]); + + dep.__set_autoload(autoload(vec![( + "psr-0", + str_map(&[("Foo", pstr("./src"))]), + )])); + dump(&mut s, package.clone().into(), true, "_19").unwrap(); + + let expected_namespace = "<?php\n\n// autoload_namespaces.php @generated by Composer\n\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\n\nreturn array(\n 'Foo' => array($vendorDir . '/dep/a/src'),\n);\n"; + assert_str_equals_file( + expected_namespace, + &format!("{}/composer/autoload_namespaces.php", s.vendor_dir), + ); + + dep.__set_autoload(autoload(vec![( + "psr-4", + str_map(&[("Acme\\Foo\\", pstr("./src-psr4"))]), + )])); + dump(&mut s, package.clone().into(), true, "_19").unwrap(); + + let expected_psr4 = "<?php\n\n// autoload_psr4.php @generated by Composer\n\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\n\nreturn array(\n 'Acme\\\\Foo\\\\' => array($vendorDir . '/dep/a/src-psr4'),\n);\n"; + assert_str_equals_file( + expected_psr4, + &format!("{}/composer/autoload_psr4.php", s.vendor_dir), + ); + + dep.__set_autoload(autoload(vec![("classmap", str_list(&["classmap"]))])); + let expected_message = format!( + "Could not scan for classes inside \"{}/dep/a/classmap\" which does not appear to be a file nor a folder", + s.vendor_dir + ); + if let Err(e) = dump(&mut s, package.clone().into(), true, "_19") { + assert_eq!(expected_message, e.to_string()); + } + + dep.__set_autoload(autoload(vec![("files", str_list(&["./test.php"]))])); + dump(&mut s, package.clone().into(), true, "_19").unwrap(); + let files = + std::fs::read_to_string(format!("{}/composer/autoload_files.php", s.vendor_dir)).unwrap(); + assert!(files.contains("$vendorDir . '/dep/a/test.php',\n")); + + package.set_autoload(autoload(vec![( + "exclude-from-classmap", + str_list(&["../excludedroot", "root/excl"]), + )])); + dep.__set_autoload(autoload(vec![( + "exclude-from-classmap", + str_list(&["../../excluded", "foo/bar"]), + )])); + let map = s + .generator + .build_package_map(&mut s.im, package.clone().into(), vec![dep.clone().into()]) + .unwrap(); + let parsed = s + .generator + .parse_autoloads(map, package.into(), PhpMixed::Bool(false)); + let excluded_root = format!( + "{}/excludedroot($|/)", + preg_quote( + &strtr( + &realpath(dirname(&s.working_dir)).unwrap_or_default(), + "\\", + "/" + ), + None + ) + ); + let root_excl = format!( + "{}/root/excl($|/)", + preg_quote( + &strtr(&realpath(&s.working_dir).unwrap_or_default(), "\\", "/"), + None + ) + ); + assert_eq!( + str_map(&[("0", pstr(&excluded_root)), ("1", pstr(&root_excl))]), + parsed["exclude-from-classmap"] + ); } /// ref: AutoloadGeneratorTest::platformCheckProvider — builds the link map for a requires/provides/ diff --git a/crates/shirabe/tests/autoload/class_loader_test.rs b/crates/shirabe/tests/autoload/class_loader_test.rs index 582e24fa..998683c9 100644 --- a/crates/shirabe/tests/autoload/class_loader_test.rs +++ b/crates/shirabe/tests/autoload/class_loader_test.rs @@ -1,13 +1,37 @@ //! ref: composer/tests/Composer/Test/Autoload/ClassLoaderTest.php use shirabe::autoload::class_loader::ClassLoader; +use shirabe_php_shim::class_exists; + +/// ref: ClassLoaderTest::getLoadClassTests +fn get_load_class_tests() -> Vec<&'static str> { + vec![ + "Namespaced\\Foo", + "Pearlike_Foo", + "ShinyVendor\\ShinyPackage\\SubNamespace\\Foo", + ] +} #[test] -#[ignore = "depends on PHP runtime class_exists() to verify loadClass defined a class; no Rust equivalent"] +#[ignore = "shirabe_php_shim::class_exists models a fixed set of classes available in a PHP CLI environment; loadClass cannot add to it because including a PHP file does not define a class on the Rust side"] fn test_load_class() { - // TODO(phase-d): loadClass() include()s a fixture and PHPUnit asserts via class_exists(); - // Rust has no equivalent of runtime class definition/loading. - todo!() + let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../composer/tests/Composer/Test/Autoload/Fixtures") + .canonicalize() + .unwrap() + .display() + .to_string(); + + for class in get_load_class_tests() { + let mut loader = ClassLoader::new(None); + loader.add("Namespaced\\", vec![fixtures.clone()], false); + loader.add("Pearlike_", vec![fixtures.clone()], false); + loader + .add_psr4("ShinyVendor\\ShinyPackage\\", vec![fixtures.clone()], false) + .unwrap(); + loader.load_class(class); + assert!(class_exists(class), "->loadClass() loads '{}'", class); + } } #[test] @@ -17,9 +41,10 @@ fn test_get_prefixes_with_no_psr0_configuration() { } #[test] -#[ignore = "depends on PHP serialize()/unserialize() round-trip of ClassLoader; no Rust equivalent"] +#[ignore = "the round trip is `unserialize(serialize($loader))`: shirabe_php_shim::serialize takes a PhpMixed (a ClassLoader cannot be turned into one) and there is no unserialize at all, so the ClassLoader under test cannot be round-tripped"] fn test_serializability() { - // TODO(phase-d): serializes/unserializes the ClassLoader and compares every getter; PHP - // serialize()/unserialize() has no Rust equivalent here. + // TODO(phase-d): the round trip is `unserialize(serialize($loader))`. serialize() in the shim + // takes a PhpMixed, which a ClassLoader cannot be converted into, and there is no unserialize + // symbol to produce the second ClassLoader the assertions compare against. todo!() } diff --git a/crates/shirabe/tests/command/run_script_command_test.rs b/crates/shirabe/tests/command/run_script_command_test.rs index 10e171ba..311091a0 100644 --- a/crates/shirabe/tests/command/run_script_command_test.rs +++ b/crates/shirabe/tests/command/run_script_command_test.rs @@ -121,20 +121,239 @@ fn test_can_define_aliases() { drop(tear_down); } +/// ref: RunScriptCommandTest::testExecutionOfSimpleSymfonyCommand #[test] -#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the command's output would go to the worker's inherited stdio, which the in-process application tester cannot capture"] +#[serial] +#[ignore = "invoking the script name as a top-level composer command needs Application::do_run to import the user's PHP Command class as a live application command, which is a todo!() in application.rs, and the worker writes to inherited stdio the in-process application tester cannot capture"] fn test_execution_of_simple_symfony_command() { - // TODO(phase-d): the test invokes the script name as a top-level composer command, which - // requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a - // live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the worker writes to inherited stdio the tester cannot capture. - todo!() + let description = "Sample description for test command"; + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "scripts": { + "test-direct": "Test\\MyCommand", + "test-ref": ["@test-direct --inneropt innerarg"], + }, + "scripts-descriptions": { + "test-direct": description, + }, + "autoload": { + "psr-4": { + "Test\\": "", + }, + }, + })), + None, + None, + true, + ); + + std::fs::write( + "MyCommand.php", + r#"<?php + +namespace Test; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Command\Command; + +class MyCommand extends Command +{ + protected function configure(): void + { + $this->setDefinition([ + new InputArgument('req-arg', InputArgument::REQUIRED, 'Required arg.'), + new InputArgument('opt-arg', InputArgument::OPTIONAL, 'Optional arg.'), + new InputOption('inneropt', null, InputOption::VALUE_NONE, 'Option.'), + new InputOption('outeropt', null, InputOption::VALUE_OPTIONAL, 'Optional option.'), + ]); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln($input->getArgument('req-arg')); + $output->writeln((string) $input->getArgument('opt-arg')); + $output->writeln('inneropt: '.($input->getOption('inneropt') ? 'set' : 'unset')); + $output->writeln('outeropt: '.($input->getOption('outeropt') ? 'set' : 'unset')); + + return 2; + } +} +"#, + ) + .unwrap(); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("test-direct")), + (PhpMixed::from("--outeropt"), PhpMixed::from(true)), + (PhpMixed::from("req-arg"), PhpMixed::from("lala")), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!( + "lala\n\ninneropt: unset\nouteropt: set\n", + app_tester.get_display() + ); + assert_eq!(2, app_tester.get_status_code()); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("test-ref")), + (PhpMixed::from("--outeropt"), PhpMixed::from(true)), + (PhpMixed::from("req-arg"), PhpMixed::from("lala")), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!( + "innerarg\nlala\ninneropt: set\nouteropt: set\n", + app_tester.get_display() + ); + assert_eq!(2, app_tester.get_status_code()); + + // check if the description from composer.json is correctly shown + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("run-script")), + (PhpMixed::from("--list"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code, "assertCommandIsSuccessful"); + let output = app_tester.get_display(); + assert!( + output.contains(description), + "The contents of scripts-description for the test script should be printed" + ); + + drop(tear_down); } +/// ref: RunScriptCommandTest::testExecutionOfSymfonyCommandWithConfiguration #[test] -#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php) as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the command's output would go to the worker's inherited stdio, which the in-process application tester cannot capture"] +#[serial] +#[ignore = "invoking the script name as a top-level composer command needs Application::do_run to import the user's PHP Command class as a live application command, which is a todo!() in application.rs, and the worker writes to inherited stdio the in-process application tester cannot capture"] fn test_execution_of_symfony_command_with_configuration() { - // TODO(phase-d): the test invokes the script name as a top-level composer command, which - // requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php) - // as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the worker writes to inherited stdio the tester cannot capture. - todo!() + let cmd_name = "custom-cmd-123"; + let cmd_alias = format!("{}-alias", cmd_name); + let cmd_desc = "This is a Symfony command with custom configuration"; + let wrong_desc = "this should be ignored"; + + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "scripts": { + cmd_name: "Test\\MyCommandWithDefinitions", + }, + "scripts-descriptions": { + cmd_name: wrong_desc, + }, + "autoload": { + "psr-4": { + "Test\\": "", + }, + }, + })), + None, + None, + true, + ); + + std::fs::write( + "MyCommandWithDefinitions.php", + r#"<?php + +namespace Test; + +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Command\Command; + +class MyCommandWithDefinitions extends Command +{ + protected function configure(): void + { + $this + ->setDescription('__CMD_DESC__') + ->setAliases(['__CMD_ALIAS__']) + ->setDefinition([new InputArgument('req-arg', InputArgument::REQUIRED, 'Required arg.')]); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln($input->getArgument('req-arg')); + return Command::SUCCESS; + } +} +"# + .replace("__CMD_DESC__", cmd_desc) + .replace("__CMD_ALIAS__", &cmd_alias), + ) + .unwrap(); + + // makes sure the command executes with the name defined inside its `configure()`... + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from(cmd_name)), + (PhpMixed::from("req-arg"), PhpMixed::from("lala")), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!("lala\n", app_tester.get_display()); + + // ...with the alias defined there as well... + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + ( + PhpMixed::from("command"), + PhpMixed::from(cmd_alias.as_str()), + ), + (PhpMixed::from("req-arg"), PhpMixed::from("lala")), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!("lala\n", app_tester.get_display()); + + // ...and also uses its own description, instead of the one in composer.scripts-descriptions + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("run-script")), + (PhpMixed::from("--list"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code, "assertCommandIsSuccessful"); + let output = app_tester.get_display(); + assert!( + output.contains(cmd_desc), + "The custom description for the test script should be printed" + ); + assert!( + !output.contains(wrong_desc), + "The dummy description shouldn't show" + ); + + drop(tear_down); } diff --git a/crates/shirabe/tests/command/self_update_command_test.rs b/crates/shirabe/tests/command/self_update_command_test.rs index 3b73adbb..6dc64104 100644 --- a/crates/shirabe/tests/command/self_update_command_test.rs +++ b/crates/shirabe/tests/command/self_update_command_test.rs @@ -1,40 +1,102 @@ //! ref: composer/tests/Composer/Test/Command/SelfUpdateCommandTest.php use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use indexmap::IndexMap; use serial_test::serial; -use shirabe_php_shim::PhpMixed; +use shirabe_external_packages::symfony::process::Process; +use shirabe_php_shim::{PHP_BINARY, PhpMixed}; -/// ref: SelfUpdateCommandTest::setUp (portable part: initTempComposer; the composer-test.phar copy -/// is omitted because the phar fixture and Symfony Process are not ported). +/// ref: SelfUpdateCommandTest::setUp. The `composer-test.phar` copy PHP also performs here lives in +/// `set_up_with_phar` instead, so the one test that never touches the phar is not blocked by the +/// missing fixture. fn set_up() -> crate::test_case::TearDown { init_temp_composer(None, None, None, true) } +/// ref: SelfUpdateCommandTest::setUp, including the `composer-test.phar` copy. Returns the tear-down +/// guard and `$this->phar`. +fn set_up_with_phar() -> (crate::test_case::TearDown, String) { + let tear_down = set_up(); + let phar = tear_down.working_dir().join("composer.phar"); + std::fs::copy( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../composer/tests/composer-test.phar"), + &phar, + ) + .unwrap(); + + (tear_down, phar.display().to_string()) +} + +/// ref: SelfUpdateCommandTest::channelOptions +fn channel_options() -> Vec<(&'static str, &'static str)> { + vec![ + ("--stable", "stable channel"), + ("--preview", "preview channel"), + ("--snapshot", "snapshot channel"), + ] +} + #[test] #[serial] -#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update'])` running composer-test.phar \ - over HTTP; requires Symfony Process and the composer-test.phar fixture, neither ported"] +#[ignore = "composer-test.phar is built by AllFunctionalTest::testBuildPhar via bin/compile, which has no equivalent here, so the fixture set_up_with_phar copies can never exist"] fn test_successful_update() { - let _tear_down = set_up(); + let (_tear_down, phar) = set_up_with_phar(); + + if shirabe::composer::VERSION != concat!("@package_version", "@") { + eprintln!( + "skipping: On releases this test can fail to upgrade as we are already on latest version" + ); + return; + } + + let mut app_tester = Process::new( + vec![PHP_BINARY.to_string(), phar, "self-update".to_string()], + None, + None, + PhpMixed::Null, + None, + ) + .unwrap(); + let status = app_tester.run(None, IndexMap::new()).unwrap(); + assert_eq!(0, status, "{}", app_tester.get_error_output().unwrap()); - // TODO(phase-d): spawns `new Process([PHP_BINARY, $this->phar, 'self-update'])` running - // composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar fixture, - // neither ported. - todo!() + assert!( + app_tester + .get_output() + .unwrap() + .contains("Upgrading to version") + ); } #[test] #[serial] -#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update', '2.4.0'])` running \ - composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar \ - fixture, neither ported"] +#[ignore = "composer-test.phar is built by AllFunctionalTest::testBuildPhar via bin/compile, which has no equivalent here, so the fixture set_up_with_phar copies can never exist"] fn test_update_to_specific_version() { - let _tear_down = set_up(); + let (_tear_down, phar) = set_up_with_phar(); + + let mut app_tester = Process::new( + vec![ + PHP_BINARY.to_string(), + phar, + "self-update".to_string(), + "2.4.0".to_string(), + ], + None, + None, + PhpMixed::Null, + None, + ) + .unwrap(); + let status = app_tester.run(None, IndexMap::new()).unwrap(); + assert_eq!(0, status, "{}", app_tester.get_error_output().unwrap()); - // TODO(phase-d): spawns `new Process([PHP_BINARY, $this->phar, 'self-update', '2.4.0'])` - // running composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar - // fixture, neither ported. - todo!() + assert!( + app_tester + .get_output() + .unwrap() + .contains("Upgrading to version 2.4.0") + ); } #[test] @@ -63,14 +125,38 @@ fn test_update_with_invalid_option_throws_exception() { #[test] #[serial] -#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update', $option])` running \ - composer-test.phar over HTTP (data provider: --stable/--preview/--snapshot); requires \ - Symfony Process and the composer-test.phar fixture, neither ported"] +#[ignore = "composer-test.phar is built by AllFunctionalTest::testBuildPhar via bin/compile, which has no equivalent here, so the fixture set_up_with_phar copies can never exist"] fn test_update_to_different_channel() { - let _tear_down = set_up(); + for (option, expected_output) in channel_options() { + let (_tear_down, phar) = set_up_with_phar(); + + if shirabe::composer::VERSION != concat!("@package_version", "@") + && ["--stable", "--preview"].contains(&option) + { + eprintln!( + "skipping: On releases this test can fail to upgrade as we are already on latest version" + ); + continue; + } + + let mut app_tester = Process::new( + vec![ + PHP_BINARY.to_string(), + phar, + "self-update".to_string(), + option.to_string(), + ], + None, + None, + PhpMixed::Null, + None, + ) + .unwrap(); + let status = app_tester.run(None, IndexMap::new()).unwrap(); + assert_eq!(0, status, "{}", app_tester.get_error_output().unwrap()); - // TODO(phase-d): spawns `new Process([PHP_BINARY, $this->phar, 'self-update', $option])` - // running composer-test.phar over HTTP (data provider: --stable/--preview/--snapshot); - // requires Symfony Process and the composer-test.phar fixture, neither ported. - todo!() + let output = app_tester.get_output().unwrap(); + assert!(output.contains("Upgrading to version")); + assert!(output.contains(expected_output)); + } } diff --git a/crates/shirabe/tests/downloader/file_downloader_test.rs b/crates/shirabe/tests/downloader/file_downloader_test.rs index 65dc1a51..469bf473 100644 --- a/crates/shirabe/tests/downloader/file_downloader_test.rs +++ b/crates/shirabe/tests/downloader/file_downloader_test.rs @@ -177,20 +177,24 @@ fn test_download_but_file_is_unsaved() { } #[test] -#[ignore = "requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey plus PreFileDownloadEvent::setProcessedUrl dispatch, which is TODO(plugin) in FileDownloader::download"] +#[ignore = "the listener is a closure that mutates the event (setProcessedUrl), but Callable::Closure receives `&dyn EventInterface`, so it cannot; and CacheMock has no copy_to/copy_from hooks to assert the cache key on"] fn test_download_with_custom_processed_url() { - // TODO(phase-d): requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey - // plus PreFileDownloadEvent::setProcessedUrl dispatch, which is TODO(plugin) in - // FileDownloader::download. + // TODO(phase-d): the PRE_FILE_DOWNLOAD listener is a closure calling + // PreFileDownloadEvent::setProcessedUrl, but Callable::Closure is + // `Fn(&dyn EventInterface)`, so a listener cannot mutate the event it receives. The Cache + // half is likewise inexpressible: CacheMock carries only finder/gc overrides, with no + // copy_to/copy_from hook to assert the cache key on. todo!() } #[test] -#[ignore = "requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey plus PreFileDownloadEvent::setCustomCacheKey dispatch, which is TODO(plugin) in FileDownloader::download"] +#[ignore = "the listener is a closure that mutates the event (setCustomCacheKey), but Callable::Closure receives `&dyn EventInterface`, so it cannot; and CacheMock has no copy_to/copy_from hooks to assert the cache key on"] fn test_download_with_custom_cache_key() { - // TODO(phase-d): requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey - // plus PreFileDownloadEvent::setCustomCacheKey dispatch, which is TODO(plugin) in - // FileDownloader::download. + // TODO(phase-d): the PRE_FILE_DOWNLOAD listener is a closure calling + // PreFileDownloadEvent::setCustomCacheKey, but Callable::Closure is + // `Fn(&dyn EventInterface)`, so a listener cannot mutate the event it receives. The Cache + // half is likewise inexpressible: CacheMock carries only finder/gc overrides, with no + // copy_to/copy_from hook to assert the cache key on. todo!() } diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs index 674c4a80..d97c14f0 100644 --- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs +++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs @@ -1,5 +1,6 @@ //! ref: composer/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +use crate::io_mock::{Expectation, get_io_mock}; use crate::process_executor_mock::{ProcessExecutorMockGuard, cmd, get_process_executor_mock}; use indexmap::IndexMap; use serial_test::serial; @@ -13,6 +14,7 @@ use shirabe::filter::PlatformRequirementFilterInterface; use shirabe::installer::{InstallationManagerInterface, InstallerEvents, InstallerInterface}; use shirabe::io::IOInterface; use shirabe::io::buffer_io::BufferIO; +use shirabe::io::io_interface; use shirabe::package::{ LockerInterface, PackageInterfaceHandle, RootPackageHandle, RootPackageInterfaceHandle, }; @@ -111,6 +113,18 @@ fn dispatcher_with_listeners( dispatcher } +/// ref: EventDispatcherTest::getDispatcherStubForListenersTest — same mocked `getListeners`, but +/// constructed without a ProcessExecutor. +fn dispatcher_stub_for_listeners_test( + composer: &ComposerHandle, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + listeners: Vec<&str>, +) -> EventDispatcher { + let mut dispatcher = EventDispatcher::new(composer.upcast().downgrade(), io, None); + dispatcher.__set_get_listeners_override(listeners_const(listeners)); + dispatcher +} + fn listeners_const(listeners: Vec<&str>) -> Box<dyn Fn(&dyn EventInterface) -> Vec<Callable>> { let listeners: Vec<String> = listeners.into_iter().map(|s| s.to_string()).collect(); Box::new(move |_event| listeners.iter().cloned().map(Callable::String).collect()) @@ -342,13 +356,47 @@ fn test_dispatcher_doesnt_return_skipped_scripts() { // unilaterally under the no-test-alteration rule. #[test] +#[serial] #[ignore = "listener `EventDispatcherTest::call` is a static method of the PHPUnit test class itself; the PHP worker cannot load it (extends PHPUnit\\Framework\\TestCase, phpunit absent from composer/vendor) — see the note above the ignored block"] fn test_listener_exceptions_are_caught() { let _tear_down = TearDown; - // TODO(phase-d): the listener is a static method of the PHPUnit test class itself, which - // the PHP worker cannot load (phpunit is absent from composer/vendor); pending a decision on - // providing the listener methods to the child process. - todo!() + + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io_mock.clone(); + + let composer = create_composer_instance(); + let mut dispatcher = dispatcher_stub_for_listeners_test( + &composer, + io_dyn, + vec!["Composer\\Test\\EventDispatcher\\EventDispatcherTest::call"], + ); + + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::text("> Composer\\Test\\EventDispatcher\\EventDispatcherTest::call"), + Expectation::text( + "Script Composer\\Test\\EventDispatcher\\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception", + ), + ], + true, + ) + .unwrap(); + + let result = dispatcher.dispatch_script( + ScriptEvents::POST_INSTALL_CMD, + false, + vec![], + IndexMap::new(), + ); + + let e = result.expect_err("expected RuntimeException"); + assert!( + e.downcast_ref::<shirabe_php_shim::RuntimeException>() + .is_some(), + "got: {e:?}" + ); } // PHP mocks `Composer\Autoload\AutoloadGenerator` with onlyMethods(['buildPackageMap', @@ -535,43 +583,214 @@ fn test_dispatcher_pass_dev_mode_to_autoload_generator_for_script_events() { } #[test] -#[ignore = "listeners are object-method array callables ([\\$this, 'someMethod']) invoked + removed by object identity; the array-callable invocation path is an unimplemented plugin-runtime stub"] +#[serial] +#[ignore = "the object-method listeners are methods of the PHPUnit test class itself; invoking them sends a CallMethod for a phandle that has no counterpart in the worker, which cannot load that class (extends PHPUnit\\Framework\\TestCase, phpunit absent from composer/vendor) — see the note above the ignored block"] fn test_dispatcher_remove_listener() { let _tear_down = TearDown; - // TODO(phase-d): listeners are object-method array callables ([$this, 'someMethod']) invoked - // and removed by object identity; the array-callable invocation path is an unimplemented - // plugin-runtime stub - todo!() + + let composer = create_composer_instance(); + + let mut repository_manager = MockRepositoryManager::new(); + repository_manager + .expect_get_local_repository() + .returning(|| RepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap())); + composer + .borrow_mut() + .set_repository_manager(std::rc::Rc::new(std::cell::RefCell::new( + repository_manager, + ))); + composer + .borrow_mut() + .set_installation_manager(std::rc::Rc::new(std::cell::RefCell::new( + MockInstallationManager::new(), + ))); + + let (process, _process_guard) = + get_process_executor_mock(vec![], false, MockHandler::default()); + let io = buffer_io_verbose(); + let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone(); + let mut dispatcher = EventDispatcher::new(composer.upcast().downgrade(), io_dyn, Some(process)); + + // PHP's `[$this, 'someMethod']` is an array callable whose object half is the test instance. + // `Callable::PhpMethod` is the port's shape for an object-half callable: it carries the + // cross-RPC identity `remove_listener` compares, and echoes `Class->method` like PHP does. + let this = shirabe_php_rpc::PhpObjHandle { + phandle: 1, + class: "Composer\\Test\\EventDispatcher\\EventDispatcherTest".to_string(), + implements: vec![], + }; + let listener = Callable::PhpMethod(this.clone(), "someMethod".to_string()); + let listener2 = Callable::PhpMethod(this.clone(), "someMethod2".to_string()); + let listener3 = Callable::String( + "Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod".to_string(), + ); + + dispatcher.add_listener("ev1", listener.clone(), 0); + dispatcher.add_listener("ev1", listener.clone(), 1); + dispatcher.add_listener("ev1", listener2, 1); + dispatcher.add_listener("ev1", listener3.clone(), 0); + dispatcher.add_listener("ev2", listener3, 0); + dispatcher.add_listener("ev2", listener, 0); + dispatcher.dispatch(Some("ev1"), None).unwrap(); + dispatcher.dispatch(Some("ev2"), None).unwrap(); + + let mut expected = format!( + "> ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod{eol}\ + > ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod2{eol}\ + > ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod{eol}\ + > ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}\ + > ev2: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}\ + > ev2: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod{eol}", + eol = PHP_EOL + ); + assert_eq!(expected, io.borrow().get_output()); + + dispatcher.remove_listener(&this); + dispatcher.dispatch(Some("ev1"), None).unwrap(); + dispatcher.dispatch(Some("ev2"), None).unwrap(); + + expected += &format!( + "> ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}\ + > ev2: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}", + eol = PHP_EOL + ); + assert_eq!(expected, io.borrow().get_output()); } #[test] +#[serial] #[ignore = "listener `EventDispatcherTest::someMethod` is a static method of the PHPUnit test class itself; the PHP worker cannot load it — see the note above the ignored block"] fn test_dispatcher_can_execute_cli_and_php_in_same_event_script_stack() { let _tear_down = TearDown; - // TODO(phase-d): the PHP-script listener is a static method of the PHPUnit test class - // itself, which the PHP worker cannot load; pending a decision on providing the listener - // methods to the child process. - todo!() + + let (process, _process_guard) = get_process_executor_mock( + vec![cmd("echo -n foo"), cmd("echo -n bar")], + true, + MockHandler::default(), + ); + + let composer = create_composer_instance(); + let io = buffer_io_verbose(); + let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone(); + + let mut dispatcher = dispatcher_with_listeners( + &composer, + io_dyn, + process, + listeners_const(vec![ + "echo -n foo", + "Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod", + "echo -n bar", + ]), + ); + + dispatcher + .dispatch_script( + ScriptEvents::POST_INSTALL_CMD, + false, + vec![], + IndexMap::new(), + ) + .unwrap(); + + let expected = format!( + "> post-install-cmd: echo -n foo{eol}> post-install-cmd: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}> post-install-cmd: echo -n bar{eol}", + eol = PHP_EOL + ); + assert_eq!(expected, io.borrow().get_output()); } #[test] +#[serial] #[ignore = "listener `EventDispatcherTest::getTestEnv` is a static method of the PHPUnit test class itself; the PHP worker cannot load it — see the note above the ignored block"] fn test_dispatcher_can_put_env() { let _tear_down = TearDown; - // TODO(phase-d): the second listener is a static method of the PHPUnit test class itself, - // which the PHP worker cannot load; pending a decision on providing the listener methods to - // the child process. - todo!() + + let (process, _process_guard) = + get_process_executor_mock(vec![], false, MockHandler::default()); + + let composer = create_composer_instance(); + let io = buffer_io_verbose(); + let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone(); + + let mut dispatcher = dispatcher_with_listeners( + &composer, + io_dyn, + process, + listeners_const(vec![ + "@putenv ABC=123", + "Composer\\Test\\EventDispatcher\\EventDispatcherTest::getTestEnv", + ]), + ); + + dispatcher + .dispatch_script( + ScriptEvents::POST_INSTALL_CMD, + false, + vec![], + IndexMap::new(), + ) + .unwrap(); + + let expected = format!( + "> post-install-cmd: @putenv ABC=123{eol}> post-install-cmd: Composer\\Test\\EventDispatcher\\EventDispatcherTest::getTestEnv{eol}", + eol = PHP_EOL + ); + assert_eq!(expected, io.borrow().get_output()); } #[test] +#[serial] #[ignore = "listeners (createsVendorBinFolderChecksEnv*) are static methods of the PHPUnit test class itself; the PHP worker cannot load them — see the note above the ignored block"] fn test_dispatcher_appends_dir_bin_on_path_for_every_listener() { let _tear_down = TearDown; - // TODO(phase-d): the listeners are static methods of the PHPUnit test class itself, which - // the PHP worker cannot load; pending a decision on providing the listener methods to the - // child process. - todo!() + + let current_directory_bkp = Platform::get_cwd(false).unwrap(); + let composer_bin_dir_bkp = Platform::get_env("COMPOSER_BIN_DIR"); + // ref: __DIR__ of EventDispatcherTest.php, where the listeners create `vendor/bin`. + let php_test_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../composer/tests/Composer/Test/EventDispatcher") + .canonicalize() + .unwrap(); + std::env::set_current_dir(&php_test_dir).unwrap(); + Platform::put_env( + "COMPOSER_BIN_DIR", + &format!("{}/vendor/bin", php_test_dir.display()), + ); + + let (process, _process_guard) = + get_process_executor_mock(vec![], false, MockHandler::default()); + + let composer = create_composer_instance(); + let io = buffer_io_verbose(); + let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone(); + + let mut dispatcher = dispatcher_with_listeners( + &composer, + io_dyn, + process, + listeners_const(vec![ + "Composer\\Test\\EventDispatcher\\EventDispatcherTest::createsVendorBinFolderChecksEnvDoesNotContainsBin", + "Composer\\Test\\EventDispatcher\\EventDispatcherTest::createsVendorBinFolderChecksEnvContainsBin", + ]), + ); + + dispatcher + .dispatch_script( + ScriptEvents::POST_INSTALL_CMD, + false, + vec![], + IndexMap::new(), + ) + .unwrap(); + std::fs::remove_dir(php_test_dir.join("vendor/bin")).unwrap(); + std::fs::remove_dir(php_test_dir.join("vendor")).unwrap(); + + std::env::set_current_dir(¤t_directory_bkp).unwrap(); + match composer_bin_dir_bkp { + Some(dir) if !dir.is_empty() => Platform::put_env("COMPOSER_BIN_DIR", &dir), + _ => Platform::clear_env("COMPOSER_BIN_DIR"), + } } #[test] diff --git a/crates/shirabe/tests/event_dispatcher/main.rs b/crates/shirabe/tests/event_dispatcher/main.rs index eac717d7..f51b0f2d 100644 --- a/crates/shirabe/tests/event_dispatcher/main.rs +++ b/crates/shirabe/tests/event_dispatcher/main.rs @@ -1,3 +1,5 @@ +#[path = "../common/io_mock.rs"] +mod io_mock; #[path = "../common/io_stub.rs"] mod io_stub; #[path = "../common/process_executor_mock.rs"] diff --git a/crates/shirabe/tests/util/auth_helper_test.rs b/crates/shirabe/tests/util/auth_helper_test.rs index 91d9cb75..ef5daaf7 100644 --- a/crates/shirabe/tests/util/auth_helper_test.rs +++ b/crates/shirabe/tests/util/auth_helper_test.rs @@ -757,16 +757,48 @@ fn test_prompt_auth_if_needed_multiple_bitbucket_downloads() { } #[test] -#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \ -trigger_error/E_USER_DEPRECATED; the PHP error-handler subsystem is not modeled"] +#[ignore = "addAuthenticationHeader opens with trigger_error(E_USER_DEPRECATED), and \ +shirabe_php_shim::trigger_error is a todo!()"] fn test_add_authentication_header_with_custom_headers() { - // TODO(phase-d): exercises AuthHelper::addAuthenticationHeader, a deprecated wrapper - // around addAuthenticationOptions that PHP implements via - // trigger_error(E_USER_DEPRECATED). It has not been ported to Rust (no - // add_authentication_header method exists on AuthHelper) because the PHP - // error-handler subsystem it relies on is not modeled — same limitation as - // error_handler_test.rs. - todo!() + let mut f = set_up(); + let headers = vec![ + "Accept-Encoding: gzip".to_string(), + "Connection: close".to_string(), + ]; + let origin = "example.org"; + let url = "https://example.org/packages.json"; + let custom_headers = vec![ + "API-TOKEN: abc123".to_string(), + "X-CUSTOM-HEADER: value".to_string(), + ]; + let headers_json = json_encode(&PhpMixed::List( + custom_headers + .iter() + .map(|h| PhpMixed::String(h.clone())) + .collect(), + )) + .unwrap(); + + expects_authentication(&f.io, origin, &headers_json, "custom-headers"); + + f.io.borrow_mut() + .expects( + vec![Expectation::text( + "Using custom HTTP headers for authentication", + )], + true, + ) + .unwrap(); + + let mut expected_headers = headers.clone(); + expected_headers.extend(custom_headers); + + assert_eq!( + expected_headers, + f.auth_helper + .add_authentication_header(headers, origin, url) + .unwrap() + ); } #[test] diff --git a/crates/shirabe/tests/util/error_handler_test.rs b/crates/shirabe/tests/util/error_handler_test.rs index ed5e6a29..efaf4e11 100644 --- a/crates/shirabe/tests/util/error_handler_test.rs +++ b/crates/shirabe/tests/util/error_handler_test.rs @@ -5,16 +5,17 @@ // trigger those by undefined-index access / array_merge misuse. There is no equivalent // runtime mechanism in Rust to port faithfully. -// TODO(phase-d): ErrorHandler::register() installs a PHP set_error_handler; no Rust equivalent. +use shirabe::util::ErrorHandler; +use shirabe_php_shim::restore_error_handler; + #[allow(dead_code)] fn set_up() { - todo!() + ErrorHandler::register(None); } -// TODO(phase-d): restore_error_handler() is PHP runtime machinery; no Rust equivalent. #[allow(dead_code)] fn tear_down() { - todo!() + restore_error_handler(); } #[allow(dead_code)] diff --git a/crates/shirabe/tests/util/process_executor_test.rs b/crates/shirabe/tests/util/process_executor_test.rs index 2ce43f3c..268bc548 100644 --- a/crates/shirabe/tests/util/process_executor_test.rs +++ b/crates/shirabe/tests/util/process_executor_test.rs @@ -15,7 +15,7 @@ use shirabe_external_packages::symfony::console::output::buffered_output::Buffer use shirabe_external_packages::symfony::console::output::output_interface::{ OutputInterface, VERBOSITY_DEBUG, VERBOSITY_NORMAL, }; -use shirabe_php_shim::{PHP_EOL, trim}; +use shirabe_php_shim::{PHP_EOL, ob_get_clean, ob_start, trim}; #[test] fn test_execute_captures_output() { @@ -25,16 +25,16 @@ fn test_execute_captures_output() { assert_eq!(format!("foo{}", PHP_EOL), output); } -#[ignore = "requires PHP output buffering (ob_start/ob_get_clean) to capture stdout; no equivalent symbol"] +#[ignore = "shirabe_php_shim::ob_start/ob_get_clean are todo!(): the shim has no echo-to-buffer routing, and ProcessExecutor::execute with FORWARD_OUTPUT and io=None writes straight to the real process stdout"] #[test] fn test_execute_outputs_if_not_captured() { - // TODO(phase-d): requires PHP output buffering (ob_start/ob_get_clean) to capture - // stdout; no equivalent symbol. ProcessExecutor::execute with - // ProcessExecutor::FORWARD_OUTPUT and io=None writes straight to the real process - // stdout (see output_handler's `print!`), and there is no safe way to capture that - // from within a parallel cargo test process without redirecting the real stdout file - // descriptor, which is unsafe under `cargo test`'s default multi-threaded runner. - todo!() + let mut process = ProcessExecutor::new(None); + ob_start(); + process + .execute("echo foo", ProcessExecutor::FORWARD_OUTPUT, None) + .unwrap(); + let output = ob_get_clean(); + assert_eq!(Some(format!("foo{}", PHP_EOL)), output); } #[test] @@ -139,18 +139,17 @@ fn test_doesnt_hide_ports() { ); } -#[ignore = "splitLines is called with null in the PHP test, but split_lines accepts only &str (no ?string/Option overload)"] #[test] fn test_split_lines() { - // TODO(phase-d): splitLines is called with null in the PHP test - // ($process->splitLines(null)), but ProcessExecutor::split_lines here takes `&str`, not - // `Option<&str>` (PHP's `?string`). Porting this data point faithfully means widening - // split_lines's signature to Option<&str>, which touches every call site - // (package/version/version_guesser.rs, util/git.rs, - // repository/vcs/{hg,fossil,git,svn}_driver.rs — 13 call sites in total, all currently - // passing `&str`). That is a production API change beyond this test file; flagged for - // a design decision rather than made unilaterally. - todo!() + let process = ProcessExecutor::new(None); + assert!(process.split_lines("").is_empty()); + // PHP: $process->splitLines(null). `split_lines` takes `&str` where PHP takes `?string`, and + // its body opens with `trim((string) $output)`, so the caller performs the null-to-"" cast. + assert!(process.split_lines("").is_empty()); + assert_eq!(vec!["foo"], process.split_lines("foo")); + assert_eq!(vec!["foo", "bar"], process.split_lines("foo\nbar")); + assert_eq!(vec!["foo", "bar"], process.split_lines("foo\r\nbar")); + assert_eq!(vec!["foo", "bar"], process.split_lines("foo\r\nbar\n")); } #[test] @@ -185,14 +184,13 @@ fn test_console_io_does_not_format_symfony_console_style() { ); } -#[ignore = "executeAsync returns a Process, not a cancelable promise; no promise/cancel symbol exists"] +#[ignore = "none of the three symbols this test drives exist: execute_async returns a plain future with no cancel(), and ProcessExecutor has no count_active_jobs or wait (PHP's $jobs/$maxJobs queue is a tokio semaphore here)"] #[test] fn test_execute_async_cancel() { - // TODO(phase-d): PHP's executeAsync returns a React\Promise\PromiseInterface with - // cancel(); Rust's execute_async returns anyhow::Result<Process> directly (see the - // comment on ProcessExecutor::execute_async: "no test seam in the external-packages - // crate"), so there is no promise/cancel symbol to drive this test's - // `$promise->cancel()` step. + // TODO(phase-d): PHP's executeAsync returns a React\Promise\PromiseInterface with cancel(), + // and the test reads countActiveJobs() around it and then calls wait(). execute_async here + // returns a plain future with no cancel(), and ProcessExecutor has neither count_active_jobs + // nor wait: the PHP job queue those methods expose is a tokio semaphore in this port. todo!() } |
