diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-04 04:07:41 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-04 05:43:24 +0900 |
| commit | 695365a0ad68e4534425c64b7a6a4b6598b68ef7 (patch) | |
| tree | a906972618f08e31012f721c717321bd08164cde /crates/shirabe/tests/plugin | |
| parent | 261516d5ce6f8b0d69cf9e3da7dd2f0ef1cdc36a (diff) | |
| download | php-shirabe-695365a0ad68e4534425c64b7a6a4b6598b68ef7.tar.gz php-shirabe-695365a0ad68e4534425c64b7a6a4b6598b68ef7.tar.zst php-shirabe-695365a0ad68e4534425c64b7a6a4b6598b68ef7.zip | |
feat(plugin): dispatch plugin event subscribers through the RPC worker
Wires the addPlugin subscriber branch end to end: EventSubscriberInterface
and Capable become fallible and dyn-compatible (their sole implementor is
the PHP plugin proxy, which answers getSubscribedEvents over RPC), listeners
register as Callable::PhpMethod and are invoked with a per-call event handle,
and the R table now drops entries when a child-side stub destructs. The R
table keeps its IndexMap with monotonically increasing handles, so released
handles are never reused and no generation counter is needed.
Upstream has no subscriber-plugin test, so the path is covered by a
Shirabe-owned fixture exercising all three getSubscribedEvents shapes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/plugin')
5 files changed, 215 insertions, 13 deletions
diff --git a/crates/shirabe/tests/plugin/fixtures/subscriber-v1/Subscriber/Plugin.php b/crates/shirabe/tests/plugin/fixtures/subscriber-v1/Subscriber/Plugin.php new file mode 100644 index 00000000..f75763ba --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/subscriber-v1/Subscriber/Plugin.php @@ -0,0 +1,61 @@ +<?php + +namespace Subscriber; + +use Composer\Composer; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; + +class Plugin implements PluginInterface, EventSubscriberInterface +{ + public $version = 'subscriber-v1'; + + /** @var IOInterface */ + private $io; + + public function activate(Composer $composer, IOInterface $io) + { + $this->io = $io; + $io->write('activate subscriber-v1'); + } + + public function deactivate(Composer $composer, IOInterface $io) + { + } + + public function uninstall(Composer $composer, IOInterface $io) + { + } + + public static function getSubscribedEvents() + { + return [ + 'post-install-cmd' => 'onPostInstall', + 'shirabe-priority-event' => [['early', 10], ['late', -10]], + 'shirabe-false-event' => ['returnsFalse', 0], + ]; + } + + public function onPostInstall($event) + { + $this->io->write('subscriber saw ' . $event->getName()); + } + + public function early($event) + { + $this->io->write('early listener'); + } + + public function late($event) + { + $this->io->write('late listener'); + } + + public function returnsFalse($event) + { + $this->io->write('failing listener'); + + return false; + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/subscriber-v1/composer.json b/crates/shirabe/tests/plugin/fixtures/subscriber-v1/composer.json new file mode 100644 index 00000000..40659606 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/subscriber-v1/composer.json @@ -0,0 +1,12 @@ +{ + "name": "subscriber-v1", + "version": "1.0.0", + "type": "composer-plugin", + "autoload": { "psr-0": { "Subscriber": "" } }, + "extra": { + "class": "Subscriber\\Plugin" + }, + "require": { + "composer-plugin-api": "^2.0" + } +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index 62c8cfa8..3a66bb75 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -4,3 +4,4 @@ mod async_runtime; mod config_stub; mod plugin_installer_test; +mod subscriber_test; diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs index 5dfb6e06..70548ba6 100644 --- a/crates/shirabe/tests/plugin/plugin_installer_test.rs +++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs @@ -35,7 +35,7 @@ use tempfile::TempDir; /// The register/activate flow runs the plugin in the real PHP worker; without a PHP binary the /// worker cannot start. Tests exercising it return early, following the convention of the /// non-mock tests in `shirabe-php-rpc`. -fn php_runtime_available() -> bool { +pub(crate) fn php_runtime_available() -> bool { PhpExecutableFinder::new().find(false).is_some() } @@ -45,7 +45,7 @@ fn php_runtime_available() -> bool { /// race the other's `class_exists` checks, so the worker-touching tests run serialized. static PHP_WORKER_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(()); -fn lock_php_worker() -> std::sync::MutexGuard<'static, ()> { +pub(crate) fn lock_php_worker() -> std::sync::MutexGuard<'static, ()> { PHP_WORKER_TESTS .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()) @@ -62,6 +62,16 @@ fn fixtures_dir() -> String { .to_string() } +/// Shirabe-owned fixtures with no upstream counterpart (see `subscriber_test.rs`). +pub(crate) fn shirabe_fixtures_dir() -> String { + let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/plugin/fixtures"); + dir.canonicalize() + .expect("the Shirabe plugin fixtures directory must exist") + .to_str() + .unwrap() + .to_string() +} + // PHP mocks `Composer\Downloader\DownloadManager`; install/update/remove resolve to null and the // other methods are never reached by these tests. mockall::mock! { @@ -184,7 +194,16 @@ impl InstallationManagerInterface for MockInstallationManager { } fn get_install_path(&self, package: PackageInterfaceHandle) -> Option<String> { - Some(format!("{}/{}", fixtures_dir(), package.get_pretty_name())) + let upstream = format!("{}/{}", fixtures_dir(), package.get_pretty_name()); + if std::path::Path::new(&upstream).exists() { + return Some(upstream); + } + // Shirabe-specific fixtures (subscriber_test) live next to this test binary. + Some(format!( + "{}/{}", + shirabe_fixtures_dir(), + package.get_pretty_name() + )) } fn set_output_progress(&mut self, _output_progress: bool) {} @@ -214,20 +233,20 @@ fn locker_installation_manager( } #[derive(Debug)] -struct SetUp { - io: std::rc::Rc<std::cell::RefCell<BufferIO>>, - io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, - pm: std::rc::Rc<std::cell::RefCell<PluginManager>>, +pub(crate) struct SetUp { + pub(crate) io: std::rc::Rc<std::cell::RefCell<BufferIO>>, + pub(crate) io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + pub(crate) pm: std::rc::Rc<std::cell::RefCell<PluginManager>>, autoload_generator: std::rc::Rc<std::cell::RefCell<AutoloadGenerator>>, packages: Vec<PackageInterfaceHandle>, - repository: InstalledRepositoryInterfaceHandle, + pub(crate) repository: InstalledRepositoryInterfaceHandle, // Keeps the Composer alive; PluginManager only holds a weak back-reference to it. - composer: ComposerHandle, + pub(crate) composer: ComposerHandle, // PHP's tearDown() removes this directory; TempDir does the same on drop. _directory: TempDir, } -fn set_up() -> SetUp { +pub(crate) fn set_up() -> SetUp { let loader = JsonLoader::new(Box::new(ArrayLoader::new(None, false))); let mut packages = vec![]; let directory = TempDir::new().unwrap(); @@ -380,7 +399,7 @@ fn plugin_property( } } -fn new_installer(set_up: &SetUp) -> PluginInstaller { +pub(crate) fn new_installer(set_up: &SetUp) -> PluginInstaller { PluginInstaller::new( set_up.io_dyn.clone(), set_up.composer.upcast().downgrade(), @@ -740,9 +759,9 @@ impl PluginInterface for CapablePlugin { } impl Capable for CapablePlugin { - fn get_capabilities(&self) -> IndexMap<String, String> { + fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, String>> { *self.get_capabilities_calls.borrow_mut() += 1; - IndexMap::new() + Ok(IndexMap::new()) } } diff --git a/crates/shirabe/tests/plugin/subscriber_test.rs b/crates/shirabe/tests/plugin/subscriber_test.rs new file mode 100644 index 00000000..8de3d8c2 --- /dev/null +++ b/crates/shirabe/tests/plugin/subscriber_test.rs @@ -0,0 +1,109 @@ +//! Shirabe-specific integration tests for the subscriber plugin path: upstream Composer +//! has no test that exercises `addSubscriber`/`getSubscribedEvents` through a real plugin, so +//! these tests use a Shirabe-owned fixture (`fixtures/subscriber-v1`) instead of a ported one. + +use crate::async_runtime::run; +use crate::plugin_installer_test::{lock_php_worker, new_installer, php_runtime_available, set_up}; +use shirabe::installer::InstallerInterface; +use shirabe::package::PackageInterfaceHandle; +use shirabe::package::loader::{ArrayLoader, JsonLoader, JsonLoaderInput}; + +fn subscriber_fixture_package() -> PackageInterfaceHandle { + let loader = JsonLoader::new(Box::new(ArrayLoader::new(None, false))); + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/plugin/fixtures/subscriber-v1/composer.json"); + loader + .load(JsonLoaderInput::String( + path.canonicalize().unwrap().to_str().unwrap().to_string(), + )) + .unwrap() +} + +/// Installs the subscriber fixture plugin: activate runs in the PHP child and +/// `addSubscriber` registers its listeners with the event dispatcher. +fn install_subscriber_plugin(set_up: &crate::plugin_installer_test::SetUp) { + let installer = new_installer(set_up); + set_up.pm.borrow_mut().load_installed_plugins().unwrap(); + run(installer.install(&set_up.repository, subscriber_fixture_package())).unwrap(); + assert_eq!("activate subscriber-v1\n", set_up.io.borrow().get_output()); +} + +fn dispatch(set_up: &crate::plugin_installer_test::SetUp, event_name: &str) -> i64 { + let dispatcher = set_up.composer.borrow().get_event_dispatcher(); + let result = dispatcher.borrow_mut().dispatch(Some(event_name), None); + result.unwrap() +} + +#[test] +fn test_subscriber_listener_receives_event() { + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + install_subscriber_plugin(&set_up); + + // 'post-install-cmd' => 'onPostInstall' (bare method-name shape); the listener calls + // $event->getName() back over RPC. + let return_code = dispatch(&set_up, "post-install-cmd"); + + assert_eq!(0, return_code); + assert_eq!( + "activate subscriber-v1\nsubscriber saw post-install-cmd\n", + set_up.io.borrow().get_output() + ); +} + +#[test] +fn test_subscriber_listeners_run_in_priority_order() { + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + install_subscriber_plugin(&set_up); + + // 'shirabe-priority-event' => [['early', 10], ['late', -10]] (multi-handler shape). + let return_code = dispatch(&set_up, "shirabe-priority-event"); + + assert_eq!(0, return_code); + assert_eq!( + "activate subscriber-v1\nearly listener\nlate listener\n", + set_up.io.borrow().get_output() + ); +} + +#[test] +fn test_subscriber_listener_returning_false_sets_return_code() { + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + install_subscriber_plugin(&set_up); + + // 'shirabe-false-event' => ['returnsFalse', 0] (method+priority shape); PHP maps a false + // listener return to exit code 1. + let return_code = dispatch(&set_up, "shirabe-false-event"); + + assert_eq!(1, return_code); + assert_eq!( + "activate subscriber-v1\nfailing listener\n", + set_up.io.borrow().get_output() + ); +} + +#[test] +fn test_unrelated_event_does_not_reach_the_subscriber() { + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + install_subscriber_plugin(&set_up); + + let return_code = dispatch(&set_up, "shirabe-unrelated-event"); + + assert_eq!(0, return_code); + assert_eq!("activate subscriber-v1\n", set_up.io.borrow().get_output()); +} |
