From 20f7a7826ae048d249e0d837ca6393a5f09c9ba6 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Mon, 3 Aug 2026 01:30:26 +0900 Subject: feat(event-dispatcher): run composer.json PHP scripts through the RPC worker Implement the two script execution paths that previously stopped at todo!(): a Class::method listener is invoked as CallStaticMethod with the event crossing the boundary as a proxy-stub handle, and a Command-class listener runs inside a throwaway bare Symfony Application hosted by the worker via a generated snippet, its BufferedOutput written back through the dispatcher's IO. makeAutoloader is ported for real (canonical-package hash, setDevMode, buildPackageMap/parseAutoloads/createLoader), and the class_exists/is_callable/is_a/defined guards now query the worker, whose script autoloader resolves classes by asking the Rust-side ClassLoader over the reverse channel. EventInterface gains as_any (the IOInterface downcast pattern) so the concrete event type is reachable behind the trait object. Application::do_run now registers ScriptAliasCommand entries as typed commands, unblocking the run-script --list/alias tests; the dev-mode-to-generator test is ported with local mockall mocks. The remaining ignored tests carry re-verified reasons: the listener methods live on the PHPUnit test class itself (unloadable in the worker), or the test needs live import of a user PHP Command class into the Application. Co-Authored-By: Claude Fable 5 --- .../tests/command/run_script_command_test.rs | 49 ++-- .../event_dispatcher/event_dispatcher_test.rs | 246 ++++++++++++++++++--- 2 files changed, 235 insertions(+), 60 deletions(-) (limited to 'crates/shirabe/tests') diff --git a/crates/shirabe/tests/command/run_script_command_test.rs b/crates/shirabe/tests/command/run_script_command_test.rs index 6e40bde0..74d05758 100644 --- a/crates/shirabe/tests/command/run_script_command_test.rs +++ b/crates/shirabe/tests/command/run_script_command_test.rs @@ -10,33 +10,25 @@ use shirabe_php_shim::PhpMixed; /// `ScriptEvent` passed to `hasEventListeners` matches the script name AND its `isDevMode()` equals /// the computed dev mode (`dev || !noDev`) -- the latter being the whole point of the test. #[test] -#[ignore = "PHP asserts (a) via mocked hasEventListeners that the ScriptEvent has isDevMode() == \ - (dev || !no_dev) and (b) via mocked dispatchScript that it is called once with \ - ($script, $expectedDevMode, []). Neither expectation is expressible: (a) the \ - __set_get_listeners_override callback only sees &dyn EventInterface \ - (src/event_dispatcher/event.rs:49), which has no as_any/downcast seam to reach the \ - concrete ScriptEvent::is_dev_mode (src/script/event.rs:51) -- adding one is a \ - cross-cutting trait change over every event type, not a small test seam; (b) \ - dispatch_script is a concrete method with no call-recording seam, and letting the \ - real one run would execute listeners for real. The faithful body is therefore \ - inexpressible and is left as todo!()."] +#[ignore = "PHP mocks RunScriptCommand itself (onlyMethods incl. requireComposer -> a composer \ + whose EventDispatcher is a hasEventListeners/dispatchScript recording mock) and \ + drives run() with mocked Input/Output. The Rust RunScriptCommand has no \ + requireComposer override seam and Input/Output are concrete types, so the mocked \ + harness is inexpressible; the event-side isDevMode downcast now exists \ + (EventInterface::as_any), but that alone does not unblock the test."] fn test_detect_and_pass_dev_mode_to_event_and_to_dispatching() { - // TODO(phase-d): PHP asserts (a) via mocked hasEventListeners that the ScriptEvent has - // isDevMode() == (dev || !no_dev) and (b) via mocked dispatchScript that it is called once - // with ($script, $expectedDevMode, []). Neither expectation is expressible: (a) the - // __set_get_listeners_override callback only sees &dyn EventInterface - // (src/event_dispatcher/event.rs:49), which has no as_any/downcast seam to reach the concrete - // ScriptEvent::is_dev_mode (src/script/event.rs:51) -- adding one is a cross-cutting trait - // change over every event type, not a small test seam; (b) dispatch_script is a concrete - // method with no call-recording seam, and letting the real one run would execute listeners - // for real. The faithful body is therefore inexpressible and is left as todo!(). + // TODO(phase-d): PHP mocks RunScriptCommand itself (onlyMethods incl. requireComposer -> a + // composer whose EventDispatcher is a hasEventListeners/dispatchScript recording mock) and + // drives run() with mocked Input/Output. The Rust RunScriptCommand has no requireComposer + // override seam and Input/Output are concrete types, so the mocked harness is + // inexpressible; the event-side isDevMode downcast now exists (EventInterface::as_any), but + // that alone does not unblock the test. todo!() } /// ref: RunScriptCommandTest::testCanListScripts #[test] #[serial] -#[ignore = "Application::do_run registers composer.json scripts as commands; that path calls loader.register (class_loader.rs:288 -> spl_autoload_register at runtime.rs:231) which is a todo!() stub. With a 'scripts' key present, app_tester.run() panics there before the command executes"] fn test_can_list_scripts() { let tear_down = init_temp_composer( Some(&serde_json::json!({ @@ -82,7 +74,6 @@ fn test_can_list_scripts() { /// ref: RunScriptCommandTest::testCanDefineAliases #[test] #[serial] -#[ignore = "Application::do_run registers composer.json scripts as commands; that path calls loader.register (class_loader.rs:288 -> spl_autoload_register at runtime.rs:231) which is a todo!() stub. With a 'scripts' key present, app_tester.run() panics there before the command executes"] fn test_can_define_aliases() { let expected_aliases = vec!["one", "two", "three"]; @@ -131,19 +122,19 @@ fn test_can_define_aliases() { } #[test] -#[ignore = "requires writing and executing a PHP-generated Symfony Command class (file_put_contents MyCommand.php) loaded via composer autoload; fundamentally unportable, no PHP runtime command loading in shirabe"] +#[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; PHP-side Application milestone). The EventDispatcher-side Command-class path alone cannot satisfy the direct invocation and its argument definitions"] fn test_execution_of_simple_symfony_command() { - // TODO(phase-d): requires writing and executing a PHP-generated Symfony Command class - // (file_put_contents MyCommand.php) loaded via composer autoload; fundamentally unportable, no - // PHP runtime command loading in shirabe. + // 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; PHP-side Application milestone). todo!() } #[test] -#[ignore = "requires writing and executing a PHP-generated Symfony Command class (file_put_contents MyCommandWithDefinitions.php) loaded via composer autoload; fundamentally unportable, no PHP runtime command loading in shirabe"] +#[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; PHP-side Application milestone). The EventDispatcher-side Command-class path alone cannot satisfy the direct invocation and its argument definitions"] fn test_execution_of_symfony_command_with_configuration() { - // TODO(phase-d): requires writing and executing a PHP-generated Symfony Command class - // (file_put_contents MyCommandWithDefinitions.php) loaded via composer autoload; fundamentally - // unportable, no PHP runtime command loading in shirabe. + // 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; PHP-side Application milestone). todo!() } diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs index e8413c17..e9620158 100644 --- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs +++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs @@ -3,20 +3,30 @@ use crate::process_executor_mock::{ProcessExecutorMockGuard, cmd, get_process_executor_mock}; use indexmap::IndexMap; use serial_test::serial; +use shirabe::autoload::{AutoloadGeneratorInterface, ClassLoader}; use shirabe::composer::{ComposerHandle, PartialOrFullComposer}; use shirabe::config::Config; use shirabe::dependency_resolver::Transaction; +use shirabe::dependency_resolver::operation::AnyOperation; use shirabe::event_dispatcher::{Callable, EventDispatcher, EventInterface}; -use shirabe::installer::InstallerEvents; +use shirabe::filter::PlatformRequirementFilterInterface; +use shirabe::installer::{InstallationManagerInterface, InstallerEvents, InstallerInterface}; use shirabe::io::IOInterface; use shirabe::io::buffer_io::BufferIO; -use shirabe::package::{RootPackageHandle, RootPackageInterfaceHandle}; +use shirabe::package::{ + LockerInterface, PackageInterfaceHandle, RootPackageHandle, RootPackageInterfaceHandle, +}; +use shirabe::repository::{ + InstalledArrayRepository, InstalledRepositoryInterface, RepositoryInterfaceHandle, + RepositoryManagerInterface, +}; use shirabe::script::Event as ScriptEvent; use shirabe::script::ScriptEvents; use shirabe::util::platform::Platform; use shirabe::util::process_executor::{MockHandler, ProcessExecutor}; +use shirabe_class_map_generator::class_map::ClassMap; use shirabe_external_packages::symfony::console::output::output_interface; -use shirabe_php_shim::PHP_EOL; +use shirabe_php_shim::{PHP_EOL, PhpMixed}; fn tear_down() { Platform::clear_env("COMPOSER_SKIP_SCRIPTS"); @@ -322,32 +332,206 @@ fn test_dispatcher_doesnt_return_skipped_scripts() { let _ = &mut event; } -// The remaining ignored tests drive listeners that invoke PHP scripts (`Class::method`), require -// the autoloader rebuild of `make_autoloader` (an intentional no-op in the port), or rely on -// object-identity callables. None of those seams exist in the Rust port (the PHP-script -// invocation path is an unimplemented plugin-runtime `todo!`), so they remain ignored. +// The remaining ignored tests use, as their listeners, static methods of the PHPUnit test class +// `Composer\Test\EventDispatcher\EventDispatcherTest` itself (or object-identity array +// callables). The PHP-script invocation path is implemented (execute_event_php_script sends a +// CallStaticMethod over the RPC channel), but the worker child process cannot load that test +// class: it extends PHPUnit\Framework\TestCase and phpunit is not part of composer/vendor. +// Making these pass needs a decision on how to provide the listener methods to the child (e.g. a +// stand-in fixture class with the same FQCN and method bodies), which is not a call to make +// unilaterally under the no-test-alteration rule. #[test] -#[ignore = "listener `EventDispatcherTest::call` is a PHP-script callable; dynamic static-method invocation requires the plugin runtime (execute_event_php_script is todo!())"] +#[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): listener `EventDispatcherTest::call` is a PHP-script callable; dynamic - // static-method invocation requires the plugin runtime (execute_event_php_script is todo!()) + // 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!() } +// PHP mocks `Composer\Autoload\AutoloadGenerator` with onlyMethods(['buildPackageMap', +// 'parseAutoloads', 'createLoader', 'setDevMode']). +mockall::mock! { + #[derive(Debug)] + pub AutoloadGenerator {} + impl AutoloadGeneratorInterface for AutoloadGenerator { + fn set_dev_mode(&mut self, dev_mode: bool); + fn set_class_map_authoritative(&mut self, class_map_authoritative: bool); + fn set_apcu(&mut self, apcu: bool, apcu_prefix: Option); + fn set_run_scripts(&mut self, run_scripts: bool); + fn set_dry_run(&mut self, dry_run: bool); + fn set_platform_requirement_filter( + &mut self, + platform_requirement_filter: std::rc::Rc, + ); + fn dump<'a>( + &mut self, + config: &Config, + local_repo: &mut dyn InstalledRepositoryInterface, + root_package: RootPackageInterfaceHandle, + installation_manager: &mut dyn InstallationManagerInterface, + target_dir: &str, + scan_psr_packages: bool, + suffix: Option, + locker: Option<&'a mut dyn LockerInterface>, + strict_ambiguous: bool, + ) -> anyhow::Result; + fn build_package_map( + &self, + installation_manager: &mut dyn InstallationManagerInterface, + root_package: RootPackageInterfaceHandle, + packages: Vec, + ) -> anyhow::Result)>>; + fn parse_autoloads( + &self, + package_map: Vec<(PackageInterfaceHandle, Option)>, + root_package: RootPackageInterfaceHandle, + filtered_dev_packages: PhpMixed, + ) -> IndexMap; + fn create_loader<'a>( + &self, + autoloads: &IndexMap, + vendor_dir: Option, + ) -> ClassLoader; + } +} + +// PHP mocks `Composer\Repository\RepositoryManager` with onlyMethods(['getLocalRepository']). +mockall::mock! { + #[derive(Debug)] + pub RepositoryManager {} + impl RepositoryManagerInterface for RepositoryManager { + fn get_local_repository(&self) -> RepositoryInterfaceHandle; + fn get_repositories(&self) -> &Vec; + fn create_repository<'a>( + &self, + r#type: &str, + config: IndexMap, + name: Option<&'a str>, + ) -> anyhow::Result; + fn add_repository(&mut self, repository: RepositoryInterfaceHandle); + fn set_local_repository(&mut self, repository: RepositoryInterfaceHandle); + } +} + +// PHP mocks `Composer\Installer\InstallationManager` with disableOriginalConstructor(). +mockall::mock! { + #[derive(Debug)] + pub InstallationManager {} + impl InstallationManagerInterface for InstallationManager { + fn add_installer(&mut self, installer: Box); + fn remove_installer(&mut self, installer: &dyn InstallerInterface); + fn disable_plugins(&mut self); + fn is_package_installed( + &mut self, + repo: &mut dyn InstalledRepositoryInterface, + package: PackageInterfaceHandle, + ) -> anyhow::Result; + fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle); + fn execute( + &mut self, + repo: &mut dyn InstalledRepositoryInterface, + operations: Vec, + dev_mode: bool, + run_scripts: bool, + download_only: bool, + ) -> anyhow::Result<()>; + fn get_install_path(&self, package: PackageInterfaceHandle) -> Option; + fn set_output_progress(&mut self, output_progress: bool); + fn notify_installs(&mut self, io: std::rc::Rc>); + } +} + +/// ref: EventDispatcherTest::testDispatcherPassDevModeToAutoloadGeneratorForScriptEvents #[test] -#[ignore = "EventDispatcher::make_autoloader (PHP makeAutoloader, called from doDispatch's script branches) is an intentional no-op in the port, so AutoloadGeneratorInterface::set_dev_mode is never invoked and a set_dev_mode spy would observe nothing"] +#[serial] fn test_dispatcher_pass_dev_mode_to_autoload_generator_for_script_events() { let _tear_down = TearDown; - // TODO(phase-d): the PHP test spies on AutoloadGenerator::setDevMode, which PHP calls from - // makeAutoloader (invoked from doDispatch's script branches; it rebuilds and registers the - // project autoloader — loader->unregister, setDevMode(event->isDevMode()), buildPackageMap, - // parseAutoloads, createLoader->register — so that PHP-script listeners can be invoked). The - // Rust EventDispatcher::make_autoloader is an intentional no-op (see its TODO(plugin) - // marker), so set_dev_mode is never reached. A spy could be written against - // `dyn AutoloadGeneratorInterface` once make_autoloader does the real work. - todo!() + if !ensure_php_binary() { + // The php-script listener path queries class_exists through the PHP worker. + return; + } + + // dataProvider provideDevModes + for dev_mode in [true, false] { + let composer = create_composer_instance(); + + let mut generator = MockAutoloadGenerator::new(); + generator + .expect_set_dev_mode() + .with(mockall::predicate::eq(dev_mode)) + .times(1..) + .return_const(()); + generator + .expect_build_package_map() + .returning(|_, _, _| Ok(Vec::new())); + generator.expect_parse_autoloads().returning(|_, _, _| { + [ + ("psr-0".to_string(), PhpMixed::List(vec![])), + ("psr-4".to_string(), PhpMixed::List(vec![])), + ("classmap".to_string(), PhpMixed::List(vec![])), + ("files".to_string(), PhpMixed::List(vec![])), + ("exclude-from-classmap".to_string(), PhpMixed::List(vec![])), + ] + .into_iter() + .collect() + }); + generator + .expect_create_loader() + .returning(|_, _| ClassLoader::new(None)); + composer + .borrow_mut() + .set_autoload_generator(std::rc::Rc::new(std::cell::RefCell::new(generator))); + + let package: RootPackageInterfaceHandle = RootPackageHandle::new( + "foo".to_string(), + "1.0.0.0".to_string(), + "1.0.0".to_string(), + ) + .into(); + let mut scripts: IndexMap> = IndexMap::new(); + scripts.insert( + "scriptName".to_string(), + vec!["ClassName::testMethod".to_string()], + ); + package.set_scripts(scripts); + composer.borrow_mut().set_package(package); + + 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 mut dispatcher = + EventDispatcher::new(composer.upcast().downgrade(), null_io(), Some(process)); + + let mut event = ScriptEvent::new( + "scriptName".to_string(), + composer.downgrade(), + null_io(), + dev_mode, + Vec::new(), + IndexMap::new(), + ); + + dispatcher + .dispatch(Some("scriptName"), Some(&mut event)) + .unwrap(); + } } #[test] @@ -361,32 +545,32 @@ fn test_dispatcher_remove_listener() { } #[test] -#[ignore = "mixes a PHP-script listener (EventDispatcherTest::someMethod) into the stack; dynamic static-method invocation requires the plugin runtime (execute_event_php_script is todo!())"] +#[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): mixes a PHP-script listener (EventDispatcherTest::someMethod) into the - // stack; dynamic static-method invocation requires the plugin runtime - // (execute_event_php_script is todo!()) + // 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!() } #[test] -#[ignore = "second listener EventDispatcherTest::getTestEnv is a PHP-script callable; dynamic static-method invocation requires the plugin runtime (execute_event_php_script is todo!())"] +#[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): second listener EventDispatcherTest::getTestEnv is a PHP-script callable; - // dynamic static-method invocation requires the plugin runtime (execute_event_php_script is - // todo!()) + // 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!() } #[test] -#[ignore = "listeners are PHP-script callables (createsVendorBinFolderChecksEnv*) asserting on PATH; dynamic static-method invocation requires the plugin runtime (execute_event_php_script is todo!())"] +#[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): listeners are PHP-script callables (createsVendorBinFolderChecksEnv*) - // asserting on PATH; dynamic static-method invocation requires the plugin runtime - // (execute_event_php_script is todo!()) + // 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!() } -- cgit v1.3.1