aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/plugin
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-04 02:25:28 +0900
committernsfisis <nsfisis@gmail.com>2026-08-04 05:43:18 +0900
commita02fc7d728a9973a3275a0f47604081c4439b424 (patch)
treeb1a73d2eb6b17bb6c204318b58d75ae6cf46cc7a /crates/shirabe/tests/plugin
parent20f7a7826ae048d249e0d837ca6393a5f09c9ba6 (diff)
downloadphp-shirabe-a02fc7d728a9973a3275a0f47604081c4439b424.tar.gz
php-shirabe-a02fc7d728a9973a3275a0f47604081c4439b424.tar.zst
php-shirabe-a02fc7d728a9973a3275a0f47604081c4439b424.zip
feat(plugin): activate plugins through the PHP RPC worker
Implement the remainder of PluginManager::registerPackage: the plugin autoload map is built by the ported createLoader/parseAutoloads and served to the worker over the existing reverse-RPC autoloader, files entries go through a composerRequire-equivalent glue call, and already-defined classes take the upstream _composer_tmp rename/eval path. Instantiation uses the new NewObject/CallPhpMethod lanes backed by a P table in the worker; PhpPluginProxy adapts the resulting handle to PluginInterface, with $composer/$io exposed to plugin callbacks via an R table (unsupported methods stay explicit errors). Hand-written proxy stubs cover Composer, PartialComposer and the IO hierarchy, and the stub autoloader is re-prepended after loading the Composer PHP runtime so its vendor autoloader cannot shadow proxied FQCNs. FilesystemRepository::write now mirrors InstalledVersions::reload into a running worker (class_exists-guarded, so an unloaded class keeps its upstream lazy-load behavior), removing the previously undefined observation window. The installer pipeline passes the installed repository as a shared handle instead of a long-lived `&mut dyn`: plugin registration runs inside InstallationManager::execute and re-enters the same local repository through the RepositoryManager, which would panic on the RefCell re-borrow under the old shape. PluginInterface lifecycle methods now take an owned ComposerHandle (plugins retain $composer past the call) and return anyhow::Result (PHP plugin code may throw); the plugin list uses shared ownership so the identity comparison of removePlugin survives the dual storage in registeredPlugins, matching PHP reference semantics. Ports the activate/upgrade/uninstall tests of PluginInstallerTest, serialized across the shared worker process whose persistent class table is exactly what exercises the rename path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/plugin')
-rw-r--r--crates/shirabe/tests/plugin/main.rs2
-rw-r--r--crates/shirabe/tests/plugin/plugin_installer_test.rs665
2 files changed, 545 insertions, 122 deletions
diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs
index 5cb77de4..62c8cfa8 100644
--- a/crates/shirabe/tests/plugin/main.rs
+++ b/crates/shirabe/tests/plugin/main.rs
@@ -1,3 +1,5 @@
+#[path = "../common/async_runtime.rs"]
+mod async_runtime;
#[path = "../common/config_stub.rs"]
mod config_stub;
diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs
index ca2dd879..e3f89851 100644
--- a/crates/shirabe/tests/plugin/plugin_installer_test.rs
+++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs
@@ -1,28 +1,201 @@
//! ref: composer/tests/Composer/Test/Plugin/PluginInstallerTest.php
-use crate::config_stub::ConfigStubBuilder;
+use crate::async_runtime::run;
use indexmap::IndexMap;
+use shirabe::autoload::AutoloadGenerator;
use shirabe::composer::{Composer, ComposerHandle, PartialOrFullComposer};
use shirabe::config::Config;
+use shirabe::dependency_resolver::operation::AnyOperation;
+use shirabe::downloader::{DownloadManagerInterface, DownloaderInterface};
+use shirabe::event_dispatcher::EventDispatcher;
use shirabe::factory::DisablePlugins;
-use shirabe::installer::InstallationManager;
+use shirabe::installer::{
+ InstallationManager, InstallationManagerInterface, InstallerInterface, PluginInstaller,
+};
use shirabe::io::IOInterface;
use shirabe::io::buffer_io::BufferIO;
use shirabe::json::JsonFile;
-use shirabe::package::{Locker, LockerInterface};
+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::repository::{
+ InstalledArrayRepository, InstalledRepositoryInterfaceHandle, RepositoryInterfaceHandle,
+ RepositoryManagerInterface,
+};
use shirabe::util::Platform;
use shirabe::util::http_downloader::HttpDownloader;
use shirabe::util::r#loop::Loop;
use shirabe::util::process_executor::ProcessExecutor;
use shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL;
+use shirabe_external_packages::symfony::process::PhpExecutableFinder;
use shirabe_php_shim::PhpMixed;
+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 {
+ PhpExecutableFinder::new().find(false).is_some()
+}
+
+/// All tests in this binary share the single PHP worker, whose loaded-class table persists
+/// across tests just like PHPUnit's single-process runs (that sharing is what exercises the
+/// `_composer_tmp` rename path). Interleaving two tests would let one test's class definitions
+/// 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, ()> {
+ PHP_WORKER_TESTS
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+}
+
+/// `__DIR__ . '/Fixtures'` of the upstream test class.
+fn fixtures_dir() -> String {
+ let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/Composer/Test/Plugin/Fixtures");
+ dir.canonicalize()
+ .expect("the Composer checkout must provide the plugin fixtures")
+ .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! {
+ #[derive(Debug)]
+ pub DownloadManager {}
+ #[async_trait::async_trait(?Send)]
+ impl DownloadManagerInterface for DownloadManager {
+ fn set_prefer_source(&mut self, prefer_source: bool);
+ fn set_prefer_dist(&mut self, prefer_dist: bool);
+ fn get_downloader_for_package(
+ &self,
+ package: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn DownloaderInterface>>>>;
+ async fn download(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn prepare(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn install(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn update(
+ &self,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn remove(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn cleanup(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ }
+}
+
+/// PHP mocks `Composer\Repository\RepositoryManager` so that getLocalRepository returns the
+/// test repository; the other methods are never reached.
+#[derive(Debug)]
+struct MockRepositoryManager {
+ local: RepositoryInterfaceHandle,
+ repositories: Vec<RepositoryInterfaceHandle>,
+}
+
+impl RepositoryManagerInterface for MockRepositoryManager {
+ fn get_local_repository(&self) -> RepositoryInterfaceHandle {
+ self.local.clone()
+ }
+
+ fn get_repositories(&self) -> &Vec<RepositoryInterfaceHandle> {
+ &self.repositories
+ }
+
+ fn create_repository(
+ &self,
+ _type: &str,
+ _config: IndexMap<String, PhpMixed>,
+ _name: Option<&str>,
+ ) -> anyhow::Result<RepositoryInterfaceHandle> {
+ unimplemented!("not exercised by PluginInstallerTest")
+ }
+
+ fn add_repository(&mut self, _repository: RepositoryInterfaceHandle) {
+ unimplemented!("not exercised by PluginInstallerTest")
+ }
+
+ fn set_local_repository(&mut self, repository: RepositoryInterfaceHandle) {
+ self.local = repository;
+ }
+}
+
+/// PHP mocks `Composer\Installer\InstallationManager` so that getInstallPath maps a package to
+/// `__DIR__.'/Fixtures/'.$package->getPrettyName()`; every other method keeps the PHPUnit mock
+/// default (no-op / falsy).
+#[derive(Debug)]
+struct MockInstallationManager;
+
+impl InstallationManagerInterface for MockInstallationManager {
+ 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: &InstalledRepositoryInterfaceHandle,
+ _package: PackageInterfaceHandle,
+ ) -> anyhow::Result<bool> {
+ Ok(false)
+ }
+
+ fn ensure_binaries_presence(&mut self, _package: PackageInterfaceHandle) {}
+
+ fn execute(
+ &mut self,
+ _repo: &InstalledRepositoryInterfaceHandle,
+ _operations: Vec<AnyOperation>,
+ _dev_mode: bool,
+ _run_scripts: bool,
+ _download_only: bool,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ fn get_install_path(&self, package: PackageInterfaceHandle) -> Option<String> {
+ Some(format!("{}/{}", fixtures_dir(), package.get_pretty_name()))
+ }
+
+ fn set_output_progress(&mut self, _output_progress: bool) {}
+
+ fn notify_installs(&mut self, _io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) {}
+}
/// Equivalent to PHP setUp()'s `new InstallationManager(...)`, used only to satisfy
-/// `Locker::new`'s constructor argument; it is never exercised by the currently-portable
-/// tests below.
-fn installation_manager(
+/// `Locker::new`'s concrete constructor argument (PHP hands the same InstallationManager mock to
+/// the Locker, which never touches it in these tests).
+fn locker_installation_manager(
io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
) -> std::rc::Rc<std::cell::RefCell<InstallationManager>> {
let config = std::rc::Rc::new(std::cell::RefCell::new(Config::new(false, None)));
@@ -42,175 +215,411 @@ fn installation_manager(
#[derive(Debug)]
struct SetUp {
- #[allow(dead_code)]
- io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ 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>>,
+ autoload_generator: std::rc::Rc<std::cell::RefCell<AutoloadGenerator>>,
+ packages: Vec<PackageInterfaceHandle>,
+ repository: InstalledRepositoryInterfaceHandle,
// Keeps the Composer alive; PluginManager only holds a weak back-reference to it.
- #[allow(dead_code)]
composer: ComposerHandle,
+ // PHP's tearDown() removes this directory; TempDir does the same on drop.
+ _directory: TempDir,
}
-/// Builds a `Composer` the way PHP's setUp() does (config with `allow-plugins => true`
-/// and a `Locker` backed by /dev/null) and constructs a `PluginManager` from it.
-///
-/// PHP's setUp() additionally mocks DownloadManager/RepositoryManager/InstallationManager/
-/// EventDispatcher, loads 8 plugin-vN fixture packages, and creates a temp fixtures
-/// directory. None of that is reproduced here: every test that would exercise it depends on
-/// `PluginManager::register_package` actually instantiating a plugin class, which is an
-/// unported runtime concern (`TODO(plugin)` in `plugin/plugin_manager.rs`) — those tests stay
-/// `#[ignore]` below. Only the tests that call `PluginManager::get_plugin_capability` directly
-/// with a hand-built plugin object are portable, and they need nothing more than `pm` itself.
fn set_up() -> SetUp {
- let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = std::rc::Rc::new(
- std::cell::RefCell::new(BufferIO::new(String::new(), VERBOSITY_NORMAL, None).unwrap()),
- );
+ let loader = JsonLoader::new(Box::new(ArrayLoader::new(None, false)));
+ let mut packages = vec![];
+ let directory = TempDir::new().unwrap();
+ let directory_path = directory.path().to_str().unwrap().to_string();
+ for i in 1..=8 {
+ std::fs::create_dir_all(format!("{}/Fixtures/plugin-v{}", directory_path, i)).unwrap();
+ packages.push(
+ loader
+ .load(JsonLoaderInput::String(format!(
+ "{}/plugin-v{}/composer.json",
+ fixtures_dir(),
+ i
+ )))
+ .unwrap(),
+ );
+ }
- let config = ConfigStubBuilder::new()
- .with("allow-plugins", PhpMixed::Bool(true))
- .build_shared();
+ let mut dm = MockDownloadManager::new();
+ dm.expect_install().returning(|_, _| Ok(None));
+ dm.expect_update().returning(|_, _, _| Ok(None));
+ dm.expect_remove().returning(|_, _| Ok(None));
- let json_file = JsonFile::new(Platform::get_dev_null(), None, Some(io.clone())).unwrap();
- let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
- io.clone(),
- ))));
- let locker: std::rc::Rc<std::cell::RefCell<dyn LockerInterface>> =
- std::rc::Rc::new(std::cell::RefCell::new(Locker::new(
- io.clone(),
- json_file,
- installation_manager(&io),
- "{}",
- process,
- )));
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+
+ let repository_manager = MockRepositoryManager {
+ local: repository.as_repository_handle(),
+ repositories: vec![],
+ };
+
+ let installation_manager = MockInstallationManager;
+
+ let io = std::rc::Rc::new(std::cell::RefCell::new(
+ BufferIO::new(String::new(), VERBOSITY_NORMAL, None).unwrap(),
+ ));
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone();
+
+ let composer_rc = std::rc::Rc::new(std::cell::RefCell::new(PartialOrFullComposer::Full(
+ Composer::new(),
+ )));
+ let composer = ComposerHandle::from_rc_unchecked(composer_rc.clone());
- let mut composer = Composer::new();
- composer.set_config(config);
- composer.set_locker(locker);
+ // PHP hands the AutoloadGenerator a mocked EventDispatcher (disabled constructor); the Rust
+ // AutoloadGenerator requires a concrete EventDispatcher, and none of these tests reach it.
+ let dispatcher = std::rc::Rc::new(std::cell::RefCell::new(EventDispatcher::new(
+ composer.upcast().downgrade(),
+ io_dyn.clone(),
+ None,
+ )));
+ let autoload_generator = std::rc::Rc::new(std::cell::RefCell::new(AutoloadGenerator::new(
+ dispatcher,
+ Some(io_dyn.clone()),
+ )));
- let composer = ComposerHandle::from_rc_unchecked(std::rc::Rc::new(std::cell::RefCell::new(
- PartialOrFullComposer::Full(composer),
+ let mut config = Config::new(false, None);
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "vendor-dir".to_string(),
+ PhpMixed::String(format!("{}/Fixtures/", directory_path)),
+ );
+ config_section.insert(
+ "home".to_string(),
+ PhpMixed::String(format!("{}/Fixtures", directory_path)),
+ );
+ config_section.insert(
+ "bin-dir".to_string(),
+ PhpMixed::String(format!("{}/Fixtures/bin", directory_path)),
+ );
+ config_section.insert("allow-plugins".to_string(), PhpMixed::Bool(true));
+ let mut merged: IndexMap<String, PhpMixed> = IndexMap::new();
+ merged.insert("config".to_string(), PhpMixed::Array(config_section));
+ config.merge(&merged, Config::SOURCE_UNKNOWN);
+
+ {
+ let mut c = composer.borrow_mut();
+ c.set_config(std::rc::Rc::new(std::cell::RefCell::new(config)));
+ c.set_download_manager(std::rc::Rc::new(std::cell::RefCell::new(dm)));
+ c.set_repository_manager(std::rc::Rc::new(std::cell::RefCell::new(
+ repository_manager,
+ )));
+ c.set_installation_manager(std::rc::Rc::new(std::cell::RefCell::new(
+ installation_manager,
+ )));
+ c.set_autoload_generator(autoload_generator.clone());
+ }
+ let real_dispatcher = std::rc::Rc::new(std::cell::RefCell::new(EventDispatcher::new(
+ composer.upcast().downgrade(),
+ io_dyn.clone(),
+ None,
)));
+ composer.borrow_mut().set_event_dispatcher(real_dispatcher);
+ composer.borrow_mut().set_package(
+ RootPackageHandle::new(
+ "dummy/root".to_string(),
+ "1.0.0.0".to_string(),
+ "1.0.0".to_string(),
+ )
+ .into(),
+ );
+ {
+ let json_file =
+ JsonFile::new(Platform::get_dev_null(), None, Some(io_dyn.clone())).unwrap();
+ let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
+ io_dyn.clone(),
+ ))));
+ let locker: std::rc::Rc<std::cell::RefCell<dyn LockerInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(Locker::new(
+ io_dyn.clone(),
+ json_file,
+ locker_installation_manager(&io_dyn),
+ "{}",
+ process,
+ )));
+ composer.borrow_mut().set_locker(locker);
+ }
- let pm = PluginManager::new(io.clone(), composer.downgrade(), None, DisablePlugins::None);
+ let pm = std::rc::Rc::new(std::cell::RefCell::new(PluginManager::new(
+ io_dyn.clone(),
+ composer.downgrade(),
+ None,
+ DisablePlugins::None,
+ )));
+ composer.borrow_mut().set_plugin_manager(pm.clone());
SetUp {
io,
- pm: std::rc::Rc::new(std::cell::RefCell::new(pm)),
+ io_dyn,
+ pm,
+ autoload_generator,
+ packages,
+ repository,
composer,
+ _directory: directory,
}
}
-/// PHP's tearDown() removes the temp fixtures directory created by setUp(); `set_up` above
-/// creates no such directory, so there is nothing to clean up.
-fn tear_down() {}
-
-struct TearDown;
-
-impl Drop for TearDown {
- fn drop(&mut self) {
- tear_down();
+/// PHPUnit asserts `$plugins[$i]->version` etc.; the plugin entity lives in the PHP child, so
+/// the property is read over RPC through the proxy's test helper.
+fn plugin_property(
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
+ name: &str,
+) -> String {
+ let plugin = plugin.borrow();
+ let proxy = plugin
+ .as_php_plugin_proxy()
+ .expect("registered plugins are PHP-backed proxies");
+ match proxy.__get_property(name).unwrap() {
+ PhpMixed::String(s) => s,
+ other => panic!("property `{name}` is not a string: {other:?}"),
}
}
-// The plugin system requires the PHP runtime to load and instantiate plugin classes.
-// `PluginInstaller::install`/`update` never call `PluginManager::register_package` (the calls
-// are commented out in installer/plugin_installer.rs, TODO(plugin)), and `register_package`
-// itself never instantiates a plugin class or calls `add_plugin` (TODO(plugin) in
-// plugin/plugin_manager.rs). So `PluginManager::get_plugins()` can never contain the plugin
-// instances these tests assert on.
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v1) are not implemented (TODO(plugin))"]
+fn new_installer(set_up: &SetUp) -> PluginInstaller {
+ PluginInstaller::new(
+ set_up.io_dyn.clone(),
+ set_up.composer.upcast().downgrade(),
+ None,
+ None,
+ )
+}
+
#[test]
fn test_install_new_plugin() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v1)
- // are not implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [].
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.install(&set_up.repository, set_up.packages[0].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("installer-v1", plugin_property(&plugins[0], "version"));
+ assert_eq!("activate v1\n", set_up.io.borrow().get_output());
}
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes are not implemented (TODO(plugin))"]
#[test]
fn test_install_plugin_with_root_package_having_files_autoload() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [].
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ set_up.autoload_generator.borrow_mut().set_dev_mode(true);
+ let files_autoload = format!("{}/files_autoload_which_should_not_run.php", fixtures_dir());
+ let mut autoload: IndexMap<String, PhpMixed> = IndexMap::new();
+ autoload.insert(
+ "files".to_string(),
+ PhpMixed::List(vec![PhpMixed::String(files_autoload)]),
+ );
+ let root = set_up.composer.borrow().get_package().clone();
+ root.set_autoload(autoload.clone());
+ root.set_dev_autoload(autoload);
+
+ run(installer.install(&set_up.repository, set_up.packages[0].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("activate v1\n", set_up.io.borrow().get_output());
+ assert_eq!("installer-v1", plugin_property(&plugins[0], "version"));
}
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v4) are not implemented (TODO(plugin))"]
#[test]
fn test_install_multiple_plugins() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v4)
- // are not implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [$this->packages[3]].
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[3].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.install(&set_up.repository, set_up.packages[3].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("plugin1", plugin_property(&plugins[0], "name"));
+ assert_eq!("installer-v4", plugin_property(&plugins[0], "version"));
+ assert_eq!("plugin2", plugin_property(&plugins[1], "name"));
+ assert_eq!("installer-v4", plugin_property(&plugins[1], "version"));
+ assert_eq!(
+ "activate v4-plugin1\nactivate v4-plugin2\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller.update and runtime plugin class loading/deactivation are not implemented (TODO(plugin))"]
#[test]
fn test_upgrade_with_new_class_name() {
- // TODO(phase-d): PluginInstaller.update and runtime plugin class loading/deactivation are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: getPackages returns [$this->packages[0]]; hasPackage answers (true, false), which a
+ // real repository seeded with the initial package reproduces naturally.
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[0].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.update(
+ &set_up.repository,
+ set_up.packages[0].clone(),
+ set_up.packages[1].clone(),
+ ))
+ .unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ // PHP: assertCount(1, $plugins); $plugins[1]->version — unset() keeps array keys, so the
+ // remaining plugin sits at key 1. The Vec port reindexes; the remaining plugin is [0].
+ assert_eq!(1, plugins.len());
+ assert_eq!("installer-v2", plugin_property(&plugins[0], "version"));
+ assert_eq!(
+ "activate v1\ndeactivate v1\nactivate v2\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller.uninstall and runtime plugin class loading/uninstall hook are not implemented (TODO(plugin))"]
#[test]
fn test_uninstall() {
- // TODO(phase-d): PluginInstaller.uninstall and runtime plugin class loading/uninstall hook are
- // not implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[0].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.uninstall(&set_up.repository, set_up.packages[0].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!(0, plugins.len());
+ assert_eq!(
+ "activate v1\ndeactivate v1\nuninstall v1\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller.update and runtime plugin class loading/deactivation are not implemented (TODO(plugin))"]
#[test]
fn test_upgrade_with_same_class_name() {
- // TODO(phase-d): PluginInstaller.update and runtime plugin class loading/deactivation are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: getPackages returns [$this->packages[1]]; hasPackage answers (true, false).
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[1].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.update(
+ &set_up.repository,
+ set_up.packages[1].clone(),
+ set_up.packages[2].clone(),
+ ))
+ .unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("installer-v3", plugin_property(&plugins[0], "version"));
+ assert_eq!(
+ "activate v2\ndeactivate v2\nactivate v3\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes are not implemented (TODO(plugin))"]
#[test]
fn test_register_plugin_only_one_time() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [].
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.install(&set_up.repository, set_up.packages[0].clone())).unwrap();
+ run(installer.install(
+ &set_up.repository,
+ PackageInterfaceHandle::dup(&set_up.packages[0]),
+ ))
+ .unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!(1, plugins.len());
+ assert_eq!("installer-v1", plugin_property(&plugins[0], "version"));
+ assert_eq!("activate v1\n", set_up.io.borrow().get_output());
}
-// PluginManager::register_package's version-constraint check against composer-plugin-api is
-// fully ported and does gate loading correctly, but `getPluginApiVersion()` returns a hardcoded
-// constant (plugin_interface::PLUGIN_API_VERSION) with no seam to override it per-test the way
-// PHP's `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])` does, and
-// even a matching version can never produce a registered plugin (register_package's
-// instantiate-and-add_plugin step is an unported TODO(plugin) stub). Both blockers must be
-// resolved together; a partial port (e.g. only the count==0 branches) would drop assertions the
-// test relies on, which is disallowed.
-#[ignore = "Requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP classes; not implemented (TODO(plugin))"]
+// PluginManager::get_plugin_api_version returns a hardcoded constant
+// (plugin_interface::PLUGIN_API_VERSION) with no seam to override it per-test the way PHP's
+// `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])` does.
+#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"]
#[test]
fn test_star_plugin_version_works_with_any_api_version() {
- // TODO(phase-d): requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP
- // classes; not implemented (TODO(plugin)).
+ // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam
+ // (TODO(plugin)).
todo!()
}
-#[ignore = "Requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP classes; not implemented (TODO(plugin))"]
+#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"]
#[test]
fn test_plugin_constraint_works_only_with_certain_api_version() {
- // TODO(phase-d): requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP
- // classes; not implemented (TODO(plugin)).
+ // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam
+ // (TODO(plugin)).
todo!()
}
-#[ignore = "Requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP classes; not implemented (TODO(plugin))"]
+#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"]
#[test]
fn test_plugin_range_constraints_work_only_with_certain_api_version() {
- // TODO(phase-d): requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP
- // classes; not implemented (TODO(plugin)).
+ // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam
+ // (TODO(plugin)).
todo!()
}
-#[ignore = "get_plugin_capabilities requires a registered plugin, which register_package never produces (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime instantiation is also unported"]
+#[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_capabilities requires a registered plugin, which register_package
- // never produces (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/
- // BaseCommand runtime instantiation is also unported.
+ // 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!()
}
@@ -224,30 +633,36 @@ struct NoopPlugin;
impl PluginInterface for NoopPlugin {
fn activate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn deactivate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn uninstall(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ fn get_class_name(&self) -> String {
+ "NoopPlugin".to_string()
}
}
#[test]
fn test_incapable_plugin_is_correctly_detected() {
let set_up = set_up();
- let _tear_down = TearDown;
let plugin = NoopPlugin;
let result = set_up
@@ -293,23 +708,30 @@ struct CapablePlugin {
impl PluginInterface for CapablePlugin {
fn activate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn deactivate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn uninstall(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ fn get_class_name(&self) -> String {
+ "CapablePlugin".to_string()
}
fn as_capable(&self) -> Option<&dyn Capable> {
@@ -327,7 +749,6 @@ impl Capable for CapablePlugin {
#[test]
fn test_querying_non_provided_capability_returns_null_safely() {
let set_up = set_up();
- let _tear_down = TearDown;
let plugin = CapablePlugin {
get_capabilities_calls: std::cell::RefCell::new(0),