diff options
Diffstat (limited to 'crates/shirabe')
6 files changed, 200 insertions, 52 deletions
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index b22c7c0a..ece83533 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -1150,6 +1150,16 @@ try {{ .into() })?; + // The event stub the script receives hands out stubs of the Composer object graph, and + // those extend and implement the real Composer contracts (`PackageInterface` and the + // rest), which only the Composer PHP runtime carries. + let cache_dir = self + .composer() + .borrow_partial() + .get_config() + .borrow() + .get_str("cache-dir")?; + Self::ensure_composer_php_runtime(std::path::Path::new(&cache_dir))?; Self::ensure_script_autoloader()?; let rhandle = shirabe_php_rpc::alloc_rhandle(); let mut dispatcher = ScriptRpcDispatcher { @@ -1640,12 +1650,12 @@ try {{ /// Serves `CallRustMethod` requests issued by the PHP worker while a script-related call is in /// flight: Rust handle 0 is the runtime service endpoint (autoload lookups against the -/// Rust-side [`ClassLoader`]), and at most one live event handle is exposed per dispatched -/// call. +/// Rust-side [`ClassLoader`]), every other rhandle resolves through the R table, except the +/// one live event handle exposed per dispatched call. /// -/// TODO(plugin): this per-call scope stands in for persistent R-table registration; a stub -/// retained by the script beyond the call observes an unknown handle error instead of the -/// live object. +/// TODO(plugin): a script that stores the event stub beyond its own call observes an unknown +/// handle error afterwards; keeping events in the R table needs full proxying of the object +/// graph an event exposes, which does not exist yet. struct ScriptRpcDispatcher<'a> { loader: Option<ClassLoader>, event: Option<(u64, &'a dyn EventInterface)>, @@ -1691,10 +1701,11 @@ impl RustMethodDispatcher for ScriptRpcDispatcher<'_> { Some((event_rhandle, event)) if event_rhandle == rhandle => { dispatch_event_method(event, method_name) } - _ => Err(runtime_throw(format!( - "unknown Rust handle {rhandle} (script-event handles are scoped to a single \ - dispatched call)" - ))), + _ => crate::plugin::php_plugin_proxy::dispatch_r_table_method( + rhandle, + method_name, + &args, + ), } } } diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index 0cd36b18..9ebdbbff 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -340,52 +340,59 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { return dispatch_event_method(event, method_name); } - // The entity is cloned out so no table borrow is held while the handler runs (a - // handler that re-enters register_*_entity would otherwise panic on the RefCell). - let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned()); - if method_name == "__shirabeClone" { - return match entity { - Some(entity) => clone_entity(&entity), - None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), - }; + dispatch_r_table_method(rhandle, method_name, &args) + } +} + +/// Serves a method call on an R-table entity, shared by every dispatcher: the child holds a +/// stub for an entity registered by an earlier call, and the handle outlives the call that +/// minted it. +pub(crate) fn dispatch_r_table_method( + rhandle: u64, + method_name: &str, + args: &[PluginValue], +) -> Result<PluginValue, PhpThrow> { + // The entity is cloned out so no table borrow is held while the handler runs (a + // handler that re-enters register_*_entity would otherwise panic on the RefCell). + let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned()); + if method_name == "__shirabeClone" { + return match entity { + Some(entity) => clone_entity(&entity), + None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), + }; + } + if matches!(method_name, "__get" | "__set" | "__isset" | "__unset") { + return match entity { + Some(entity) => dispatch_property_access(&entity, method_name, args), + None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), + }; + } + match entity { + Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, args), + Some(RustEntity::Composer(composer)) => dispatch_composer_method(&composer, method_name), + Some(RustEntity::Config(config)) => dispatch_config_method(&config, method_name, args), + Some(RustEntity::DownloadManager(dm)) => { + dispatch_download_manager_method(&dm, method_name, args) } - if matches!(method_name, "__get" | "__set" | "__isset" | "__unset") { - return match entity { - Some(entity) => dispatch_property_access(&entity, method_name, &args), - None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), - }; + Some(RustEntity::Filesystem(fs)) => dispatch_filesystem_method(&fs, method_name, args), + Some(RustEntity::InstallationManager(im)) => { + dispatch_installation_manager_method(&im, method_name, args) } - match entity { - Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args), - Some(RustEntity::Composer(composer)) => { - dispatch_composer_method(&composer, method_name) - } - Some(RustEntity::Config(config)) => dispatch_config_method(&config, method_name, &args), - Some(RustEntity::DownloadManager(dm)) => { - dispatch_download_manager_method(&dm, method_name, &args) - } - Some(RustEntity::Filesystem(fs)) => dispatch_filesystem_method(&fs, method_name, &args), - Some(RustEntity::InstallationManager(im)) => { - dispatch_installation_manager_method(&im, method_name, &args) - } - Some(RustEntity::RepositoryManager(rm)) => { - dispatch_repository_manager_method(&rm, method_name) - } - Some(RustEntity::Repository(repository)) => { - dispatch_repository_method(&repository, method_name, &args) - } - Some(RustEntity::Package(package)) => { - dispatch_package_method(&package, method_name, &args) - } - Some(RustEntity::EventDispatcher(dispatcher)) => { - dispatch_event_dispatcher_method(&dispatcher, method_name, &args) - } - Some(RustEntity::Operation(operation)) => { - dispatch_operation_method(&operation, method_name, &args) - } - Some(RustEntity::Plugin(plugin)) => dispatch_plugin_method(&plugin, method_name), - None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), + Some(RustEntity::RepositoryManager(rm)) => { + dispatch_repository_manager_method(&rm, method_name) + } + Some(RustEntity::Repository(repository)) => { + dispatch_repository_method(&repository, method_name, args) + } + Some(RustEntity::Package(package)) => dispatch_package_method(&package, method_name, args), + Some(RustEntity::EventDispatcher(dispatcher)) => { + dispatch_event_dispatcher_method(&dispatcher, method_name, args) + } + Some(RustEntity::Operation(operation)) => { + dispatch_operation_method(&operation, method_name, args) } + Some(RustEntity::Plugin(plugin)) => dispatch_plugin_method(&plugin, method_name), + None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), } } diff --git a/crates/shirabe/tests/plugin/e2e_script_event_test.rs b/crates/shirabe/tests/plugin/e2e_script_event_test.rs new file mode 100644 index 00000000..5a0267d7 --- /dev/null +++ b/crates/shirabe/tests/plugin/e2e_script_event_test.rs @@ -0,0 +1,90 @@ +//! Script event E2E compatibility check: upstream Composer and Shirabe each run `dump-autoload` +//! in a fixture project whose `post-autoload-dump` script is a PHP static method, and the method +//! records what the `Composer\Script\Event` it receives exposes. Upstream has no test that reads +//! the Composer object graph out of an event from a script, so the whole fixture is +//! Shirabe-authored (`fixtures/e2e-script-event/`) and nothing has to be fetched; the test skips +//! only while the PHP runtime or the Composer checkout is missing. + +use crate::e2e_extension_installer_test::{copy_dir, upstream_composer_bin}; +use crate::php_worker::{lock_php_worker, php_runtime_available}; +use std::path::{Path, PathBuf}; +use tempfile::TempDir; + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/plugin/fixtures/e2e-script-event") +} + +struct Run { + exit_code: i32, + trace: String, + stdout: String, +} + +/// Runs `dump-autoload` in a fresh copy of the fixture and returns the exit code with what the +/// script wrote to its trace file and through the event's IO. +fn dump_autoload(program: &str, prefix_args: &[&str]) -> Run { + let work = TempDir::new().unwrap(); + copy_dir(&fixture_dir(), work.path()); + let project = work.path().join("project"); + let output = std::process::Command::new(program) + .args(prefix_args) + .arg("dump-autoload") + .current_dir(&project) + .env("COMPOSER_HOME", work.path().join("home")) + .env("COMPOSER_CACHE_DIR", work.path().join("cache")) + .env("COMPOSER_NO_INTERACTION", "1") + .env("COLUMNS", "120") + .env("LINES", "30") + .output() + .unwrap(); + Run { + exit_code: output.status.code().unwrap_or(-1), + trace: std::fs::read_to_string(project.join("script-event-trace.txt")).unwrap_or_default(), + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + } +} + +#[test] +fn test_script_event_object_graph_matches_upstream_composer() { + if !php_runtime_available() { + return; + } + let Some(composer_bin) = upstream_composer_bin() else { + return; + }; + let _worker = lock_php_worker(); + let composer_bin = composer_bin.to_str().unwrap().to_string(); + + let upstream = dump_autoload("php", &[composer_bin.as_str()]); + let shirabe = dump_autoload(env!("CARGO_BIN_EXE_shirabe"), &[]); + + assert_eq!(0, upstream.exit_code, "upstream dump-autoload must succeed"); + assert_eq!(upstream.exit_code, shirabe.exit_code); + assert_eq!(upstream.trace, shirabe.trace); + + // Pinned as well as compared, so a run where neither side dispatches the script cannot pass. + assert_eq!( + "\ +event=post-autoload-dump +dev=0 +root=shirabe/e2e-script-event +vendor-dir=vendor +bin-dir=bin +", + upstream.trace + ); + + let io_line = |run: &Run| -> String { + run.stdout + .lines() + .find(|line| line.starts_with("script-event: ")) + .unwrap_or_default() + .to_string() + }; + assert_eq!(io_line(&upstream), io_line(&shirabe)); + assert_eq!( + "script-event: event=post-autoload-dump dev=0 root=shirabe/e2e-script-event \ + vendor-dir=vendor bin-dir=bin", + io_line(&upstream) + ); +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json new file mode 100644 index 00000000..59fc8f13 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/composer.json @@ -0,0 +1,17 @@ +{ + "name": "shirabe/e2e-script-event", + "description": "E2E fixture project: record what a post-autoload-dump PHP script sees on its Event.", + "repositories": [ + { + "packagist.org": false + } + ], + "autoload": { + "psr-4": { + "Shirabe\\E2e\\": "src/" + } + }, + "scripts": { + "post-autoload-dump": "Shirabe\\E2e\\ScriptEventRecorder::postAutoloadDump" + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php new file mode 100644 index 00000000..84fa0da9 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-script-event/project/src/ScriptEventRecorder.php @@ -0,0 +1,22 @@ +<?php + +namespace Shirabe\E2e; + +use Composer\Script\Event; + +class ScriptEventRecorder +{ + public static function postAutoloadDump(Event $event): void + { + $composer = $event->getComposer(); + $lines = [ + 'event=' . $event->getName(), + 'dev=' . ($event->isDevMode() ? '1' : '0'), + 'root=' . $composer->getPackage()->getName(), + 'vendor-dir=' . basename($composer->getConfig()->get('vendor-dir')), + 'bin-dir=' . basename($composer->getConfig()->get('bin-dir')), + ]; + $event->getIO()->write('script-event: ' . implode(' ', $lines)); + file_put_contents(__DIR__ . '/../script-event-trace.txt', implode("\n", $lines) . "\n"); + } +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index d8defcbc..000ca3a9 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -13,6 +13,7 @@ mod e2e_installers_test; mod e2e_normalize_test; mod e2e_package_event_test; mod e2e_script_command_test; +mod e2e_script_event_test; mod plugin_installer_test; mod subscriber_test; mod value_round_trip_test; |
