aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Shirabe/RustCapablePluginStub.php17
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Shirabe/RustPluginStub.php89
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs8
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs7
-rw-r--r--crates/shirabe/src/plugin/capability/capability.rs6
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs80
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs34
-rw-r--r--crates/shirabe/tests/plugin/plugin_installer_test.rs189
8 files changed, 366 insertions, 64 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/RustCapablePluginStub.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/RustCapablePluginStub.php
new file mode 100644
index 00000000..7a7c7eef
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/RustCapablePluginStub.php
@@ -0,0 +1,17 @@
+<?php
+
+// The Capable flavour of RustPluginStub: `$plugin instanceof Capable` decides whether Composer
+// asks a plugin for capabilities, so the stub class is picked to match what the Rust-side
+// plugin implements.
+
+namespace Shirabe;
+
+use Composer\Plugin\Capable;
+
+class RustCapablePluginStub extends RustPluginStub implements Capable
+{
+ public function getCapabilities()
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getCapabilities', []);
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/RustPluginStub.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/RustPluginStub.php
new file mode 100644
index 00000000..a60c889a
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/RustPluginStub.php
@@ -0,0 +1,89 @@
+<?php
+
+// Proxy stub for a plugin whose implementation lives on the Rust side. It has no counterpart
+// class in Composer: a plugin is normally PHP code running in this process, and only a
+// Rust-implemented one needs a stand-in here (a capability constructor receives the plugin
+// itself as $ctorArgs['plugin']).
+
+namespace Shirabe;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+
+class RustPluginStub implements PluginInterface, \ShirabeRustStub
+{
+ /** @var int */
+ protected $__rhandle;
+ /** @var int */
+ protected $__epoch;
+
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
+ {
+ $this->__rhandle = $rhandle;
+ $this->__epoch = $epoch;
+ }
+
+ public function __destruct()
+ {
+ \ShirabeRustObjectRegistry::release($this->__rhandle);
+ }
+
+ public function __shirabeRustHandleDescriptor(): array
+ {
+ return [
+ '__rhandle' => $this->__rhandle,
+ '__class' => static::class,
+ '__epoch' => $this->__epoch,
+ ];
+ }
+
+ public function __clone()
+ {
+ // PHP has already shallow-copied this stub, so both copies would point at one
+ // entity and release it twice. The Rust side clones the entity instead, applying
+ // whatever __clone semantics the real class defines, and this copy rebinds to the
+ // fresh handle. Entities without clone semantics answer with an explicit error.
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust($this->__rhandle, '__shirabeClone', []);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
+ public function __get($name)
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, '__get', [$name]);
+ }
+
+ public function __set($name, $value): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]);
+ }
+
+ public function __isset($name): bool
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, '__isset', [$name]);
+ }
+
+ public function __unset($name): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, '__unset', [$name]);
+ }
+
+ public function activate(Composer $composer, IOInterface $io)
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'activate', [$composer, $io]);
+ }
+
+ public function deactivate(Composer $composer, IOInterface $io)
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'deactivate', [$composer, $io]);
+ }
+
+ public function uninstall(Composer $composer, IOInterface $io)
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'uninstall', [$composer, $io]);
+ }
+}
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index 6dfdebf5..a4742664 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -969,6 +969,14 @@ const RUNTIME_FILES: &[(&str, &str)] = &[
"Shirabe/RustCommandStub.php",
include_str!("../php/runtime/Shirabe/RustCommandStub.php"),
),
+ (
+ "Shirabe/RustPluginStub.php",
+ include_str!("../php/runtime/Shirabe/RustPluginStub.php"),
+ ),
+ (
+ "Shirabe/RustCapablePluginStub.php",
+ include_str!("../php/runtime/Shirabe/RustCapablePluginStub.php"),
+ ),
];
struct Worker {
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index 72db0543..ca6c2adc 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -1586,6 +1586,13 @@ try {{
Ok(())
}
+ /// For testing only: a test that never registers a plugin package still needs the Composer
+ /// PHP runtime in the worker before a class of its own can implement a Composer interface
+ /// there.
+ pub fn __ensure_composer_php_runtime() -> anyhow::Result<()> {
+ Self::ensure_composer_php_runtime()
+ }
+
fn composer_php_runtime_autoload() -> Option<String> {
if let Some(dir) = Platform::get_env("SHIRABE_COMPOSER_PHP_DIR") {
let path = std::path::Path::new(&dir)
diff --git a/crates/shirabe/src/plugin/capability/capability.rs b/crates/shirabe/src/plugin/capability/capability.rs
index aa70ffa0..bb634b9f 100644
--- a/crates/shirabe/src/plugin/capability/capability.rs
+++ b/crates/shirabe/src/plugin/capability/capability.rs
@@ -11,4 +11,10 @@ pub trait Capability {
fn as_command_provider(&self) -> Option<&dyn CommandProvider> {
None
}
+
+ /// For testing only: recovers the PHP-backed proxy so tests can read capability properties
+ /// the way PHPUnit asserts `$capability->args`.
+ fn __as_php_capability_proxy(&self) -> Option<&crate::plugin::PhpCapabilityProxy> {
+ None
+ }
}
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 715a21dd..8e24dbfe 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -54,6 +54,7 @@ enum RustEntity {
std::rc::Rc<std::cell::RefCell<dyn crate::event_dispatcher::EventDispatcherInterface>>,
),
Operation(std::rc::Rc<AnyOperation>),
+ Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>),
}
/// The pointer identity backing R-table interning: the same shared instance must always cross
@@ -74,6 +75,7 @@ fn entity_ptr_id(entity: &RustEntity) -> usize {
std::rc::Rc::as_ptr(dispatcher) as *const () as usize
}
RustEntity::Operation(operation) => std::rc::Rc::as_ptr(operation) as *const () as usize,
+ RustEntity::Plugin(plugin) => std::rc::Rc::as_ptr(plugin) as *const () as usize,
}
}
@@ -196,6 +198,22 @@ pub(crate) fn package_handle_value(
rust_handle_value(rhandle, class)
}
+/// Registers a Rust-implemented plugin and returns its wire descriptor. A PHP-implemented
+/// plugin never takes this route: its entity lives in the child's P table already.
+pub fn plugin_handle_value(
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
+) -> PluginValue {
+ // TODO(plugin): a Rust-implemented plugin that is also an EventSubscriberInterface crosses
+ // as one of these two classes, so `instanceof EventSubscriberInterface` is false in the
+ // child; Composer's own subscriber dispatch runs on the Rust side and never asks.
+ let class = match plugin.borrow().as_capable() {
+ Some(_) => "Shirabe\\RustCapablePluginStub",
+ None => "Shirabe\\RustPluginStub",
+ };
+ let rhandle = register_entity(RustEntity::Plugin(plugin.clone()));
+ rust_handle_value(rhandle, class)
+}
+
/// Registers a solver operation and returns its wire descriptor.
pub(crate) fn operation_handle_value(operation: &std::rc::Rc<AnyOperation>) -> PluginValue {
let class = operation_stub_class(operation);
@@ -363,6 +381,7 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
Some(RustEntity::Operation(operation)) => {
dispatch_operation_method(&operation, method_name, &args)
}
+ Some(RustEntity::Plugin(plugin)) => dispatch_plugin_method(&plugin, method_name),
None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
}
}
@@ -539,7 +558,8 @@ fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> {
| RustEntity::RepositoryManager(_)
| RustEntity::Repository(_)
| RustEntity::EventDispatcher(_)
- | RustEntity::Operation(_) => {
+ | RustEntity::Operation(_)
+ | RustEntity::Plugin(_) => {
return Err(runtime_throw(
"cloning this Rust-side entity over RPC is not supported".to_string(),
));
@@ -592,6 +612,33 @@ fn dispatch_property_access(
)))
}
+fn dispatch_plugin_method(
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
+ method_name: &str,
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "getCapabilities" => {
+ let plugin = plugin.borrow();
+ let capable = plugin.as_capable().ok_or_else(|| {
+ runtime_throw(format!(
+ "plugin {} does not implement Capable",
+ plugin.get_class_name()
+ ))
+ })?;
+ let capabilities = capable
+ .get_capabilities()
+ .map_err(|error| runtime_throw(format!("getCapabilities failed: {error:#}")))?;
+ Ok(PluginValue::from_php_mixed(&PhpMixed::Array(capabilities)))
+ }
+ // TODO(plugin): the lifecycle methods would have to turn the `$composer`/`$io` stubs the
+ // child passes back into the Rust handles they stand for; nothing calls them, because
+ // Composer activates a Rust-implemented plugin on the Rust side.
+ other => Err(runtime_throw(format!(
+ "the plugin method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
fn dispatch_composer_method(
composer: &ComposerHandle,
method_name: &str,
@@ -2582,9 +2629,38 @@ impl PhpCapabilityProxy {
pub(crate) fn new(handle: PhpObjHandle) -> Self {
Self { handle }
}
+
+ /// For testing only: the entity descriptor, whose class and interface list answer the
+ /// `assertInstanceOf` checks PHPUnit makes on a capability.
+ pub fn __handle(&self) -> &PhpObjHandle {
+ &self.handle
+ }
+
+ /// For testing only: reads a public property of the capability entity in the child. Unlike
+ /// `PhpPluginProxy::__get_property` the value keeps its wire form, so a test can assert the
+ /// object identity behind a handle instead of only the plain data around it.
+ pub fn __get_property(&self, name: &str) -> anyhow::Result<PluginValue> {
+ let outcome = shirabe_php_rpc::call_function(
+ "__shirabe_get_property",
+ vec![
+ PluginValue::PhpHandle(self.handle.clone()),
+ PluginValue::string(name),
+ ],
+ )?;
+ match outcome {
+ Ok(value) => Ok(value),
+ Err(throw) => {
+ Err(shirabe_php_shim::RuntimeException::with_code(throw.message, throw.code).into())
+ }
+ }
+ }
}
-impl Capability for PhpCapabilityProxy {}
+impl Capability for PhpCapabilityProxy {
+ fn __as_php_capability_proxy(&self) -> Option<&PhpCapabilityProxy> {
+ Some(self)
+ }
+}
impl Drop for PhpCapabilityProxy {
fn drop(&mut self) {
diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs
index 0746a9b4..0667c6a9 100644
--- a/crates/shirabe/src/plugin/plugin_manager.rs
+++ b/crates/shirabe/src/plugin/plugin_manager.rs
@@ -16,6 +16,7 @@ use crate::plugin::PluginBlockedException;
use crate::plugin::capability::Capability;
use crate::plugin::php_plugin_proxy::{
PhpCapabilityProxy, PhpCommandProviderProxy, PhpPluginProxy, PluginRpcDispatcher, php_is_a,
+ plugin_handle_value,
};
use crate::plugin::plugin_interface::{self, PluginInterface};
use crate::repository::InstalledRepository;
@@ -1031,15 +1032,16 @@ impl PluginManager {
pub fn get_plugin_capability(
&self,
- plugin: &dyn PluginInterface,
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
capability_class_name: &str,
ctor_args: IndexMap<String, PluginValue>,
) -> anyhow::Result<Option<Box<dyn Capability>>> {
- let capability_class =
- match self.get_capability_implementation_class_name(plugin, capability_class_name)? {
- Some(c) => c,
- None => return Ok(None),
- };
+ let capability_class = match self
+ .get_capability_implementation_class_name(&*plugin.borrow(), capability_class_name)?
+ {
+ Some(c) => c,
+ None => return Ok(None),
+ };
// PHP: if (!class_exists($capabilityClass))
let exists = unwrap_php_result(call_function_with_dispatcher(
@@ -1051,23 +1053,21 @@ impl PluginManager {
return Err(RuntimeException::new(format!(
"Cannot instantiate Capability, as class {} from plugin {} does not exist.",
capability_class,
- plugin.get_class_name()
+ plugin.borrow().get_class_name()
))
.into());
}
// PHP: $ctorArgs['plugin'] = $plugin; the capability constructor receives the plugin
- // instance itself, so a plugin with no PHP-side entity cannot be represented.
- let plugin_value = match plugin.__as_php_plugin_proxy() {
+ // instance itself. A PHP-implemented plugin already has an entity in the child's P
+ // table; a Rust-implemented one crosses as a handle to its R-table entity.
+ let plugin_value = match plugin.borrow().__as_php_plugin_proxy() {
Some(proxy) => PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle {
phandle: proxy.phandle,
class: proxy.class.clone(),
implements: proxy.implements.clone(),
}),
- None => anyhow::bail!(
- "cannot instantiate capability {capability_class}: plugin {} has no PHP-side entity to pass as $ctorArgs['plugin']",
- plugin.get_class_name()
- ),
+ None => plugin_handle_value(plugin),
};
let mut ctor_args = ctor_args;
ctor_args.insert("plugin".to_string(), plugin_value);
@@ -1121,11 +1121,9 @@ impl PluginManager {
) -> anyhow::Result<Vec<Box<dyn Capability>>> {
let mut capabilities: Vec<Box<dyn Capability>> = vec![];
for plugin in self.get_plugins() {
- if let Some(capability) = self.get_plugin_capability(
- &*plugin.borrow(),
- capability_class_name,
- ctor_args.clone(),
- )? {
+ if let Some(capability) =
+ self.get_plugin_capability(plugin, capability_class_name, ctor_args.clone())?
+ {
capabilities.push(capability);
}
}
diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs
index 0ef36923..b713104e 100644
--- a/crates/shirabe/tests/plugin/plugin_installer_test.rs
+++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs
@@ -20,7 +20,9 @@ use shirabe::package::{
CompletePackageHandle, Locker, LockerInterface, PackageInterfaceHandle, RootPackageHandle,
};
use shirabe::plugin::plugin_interface::PluginInterface;
-use shirabe::plugin::{Capable, PluginManager, composer_handle_value, io_handle_value};
+use shirabe::plugin::{
+ Capable, PluginManager, composer_handle_value, io_handle_value, plugin_handle_value,
+};
use shirabe::repository::{
InstalledArrayRepository, InstalledRepositoryInterfaceHandle, RepositoryInterfaceHandle,
RepositoryManagerInterface,
@@ -29,6 +31,7 @@ use shirabe::util::Platform;
use shirabe::util::http_downloader::HttpDownloader;
use shirabe::util::r#loop::Loop;
use shirabe::util::process_executor::ProcessExecutor;
+use shirabe_php_rpc::PluginValue;
use shirabe_php_shim::Catch as _;
use shirabe_php_shim::PhpMixed;
use shirabe_semver::VersionParser;
@@ -801,7 +804,8 @@ impl PluginInterface for NoopPlugin {
fn test_incapable_plugin_is_correctly_detected() {
let set_up = set_up();
- let plugin = NoopPlugin;
+ let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(NoopPlugin));
let result = set_up
.pm
.borrow()
@@ -810,14 +814,82 @@ fn test_incapable_plugin_is_correctly_detected() {
assert!(result.is_none());
}
-#[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))"]
+/// PHPUnit autoloads `Composer\Test\Plugin\Mock\Capability` from the Composer checkout; the
+/// worker resolves `Composer\` to `src/Composer` only, so the class file is loaded by path.
+fn load_mock_capability_class() {
+ EventDispatcher::__ensure_composer_php_runtime().unwrap();
+ let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/Composer/Test/Plugin/Mock/Capability.php")
+ .canonicalize()
+ .expect("the Composer checkout must provide the plugin test mocks");
+ shirabe_php_rpc::call_function(
+ "__shirabe_require",
+ vec![PluginValue::string(path.to_str().unwrap())],
+ )
+ .unwrap()
+ .unwrap();
+}
+
#[test]
fn test_capability_implements_composer_plugin_api_class_and_is_constructed_with_args() {
- // 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!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ load_mock_capability_class();
+
+ let capability_api = "Composer\\Plugin\\Capability\\Capability";
+ let capability_implementation = "Composer\\Test\\Plugin\\Mock\\Capability";
+
+ let get_capabilities_calls = std::rc::Rc::new(std::cell::RefCell::new(0));
+ let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(CapablePlugin {
+ capabilities: IndexMap::from([(
+ capability_api.to_string(),
+ PhpMixed::String(capability_implementation.to_string()),
+ )]),
+ get_capabilities_calls: get_capabilities_calls.clone(),
+ }));
+
+ let capability = set_up
+ .pm
+ .borrow()
+ .get_plugin_capability(
+ &plugin,
+ capability_api,
+ IndexMap::from([
+ ("a".to_string(), PluginValue::Int(1)),
+ ("b".to_string(), PluginValue::Int(2)),
+ ]),
+ )
+ .unwrap()
+ .expect("the plugin provides an implementation of the queried capability");
+ let capability = capability
+ .__as_php_capability_proxy()
+ .expect("a capability built in the child is a PHP-backed proxy");
+
+ // PHP: assertInstanceOf($capabilityApi, $capability)
+ assert!(
+ capability
+ .__handle()
+ .implements
+ .iter()
+ .any(|interface| interface == capability_api)
+ );
+ // PHP: assertInstanceOf($capabilityImplementation, $capability)
+ assert_eq!(capability_implementation, capability.__handle().class);
+ // PHP: assertSame(['a' => 1, 'b' => 2, 'plugin' => $plugin], $capability->args)
+ assert_eq!(
+ PluginValue::Array(IndexMap::from([
+ (b"a".to_vec(), PluginValue::Int(1)),
+ (b"b".to_vec(), PluginValue::Int(2)),
+ (b"plugin".to_vec(), plugin_handle_value(&plugin)),
+ ])),
+ capability.__get_property("args").unwrap()
+ );
+ // PHP: ->expects($this->once())->method('getCapabilities').
+ assert_eq!(1, *get_capabilities_calls.borrow());
}
/// PHP data provider `invalidImplementationClassNames`, one PhpMixed per entry.
@@ -835,36 +907,50 @@ fn invalid_implementation_class_names() -> Vec<PhpMixed> {
]
}
-#[test]
-fn test_querying_with_invalid_capability_class_name_throws() {
+/// PHP: testQueryingWithInvalidCapabilityClassNameThrows, whose `$expect` parameter the second
+/// data-driven test overrides by calling this one directly.
+fn assert_querying_with_invalid_capability_class_name_throws(
+ invalid_implementation_class_names: &PhpMixed,
+ expect: fn(&anyhow::Error) -> bool,
+ expect_name: &str,
+) {
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 {
+ let set_up = set_up();
+ let get_capabilities_calls = std::rc::Rc::new(std::cell::RefCell::new(0));
+ let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(CapablePlugin {
capabilities: IndexMap::from([(
capability_api.to_string(),
- invalid_implementation_class_name.clone(),
+ invalid_implementation_class_names.clone(),
)]),
- get_capabilities_calls: std::cell::RefCell::new(0),
+ get_capabilities_calls: get_capabilities_calls.clone(),
+ }));
+
+ let err =
+ match set_up
+ .pm
+ .borrow()
+ .get_plugin_capability(&plugin, capability_api, IndexMap::new())
+ {
+ Err(err) => err,
+ Ok(_) => panic!("expected {expect_name} for {invalid_implementation_class_names:?}"),
};
+ assert!(
+ expect(&err),
+ "expected {expect_name} for {invalid_implementation_class_names:?}, got: {err}"
+ );
+ // PHP: ->expects($this->once())->method('getCapabilities').
+ assert_eq!(1, *get_capabilities_calls.borrow());
+}
- 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.is_instanceof::<shirabe_php_shim::UnexpectedValueException>(),
- "expected UnexpectedValueException for {invalid_implementation_class_name:?}, got: {err}"
+#[test]
+fn test_querying_with_invalid_capability_class_name_throws() {
+ for invalid_implementation_class_name in invalid_implementation_class_names() {
+ assert_querying_with_invalid_capability_class_name_throws(
+ &invalid_implementation_class_name,
+ |err| err.is_instanceof::<shirabe_php_shim::UnexpectedValueException>(),
+ "UnexpectedValueException",
);
- // PHP: ->expects($this->once())->method('getCapabilities').
- assert_eq!(1, *plugin.get_capabilities_calls.borrow());
}
}
@@ -875,7 +961,8 @@ fn test_querying_with_invalid_capability_class_name_throws() {
#[derive(Debug)]
struct CapablePlugin {
capabilities: IndexMap<String, PhpMixed>,
- get_capabilities_calls: std::cell::RefCell<i64>,
+ // Shared with the test body, which only ever holds the plugin as a `dyn PluginInterface`.
+ get_capabilities_calls: std::rc::Rc<std::cell::RefCell<i64>>,
}
impl PluginInterface for CapablePlugin {
@@ -923,10 +1010,12 @@ impl Capable for CapablePlugin {
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),
- };
+ let get_capabilities_calls = std::rc::Rc::new(std::cell::RefCell::new(0));
+ let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(CapablePlugin {
+ capabilities: IndexMap::new(),
+ get_capabilities_calls: get_capabilities_calls.clone(),
+ }));
let result = set_up
.pm
@@ -938,16 +1027,28 @@ fn test_querying_non_provided_capability_returns_null_safely() {
)
.unwrap();
assert!(result.is_none());
- assert_eq!(1, *plugin.get_capabilities_calls.borrow());
+ assert_eq!(1, *get_capabilities_calls.borrow());
+}
+
+/// PHP data provider `nonExistingOrInvalidImplementationClassTypes`.
+fn non_existing_or_invalid_implementation_class_types() -> Vec<&'static str> {
+ vec!["\\stdClass", "NonExistentClassLikeMiddleClass"]
}
-#[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): 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!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ // `new \stdClass($ctorArgs)` receives the plugin, whose proxy stub implements the real
+ // PluginInterface in the child.
+ EventDispatcher::__ensure_composer_php_runtime().unwrap();
+ for wrong_implementation_class_type in non_existing_or_invalid_implementation_class_types() {
+ assert_querying_with_invalid_capability_class_name_throws(
+ &PhpMixed::String(wrong_implementation_class_type.to_string()),
+ |err| err.is_instanceof::<shirabe_php_shim::RuntimeException>(),
+ "RuntimeException",
+ );
+ }
}