aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-03 01:30:26 +0900
committernsfisis <nsfisis@gmail.com>2026-08-03 01:30:26 +0900
commit20f7a7826ae048d249e0d837ca6393a5f09c9ba6 (patch)
treee35b541857c47543b54afcacf58b82ef3a7cce70 /crates/shirabe/tests
parentbf7ec47524a068c7fa7658f03f4dd44957f492a6 (diff)
downloadphp-shirabe-20f7a7826ae048d249e0d837ca6393a5f09c9ba6.tar.gz
php-shirabe-20f7a7826ae048d249e0d837ca6393a5f09c9ba6.tar.zst
php-shirabe-20f7a7826ae048d249e0d837ca6393a5f09c9ba6.zip
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 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests')
-rw-r--r--crates/shirabe/tests/command/run_script_command_test.rs49
-rw-r--r--crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs246
2 files changed, 235 insertions, 60 deletions
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<String>);
+ 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<dyn PlatformRequirementFilterInterface>,
+ );
+ 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<String>,
+ locker: Option<&'a mut dyn LockerInterface>,
+ strict_ambiguous: bool,
+ ) -> anyhow::Result<ClassMap>;
+ fn build_package_map(
+ &self,
+ installation_manager: &mut dyn InstallationManagerInterface,
+ root_package: RootPackageInterfaceHandle,
+ packages: Vec<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Vec<(PackageInterfaceHandle, Option<String>)>>;
+ fn parse_autoloads(
+ &self,
+ package_map: Vec<(PackageInterfaceHandle, Option<String>)>,
+ root_package: RootPackageInterfaceHandle,
+ filtered_dev_packages: PhpMixed,
+ ) -> IndexMap<String, PhpMixed>;
+ fn create_loader<'a>(
+ &self,
+ autoloads: &IndexMap<String, PhpMixed>,
+ vendor_dir: Option<String>,
+ ) -> 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<RepositoryInterfaceHandle>;
+ fn create_repository<'a>(
+ &self,
+ r#type: &str,
+ config: IndexMap<String, PhpMixed>,
+ name: Option<&'a str>,
+ ) -> anyhow::Result<RepositoryInterfaceHandle>;
+ 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<dyn InstallerInterface>);
+ 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<bool>;
+ fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle);
+ fn execute(
+ &mut self,
+ repo: &mut dyn InstalledRepositoryInterface,
+ operations: Vec<AnyOperation>,
+ dev_mode: bool,
+ run_scripts: bool,
+ download_only: bool,
+ ) -> anyhow::Result<()>;
+ fn get_install_path(&self, package: PackageInterfaceHandle) -> Option<String>;
+ fn set_output_progress(&mut self, output_progress: bool);
+ fn notify_installs(&mut self, io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>);
+ }
+}
+
+/// 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<String, Vec<String>> = 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!()
}