diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-07 20:51:59 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-07 22:23:23 +0900 |
| commit | c82da84ae1a8cee23670a74a646584ab637308d1 (patch) | |
| tree | 1c985827e9d85d11c1d8b5ded70f5cd424c1a60b /crates/shirabe/tests | |
| parent | 18a37a098157a98edbc8473e9c0d8dff3e8a88fa (diff) | |
| download | php-shirabe-c82da84ae1a8cee23670a74a646584ab637308d1.tar.gz php-shirabe-c82da84ae1a8cee23670a74a646584ab637308d1.tar.zst php-shirabe-c82da84ae1a8cee23670a74a646584ab637308d1.zip | |
InstallationManager left both PRE_PACKAGE_* and POST_PACKAGE_* as empty
stubs, so a subscriber never ran at all and the difference from upstream was
silent rather than an explicit error.
Operations cross the boundary as R-table entities with generated proxy
stubs. Materializing them the way a Link crosses is not possible: a
materialized value is revived by unserialize() on the child side, so its
properties never pass through the wire decoder and a nested handle
descriptor would not come back as a stub — and an operation always holds a
PackageInterface. execute() now shares one Rc per operation through the
whole batch pipeline, so a plugin sees one object for both the pre- and the
post-event of an operation, as it does in PHP.
POST_PACKAGE_* also moves out of the operation's promise chain into the
post-exec callback list PHP runs after waitOnPromises().
The stub generator materializes non-public class constants verbatim now,
which the operation classes need for their `protected const TYPE`: a
constant has no entity behind it, so a copy in the worker cannot diverge,
and keeping the declared visibility exposes nothing the real class hides.
The E2E fixture added here compares the recorded events against upstream
Composer. It also surfaced that upstream starts an operation's chain where
it is built (a null prepare() becomes an already-fulfilled React promise
whose handlers run through the immediately drained queue) while this port
only drives its futures in wait_on_promises, so the repository state a
pre-event observes differs; that half of the comparison is a separate
`#[ignore]`d test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests')
7 files changed, 255 insertions, 0 deletions
diff --git a/crates/shirabe/tests/plugin/e2e_package_event_test.rs b/crates/shirabe/tests/plugin/e2e_package_event_test.rs new file mode 100644 index 00000000..ef334685 --- /dev/null +++ b/crates/shirabe/tests/plugin/e2e_package_event_test.rs @@ -0,0 +1,105 @@ +//! Package event E2E compatibility check: upstream Composer and Shirabe each install a fixture +//! project whose plugin subscribes to every `PackageEvents` constant and appends what each event +//! exposes to a trace file. Upstream has no test that dispatches package events through a real +//! plugin, so the whole fixture is Shirabe-authored (`fixtures/e2e-package-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::plugin_installer_test::{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-package-event") +} + +struct Run { + exit_code: i32, + trace: String, + repo_trace: String, +} + +/// Runs `install` in a fresh copy of the fixture and returns the exit code with the traces the +/// plugin wrote. +fn install(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("install") + .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(); + let read = |name: &str| std::fs::read_to_string(project.join(name)).unwrap_or_default(); + Run { + exit_code: output.status.code().unwrap_or(-1), + trace: read("package-event-trace.txt"), + repo_trace: read("package-event-repo-trace.txt"), + } +} + +#[test] +fn test_package_events_match_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 = install("php", &[composer_bin.as_str()]); + let shirabe = install(env!("CARGO_BIN_EXE_shirabe"), &[]); + + assert_eq!(0, upstream.exit_code, "upstream install 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 anything cannot pass. + // The plugin is activated by the very batch it observes, hence the first line: its own + // post-package-install, deferred until after the batch's operations have run. The two + // packages of the next batch report their pre-events before either post-event for the same + // reason. + assert_eq!( + "\ +post-package-install devMode=1 class=Composer\\DependencyResolver\\Operation\\InstallOperation type=install packages=shirabe-test/package-event-recorder operations=3 root=shirabe/e2e-package-event show=Installing <info>shirabe-test/package-event-recorder</info> (<comment>1.0.0</comment>) +pre-package-install devMode=1 class=Composer\\DependencyResolver\\Operation\\InstallOperation type=install packages=shirabe-test/lib-a operations=3 root=shirabe/e2e-package-event show=Installing <info>shirabe-test/lib-a</info> (<comment>1.0.0</comment>) +pre-package-install devMode=1 class=Composer\\DependencyResolver\\Operation\\InstallOperation type=install packages=shirabe-test/lib-b operations=3 root=shirabe/e2e-package-event show=Installing <info>shirabe-test/lib-b</info> (<comment>1.0.0</comment>) +post-package-install devMode=1 class=Composer\\DependencyResolver\\Operation\\InstallOperation type=install packages=shirabe-test/lib-a operations=3 root=shirabe/e2e-package-event show=Installing <info>shirabe-test/lib-a</info> (<comment>1.0.0</comment>) +post-package-install devMode=1 class=Composer\\DependencyResolver\\Operation\\InstallOperation type=install packages=shirabe-test/lib-b operations=3 root=shirabe/e2e-package-event show=Installing <info>shirabe-test/lib-b</info> (<comment>1.0.0</comment>) +", + upstream.trace + ); +} + +// Upstream's operation chain starts running where it is built, because a `prepare()` that +// returns null becomes an already-fulfilled React promise whose `then()` handlers run through +// the immediately drained queue; the pre-event of the next operation therefore already sees the +// previous one installed. Shirabe builds a lazy future per operation and only drives them in +// wait_on_promises, so every pre-event of a batch sees the repository as it was before the +// batch. Upstream: 1 / 1 / 2 / 3 / 3, Shirabe: 1 / 1 / 1 / 3 / 3. +#[ignore = "operation chains run where they are built upstream but only in wait_on_promises here, so the repository state a package event observes differs (TODO(phase-c) promise cluster)"] +#[test] +fn test_local_repository_seen_by_package_events_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 = install("php", &[composer_bin.as_str()]); + let shirabe = install(env!("CARGO_BIN_EXE_shirabe"), &[]); + + assert_eq!(upstream.repo_trace, shirabe.repo_trace); +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-package-event/packages/lib-a/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/packages/lib-a/composer.json new file mode 100644 index 00000000..50b2eece --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/packages/lib-a/composer.json @@ -0,0 +1,5 @@ +{ + "name": "shirabe-test/lib-a", + "version": "1.0.0", + "description": "Fixture package installed while the recorder plugin is active." +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-package-event/packages/lib-b/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/packages/lib-b/composer.json new file mode 100644 index 00000000..96713f8d --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/packages/lib-b/composer.json @@ -0,0 +1,5 @@ +{ + "name": "shirabe-test/lib-b", + "version": "1.0.0", + "description": "Fixture dev package, so the recorded events carry devMode." +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-package-event/plugin/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/plugin/composer.json new file mode 100644 index 00000000..7614dc95 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/plugin/composer.json @@ -0,0 +1,17 @@ +{ + "name": "shirabe-test/package-event-recorder", + "version": "1.0.0", + "type": "composer-plugin", + "description": "Fixture plugin recording every PackageEvent it is subscribed to.", + "autoload": { + "psr-4": { + "ShirabeTest\\PackageEvent\\": "src/" + } + }, + "require": { + "composer-plugin-api": "^2.0" + }, + "extra": { + "class": "ShirabeTest\\PackageEvent\\Plugin" + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-package-event/plugin/src/Plugin.php b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/plugin/src/Plugin.php new file mode 100644 index 00000000..d904c0e9 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/plugin/src/Plugin.php @@ -0,0 +1,87 @@ +<?php + +namespace ShirabeTest\PackageEvent; + +use Composer\Composer; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\IO\IOInterface; +use Composer\Installer\PackageEvent; +use Composer\Installer\PackageEvents; +use Composer\Plugin\PluginInterface; + +/** + * Appends one line per PackageEvent to package-event-trace.txt, so both the order the events + * arrive in and everything the event exposes are comparable between implementations. + * + * The local repository is observed into a second file, because its size at the moment an event + * fires reports how far the batch's operations have run rather than anything the event carries. + */ +class Plugin implements PluginInterface, EventSubscriberInterface +{ + /** @var IOInterface */ + private $io; + + public function activate(Composer $composer, IOInterface $io): void + { + $this->io = $io; + } + + public function deactivate(Composer $composer, IOInterface $io): void + { + } + + public function uninstall(Composer $composer, IOInterface $io): void + { + } + + public static function getSubscribedEvents() + { + return [ + PackageEvents::PRE_PACKAGE_INSTALL => 'onPackageEvent', + PackageEvents::POST_PACKAGE_INSTALL => 'onPackageEvent', + PackageEvents::PRE_PACKAGE_UPDATE => 'onPackageEvent', + PackageEvents::POST_PACKAGE_UPDATE => 'onPackageEvent', + PackageEvents::PRE_PACKAGE_UNINSTALL => 'onPackageEvent', + PackageEvents::POST_PACKAGE_UNINSTALL => 'onPackageEvent', + ]; + } + + public function onPackageEvent(PackageEvent $event): void + { + $operation = $event->getOperation(); + $line = implode(' ', [ + $event->getName(), + 'devMode=' . ($event->isDevMode() ? '1' : '0'), + 'class=' . get_class($operation), + 'type=' . $operation->getOperationType(), + 'packages=' . $this->packages($operation), + 'operations=' . count($event->getOperations()), + 'root=' . $event->getComposer()->getPackage()->getName(), + 'show=' . $operation->show(false), + ]); + $this->io->write('package-event: ' . $line); + file_put_contents('package-event-trace.txt', $line . "\n", FILE_APPEND); + file_put_contents( + 'package-event-repo-trace.txt', + $event->getName() . ' localRepo=' . count($event->getLocalRepo()->getPackages()) . "\n", + FILE_APPEND + ); + } + + private function packages(OperationInterface $operation): string + { + if ($operation instanceof UpdateOperation) { + return $operation->getInitialPackage()->getPrettyName() + . '->' . $operation->getTargetPackage()->getPrettyName(); + } + if ($operation instanceof InstallOperation || $operation instanceof UninstallOperation) { + return $operation->getPackage()->getPrettyName(); + } + + return 'n/a'; + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-package-event/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/project/composer.json new file mode 100644 index 00000000..a0e63a15 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-package-event/project/composer.json @@ -0,0 +1,35 @@ +{ + "name": "shirabe/e2e-package-event", + "description": "E2E fixture project: record the package events a plugin receives during install.", + "repositories": [ + { + "type": "path", + "url": "../plugin", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "../packages/*", + "options": { + "symlink": false + } + }, + { + "packagist.org": false + } + ], + "require": { + "shirabe-test/package-event-recorder": "1.0.0", + "shirabe-test/lib-a": "1.0.0" + }, + "require-dev": { + "shirabe-test/lib-b": "1.0.0" + }, + "config": { + "allow-plugins": { + "shirabe-test/package-event-recorder": true + } + } +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index 7e32a616..f1237731 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -9,6 +9,7 @@ mod e2e_extension_installer_test; mod e2e_installer_test; mod e2e_installers_test; mod e2e_normalize_test; +mod e2e_package_event_test; mod plugin_installer_test; mod subscriber_test; mod value_round_trip_test; |
