diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-31 00:07:36 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-31 00:07:36 +0900 |
| commit | dc1f030e3904677cd65eac0a90c1d850e0ad1bbf (patch) | |
| tree | 36f5177d90ae34f1b0b28cae5d1a3075acb2f8bd /crates/shirabe/tests | |
| parent | a38ed045e9981664d90c05ab43ab4ab6026eb31b (diff) | |
| download | php-shirabe-dc1f030e3904677cd65eac0a90c1d850e0ad1bbf.tar.gz php-shirabe-dc1f030e3904677cd65eac0a90c1d850e0ad1bbf.tar.zst php-shirabe-dc1f030e3904677cd65eac0a90c1d850e0ad1bbf.zip | |
feat(plugin): serve HttpDownloader as a proxy stub
A downloader carries its own options, TLS defaults and request backends, and
what it shares with the graph is the IO it collects authentication into and
the config it reads. Plugin code writing `new HttpDownloader($io, $config)`
therefore allocates a Rust-side entity of its own rather than a second
downloader the graph knows nothing about, and the guard that shadowed the
class in the worker is gone.
The request surface is not served yet: get() and copy() have no wire
representation for the Response they return, and the async surface resolves
its promises with one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests')
5 files changed, 213 insertions, 0 deletions
diff --git a/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs b/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs new file mode 100644 index 00000000..ecb0b34c --- /dev/null +++ b/crates/shirabe/tests/plugin/e2e_http_downloader_test.rs @@ -0,0 +1,77 @@ +//! HttpDownloader E2E compatibility check: upstream Composer and Shirabe each install a fixture +//! project whose plugin builds its own `HttpDownloader` and writes what every call on it reports +//! to a trace file. Upstream has no test that drives a downloader from plugin code, so the whole +//! fixture is Shirabe-authored (`fixtures/e2e-http-downloader/`) 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-http-downloader") +} + +struct Run { + exit_code: i32, + trace: String, +} + +/// Runs `install` in a fresh copy of the fixture and returns the exit code with the plugin's trace. +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(); + Run { + exit_code: output.status.code().unwrap_or(-1), + trace: std::fs::read_to_string(project.join("http-downloader-trace.txt")) + .unwrap_or_default(), + } +} + +#[test] +fn test_plugin_owned_http_downloader_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!(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 wrote a trace cannot pass. The + // second options line is the evidence that both worlds merge into one value rather than each + // holding its own copy of the map. + assert_eq!( + "\ +event=post-update-cmd +class=\"Composer\\\\Util\\\\HttpDownloader\" instanceof=true +options header=[\"X-Probe: 1\"] +options merged=[\"X-Probe: 2\"] +isCurlEnabled=true +hints other=null transport=null +outputWarnings=ok +", + upstream.trace + ); +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/composer.json new file mode 100644 index 00000000..08eec6ce --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/composer.json @@ -0,0 +1,17 @@ +{ + "name": "shirabe-test/http-downloader-probe", + "version": "1.0.0", + "type": "composer-plugin", + "description": "Fixture plugin driving an HttpDownloader it constructs itself.", + "autoload": { + "psr-4": { + "ShirabeTest\\HttpDownloader\\": "src/" + } + }, + "require": { + "composer-plugin-api": "^2.0" + }, + "extra": { + "class": "ShirabeTest\\HttpDownloader\\Plugin" + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php new file mode 100644 index 00000000..504c2108 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/plugin/src/Plugin.php @@ -0,0 +1,94 @@ +<?php + +namespace ShirabeTest\HttpDownloader; + +use Composer\Composer; +use Composer\Downloader\TransportException; +use Composer\EventDispatcher\EventSubscriberInterface; +use Composer\IO\IOInterface; +use Composer\Plugin\PluginInterface; +use Composer\Script\Event; +use Composer\Script\ScriptEvents; +use Composer\Util\HttpDownloader; + +/** + * Drives an HttpDownloader the plugin constructs itself and appends what every call reports to + * http-downloader-trace.txt, so the surface can be compared line by line between implementations: + * the identity of the object a plugin gets, the options it merges, and the static helpers. + * Nothing here reaches the network — the trace has to be reproducible offline. + */ +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() + { + // 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 + { + $downloader = new HttpDownloader($this->io, $event->getComposer()->getConfig()); + $lines = ['event=' . $event->getName()]; + + $lines[] = 'class=' . json_encode(\get_class($downloader)) + . ' instanceof=' . json_encode($downloader instanceof HttpDownloader); + + // Only the key the plugin set is compared: the rest of the map is the TLS defaults, whose + // CA paths depend on the machine rather than on the implementation. + $downloader->setOptions(['http' => ['header' => ['X-Probe: 1']]]); + $options = $downloader->getOptions(); + $lines[] = 'options header=' . json_encode($options['http']['header'] ?? null); + $downloader->setOptions(['http' => ['header' => ['X-Probe: 2']]]); + $lines[] = 'options merged=' . json_encode($downloader->getOptions()['http']['header'] ?? null); + + $lines[] = 'isCurlEnabled=' . json_encode(HttpDownloader::isCurlEnabled()); + + // getExceptionHints() only inspects the exception it is handed; both arguments below stay + // clear of the branch that probes connectivity. + $lines[] = 'hints other=' . json_encode(HttpDownloader::getExceptionHints(new \RuntimeException('x'))) + . ' transport=' . json_encode(HttpDownloader::getExceptionHints(new TransportException('plain', 400))); + + // The version constraint cannot match, so this reaches Composer::getVersion() and the + // version parser without writing anything to the terminal. + $lines[] = 'outputWarnings=' . $this->describe(static function () use ($event): void { + HttpDownloader::outputWarnings( + $event->getIO(), + 'https://example.org/packages.json', + ['warning' => 'unreachable', 'warning-versions' => '^0.0.1'] + ); + }); + + file_put_contents('http-downloader-trace.txt', implode("\n", $lines) . "\n"); + } + + private function describe(callable $call): string + { + try { + $call(); + + return 'ok'; + } catch (\Throwable $e) { + return \get_class($e) . ': ' . $e->getMessage(); + } + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/project/composer.json new file mode 100644 index 00000000..42318a10 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-http-downloader/project/composer.json @@ -0,0 +1,24 @@ +{ + "name": "shirabe/e2e-http-downloader", + "description": "E2E fixture project: record what a plugin's own HttpDownloader reports.", + "repositories": [ + { + "type": "path", + "url": "../plugin", + "options": { + "symlink": false + } + }, + { + "packagist.org": false + } + ], + "require": { + "shirabe-test/http-downloader-probe": "1.0.0" + }, + "config": { + "allow-plugins": { + "shirabe-test/http-downloader-probe": true + } + } +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index 3ef3f647..c62499e5 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -9,6 +9,7 @@ mod alias_package_test; mod e2e_command_provider_test; mod e2e_exception_test; mod e2e_extension_installer_test; +mod e2e_http_downloader_test; mod e2e_installer_test; mod e2e_installers_test; mod e2e_normalize_test; |
