aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/plugin/fixtures
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-30 23:57:25 +0900
committernsfisis <nsfisis@gmail.com>2026-08-30 23:57:25 +0900
commita38ed045e9981664d90c05ab43ab4ab6026eb31b (patch)
tree537dbd660882e9af7eec9ddcb7abee4c8aae6e1a /crates/shirabe/tests/plugin/fixtures
parentd3bc3354c9705dfc6dc5e9b9adb5eb64d41e4c49 (diff)
downloadphp-shirabe-a38ed045e9981664d90c05ab43ab4ab6026eb31b.tar.gz
php-shirabe-a38ed045e9981664d90c05ab43ab4ab6026eb31b.tar.zst
php-shirabe-a38ed045e9981664d90c05ab43ab4ab6026eb31b.zip
feat(plugin): carry an exception's class and state across the boundary
A Rust-side failure reached plugin code as a RuntimeException whose message carried the name of the call that failed, so `catch (TransportException $e)` never matched and the status code the plugin branches on was gone. The Throw frame now names the class the exception was thrown as and carries the state that class declares beyond message and code. \Shirabe\MaterializedThrowable rebuilds it in the child: `new $class($message, $code)` for a class whose constructor has \Exception's shape, then the properties by reflection. A class the child cannot build that way keeps the RuntimeException shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/plugin/fixtures')
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/composer.json17
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/src/Plugin.php92
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-exception/project/composer.json24
3 files changed, 133 insertions, 0 deletions
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/composer.json
new file mode 100644
index 00000000..4bc436ec
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/composer.json
@@ -0,0 +1,17 @@
+{
+ "name": "shirabe-test/exception-probe",
+ "version": "1.0.0",
+ "type": "composer-plugin",
+ "description": "Fixture plugin recording the exceptions a Composer service raises at it.",
+ "autoload": {
+ "psr-4": {
+ "ShirabeTest\\Exception\\": "src/"
+ }
+ },
+ "require": {
+ "composer-plugin-api": "^2.0"
+ },
+ "extra": {
+ "class": "ShirabeTest\\Exception\\Plugin"
+ }
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/src/Plugin.php b/crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/src/Plugin.php
new file mode 100644
index 00000000..547f4fbb
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-exception/plugin/src/Plugin.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace ShirabeTest\Exception;
+
+use Composer\Composer;
+use Composer\EventDispatcher\EventSubscriberInterface;
+use Composer\IO\IOInterface;
+use Composer\Plugin\PluginInterface;
+use Composer\Script\Event;
+use Composer\Script\ScriptEvents;
+use Composer\Util\Filesystem;
+
+/**
+ * Records what a plugin sees when a Composer service raises an exception at it: the class it was
+ * thrown as, its message and code, whether it is still an instance of the parent classes the real
+ * hierarchy gives it, and whether a `catch` naming that class matches. Composer plugins branch on
+ * the exception class rather than on its message, so the whole surface is compared line by line
+ * between implementations.
+ */
+class Plugin implements PluginInterface, EventSubscriberInterface
+{
+ public function activate(Composer $composer, IOInterface $io): void
+ {
+ }
+
+ public function deactivate(Composer $composer, IOInterface $io): void
+ {
+ }
+
+ public function uninstall(Composer $composer, IOInterface $io): void
+ {
+ }
+
+ public static function getSubscribedEvents()
+ {
+ // Whether an install resolves or replays a lock file decides which of the two fires, so
+ // both are subscribed and the trace records the one that ran.
+ return [
+ ScriptEvents::POST_INSTALL_CMD => 'onPostCommand',
+ ScriptEvents::POST_UPDATE_CMD => 'onPostCommand',
+ ];
+ }
+
+ public function onPostCommand(Event $event): void
+ {
+ $filesystem = new Filesystem();
+ $lines = ['event=' . $event->getName()];
+
+ $lines[] = 'findShortestPath ' . $this->describe(static function () use ($filesystem): void {
+ $filesystem->findShortestPath('relative', '/absolute');
+ });
+ $lines[] = 'findShortestPathCode ' . $this->describe(static function () use ($filesystem): void {
+ $filesystem->findShortestPathCode('/absolute', 'relative');
+ });
+
+ // A different class through the same seam, so the trace shows the class travelling rather
+ // than every failure arriving under one name.
+ file_put_contents('not-a-directory', '');
+ $lines[] = 'ensureDirectoryExists ' . $this->describe(static function () use ($filesystem): void {
+ $filesystem->ensureDirectoryExists('not-a-directory');
+ });
+
+ // get_class() answers for the object; a catch clause answers for the class hierarchy the
+ // child holds, which is what plugin code is actually written against.
+ try {
+ $filesystem->findShortestPath('relative', '/absolute');
+ $caught = 'nothing-thrown';
+ } catch (\InvalidArgumentException $e) {
+ $caught = 'InvalidArgumentException';
+ } catch (\Throwable $e) {
+ $caught = 'unmatched:' . \get_class($e);
+ }
+ $lines[] = 'catch-clause=' . $caught;
+
+ file_put_contents('exception-trace.txt', implode("\n", $lines) . "\n");
+ }
+
+ private function describe(callable $call): string
+ {
+ try {
+ $call();
+
+ return 'class=none';
+ } catch (\Throwable $e) {
+ return 'class=' . json_encode(\get_class($e))
+ . ' message=' . json_encode($e->getMessage())
+ . ' code=' . json_encode($e->getCode())
+ . ' logic=' . json_encode($e instanceof \LogicException)
+ . ' runtime=' . json_encode($e instanceof \RuntimeException);
+ }
+ }
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-exception/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-exception/project/composer.json
new file mode 100644
index 00000000..da06cc93
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-exception/project/composer.json
@@ -0,0 +1,24 @@
+{
+ "name": "shirabe/e2e-exception",
+ "description": "E2E fixture project: record the exceptions a plugin catches from Composer services.",
+ "repositories": [
+ {
+ "type": "path",
+ "url": "../plugin",
+ "options": {
+ "symlink": false
+ }
+ },
+ {
+ "packagist.org": false
+ }
+ ],
+ "require": {
+ "shirabe-test/exception-probe": "1.0.0"
+ },
+ "config": {
+ "allow-plugins": {
+ "shirabe-test/exception-probe": true
+ }
+ }
+}