diff options
Diffstat (limited to 'crates/shirabe')
29 files changed, 1593 insertions, 511 deletions
diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index 7312a60b..a907bc09 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -1011,9 +1011,11 @@ impl CreateProjectCommand { let mut im = installation_manager.borrow_mut(); im.set_output_progress(!no_progress); im.add_installer(Box::new(project_installer)); - let mut installed_repo = InstalledArrayRepository::new()?; + let installed_repo = crate::repository::InstalledRepositoryInterfaceHandle::new( + InstalledArrayRepository::new()?, + ); im.execute( - &mut installed_repo, + &installed_repo, vec![InstallOperation::new(package.clone()).into()], true, true, diff --git a/crates/shirabe/src/command/reinstall_command.rs b/crates/shirabe/src/command/reinstall_command.rs index 95fde5ae..0d3567a2 100644 --- a/crates/shirabe/src/command/reinstall_command.rs +++ b/crates/shirabe/src/command/reinstall_command.rs @@ -244,32 +244,23 @@ impl Command for ReinstallCommand { let uninstall_operations: Vec<AnyOperation> = uninstall_operations.into_iter().map(Into::into).collect(); - { - let mut local_repo_ref = local_repo.borrow_mut(); - let repo = local_repo_ref - .as_installed_repository_interface_mut() - .expect("local repository must be an InstalledRepositoryInterface"); - installation_manager.borrow_mut().execute( - repo, - uninstall_operations, - dev_mode, - true, - false, - )?; - } - { - let mut local_repo_ref = local_repo.borrow_mut(); - let repo = local_repo_ref - .as_installed_repository_interface_mut() - .expect("local repository must be an InstalledRepositoryInterface"); - installation_manager.borrow_mut().execute( - repo, - install_operations.clone(), - dev_mode, - true, - false, - )?; - } + let repo = crate::repository::InstalledRepositoryInterfaceHandle::from_repository_handle( + &local_repo, + ); + installation_manager.borrow_mut().execute( + &repo, + uninstall_operations, + dev_mode, + true, + false, + )?; + installation_manager.borrow_mut().execute( + &repo, + install_operations.clone(), + dev_mode, + true, + false, + )?; if !input .borrow() diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 2097c83d..f5b85ca1 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -596,7 +596,7 @@ impl EventDispatcher { // The user's command class extends Symfony's Command, so the child // process needs the real symfony/console classes before it can even // autoload the user class. - self.ensure_composer_php_runtime()?; + Self::ensure_composer_php_runtime()?; if !self.php_runtime_bool( "class_exists", vec![PluginValue::string(class_name.clone())], @@ -699,7 +699,7 @@ try {{ "false" }, ); - self.ensure_script_autoloader()?; + Self::ensure_script_autoloader()?; let mut dispatcher = ScriptRpcDispatcher { loader: self.loader.clone(), event: None, @@ -1054,7 +1054,7 @@ try {{ })); }; - self.ensure_script_autoloader()?; + Self::ensure_script_autoloader()?; let rhandle = shirabe_php_rpc::alloc_rhandle(); let mut dispatcher = ScriptRpcDispatcher { loader: self.loader.clone(), @@ -1435,7 +1435,7 @@ try {{ /// Makes the worker's script-class autoloader active, so class queries and script execution /// in the child can resolve classes through the Rust-side [`ClassLoader`] built by /// [`Self::make_autoloader`]. - fn ensure_script_autoloader(&self) -> anyhow::Result<()> { + pub(crate) fn ensure_script_autoloader() -> anyhow::Result<()> { unwrap_php_result(call_function( "__shirabe_enable_script_autoloader", Vec::new(), @@ -1445,7 +1445,7 @@ try {{ /// Loads the Composer PHP runtime (symfony/console and friends) into the worker, needed /// before a `scripts` Command class can be autoloaded and hosted. - fn ensure_composer_php_runtime(&self) -> anyhow::Result<()> { + pub(crate) fn ensure_composer_php_runtime() -> anyhow::Result<()> { // TODO(plugin): the real PHP classes are taken from a Composer checkout for now; how // they ship with a released Shirabe binary is part of the plugin distribution work. let autoload = Self::composer_php_runtime_autoload().ok_or_else(|| { @@ -1484,7 +1484,7 @@ try {{ /// Runs a boolean runtime query (`class_exists`, `is_a`, ...) inside the PHP worker, with /// the script autoloader active so the query can trigger class loading. fn php_runtime_bool(&self, function: &str, args: Vec<PluginValue>) -> anyhow::Result<bool> { - self.ensure_script_autoloader()?; + Self::ensure_script_autoloader()?; let mut dispatcher = ScriptRpcDispatcher { loader: self.loader.clone(), event: None, @@ -1616,7 +1616,7 @@ fn runtime_throw(message: String) -> PhpThrow { /// Collapses the two failure lanes of an RPC call into `anyhow`: the callers here treat a PHP /// exception raised during a runtime query as fatal for the current dispatch. -fn unwrap_php_result( +pub(crate) fn unwrap_php_result( outcome: anyhow::Result<Result<PluginValue, PhpThrow>>, ) -> anyhow::Result<PluginValue> { match outcome? { diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index 29dc3e05..903f6d35 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -42,7 +42,6 @@ use crate::plugin::PluginEvents; use crate::plugin::PluginManager; use crate::repository::FilesystemRepository; use crate::repository::InstalledFilesystemRepository; -use crate::repository::InstalledRepositoryInterface; use crate::repository::RepositoryFactory; use crate::repository::RepositoryManager; use crate::util::Filesystem; @@ -892,13 +891,9 @@ impl Factory { // once everything is initialized we can // purge packages from local repos if they have been deleted on the filesystem // PHP: $this->purgePackages($rm->getLocalRepository(), $im); - // TODO(phase-c): the rm/im locals are still in scope (Rc-shared with composer), but - // purge_packages wants `&mut dyn InstalledRepositoryInterface` and - // RepositoryManager::get_local_repository yields a RepositoryInterfaceHandle that - // exposes no raw &mut InstalledRepositoryInterface view (only per-method helpers that - // borrow internally). Wiring this needs such an accessor plus completing - // purge_packages' removal body (repo.removePackage), which is itself still a stub. - // self.purge_packages(rm.get_local_repository(), &mut im)?; + // TODO(phase-c): purge_packages' removal body (repo.removePackage for packages + // deleted on the filesystem) is still a stub; wire this call once implemented. + // self.purge_packages(&InstalledRepositoryInterfaceHandle::from_repository_handle(&rm.get_local_repository()), &mut im)?; } Ok(PartialComposerHandle::from_rc(composer)) @@ -1316,10 +1311,11 @@ impl Factory { fn purge_packages( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &crate::repository::InstalledRepositoryInterfaceHandle, im: &mut InstallationManager, ) -> anyhow::Result<()> { - for package in repo.get_packages()? { + let packages = repo.borrow_mut().get_packages()?; + for package in packages { if !im.is_package_installed(repo, package.clone())? { let _ = package; } diff --git a/crates/shirabe/src/installer.rs b/crates/shirabe/src/installer.rs index d46d693d..f68ab35f 100644 --- a/crates/shirabe/src/installer.rs +++ b/crates/shirabe/src/installer.rs @@ -1257,17 +1257,15 @@ impl Installer { if self.execute_operations { local_repo.set_dev_package_names(self.locker.borrow_mut().get_dev_package_names()?); - let mut local_repo_ref = local_repo.borrow_mut(); self.installation_manager.borrow_mut().execute( - local_repo_ref - .as_installed_repository_interface_mut() - .unwrap(), + &crate::repository::InstalledRepositoryInterfaceHandle::from_repository_handle( + &local_repo, + ), local_repo_transaction.get_operations().clone(), self.dev_mode, self.run_scripts, self.download_only, )?; - drop(local_repo_ref); // see https://github.com/composer/composer/issues/2764 if !local_repo_transaction.get_operations().is_empty() { diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index cf75469c..8d248a76 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -16,6 +16,7 @@ use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::package::PackageInterfaceHandle; use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Platform; use crate::util::r#loop::Loop; use crate::util::sync_executor; @@ -198,17 +199,18 @@ impl InstallationManager { /// Checks whether provided package is installed in one of the registered installers. pub fn is_package_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { // For testing only (ref InstallationManagerMock::isPackageInstalled). if self.mock.is_some() { - return repo.has_package(package); + return repo.borrow_mut().has_package(package); } if let Some(alias) = package.as_alias() { let alias_of: PackageInterfaceHandle = alias.get_alias_of().into(); - return Ok(repo.has_package(package)? && self.is_package_installed(repo, alias_of)?); + return Ok(repo.borrow_mut().has_package(package)? + && self.is_package_installed(repo, alias_of)?); } self.get_installer(&package.get_type())? @@ -236,7 +238,7 @@ impl InstallationManager { /// Executes solver operation. pub fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, @@ -248,6 +250,7 @@ impl InstallationManager { // borrowed across the loop without also borrowing `&self`. if let Some(mock) = self.mock.as_mut() { let _ = (dev_mode, run_scripts, download_only); + let mut repo = repo.borrow_mut(); for operation in operations { let trace = shirabe_php_shim::strip_tags(&operation.to_string()); match operation { @@ -318,11 +321,6 @@ impl InstallationManager { let all_operations: Vec<AnyOperation> = operations.clone(); - // The concurrent operation chains share the repository; each chain borrows it only in - // synchronous sections, never across an await. - let repo_cell: std::cell::RefCell<&mut dyn InstalledRepositoryInterface> = - std::cell::RefCell::new(repo); - let result: anyhow::Result<()> = (|| -> anyhow::Result<()> { // execute operations in batches to make sure download-modifying-plugins are installed // before the other packages get downloaded @@ -363,7 +361,7 @@ impl InstallationManager { for batch_to_execute in batches { sync_executor::block_on(self.download_and_execute_batch( - &repo_cell, + repo, batch_to_execute, &mut cleanup_promises, dev_mode, @@ -394,7 +392,7 @@ impl InstallationManager { // do a last write so that we write the repository even if nothing changed // as that can trigger an update of some files like InstalledVersions.php if // running a new composer version - repo_cell.into_inner().write(dev_mode, self); + repo.borrow_mut().write(dev_mode, self); Ok(()) } @@ -402,7 +400,7 @@ impl InstallationManager { #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] async fn download_and_execute_batch( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operations: IndexMap<i64, AnyOperation>, cleanup_promises: &mut IndexMap< i64, @@ -544,7 +542,7 @@ impl InstallationManager { async fn execute_batch( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operations: IndexMap<i64, AnyOperation>, cleanup_promises: &IndexMap< i64, @@ -577,10 +575,10 @@ impl InstallationManager { } match &operation { AnyOperation::MarkAliasInstalled(op) => { - self.mark_alias_installed(&mut **repo.borrow_mut(), op)?; + self.mark_alias_installed(&mut *repo.borrow_mut(), op)?; } AnyOperation::MarkAliasUninstalled(op) => { - self.mark_alias_uninstalled(&mut **repo.borrow_mut(), op); + self.mark_alias_uninstalled(&mut *repo.borrow_mut(), op); } _ => {} } @@ -701,7 +699,7 @@ impl InstallationManager { /// Executes install operation. pub async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operation: &InstallOperation, ) -> anyhow::Result<Option<PhpMixed>> { let package = operation.get_package(); @@ -716,7 +714,7 @@ impl InstallationManager { /// Executes update operation. pub async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operation: &UpdateOperation, ) -> anyhow::Result<Option<PhpMixed>> { let initial = operation.get_initial_package().clone(); @@ -744,7 +742,7 @@ impl InstallationManager { /// Uninstalls package. pub async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operation: &UninstallOperation, ) -> anyhow::Result<Option<PhpMixed>> { let package = operation.get_package(); @@ -1017,13 +1015,13 @@ pub trait InstallationManagerInterface: std::fmt::Debug { fn disable_plugins(&mut self); fn is_package_installed( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool>; fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle); fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, @@ -1053,7 +1051,7 @@ impl InstallationManagerInterface for InstallationManager { fn is_package_installed( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { InstallationManager::is_package_installed(self, repo, package) @@ -1065,7 +1063,7 @@ impl InstallationManagerInterface for InstallationManager { fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, diff --git a/crates/shirabe/src/installer/installer_interface.rs b/crates/shirabe/src/installer/installer_interface.rs index f2c0cd7c..bc464735 100644 --- a/crates/shirabe/src/installer/installer_interface.rs +++ b/crates/shirabe/src/installer/installer_interface.rs @@ -3,7 +3,7 @@ use crate::installer::BinaryPresenceInterface; use crate::installer::PluginInstaller; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use shirabe_php_shim::PhpMixed; #[async_trait::async_trait(?Send)] @@ -12,7 +12,7 @@ pub trait InstallerInterface: std::fmt::Debug { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool>; @@ -29,25 +29,26 @@ pub trait InstallerInterface: std::fmt::Debug { prev_package: Option<PackageInterfaceHandle>, ) -> anyhow::Result<Option<PhpMixed>>; - // install/update/uninstall take the repository behind a RefCell: the concurrent operation - // chains share it, and implementations must borrow it only in synchronous sections (never - // across an await). + // install/update/uninstall take the repository as a shared handle: the concurrent operation + // chains share it (and re-entrant flows like plugin registration reach the same repository + // through RepositoryManager), so implementations must borrow it only in synchronous + // sections (never across an await). async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>>; async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>>; async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>>; diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs index 976c5a1f..c5a9608e 100644 --- a/crates/shirabe/src/installer/library_installer.rs +++ b/crates/shirabe/src/installer/library_installer.rs @@ -8,7 +8,7 @@ use crate::installer::BinaryPresenceInterface; use crate::installer::InstallerInterface; use crate::io::IOInterface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Filesystem; use crate::util::Platform; use crate::util::Silencer; @@ -241,10 +241,10 @@ impl InstallerInterface for LibraryInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { - if !repo.has_package(package.clone())? { + if !repo.borrow_mut().has_package(package.clone())? { return Ok(false); } @@ -315,7 +315,7 @@ impl InstallerInterface for LibraryInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { self.initialize_vendor_dir(); @@ -346,7 +346,7 @@ impl InstallerInterface for LibraryInstaller { async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -380,7 +380,7 @@ impl InstallerInterface for LibraryInstaller { async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { if !repo.borrow_mut().has_package(package.clone())? { diff --git a/crates/shirabe/src/installer/metapackage_installer.rs b/crates/shirabe/src/installer/metapackage_installer.rs index 26032a6d..e2821fd3 100644 --- a/crates/shirabe/src/installer/metapackage_installer.rs +++ b/crates/shirabe/src/installer/metapackage_installer.rs @@ -8,7 +8,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use shirabe_php_shim::{InvalidArgumentException, PhpMixed}; #[derive(Debug)] @@ -30,10 +30,10 @@ impl InstallerInterface for MetapackageInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { - repo.has_package(package) + repo.borrow_mut().has_package(package) } async fn download( @@ -64,7 +64,7 @@ impl InstallerInterface for MetapackageInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { self.io.write_error3( @@ -81,7 +81,7 @@ impl InstallerInterface for MetapackageInstaller { async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -111,7 +111,7 @@ impl InstallerInterface for MetapackageInstaller { async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { if !repo.borrow_mut().has_package(package.clone())? { diff --git a/crates/shirabe/src/installer/noop_installer.rs b/crates/shirabe/src/installer/noop_installer.rs index d76ad4ad..68a2b981 100644 --- a/crates/shirabe/src/installer/noop_installer.rs +++ b/crates/shirabe/src/installer/noop_installer.rs @@ -2,7 +2,7 @@ use crate::installer::InstallerInterface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use shirabe_php_shim::{InvalidArgumentException, PhpMixed}; #[derive(Debug)] @@ -16,10 +16,10 @@ impl InstallerInterface for NoopInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { - repo.has_package(package) + repo.borrow_mut().has_package(package) } async fn download( @@ -50,7 +50,7 @@ impl InstallerInterface for NoopInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { let mut repo = repo.borrow_mut(); @@ -63,7 +63,7 @@ impl InstallerInterface for NoopInstaller { async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -86,7 +86,7 @@ impl InstallerInterface for NoopInstaller { async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { let mut repo = repo.borrow_mut(); diff --git a/crates/shirabe/src/installer/plugin_installer.rs b/crates/shirabe/src/installer/plugin_installer.rs index c0473fd0..1e7a2636 100644 --- a/crates/shirabe/src/installer/plugin_installer.rs +++ b/crates/shirabe/src/installer/plugin_installer.rs @@ -8,7 +8,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::PackageInterfaceHandle; use crate::plugin::PluginManager; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Filesystem; use crate::util::Platform; use shirabe_php_shim::{PhpMixed, UnexpectedValueException, empty}; @@ -46,7 +46,7 @@ impl PluginInstaller { async fn rollback_install( &self, e: anyhow::Error, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<()> { self.inner.io.write_error(&format!( @@ -79,7 +79,7 @@ impl InstallerInterface for PluginInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { self.inner.is_installed(repo, package) @@ -135,43 +135,56 @@ impl InstallerInterface for PluginInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { - self.inner.install(repo, package).await?; + self.inner.install(repo, package.clone()).await?; - // TODO(plugin): register package in plugin manager after install, rollback on failure Platform::workaround_filesystem_issues(); - // self.get_plugin_manager().register_package(package, true)?; - // On error: self.rollback_install(e, repo, package)?; + let result = + self.get_plugin_manager() + .borrow_mut() + .register_package(package.clone(), true, false); + if let Err(e) = result { + self.rollback_install(e, repo, package).await?; + } Ok(None) } async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { - self.inner.update(repo, initial, target).await?; + self.inner + .update(repo, initial.clone(), target.clone()) + .await?; - // TODO(plugin): deactivate initial and register target in plugin manager after update, rollback on failure Platform::workaround_filesystem_issues(); - // self.get_plugin_manager().deactivate_package(initial); - // self.get_plugin_manager().register_package(target, true)?; - // On error: self.rollback_install(e, repo, target)?; + let result = (|| -> anyhow::Result<()> { + self.get_plugin_manager() + .borrow_mut() + .deactivate_package(initial)?; + self.get_plugin_manager() + .borrow_mut() + .register_package(target.clone(), true, false)?; + Ok(()) + })(); + if let Err(e) = result { + self.rollback_install(e, repo, target).await?; + } Ok(None) } async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { - // TODO(plugin): uninstall package from plugin manager self.get_plugin_manager() .borrow_mut() - .uninstall_package(package.clone()); + .uninstall_package(package.clone())?; self.inner.uninstall(repo, package).await } diff --git a/crates/shirabe/src/installer/project_installer.rs b/crates/shirabe/src/installer/project_installer.rs index 628e4252..6fee682f 100644 --- a/crates/shirabe/src/installer/project_installer.rs +++ b/crates/shirabe/src/installer/project_installer.rs @@ -3,7 +3,7 @@ use crate::downloader::DownloadManagerInterface; use crate::installer::InstallerInterface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Filesystem; use shirabe_php_shim::{InvalidArgumentException, PhpMixed}; @@ -37,7 +37,7 @@ impl InstallerInterface for ProjectInstaller { fn is_installed( &self, - _repo: &mut dyn InstalledRepositoryInterface, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { Ok(false) @@ -94,7 +94,7 @@ impl InstallerInterface for ProjectInstaller { async fn install( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { self.download_manager @@ -105,7 +105,7 @@ impl InstallerInterface for ProjectInstaller { async fn update( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _initial: PackageInterfaceHandle, _target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -118,7 +118,7 @@ impl InstallerInterface for ProjectInstaller { async fn uninstall( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { Err(InvalidArgumentException { diff --git a/crates/shirabe/src/plugin.rs b/crates/shirabe/src/plugin.rs index a26e1173..a629e4c0 100644 --- a/crates/shirabe/src/plugin.rs +++ b/crates/shirabe/src/plugin.rs @@ -1,6 +1,7 @@ pub mod capability; pub mod capable; pub mod command_event; +pub mod php_plugin_proxy; pub mod plugin_blocked_exception; pub mod plugin_events; pub mod plugin_interface; @@ -13,6 +14,7 @@ pub mod pre_pool_create_event; pub use capability::*; pub use capable::*; pub use command_event::*; +pub use php_plugin_proxy::*; pub use plugin_blocked_exception::*; pub use plugin_events::*; pub use plugin_interface::*; diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs new file mode 100644 index 00000000..87d67ea4 --- /dev/null +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -0,0 +1,360 @@ +//! PHP-backed `PluginInterface` adapter and the R table serving its RPC callbacks. +//! +//! This module has no Composer counterpart: it is the Rust half of the plugin runtime split +//! (a real PHP child process executes the plugin code, see `docs/dev/php-rpc.md`). The plugin +//! entity lives in the worker's P table; Rust-side entities the plugin can call back into +//! (`$composer`, `$io`) live in the R table here. + +use crate::autoload::ClassLoader; +use crate::composer::ComposerHandle; +use crate::io::IOInterface; +use crate::plugin::plugin_interface::PluginInterface; +use indexmap::IndexMap; +use shirabe_php_rpc::{ + PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method, release_php_handle, +}; + +/// A Rust-side entity a PHP proxy stub points back to. +#[derive(Debug)] +enum RustEntity { + Composer(ComposerHandle), + Io(std::rc::Rc<std::cell::RefCell<dyn IOInterface>>), +} + +thread_local! { + /// The R table. Entries are strong references kept for the worker's lifetime. + /// TODO(plugin): GC (dropping entries on ReleaseRustHandle) is not implemented yet; + /// until then entities registered here are intentionally never released. + static R_TABLE: std::cell::RefCell<IndexMap<u64, RustEntity>> = + std::cell::RefCell::new(IndexMap::new()); +} + +/// Registers the Composer instance in the R table, interned by shared-pointer identity so the +/// same instance always crosses the boundary as the same handle (`===` in the child). +pub(crate) fn register_composer_entity(composer: &ComposerHandle) -> u64 { + R_TABLE.with(|table| { + let mut table = table.borrow_mut(); + let ptr = std::rc::Rc::as_ptr(composer.as_rc()) as *const () as usize; + for (rhandle, entity) in table.iter() { + if let RustEntity::Composer(existing) = entity + && std::rc::Rc::as_ptr(existing.as_rc()) as *const () as usize == ptr + { + return *rhandle; + } + } + let rhandle = shirabe_php_rpc::alloc_rhandle(); + table.insert(rhandle, RustEntity::Composer(composer.clone())); + rhandle + }) +} + +/// Registers an IO instance in the R table, interned like [`register_composer_entity`]. +pub(crate) fn register_io_entity(io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) -> u64 { + R_TABLE.with(|table| { + let mut table = table.borrow_mut(); + let ptr = std::rc::Rc::as_ptr(io) as *const () as usize; + for (rhandle, entity) in table.iter() { + if let RustEntity::Io(existing) = entity + && std::rc::Rc::as_ptr(existing) as *const () as usize == ptr + { + return *rhandle; + } + } + let rhandle = shirabe_php_rpc::alloc_rhandle(); + table.insert(rhandle, RustEntity::Io(io.clone())); + rhandle + }) +} + +/// The PHP class name (= proxy stub class) of a Rust IO instance, for the `__class` field of +/// its handle descriptor. +pub(crate) fn io_stub_class( + io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, +) -> anyhow::Result<&'static str> { + let borrowed = io.borrow(); + let any = borrowed.as_any(); + if any + .downcast_ref::<crate::io::buffer_io::BufferIO>() + .is_some() + { + Ok("Composer\\IO\\BufferIO") + } else if any + .downcast_ref::<crate::io::console_io::ConsoleIO>() + .is_some() + { + Ok("Composer\\IO\\ConsoleIO") + } else if any.downcast_ref::<crate::io::null_io::NullIO>().is_some() { + Ok("Composer\\IO\\NullIO") + } else { + // TODO(plugin): only the IO classes with hand-written proxy stubs can cross the + // boundary until a stub generator exists. + Err(anyhow::anyhow!( + "no proxy stub class is available for this IO implementation" + )) + } +} + +/// Looks a class up in every registered Rust-side `ClassLoader`, in registration order — the +/// Rust mirror of what the PHP `spl_autoload_register` stack would do in-process. +pub(crate) fn find_file_in_registered_loaders(class: &str) -> Option<String> { + for (_vendor_dir, mut loader) in ClassLoader::get_registered_loaders() { + if let Some(file) = loader.find_file(class) { + return Some(file); + } + } + None +} + +/// Serves `CallRustMethod` requests from the child while a plugin-related call is in flight: +/// rhandle 0 is the runtime service endpoint (autoload lookups), every other rhandle resolves +/// through the R table. Unsupported methods are explicit errors, never silent fallbacks. +#[derive(Debug)] +pub(crate) struct PluginRpcDispatcher; + +impl RustMethodDispatcher for PluginRpcDispatcher { + fn dispatch( + &mut self, + rhandle: u64, + method_name: &str, + args: Vec<PluginValue>, + _out_param_positions: &[u32], + ) -> Result<PluginValue, PhpThrow> { + if rhandle == 0 { + if method_name == "__shirabe_find_file" { + let class = match args.first() { + Some(PluginValue::String(bytes)) => String::from_utf8_lossy(bytes).into_owned(), + other => { + return Err(runtime_throw(format!( + "__shirabe_find_file expects a class name argument, got {other:?}" + ))); + } + }; + return Ok(match find_file_in_registered_loaders(&class) { + Some(file) => PluginValue::string(file), + None => PluginValue::Null, + }); + } + return Err(runtime_throw(format!( + "unknown runtime service method `{method_name}`" + ))); + } + + R_TABLE.with(|table| { + let table = table.borrow(); + match table.get(&rhandle) { + Some(RustEntity::Io(io)) => dispatch_io_method(io, method_name, &args), + Some(RustEntity::Composer(_)) => { + // TODO(plugin): the Composer object graph (getConfig, getRepositoryManager, + // ...) becomes reachable over RPC later. + Err(runtime_throw(format!( + "the Composer method `{method_name}` is not available over RPC yet" + ))) + } + None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), + } + }) + } +} + +fn dispatch_io_method( + io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + method_name: &str, + args: &[PluginValue], +) -> Result<PluginValue, PhpThrow> { + match method_name { + "write" | "writeError" => { + let (messages, newline, verbosity) = decode_write_args(method_name, args)?; + let borrowed = io.borrow(); + for message in &messages { + if method_name == "write" { + borrowed.write3(message, newline, verbosity); + } else { + borrowed.write_error3(message, newline, verbosity); + } + } + Ok(PluginValue::Null) + } + "isInteractive" => Ok(PluginValue::Bool(io.borrow().is_interactive())), + "isVerbose" => Ok(PluginValue::Bool(io.borrow().is_verbose())), + "isVeryVerbose" => Ok(PluginValue::Bool(io.borrow().is_very_verbose())), + "isDebug" => Ok(PluginValue::Bool(io.borrow().is_debug())), + "isDecorated" => Ok(PluginValue::Bool(io.borrow().is_decorated())), + // TODO(plugin): the remaining IOInterface surface (ask*, authentications, ...) is + // widened on demand, driven by explicit errors from real plugins. + other => Err(runtime_throw(format!( + "the IO method `{other}` is not available over RPC yet" + ))), + } +} + +/// Decodes `($messages, bool $newline, int $verbosity)`: `$messages` is a string or a list of +/// strings in PHP. +fn decode_write_args( + method_name: &str, + args: &[PluginValue], +) -> Result<(Vec<String>, bool, i64), PhpThrow> { + let messages = match args.first() { + Some(PluginValue::String(bytes)) => vec![String::from_utf8_lossy(bytes).into_owned()], + Some(PluginValue::List(items)) => { + let mut messages = Vec::with_capacity(items.len()); + for item in items { + match item { + PluginValue::String(bytes) => { + messages.push(String::from_utf8_lossy(bytes).into_owned()); + } + other => { + return Err(runtime_throw(format!( + "{method_name} expects string messages, got {other:?}" + ))); + } + } + } + messages + } + other => { + return Err(runtime_throw(format!( + "{method_name} expects a string or list of strings, got {other:?}" + ))); + } + }; + let newline = match args.get(1) { + Some(PluginValue::Bool(b)) => *b, + None => true, + other => { + return Err(runtime_throw(format!( + "{method_name} expects a bool newline flag, got {other:?}" + ))); + } + }; + let verbosity = match args.get(2) { + Some(PluginValue::Int(v)) => *v, + None => crate::io::NORMAL, + other => { + return Err(runtime_throw(format!( + "{method_name} expects an int verbosity, got {other:?}" + ))); + } + }; + Ok((messages, newline, verbosity)) +} + +fn runtime_throw(message: String) -> PhpThrow { + PhpThrow { + exception_class: "RuntimeException".to_string(), + message, + code: 0, + } +} + +/// `PluginInterface` adapter for a plugin entity living in the PHP child process: every +/// lifecycle call is forwarded as a `CallPhpMethod` RPC. +#[derive(Debug)] +pub struct PhpPluginProxy { + pub(crate) phandle: u64, + pub(crate) class: String, +} + +impl PhpPluginProxy { + pub fn new(phandle: u64, class: String) -> Self { + Self { phandle, class } + } + + fn forward_lifecycle_call( + &self, + method: &str, + composer: &ComposerHandle, + io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + ) -> anyhow::Result<()> { + let composer_rhandle = register_composer_entity(composer); + let io_rhandle = register_io_entity(io); + let io_class = io_stub_class(io)?; + let args = vec![ + PluginValue::RustHandle(RustObjHandle { + rhandle: composer_rhandle, + class: "Composer\\Composer".to_string(), + epoch: 0, + snapshot: None, + }), + PluginValue::RustHandle(RustObjHandle { + rhandle: io_rhandle, + class: io_class.to_string(), + epoch: 0, + snapshot: None, + }), + ]; + let outcome = call_php_method(self.phandle, method, args, Some(&mut PluginRpcDispatcher))?; + match outcome { + Ok(_) => Ok(()), + // TODO(plugin): the original exception class is collapsed to RuntimeException on + // this side of the boundary. + Err(throw) => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException { + message: throw.message, + code: throw.code, + })), + } + } + + /// For testing only: reads a public property of the plugin entity in the child, mirroring + /// PHPUnit assertions like `$plugins[0]->version`. + pub fn __get_property(&self, name: &str) -> anyhow::Result<shirabe_php_shim::PhpMixed> { + let outcome = shirabe_php_rpc::call_function( + "__shirabe_get_property", + vec![ + PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle { + phandle: self.phandle, + class: self.class.clone(), + implements: Vec::new(), + }), + PluginValue::string(name), + ], + )?; + match outcome { + Ok(value) => Ok(value.to_php_mixed()?), + Err(throw) => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException { + message: throw.message, + code: throw.code, + })), + } + } +} + +impl PluginInterface for PhpPluginProxy { + fn activate( + &mut self, + composer: ComposerHandle, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + ) -> anyhow::Result<()> { + self.forward_lifecycle_call("activate", &composer, &io) + } + + fn deactivate( + &mut self, + composer: ComposerHandle, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + ) -> anyhow::Result<()> { + self.forward_lifecycle_call("deactivate", &composer, &io) + } + + fn uninstall( + &mut self, + composer: ComposerHandle, + io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, + ) -> anyhow::Result<()> { + self.forward_lifecycle_call("uninstall", &composer, &io) + } + + fn get_class_name(&self) -> String { + self.class.clone() + } + + fn as_php_plugin_proxy(&self) -> Option<&PhpPluginProxy> { + Some(self) + } +} + +impl Drop for PhpPluginProxy { + fn drop(&mut self) { + // A dead worker has nothing left to release. + let _ = release_php_handle(self.phandle); + } +} diff --git a/crates/shirabe/src/plugin/plugin_interface.rs b/crates/shirabe/src/plugin/plugin_interface.rs index 5a92a87a..54fa6009 100644 --- a/crates/shirabe/src/plugin/plugin_interface.rs +++ b/crates/shirabe/src/plugin/plugin_interface.rs @@ -7,23 +7,29 @@ use crate::plugin::Capable; pub const PLUGIN_API_VERSION: &str = "2.9.0"; pub trait PluginInterface: std::fmt::Debug { + // The PHP methods return void but may throw; the owned `ComposerHandle` follows the shared + // handle policy — plugins typically store `$composer` beyond the call. fn activate( &mut self, - composer: &ComposerHandle, + composer: ComposerHandle, io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, - ); + ) -> anyhow::Result<()>; fn deactivate( &mut self, - composer: &ComposerHandle, + composer: ComposerHandle, io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, - ); + ) -> anyhow::Result<()>; fn uninstall( &mut self, - composer: &ComposerHandle, + composer: ComposerHandle, io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>, - ); + ) -> anyhow::Result<()>; + + /// PHP: `get_class($plugin)`. Rust has no runtime class name, so each implementor carries + /// the name PHP would report. + fn get_class_name(&self) -> String; // TODO(plugin): PHP-side `instanceof` checks for EventSubscriberInterface / Capable. // EventSubscriberInterface is not dyn-compatible (its only method is associated, not @@ -35,4 +41,10 @@ pub trait PluginInterface: std::fmt::Debug { fn as_capable(&self) -> Option<&dyn Capable> { None } + + /// For testing only: recovers the PHP-backed proxy so tests can read plugin properties the + /// way PHPUnit asserts `$plugins[0]->version`. + fn as_php_plugin_proxy(&self) -> Option<&crate::plugin::PhpPluginProxy> { + None + } } diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs index 572bb3b6..6760bf20 100644 --- a/crates/shirabe/src/plugin/plugin_manager.rs +++ b/crates/shirabe/src/plugin/plugin_manager.rs @@ -6,6 +6,7 @@ use crate::composer::PartialComposerHandle; use crate::composer::{ComposerHandle, ComposerWeakHandle}; +use crate::event_dispatcher::{EventDispatcher, unwrap_php_result}; use crate::factory::DisablePlugins; use crate::installer::InstallerInterface; use crate::io::IOInterface; @@ -17,16 +18,20 @@ use crate::package::base_package::{self}; use crate::package::version::VersionParser; use crate::plugin::PluginBlockedException; use crate::plugin::capability::Capability; +use crate::plugin::php_plugin_proxy::{PhpPluginProxy, PluginRpcDispatcher}; use crate::plugin::plugin_interface::{self, PluginInterface}; use crate::repository::InstalledRepository; -use crate::repository::RepositoryInterface; +use crate::repository::RepositoryInterfaceHandle; use crate::repository::RepositoryUtils; +use crate::repository::RootPackageRepository; use crate::util::PackageSorter; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; +use shirabe_php_rpc::{PluginValue, call_function_with_dispatcher}; use shirabe_php_shim::{ E_USER_DEPRECATED, PhpMixed, RuntimeException, UnexpectedValueException, array_key_exists, - get_class_obj, implode, ksort, php_regex, trigger_error, trim, var_export_str, version_compare, + dirname, file_get_contents, implode, ksort, php_regex, preg_quote, strrpos, strtr_array, + substr, trigger_error, trim, var_export_str, version_compare, }; use shirabe_semver::constraint::SimpleConstraint; @@ -37,7 +42,9 @@ pub struct PluginManager { pub(crate) global_composer: Option<PartialComposerHandle>, pub(crate) version_parser: VersionParser, pub(crate) disable_plugins: DisablePlugins, - pub(crate) plugins: Vec<Box<dyn PluginInterface>>, + // PHP stores the same plugin instance in both $plugins and $registeredPlugins (reference + // semantics); shared handles preserve the identity comparisons that relies on. + pub(crate) plugins: Vec<std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>>, pub(crate) registered_plugins: IndexMap<String, Vec<PluginOrInstaller>>, allow_plugin_rules: Option<IndexMap<String, bool>>, allow_global_plugin_rules: Option<IndexMap<String, bool>>, @@ -46,11 +53,12 @@ pub struct PluginManager { #[derive(Debug)] pub enum PluginOrInstaller { - Plugin(Box<dyn PluginInterface>), + Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>), Installer(Box<dyn InstallerInterface>), } -static mut CLASS_COUNTER: i64 = 0; +/// PHP `private static $classCounter = 0;`. +static CLASS_COUNTER: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0); impl PluginManager { pub fn new( @@ -121,7 +129,7 @@ impl PluginManager { .borrow() .get_local_repository(); self.load_repository( - &mut *repo.borrow_mut(), + &repo, false, Some(self.composer_full().borrow().get_package().clone()), )?; @@ -136,7 +144,7 @@ impl PluginManager { .get_repository_manager() .borrow() .get_local_repository(); - self.load_repository(&mut *repo.borrow_mut(), true, None)?; + self.load_repository(&repo, true, None)?; } Ok(()) } @@ -151,7 +159,7 @@ impl PluginManager { .get_repository_manager() .borrow() .get_local_repository(); - self.deactivate_repository(&mut *repo.borrow_mut(), false)?; + self.deactivate_repository(&repo, false)?; } if self.global_composer.is_some() && !self.are_plugins_disabled("global") { @@ -163,7 +171,7 @@ impl PluginManager { .get_repository_manager() .borrow() .get_local_repository(); - self.deactivate_repository(&mut *repo.borrow_mut(), true)?; + self.deactivate_repository(&repo, true)?; } Ok(()) @@ -173,7 +181,7 @@ impl PluginManager { /// /// PHP returns `$this->plugins` directly; the plugin objects are shared by reference, so this /// borrows the stored instances rather than cloning them. - pub fn get_plugins(&self) -> &[Box<dyn PluginInterface>] { + pub fn get_plugins(&self) -> &[std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>] { &self.plugins } @@ -276,10 +284,7 @@ impl PluginManager { return Ok(()); } - // TODO(plugin): the rest of registerPackage performs class-level eval() to load the plugin source. - // This is a runtime concern that requires PHP semantics; not portable to Rust without a PHP interpreter. - // The remainder of the function is mirrored as references but performs no actual loading. - let _old_installer_plugin = package.get_type() == "composer-installer"; + let old_installer_plugin = package.get_type() == "composer-installer"; if self.registered_plugins.contains_key(&package.get_name()) { return Ok(()); @@ -304,7 +309,7 @@ impl PluginManager { code: 0, }.into()); } - let _classes: Vec<String> = if let Some(arr) = class_value.and_then(|v| v.as_list()) { + let classes: Vec<String> = if let Some(arr) = class_value.and_then(|v| v.as_list()) { arr.iter() .filter_map(|v| v.as_string().map(|s| s.to_string())) .collect() @@ -317,22 +322,260 @@ impl PluginManager { ] }; - // TODO(plugin): everything below this point in the original PHP would create runtime instances: - // - clone root package and clear `files` autoloads - // - build a synthetic InstalledRepository for plugin dependencies - // - parseAutoloads / createLoader on the autoload generator - // - eval the plugin class source under a temporary class name - // - call activate(...) and register subscribers - // None of that is implementable without a PHP runtime, and so the body is intentionally left as a no-op stub. - let _ = fail_on_missing_classes; + let composer = self.composer_full(); + let local_repo = composer + .borrow() + .get_repository_manager() + .borrow() + .get_local_repository(); + let global_repo = self.global_composer.as_ref().map(|gc| { + gc.borrow_partial() + .get_repository_manager() + .borrow() + .get_local_repository() + }); + + let root_package = RootPackageInterfaceHandle::dup(composer.borrow().get_package()); + + // clear files autoload rules from the root package as the root dependencies are not + // necessarily all present yet when booting this runtime autoloader + let mut root_package_autoloads = root_package.get_autoload(); + root_package_autoloads.insert("files".to_string(), PhpMixed::List(vec![])); + root_package.set_autoload(root_package_autoloads); + let mut root_package_autoloads = root_package.get_dev_autoload(); + root_package_autoloads.insert("files".to_string(), PhpMixed::List(vec![])); + root_package.set_dev_autoload(root_package_autoloads); + + let root_package_repo = + RepositoryInterfaceHandle::new(RootPackageRepository::new(root_package.clone())); + let mut installed_repo = + InstalledRepository::new(vec![local_repo.clone(), root_package_repo]); + if let Some(global_repo) = &global_repo { + installed_repo.add_repository(global_repo.clone()); + } + + let mut autoload_packages: IndexMap<String, PackageInterfaceHandle> = IndexMap::new(); + autoload_packages.insert(package.get_name().to_string(), package.clone()); + let autoload_packages = + self.collect_dependencies(&installed_repo, autoload_packages, package.clone())?; + + let generator = composer.borrow().get_autoload_generator(); + let root_package_as_package: PackageInterfaceHandle = root_package.clone().into(); + let mut autoloads: Vec<(PackageInterfaceHandle, Option<String>)> = + vec![(root_package_as_package.clone(), Some(String::new()))]; + for (_name, autoload_package) in &autoload_packages { + if autoload_package.ptr_eq(&root_package_as_package) { + continue; + } + + let is_global_package = match &global_repo { + Some(gr) => gr.borrow_mut().has_package(autoload_package.clone())?, + None => false, + }; + let install_path = self.get_install_path(autoload_package.clone(), is_global_package); + let install_path = match install_path { + Some(p) => p, + None => continue, + }; + autoloads.push((autoload_package.clone(), Some(install_path))); + } + + let map = + generator + .borrow() + .parse_autoloads(autoloads, root_package, PhpMixed::Bool(false)); + let vendor_dir = composer + .borrow() + .get_config() + .borrow() + .get("vendor-dir") + .as_string() + .map(|s| s.to_string()); + let mut class_loader = generator.borrow().create_loader(&map, vendor_dir); + class_loader.register(false); + + // The plugin code runs in the PHP worker: load the Composer PHP runtime (contracts like + // PluginInterface) and the reverse-RPC autoloader before touching plugin classes there. + EventDispatcher::ensure_composer_php_runtime()?; + EventDispatcher::ensure_script_autoloader()?; + + if let Some(files) = map.get("files").and_then(|v| v.as_array()) { + for (file_identifier, file) in files { + // exclude laminas/laminas-zendframework-bridge:src/autoload.php as it breaks Composer in some conditions + // see https://github.com/composer/composer/issues/10349 and https://github.com/composer/composer/issues/10401 + // this hack can be removed once this deprecated package stop being installed + if file_identifier == "7e9bd612cc444b3eed788ebbe46263a0" { + continue; + } + let file = file.as_string().unwrap_or_else(|| { + panic!("autoload files entry `{file_identifier}` is not a string path") + }); + self.php_runtime_composer_require(file_identifier, file)?; + } + } + + for class in classes { + let mut class = class; + if self.php_runtime_class_exists(&class, false)? { + class = trim(&class, Some("\\")).to_string(); + let path = class_loader.find_file(&class).unwrap_or_else(|| { + panic!("plugin class `{class}` is already defined but has no autoloadable file") + }); + let code = file_get_contents(&path) + .unwrap_or_else(|| panic!("unable to read the plugin class file `{path}`")); + let class_counter = CLASS_COUNTER.load(std::sync::atomic::Ordering::Relaxed); + let separator_pos = strrpos(&class, "\\"); + let mut class_name = class.clone(); + // PHP: `if ($separatorPos)` — position 0 is falsy and keeps the full name. + if let Some(separator_pos) = separator_pos + && separator_pos != 0 + { + class_name = substr(&class, (separator_pos + 1) as i64, None); + } + let code = Preg::replace4( + format!( + "{{^((?:(?:final|readonly)\\s+)*(?:\\s*))class\\s+({})}}mi", + preg_quote(&class_name, None) + ), + &format!("$1class $2_composer_tmp{}", class_counter), + &code, + 1, + ); + let mut replacements: IndexMap<String, String> = IndexMap::new(); + replacements.insert("__FILE__".to_string(), var_export_str(&path, true)); + replacements.insert("__DIR__".to_string(), var_export_str(&dirname(&path), true)); + replacements.insert("__CLASS__".to_string(), var_export_str(&class, true)); + let code = strtr_array(&code, &replacements); + let code = Preg::replace4(r"/^\s*<\?(php)?/i", "", &code, 1); + self.php_runtime_eval(&code)?; + class = format!("{}_composer_tmp{}", class, class_counter); + CLASS_COUNTER.store(class_counter + 1, std::sync::atomic::Ordering::Relaxed); + } + + if old_installer_plugin { + // TODO(plugin): legacy composer-installer plugins need the InstallerInterface + // reverse adapter, which does not exist yet; explicit error until then. + return Err(RuntimeException { + message: format!( + "Shirabe cannot load \"{}\": legacy composer-installer plugins are not supported yet", + package.get_name() + ), + code: 0, + } + .into()); + } else if self.php_runtime_class_exists(&class, true)? { + if !self.php_runtime_is_a(&class, "Composer\\Plugin\\PluginInterface")? { + return Err(RuntimeException { + message: format!( + "Could not activate plugin \"{}\" as \"{}\" does not implement Composer\\Plugin\\PluginInterface", + package.get_name(), + class + ), + code: 0, + } + .into()); + } + let handle = self.php_runtime_new_object(&class)?; + let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> = std::rc::Rc::new( + std::cell::RefCell::new(PhpPluginProxy::new(handle.phandle, handle.class)), + ); + self.add_plugin(plugin.clone(), is_global_plugin, Some(package.clone()))?; + self.registered_plugins + .entry(package.get_name().to_string()) + .or_default() + .push(PluginOrInstaller::Plugin(plugin)); + } else if fail_on_missing_classes { + return Err(UnexpectedValueException { + message: format!( + "Plugin {} could not be initialized, class not found: {}", + package.get_name(), + class + ), + code: 0, + } + .into()); + } + } Ok(()) } + /// Runs a boolean runtime query in the PHP worker with the plugin dispatcher active. + fn php_runtime_bool(&self, function: &str, args: Vec<PluginValue>) -> anyhow::Result<bool> { + let value = unwrap_php_result(call_function_with_dispatcher( + function, + args, + Some(&mut PluginRpcDispatcher), + ))?; + match value { + PluginValue::Bool(value) => Ok(value), + other => Err(anyhow::anyhow!( + "PHP runtime query `{function}` did not return a bool: {other:?}" + )), + } + } + + fn php_runtime_class_exists(&self, class: &str, autoload: bool) -> anyhow::Result<bool> { + self.php_runtime_bool( + "class_exists", + vec![PluginValue::string(class), PluginValue::Bool(autoload)], + ) + } + + fn php_runtime_is_a(&self, class: &str, interface: &str) -> anyhow::Result<bool> { + self.php_runtime_bool( + "is_a", + vec![ + PluginValue::string(class), + PluginValue::string(interface), + PluginValue::Bool(true), + ], + ) + } + + fn php_runtime_eval(&self, code: &str) -> anyhow::Result<()> { + unwrap_php_result(call_function_with_dispatcher( + "__shirabe_eval", + vec![PluginValue::string(code)], + Some(&mut PluginRpcDispatcher), + ))?; + Ok(()) + } + + fn php_runtime_composer_require( + &self, + file_identifier: &str, + file: &str, + ) -> anyhow::Result<()> { + unwrap_php_result(call_function_with_dispatcher( + "__shirabe_composer_require", + vec![ + PluginValue::string(file_identifier), + PluginValue::string(file), + ], + Some(&mut PluginRpcDispatcher), + ))?; + Ok(()) + } + + /// PHP `new $class()` in the worker, returning the P-table handle of the new entity. + fn php_runtime_new_object(&self, class: &str) -> anyhow::Result<shirabe_php_rpc::PhpObjHandle> { + let value = unwrap_php_result(shirabe_php_rpc::new_object( + class, + vec![], + Some(&mut PluginRpcDispatcher), + ))?; + match value { + PluginValue::PhpHandle(handle) => Ok(handle), + other => Err(anyhow::anyhow!( + "instantiating `{class}` did not return a PHP handle: {other:?}" + )), + } + } + /// Deactivates a plugin package - pub fn deactivate_package(&mut self, package: PackageInterfaceHandle) { - // TODO(plugin): deactivation flow + pub fn deactivate_package(&mut self, package: PackageInterfaceHandle) -> anyhow::Result<()> { if !self.registered_plugins.contains_key(&package.get_name()) { - return; + return Ok(()); } let plugins = self @@ -349,17 +592,17 @@ impl PluginManager { .remove_installer(&*inst); } PluginOrInstaller::Plugin(p) => { - self.remove_plugin(&*p); + self.remove_plugin(&p)?; } } } + Ok(()) } /// Uninstall a plugin package - pub fn uninstall_package(&mut self, package: PackageInterfaceHandle) { - // TODO(plugin): uninstall flow + pub fn uninstall_package(&mut self, package: PackageInterfaceHandle) -> anyhow::Result<()> { if !self.registered_plugins.contains_key(&package.get_name()) { - return; + return Ok(()); } let plugins = self @@ -375,12 +618,13 @@ impl PluginManager { .borrow_mut() .remove_installer(&*inst); } - PluginOrInstaller::Plugin(mut p) => { - self.remove_plugin(&*p); - self.uninstall_plugin(&mut *p); + PluginOrInstaller::Plugin(p) => { + self.remove_plugin(&p)?; + self.uninstall_plugin(&p)?; } } } + Ok(()) } /// Returns the version of the internal composer-plugin-api package. @@ -391,11 +635,10 @@ impl PluginManager { /// Adds a plugin, activates it and registers it with the event dispatcher pub fn add_plugin( &mut self, - mut plugin: Box<dyn PluginInterface>, + plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>, is_global_plugin: bool, source_package: Option<PackageInterfaceHandle>, ) -> anyhow::Result<()> { - // TODO(plugin): plugin activation if self.are_plugins_disabled(if is_global_plugin { "global" } else { "local" }) { return Ok(()); } @@ -419,16 +662,20 @@ impl PluginManager { plugin_optional, true, )? { - self.io.write_error(&format!( - "Skipped loading \"{} from {}\" {} as it is not in config.allow-plugins", - get_class_obj(&*plugin), - sp.get_name(), - if is_global_plugin || self.running_in_global_dir { - "(installed globally) " - } else { - "" - } - )); + self.io.write_error3( + &format!( + "Skipped loading \"{} from {}\" {} as it is not in config.allow-plugins", + plugin.borrow().get_class_name(), + sp.get_name(), + if is_global_plugin || self.running_in_global_dir { + "(installed globally) " + } else { + "" + } + ), + true, + crate::io::DEBUG, + ); return Ok(()); } } @@ -441,65 +688,88 @@ impl PluginManager { if is_global_plugin || self.running_in_global_dir { details.push("installed globally".to_string()); } - self.io.write_error(&format!( - "Loading plugin {}{}", - get_class_obj(&*plugin), - if !details.is_empty() { - format!(" ({})", implode(", ", &details)) - } else { - String::new() - } - )); - plugin.activate(&self.composer_full(), self.io.clone()); + self.io.write_error3( + &format!( + "Loading plugin {}{}", + plugin.borrow().get_class_name(), + if !details.is_empty() { + format!(" ({})", implode(", ", &details)) + } else { + String::new() + } + ), + true, + crate::io::DEBUG, + ); + self.plugins.push(plugin.clone()); + plugin + .borrow_mut() + .activate(self.composer_full(), self.io.clone())?; // TODO(plugin): if plugin is EventSubscriberInterface, hook into the event dispatcher // The PHP code calls $this->composer->getEventDispatcher()->addSubscriber($plugin); // — add_subscriber here is generic over `S: EventSubscriberInterface` and cannot // accept a `&dyn EventSubscriberInterface`. Skipped until subscriber dispatch is // implemented dynamically. - let _ = (*plugin).is_event_subscriber_interface(); - self.plugins.push(plugin); + let _ = plugin.borrow().is_event_subscriber_interface(); Ok(()) } /// Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance - pub fn remove_plugin(&mut self, plugin: &dyn PluginInterface) { - // TODO(plugin): plugin removal — PHP uses identity (`===`) comparison via array_search($plugin, $this->plugins, true). - let plugin_addr = plugin as *const dyn PluginInterface as *const () as usize; - let index = self.plugins.iter().position(|p| { - (p.as_ref() as *const dyn PluginInterface as *const () as usize) == plugin_addr - }); + pub fn remove_plugin( + &mut self, + plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>, + ) -> anyhow::Result<()> { + // PHP uses identity (`===`) comparison via array_search($plugin, $this->plugins, true). + let index = self + .plugins + .iter() + .position(|p| std::rc::Rc::ptr_eq(p, plugin)); let index = match index { Some(i) => i, - None => return, + None => return Ok(()), }; - self.io - .write_error(&format!("Unloading plugin {}", get_class_obj(plugin))); - let mut removed = self.plugins.remove(index); - removed.deactivate(&self.composer_full(), self.io.clone()); + self.io.write_error3( + &format!("Unloading plugin {}", plugin.borrow().get_class_name()), + true, + crate::io::DEBUG, + ); + let removed = self.plugins.remove(index); + removed + .borrow_mut() + .deactivate(self.composer_full(), self.io.clone())?; // TODO(plugin): remove_listener accepts any callable/object in PHP; here we have // a plugin instance and need to translate to a Callable, which is not portable // without runtime reflection. - let _ = plugin; + Ok(()) } /// Notifies a plugin it is being uninstalled and should clean up - pub fn uninstall_plugin(&self, plugin: &mut dyn PluginInterface) { - // TODO(plugin): plugin uninstall hook - self.io - .write_error(&format!("Uninstalling plugin {}", get_class_obj(plugin))); - plugin.uninstall(&self.composer_full(), self.io.clone()); + pub fn uninstall_plugin( + &self, + plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>, + ) -> anyhow::Result<()> { + self.io.write_error3( + &format!("Uninstalling plugin {}", plugin.borrow().get_class_name()), + true, + crate::io::DEBUG, + ); + plugin + .borrow_mut() + .uninstall(self.composer_full(), self.io.clone())?; + Ok(()) } + // The repository stays behind its shared handle (borrowed only transiently) because + // register_package re-enters the same local repository through the RepositoryManager. fn load_repository( &mut self, - repo: &mut dyn RepositoryInterface, + repo: &RepositoryInterfaceHandle, is_global_repo: bool, root_package: Option<RootPackageInterfaceHandle>, ) -> anyhow::Result<()> { - // TODO(plugin): repository scan for plugin packages let packages = repo.get_packages()?; let mut weights: IndexMap<String, i64> = IndexMap::new(); @@ -570,10 +840,9 @@ impl PluginManager { fn deactivate_repository( &mut self, - repo: &mut dyn RepositoryInterface, + repo: &RepositoryInterfaceHandle, _is_global_repo: bool, ) -> anyhow::Result<()> { - // TODO(plugin): deactivate plugins from a repository let packages = repo.get_packages()?; // PHP: $sortedPackages = array_reverse(PackageSorter::sortPackages($packages)); let mut sorted_packages = PackageSorter::sort_packages(packages.to_vec(), IndexMap::new()); @@ -584,10 +853,10 @@ impl PluginManager { continue; } if "composer-plugin" == package.get_type() { - self.deactivate_package(package.clone()); + self.deactivate_package(package.clone())?; // Backward compatibility } else if "composer-installer" == package.get_type() { - self.deactivate_package(package.clone()); + self.deactivate_package(package.clone())?; } } @@ -686,7 +955,7 @@ impl PluginManager { return Err(UnexpectedValueException { message: format!( "Plugin {} provided invalid capability class name(s), got {}", - get_class_obj(plugin), + plugin.get_class_name(), var_export_str(capabilities.get(capability).unwrap(), true) ), code: 0, @@ -721,9 +990,11 @@ impl PluginManager { // TODO(plugin): aggregate capabilities across all loaded plugins let mut capabilities: Vec<Box<dyn Capability>> = vec![]; for plugin in self.get_plugins() { - if let Ok(Some(capability)) = - self.get_plugin_capability(&**plugin, capability_class_name, ctor_args.clone()) - { + if let Ok(Some(capability)) = self.get_plugin_capability( + &*plugin.borrow(), + capability_class_name, + ctor_args.clone(), + ) { capabilities.push(capability); } } diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index 32e4022a..b2a2530e 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -343,10 +343,20 @@ impl FilesystemRepository { ); // make sure the in memory state is up to date with on disk - // TODO(plugin): whether this reload must also be pushed to the plugin PHP child - // process is undecided; the InstalledVersions state a plugin observes after this - // dump is undefined (docs/dev/plugin-class-classification.md, "Bootstrap classes - // cannot be stub-shadowed"). + // The upstream in-process reload is split in two here: the Rust-side mirror below, + // and a push into the PHP worker where the real observers (plugins) live. The push + // is skipped when no worker is running — with no child there is nothing that could + // observe the stale state, and the worker glue additionally ignores it while the + // class is not loaded there (a later lazy load reads the freshly written + // installed.php, matching upstream observations in every case). + if shirabe_php_rpc::worker_is_running() { + crate::event_dispatcher::unwrap_php_result(shirabe_php_rpc::call_function( + "__shirabe_installed_versions_reload", + vec![shirabe_php_rpc::PluginValue::from_php_mixed( + &PhpMixed::Array(versions.clone()), + )], + ))?; + } InstalledVersions::reload(versions); // make sure the selfDir matches the expected data at runtime if the class was loaded from the vendor dir, as it may have been diff --git a/crates/shirabe/src/repository/handle.rs b/crates/shirabe/src/repository/handle.rs index 2a7239b9..1e24085b 100644 --- a/crates/shirabe/src/repository/handle.rs +++ b/crates/shirabe/src/repository/handle.rs @@ -3,8 +3,8 @@ use crate::package::BasePackageHandle; use crate::package::PackageInterfaceHandle; use crate::repository::{ - FindPackageConstraint, LoadPackagesResult, LockArrayRepository, PlatformRepository, - ProviderInfo, RepositoryInterface, SearchResult, + FindPackageConstraint, InstalledRepositoryInterface, LoadPackagesResult, LockArrayRepository, + PlatformRepository, ProviderInfo, RepositoryInterface, SearchResult, }; use indexmap::IndexMap; use shirabe_semver::constraint::AnyConstraint; @@ -192,6 +192,62 @@ impl RepositoryInterfaceHandle { } } +/// Shared handle over a repository known to implement `InstalledRepositoryInterface`. +/// +/// The installer pipeline passes this instead of a long-lived `&mut dyn +/// InstalledRepositoryInterface` so that re-entrant access to the same repository through +/// `RepositoryManager::get_local_repository()` — e.g. `PluginManager::register_package` running +/// inside `InstallationManager::execute` — borrows the shared `RefCell` only transiently. +#[derive(Debug, Clone)] +pub struct InstalledRepositoryInterfaceHandle( + std::rc::Rc<std::cell::RefCell<dyn RepositoryInterface>>, +); + +impl InstalledRepositoryInterfaceHandle { + pub fn new<T: RepositoryInterface + 'static>(repository: T) -> Self { + Self::from_repository_handle(&RepositoryInterfaceHandle::new(repository)) + } + + /// PHP has no counterpart for this narrowing: parameters typed + /// `InstalledRepositoryInterface` simply receive such an instance. Handing over a + /// repository that is not one is a programming error. + pub fn from_repository_handle(handle: &RepositoryInterfaceHandle) -> Self { + assert!( + handle.is_installed_repository_interface(), + "repository does not implement InstalledRepositoryInterface" + ); + Self(handle.as_rc().clone()) + } + + pub fn as_repository_handle(&self) -> RepositoryInterfaceHandle { + RepositoryInterfaceHandle::from_rc(self.0.clone()) + } + + pub fn borrow(&self) -> Ref<'_, dyn InstalledRepositoryInterface> { + Ref::map(self.0.borrow(), |r| { + r.as_installed_repository_interface() + .expect("checked at handle construction") + }) + } + + pub fn borrow_mut(&self) -> RefMut<'_, dyn InstalledRepositoryInterface> { + RefMut::map(self.0.borrow_mut(), |r| { + r.as_installed_repository_interface_mut() + .expect("checked at handle construction") + }) + } + + /// PHP `===` (reference identity). + pub fn ptr_eq(&self, other: &Self) -> bool { + std::rc::Rc::ptr_eq(&self.0, &other.0) + } + + /// Stable identity usable as a map key (PHP `spl_object_hash`). + pub fn ptr_id(&self) -> usize { + std::rc::Rc::as_ptr(&self.0) as *const () as usize + } +} + impl PartialEq for RepositoryInterfaceHandle { fn eq(&self, other: &Self) -> bool { std::rc::Rc::ptr_eq(&self.0, &other.0) diff --git a/crates/shirabe/src/repository/installed_array_repository.rs b/crates/shirabe/src/repository/installed_array_repository.rs index c0c0fbb7..9483a1a9 100644 --- a/crates/shirabe/src/repository/installed_array_repository.rs +++ b/crates/shirabe/src/repository/installed_array_repository.rs @@ -137,12 +137,14 @@ impl RepositoryInterface for InstalledArrayRepository { fn as_advisory_provider(&self) -> Option<&dyn AdvisoryProviderInterface> { None } - fn as_installed_repository_interface(&self) -> Option<&dyn InstalledRepositoryInterface> { + fn as_installed_repository_interface( + &self, + ) -> Option<&(dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_installed_repository_interface_mut( &mut self, - ) -> Option<&mut dyn InstalledRepositoryInterface> { + ) -> Option<&mut (dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_writable_repository_interface_mut( diff --git a/crates/shirabe/src/repository/installed_filesystem_repository.rs b/crates/shirabe/src/repository/installed_filesystem_repository.rs index 0bb962fd..1986f32a 100644 --- a/crates/shirabe/src/repository/installed_filesystem_repository.rs +++ b/crates/shirabe/src/repository/installed_filesystem_repository.rs @@ -171,12 +171,14 @@ impl RepositoryInterface for InstalledFilesystemRepository { fn as_advisory_provider(&self) -> Option<&dyn AdvisoryProviderInterface> { None } - fn as_installed_repository_interface(&self) -> Option<&dyn InstalledRepositoryInterface> { + fn as_installed_repository_interface( + &self, + ) -> Option<&(dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_installed_repository_interface_mut( &mut self, - ) -> Option<&mut dyn InstalledRepositoryInterface> { + ) -> Option<&mut (dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_writable_repository_interface_mut( diff --git a/crates/shirabe/src/repository/repository_interface.rs b/crates/shirabe/src/repository/repository_interface.rs index cece0d6f..ed1022f4 100644 --- a/crates/shirabe/src/repository/repository_interface.rs +++ b/crates/shirabe/src/repository/repository_interface.rs @@ -107,15 +107,17 @@ pub trait RepositoryInterface: std::fmt::Debug { None } + // The `+ 'static` object bound lets `InstalledRepositoryInterfaceHandle` project a + // `Ref`/`RefMut` through this method (`Ref::map` needs a lifetime-independent target). fn as_installed_repository_interface( &self, - ) -> Option<&dyn crate::repository::InstalledRepositoryInterface> { + ) -> Option<&(dyn crate::repository::InstalledRepositoryInterface + 'static)> { None } fn as_installed_repository_interface_mut( &mut self, - ) -> Option<&mut dyn crate::repository::InstalledRepositoryInterface> { + ) -> Option<&mut (dyn crate::repository::InstalledRepositoryInterface + 'static)> { None } diff --git a/crates/shirabe/tests/autoload/autoload_generator_test.rs b/crates/shirabe/tests/autoload/autoload_generator_test.rs index 5f0629dd..85e98d81 100644 --- a/crates/shirabe/tests/autoload/autoload_generator_test.rs +++ b/crates/shirabe/tests/autoload/autoload_generator_test.rs @@ -13,7 +13,7 @@ use shirabe::io::{BufferIO, IOInterface}; use shirabe::package::handle::{AliasPackageHandle, PackageHandle, RootPackageHandle}; use shirabe::package::{Link, PackageInterfaceHandle, RootPackageInterfaceHandle}; use shirabe::repository::{ - InstalledArrayRepository, InstalledRepositoryInterface, WritableRepositoryInterface, + InstalledArrayRepository, InstalledRepositoryInterfaceHandle, WritableRepositoryInterface, }; use shirabe::util::http_downloader::HttpDownloader; use shirabe::util::r#loop::Loop; @@ -39,7 +39,7 @@ impl InstallerInterface for InstallPathStubInstaller { fn is_installed( &self, - _repo: &mut dyn InstalledRepositoryInterface, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { Ok(true) @@ -64,7 +64,7 @@ impl InstallerInterface for InstallPathStubInstaller { async fn install( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { Ok(None) @@ -72,7 +72,7 @@ impl InstallerInterface for InstallPathStubInstaller { async fn update( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _initial: PackageInterfaceHandle, _target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -81,7 +81,7 @@ impl InstallerInterface for InstallPathStubInstaller { async fn uninstall( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { Ok(None) diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs index e9620158..9cd51a7b 100644 --- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs +++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs @@ -426,13 +426,13 @@ mockall::mock! { fn disable_plugins(&mut self); fn is_package_installed( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &shirabe::repository::InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool>; fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle); fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &shirabe::repository::InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, diff --git a/crates/shirabe/tests/installer/installation_manager_test.rs b/crates/shirabe/tests/installer/installation_manager_test.rs index 6cc231f5..8f2d7d4f 100644 --- a/crates/shirabe/tests/installer/installation_manager_test.rs +++ b/crates/shirabe/tests/installer/installation_manager_test.rs @@ -10,7 +10,7 @@ use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::package::PackageInterfaceHandle; use shirabe::package::handle::CompletePackageHandle; -use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterface}; +use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterfaceHandle}; use shirabe::util::http_downloader::HttpDownloader; use shirabe::util::r#loop::Loop; use shirabe_php_shim::PhpMixed; @@ -39,11 +39,9 @@ fn set_up() -> SetUp { SetUp { loop_, io } } -// Equivalent to `getMockBuilder(InstallerInterface::class)->getMock()`. mockall cannot generate an -// `#[async_trait]` impl for the async methods that take `&mut dyn InstalledRepositoryInterface` -// (the object lifetime async_trait inserts clashes with mockall's generated lifetimes), so the -// expectations live on inherent methods and a thin hand-written InstallerInterface impl forwards to -// them, dropping the unused `repo` argument exactly as the PHPUnit mock ignores it. The methods not +// Equivalent to `getMockBuilder(InstallerInterface::class)->getMock()`. The expectations live on +// inherent methods and a thin hand-written InstallerInterface impl forwards to them, dropping the +// unused `repo` argument exactly as the PHPUnit mock ignores it. The methods not // configured by any test (is_installed/download/prepare/cleanup/get_install_path, and the defaulted // as_binary_presence_interface/as_plugin_installer_mut) return the same defaults as an unconfigured // PHPUnit mock. @@ -75,7 +73,7 @@ impl InstallerInterface for MockInstaller { fn is_installed( &self, - _repo: &mut dyn InstalledRepositoryInterface, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { Ok(false) @@ -100,7 +98,7 @@ impl InstallerInterface for MockInstaller { async fn install( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { MockInstaller::install(self, package) @@ -108,7 +106,7 @@ impl InstallerInterface for MockInstaller { async fn update( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -117,7 +115,7 @@ impl InstallerInterface for MockInstaller { async fn uninstall( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { MockInstaller::uninstall(self, package) @@ -175,7 +173,7 @@ impl InstallerInterface for BinaryInstaller { fn is_installed( &self, - _repo: &mut dyn InstalledRepositoryInterface, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { Ok(false) @@ -200,7 +198,7 @@ impl InstallerInterface for BinaryInstaller { async fn install( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { Ok(None) @@ -208,7 +206,7 @@ impl InstallerInterface for BinaryInstaller { async fn update( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _initial: PackageInterfaceHandle, _target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -217,7 +215,7 @@ impl InstallerInterface for BinaryInstaller { async fn uninstall( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { Ok(None) @@ -360,11 +358,9 @@ fn test_install() { let operation = InstallOperation::new(package); - let mut repository = InstalledArrayRepository::new().unwrap(); - run(manager.install( - &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface), - &operation, - )); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + run(manager.install(&repository, &operation)); } #[test] @@ -395,11 +391,9 @@ fn test_update_with_equal_types() { let operation = UpdateOperation::new(initial, target); - let mut repository = InstalledArrayRepository::new().unwrap(); - run(manager.update( - &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface), - &operation, - )); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + run(manager.update(&repository, &operation)); } #[test] @@ -440,11 +434,9 @@ fn test_update_with_not_equal_types() { let operation = UpdateOperation::new(initial, target); - let mut repository = InstalledArrayRepository::new().unwrap(); - run(manager.update( - &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface), - &operation, - )); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + run(manager.update(&repository, &operation)); } #[test] @@ -471,11 +463,9 @@ fn test_uninstall() { let operation = UninstallOperation::new(package); - let mut repository = InstalledArrayRepository::new().unwrap(); - run(manager.uninstall( - &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface), - &operation, - )); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + run(manager.uninstall(&repository, &operation)); } #[test] diff --git a/crates/shirabe/tests/installer/library_installer_test.rs b/crates/shirabe/tests/installer/library_installer_test.rs index baa4f13a..6402d903 100644 --- a/crates/shirabe/tests/installer/library_installer_test.rs +++ b/crates/shirabe/tests/installer/library_installer_test.rs @@ -12,9 +12,7 @@ use shirabe::installer::{BinaryInstallerInterface, InstallerInterface, LibraryIn use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::package::PackageInterfaceHandle; -use shirabe::repository::InstalledArrayRepository; -use shirabe::repository::RepositoryInterface; -use shirabe::repository::WritableRepositoryInterface; +use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterfaceHandle}; use shirabe::util::filesystem::Filesystem; use shirabe_php_shim::PhpMixed; use std::fs; @@ -199,32 +197,27 @@ fn test_is_installed() { let library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); let package = get_package("test/pkg", "1.0.0"); - let mut repository = InstalledArrayRepository::new().unwrap(); - assert!( - !library - .is_installed(&mut repository, package.clone()) - .unwrap() - ); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + assert!(!library.is_installed(&repository, package.clone()).unwrap()); // package being in repo is not enough to be installed - repository.add_package(package.clone()).unwrap(); - assert!( - !library - .is_installed(&mut repository, package.clone()) - .unwrap() - ); + repository + .borrow_mut() + .add_package(package.clone()) + .unwrap(); + assert!(!library.is_installed(&repository, package.clone()).unwrap()); // package being in repo and vendor/pkg/foo dir present means it is seen as installed let pkg_dir = format!("{}/{}", setup.vendor_dir, package.get_pretty_name()); fs::create_dir_all(&pkg_dir).unwrap(); - assert!( - library - .is_installed(&mut repository, package.clone()) - .unwrap() - ); + assert!(library.is_installed(&repository, package.clone()).unwrap()); - repository.remove_package(package.clone()).unwrap(); - assert!(!library.is_installed(&mut repository, package).unwrap()); + repository + .borrow_mut() + .remove_package(package.clone()) + .unwrap(); + assert!(!library.is_installed(&repository, package).unwrap()); tear_down(&mut setup); } @@ -250,18 +243,13 @@ fn test_install() { let library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); - let mut repository = InstalledArrayRepository::new().unwrap(); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); - run(library.install( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface, - ), - package.clone(), - )) - .unwrap(); + run(library.install(&repository, package.clone())).unwrap(); // PHP asserts repository->addPackage was called once with $package. - assert!(repository.has_package(package).unwrap()); + assert!(repository.borrow_mut().has_package(package).unwrap()); assert!( std::path::Path::new(&setup.vendor_dir).exists(), @@ -308,20 +296,17 @@ fn test_update() { .returning(|_, _, _| Ok(None)); set_download_manager(&setup, dm); - let mut repository = InstalledArrayRepository::new().unwrap(); - repository.add_package(initial.clone()).unwrap(); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + repository + .borrow_mut() + .add_package(initial.clone()) + .unwrap(); // The default Filesystem is fine; the LibraryInstaller's own filesystem performs the rename. let library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); - run(library.update( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface, - ), - initial.clone(), - target.clone(), - )) - .unwrap(); + run(library.update(&repository, initial.clone(), target.clone())).unwrap(); assert!( std::path::Path::new(&new_target_dir).exists(), @@ -329,8 +314,13 @@ fn test_update() { ); assert!(!std::path::Path::new(&old_target_dir).exists()); - assert!(!repository.has_package(initial.clone()).unwrap()); - assert!(repository.has_package(target.clone()).unwrap()); + assert!( + !repository + .borrow_mut() + .has_package(initial.clone()) + .unwrap() + ); + assert!(repository.borrow_mut().has_package(target.clone()).unwrap()); assert!( std::path::Path::new(&setup.vendor_dir).exists(), @@ -342,16 +332,7 @@ fn test_update() { ); // Updating again, with the initial package no longer installed, fails. - assert!( - run(library.update( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface - ), - initial, - target - )) - .is_err() - ); + assert!(run(library.update(&repository, initial, target)).is_err()); tear_down(&mut setup); } @@ -380,30 +361,25 @@ fn test_uninstall() { // PHP mocks hasPackage to return (true, false) over two calls; a real repository // seeded with the package reproduces this naturally: present, then absent after // the first uninstall removes it. - let mut repository = InstalledArrayRepository::new().unwrap(); - repository.add_package(package.clone()).unwrap(); - - run(library.uninstall( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface, - ), - package.clone(), - )) - .unwrap(); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + repository + .borrow_mut() + .add_package(package.clone()) + .unwrap(); - assert!(!repository.has_package(package.clone()).unwrap()); + run(library.uninstall(&repository, package.clone())).unwrap(); - // Uninstalling again, with the package no longer installed, fails. assert!( - run(library.uninstall( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface - ), - package - )) - .is_err() + !repository + .borrow_mut() + .has_package(package.clone()) + .unwrap() ); + // Uninstalling again, with the package no longer installed, fails. + assert!(run(library.uninstall(&repository, package)).is_err()); + tear_down(&mut setup); } diff --git a/crates/shirabe/tests/installer/metapackage_installer_test.rs b/crates/shirabe/tests/installer/metapackage_installer_test.rs index b87fdebd..8675c00e 100644 --- a/crates/shirabe/tests/installer/metapackage_installer_test.rs +++ b/crates/shirabe/tests/installer/metapackage_installer_test.rs @@ -8,7 +8,7 @@ use crate::test_case::get_package; use shirabe::installer::{InstallerInterface, MetapackageInstaller}; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; -use shirabe::repository::{InstalledArrayRepository, RepositoryInterface}; +use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterfaceHandle}; fn installer() -> MetapackageInstaller { let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = @@ -20,17 +20,13 @@ fn installer() -> MetapackageInstaller { fn test_install() { let package = get_package("test/pkg", "1.0.0"); let installer = installer(); - let mut repository = InstalledArrayRepository::new_with_packages(vec![]).unwrap(); + let repository = InstalledRepositoryInterfaceHandle::new( + InstalledArrayRepository::new_with_packages(vec![]).unwrap(), + ); - run(installer.install( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface, - ), - package.clone(), - )) - .unwrap(); + run(installer.install(&repository, package.clone())).unwrap(); - assert!(repository.has_package(package).unwrap()); + assert!(repository.borrow_mut().has_package(package).unwrap()); } #[test] @@ -38,59 +34,41 @@ fn test_update() { let initial = get_package("test/initial", "1.0.0"); let target = get_package("test/target", "1.0.1"); let installer = installer(); - let mut repository = - InstalledArrayRepository::new_with_packages(vec![initial.clone()]).unwrap(); - - run(installer.update( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface, - ), - initial.clone(), - target.clone(), - )) - .unwrap(); + let repository = InstalledRepositoryInterfaceHandle::new( + InstalledArrayRepository::new_with_packages(vec![initial.clone()]).unwrap(), + ); - assert!(!repository.has_package(initial.clone()).unwrap()); - assert!(repository.has_package(target.clone()).unwrap()); + run(installer.update(&repository, initial.clone(), target.clone())).unwrap(); - // Updating again, with the initial package no longer installed, fails. assert!( - run(installer.update( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface - ), - initial, - target - )) - .is_err() + !repository + .borrow_mut() + .has_package(initial.clone()) + .unwrap() ); + assert!(repository.borrow_mut().has_package(target.clone()).unwrap()); + + // Updating again, with the initial package no longer installed, fails. + assert!(run(installer.update(&repository, initial, target)).is_err()); } #[test] fn test_uninstall() { let package = get_package("test/pkg", "1.0.0"); let installer = installer(); - let mut repository = - InstalledArrayRepository::new_with_packages(vec![package.clone()]).unwrap(); - - run(installer.uninstall( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface, - ), - package.clone(), - )) - .unwrap(); + let repository = InstalledRepositoryInterfaceHandle::new( + InstalledArrayRepository::new_with_packages(vec![package.clone()]).unwrap(), + ); - assert!(!repository.has_package(package.clone()).unwrap()); + run(installer.uninstall(&repository, package.clone())).unwrap(); - // Uninstalling again, with the package no longer installed, fails. assert!( - run(installer.uninstall( - &std::cell::RefCell::new( - &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface - ), - package - )) - .is_err() + !repository + .borrow_mut() + .has_package(package.clone()) + .unwrap() ); + + // Uninstalling again, with the package no longer installed, fails. + assert!(run(installer.uninstall(&repository, package)).is_err()); } 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), diff --git a/crates/shirabe/tests/repository/filesystem_repository_test.rs b/crates/shirabe/tests/repository/filesystem_repository_test.rs index c7b8f9a6..323aff9f 100644 --- a/crates/shirabe/tests/repository/filesystem_repository_test.rs +++ b/crates/shirabe/tests/repository/filesystem_repository_test.rs @@ -10,7 +10,6 @@ use shirabe::io::IOInterface; use shirabe::json::json_file::JsonFile; use shirabe::package::loader::ArrayLoader; use shirabe::package::{Link, PackageInterfaceHandle, RootAliasPackageHandle, RootPackageHandle}; -use shirabe::repository::InstalledRepositoryInterface; use shirabe::repository::RepositoryInterface; use shirabe::repository::filesystem_repository::FilesystemRepository; use shirabe::util::filesystem::Filesystem; @@ -99,13 +98,13 @@ mockall::mock! { fn disable_plugins(&mut self); fn is_package_installed( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &shirabe::repository::InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool>; fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle); fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &shirabe::repository::InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, |
