diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-04 06:33:32 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-04 06:33:32 +0900 |
| commit | 11cdaae87c37e479fea6c39447041c312410b101 (patch) | |
| tree | 06e67edf1f437850f6f319c02f1304d130081fab /crates/shirabe/tests/plugin | |
| parent | 901cbbf285ed4e9c1f7bf75b413643c117a6105a (diff) | |
| download | php-shirabe-11cdaae87c37e479fea6c39447041c312410b101.tar.gz php-shirabe-11cdaae87c37e479fea6c39447041c312410b101.tar.zst php-shirabe-11cdaae87c37e479fea6c39447041c312410b101.zip | |
feat(plugin): instantiate plugin capabilities through the PHP RPC worker
getPluginCapability was a no-op returning None. Now it runs the real
flow: class_exists in the worker, new $capabilityClass($ctorArgs) with
the plugin's own phandle spliced in as $ctorArgs['plugin'], both
instanceof checks answered by is_a in the child, and a per-interface
Rust adapter over the resulting entity (CommandProvider and the plain
Capability marker; anything else is an explicit error).
getPluginCapabilities now propagates errors instead of swallowing them.
Capable::get_capabilities widens from IndexMap<String, String> to
IndexMap<String, PhpMixed>: upstream casts the return with (array) and
validates only the queried key, so the narrow type both rejected maps
Composer accepts and made the invalidImplementationClassNames data
provider unrepresentable. The old code also returned ' 0 ' (trimmed to
the falsy '0') as a valid class name where upstream throws; the
rewritten validation follows upstream's empty/is_string/trim sequence.
CommandProvider::get_commands returns BaseCommand adapters whose name is
read back over RPC after the PHP constructor ran configure(); executing
one still needs the PHP-side Symfony Application and stays an explicit
error. The is_array / instanceof BaseCommand checks that upstream's
Application::getPluginCommands performs on the raw getCommands value
live in the adapter, because Vec<Box<dyn BaseCommand>> asserts every
element up front.
Ports testCommandProviderCapability (plugin-v8 end to end against the
real worker) and testQueryingWithInvalidCapabilityClassNameThrows (all
eight provider cases); the two tests that pass a PHPUnit mock plugin
into PHP stay ignored — a Rust-native mock has no PHP-side entity to
cross the boundary as $ctorArgs['plugin'].
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/plugin')
| -rw-r--r-- | crates/shirabe/tests/plugin/plugin_installer_test.rs | 122 |
1 files changed, 97 insertions, 25 deletions
diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs index 70548ba6..ec5f4fe3 100644 --- a/crates/shirabe/tests/plugin/plugin_installer_test.rs +++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs @@ -18,7 +18,7 @@ use shirabe::json::JsonFile; use shirabe::package::loader::{ArrayLoader, JsonLoader, JsonLoaderInput}; use shirabe::package::{Locker, LockerInterface, PackageInterfaceHandle, RootPackageHandle}; use shirabe::plugin::plugin_interface::PluginInterface; -use shirabe::plugin::{Capable, PluginManager}; +use shirabe::plugin::{Capable, PluginManager, composer_handle_value, io_handle_value}; use shirabe::repository::{ InstalledArrayRepository, InstalledRepositoryInterfaceHandle, RepositoryInterfaceHandle, RepositoryManagerInterface, @@ -633,13 +633,44 @@ fn test_plugin_range_constraints_work_only_with_certain_api_version() { todo!() } -#[ignore = "get_plugin_capability never instantiates a capability class (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime instantiation is unported"] #[test] fn test_command_provider_capability() { - // TODO(phase-d): get_plugin_capability never instantiates a capability class (TODO(plugin) - // in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime - // instantiation is also unported. - todo!() + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + // PHP mocks the repository so getPackages() returns [$this->packages[7]] (plugin-v8); + // the real InstalledArrayRepository reaches the same state by adding the package. + set_up + .repository + .borrow_mut() + .add_package(set_up.packages[7].clone()) + .unwrap(); + let _installer = new_installer(&set_up); + set_up.pm.borrow_mut().load_installed_plugins().unwrap(); + + let mut ctor_args = IndexMap::new(); + ctor_args.insert( + "composer".to_string(), + composer_handle_value(&set_up.composer), + ); + ctor_args.insert("io".to_string(), io_handle_value(&set_up.io_dyn).unwrap()); + let caps = set_up + .pm + .borrow() + .get_plugin_capabilities("Composer\\Plugin\\Capability\\CommandProvider", ctor_args) + .unwrap(); + assert_eq!(1, caps.len()); + // PHP: assertInstanceOf('Composer\Plugin\Capability\CommandProvider', $caps[0]). + let provider = caps[0] + .as_command_provider() + .expect("the capability implements CommandProvider"); + + let commands = provider.get_commands().unwrap(); + assert_eq!(1, commands.len()); + // PHP: assertInstanceOf('Composer\Command\BaseCommand', $commands[0]) is witnessed by the + // element type of Vec<Box<dyn BaseCommand>>. } // A hand-written stub is used in place of PHPUnit's @@ -692,27 +723,63 @@ fn test_incapable_plugin_is_correctly_detected() { assert!(result.is_none()); } -#[ignore = "Requires runtime instantiation of Mock\\Capability via get_plugin_capability; not implemented (TODO(plugin))"] +#[ignore = "the mocked Capable plugin is Rust-native and cannot cross the RPC boundary: getPluginCapability passes the plugin itself as $ctorArgs['plugin'] to Mock\\Capability, and the test reads $capability->args back; PluginInterface has no rust-proxy stub (TODO(plugin))"] #[test] fn test_capability_implements_composer_plugin_api_class_and_is_constructed_with_args() { - // TODO(phase-d): requires runtime instantiation of Mock\Capability via - // get_plugin_capability; not implemented (TODO(plugin)). + // TODO(phase-d): the mocked Capable plugin is Rust-native and cannot cross the RPC + // boundary — getPluginCapability must pass the plugin itself as $ctorArgs['plugin'] to + // Mock\Capability's constructor, and the test reads $capability->args back over RPC; + // PluginInterface has no rust-proxy stub (TODO(plugin)). todo!() } -// PluginManager::get_capability_implementation_class_name (via Capable::get_capabilities) -// resolves capability class names through an IndexMap<String, String>, so most of PHP's -// invalidImplementationClassNames data provider (null, 0, 1000, [1], [], stdClass) cannot be -// represented at all in the ported type — only the string entries ("", " ") could be -// constructed. Per the phase-d rule against porting a subset of a data provider, this whole -// test must stay unported rather than dropping the non-string cases. -#[ignore = "Capable::get_capabilities is typed IndexMap<String, String>; most of the invalidImplementationClassNames data provider (null, 0, 1000, [1], [], stdClass) is not representable, and porting only the string cases would drop data-provider entries (TODO(phase-d))"] +/// PHP data provider `invalidImplementationClassNames`, one PhpMixed per entry. +fn invalid_implementation_class_names() -> Vec<PhpMixed> { + vec![ + PhpMixed::Null, + PhpMixed::String(String::new()), + PhpMixed::Int(0), + PhpMixed::Int(1000), + PhpMixed::String(" ".to_string()), + PhpMixed::List(vec![PhpMixed::Int(1)]), + PhpMixed::List(vec![]), + // PHP: new \stdClass() + PhpMixed::Object(IndexMap::new()), + ] +} + #[test] fn test_querying_with_invalid_capability_class_name_throws() { - // TODO(phase-d): Capable::get_capabilities is typed IndexMap<String, String>; most of the - // invalidImplementationClassNames data provider (null, 0, 1000, [1], [], stdClass) is not - // representable, and porting only the string cases would drop data-provider entries. - todo!() + let capability_api = "Composer\\Plugin\\Capability\\Capability"; + for invalid_implementation_class_name in invalid_implementation_class_names() { + let set_up = set_up(); + let plugin = CapablePlugin { + capabilities: IndexMap::from([( + capability_api.to_string(), + invalid_implementation_class_name.clone(), + )]), + get_capabilities_calls: std::cell::RefCell::new(0), + }; + + let err = + match set_up + .pm + .borrow() + .get_plugin_capability(&plugin, capability_api, IndexMap::new()) + { + Err(err) => err, + Ok(_) => panic!( + "expected UnexpectedValueException for {invalid_implementation_class_name:?}" + ), + }; + assert!( + err.downcast_ref::<shirabe_php_shim::UnexpectedValueException>() + .is_some(), + "expected UnexpectedValueException for {invalid_implementation_class_name:?}, got: {err}" + ); + // PHP: ->expects($this->once())->method('getCapabilities'). + assert_eq!(1, *plugin.get_capabilities_calls.borrow()); + } } // A hand-written stub plays the role of PHPUnit's @@ -721,6 +788,7 @@ fn test_querying_with_invalid_capability_class_name_throws() { // is reproduced with a call counter asserted after the call. #[derive(Debug)] struct CapablePlugin { + capabilities: IndexMap<String, PhpMixed>, get_capabilities_calls: std::cell::RefCell<i64>, } @@ -759,9 +827,9 @@ impl PluginInterface for CapablePlugin { } impl Capable for CapablePlugin { - fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, String>> { + fn get_capabilities(&self) -> anyhow::Result<IndexMap<String, PhpMixed>> { *self.get_capabilities_calls.borrow_mut() += 1; - Ok(IndexMap::new()) + Ok(self.capabilities.clone()) } } @@ -770,6 +838,7 @@ fn test_querying_non_provided_capability_returns_null_safely() { let set_up = set_up(); let plugin = CapablePlugin { + capabilities: IndexMap::new(), get_capabilities_calls: std::cell::RefCell::new(0), }; @@ -786,10 +855,13 @@ fn test_querying_non_provided_capability_returns_null_safely() { assert_eq!(1, *plugin.get_capabilities_calls.borrow()); } -#[ignore = "Requires runtime get_plugin_capability with PHP-class-name capability lookup (class_exists/instanceof checks are unported TODO(plugin)); not implemented"] +#[ignore = "the '\\stdClass' data-provider case reaches new \\stdClass($ctorArgs), which needs the Rust-native mocked plugin passed as $ctorArgs['plugin'] over RPC (no PluginInterface rust-proxy stub exists, TODO(plugin)); porting only the NonExistentClassLikeMiddleClass case would drop a data-provider entry (TODO(phase-d))"] #[test] fn test_querying_with_non_existing_or_wrong_capability_class_types_throws() { - // TODO(phase-d): requires runtime get_plugin_capability with PHP-class-name capability lookup - // (class_exists/instanceof checks are unported TODO(plugin)); not implemented. + // TODO(phase-d): the '\stdClass' data-provider case reaches new \stdClass($ctorArgs), + // which needs the Rust-native mocked plugin passed as $ctorArgs['plugin'] over RPC — no + // PluginInterface rust-proxy stub exists (TODO(plugin)). Porting only the + // NonExistentClassLikeMiddleClass case (which fails at class_exists, before the plugin is + // passed) would drop a data-provider entry. todo!() } |
