diff options
Diffstat (limited to 'crates')
8 files changed, 291 insertions, 0 deletions
diff --git a/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs b/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs new file mode 100644 index 00000000..c78af3cd --- /dev/null +++ b/crates/shirabe/tests/plugin/e2e_extension_installer_test.rs @@ -0,0 +1,163 @@ +//! Real-plugin E2E compatibility check: upstream Composer and Shirabe each +//! run `install` on a pristine copy of the pinned phpstan/extension-installer fixture project +//! at the same filesystem path, and every produced artifact (composer.lock and the whole +//! vendor tree, including the plugin-generated GeneratedConfig.php) must match byte for byte. +//! +//! Prerequisites: the PHP runtime, the Composer checkout, and the plugin under test in +//! `fixtures/e2e/ext/` — run `fixtures/e2e/fetch` once to populate it. The test skips while +//! any of these is missing. Test runs themselves are offline: the fixture project resolves +//! everything from local repositories. + +use crate::plugin_installer_test::{lock_php_worker, php_runtime_available}; +use indexmap::IndexMap; +use std::path::{Path, PathBuf}; +use tempfile::TempDir; + +fn fixture_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/plugin/fixtures/e2e") +} + +/// The upstream Composer checkout used as the comparison oracle (and as the PHP runtime of +/// the worker). Absent checkout means the oracle cannot run; the test returns early, +/// following the convention of the non-mock tests in `shirabe-php-rpc`. +fn upstream_composer_bin() -> Option<PathBuf> { + let root = match std::env::var("SHIRABE_COMPOSER_PHP_DIR") { + Ok(dir) => PathBuf::from(dir), + Err(_) => Path::new(env!("CARGO_MANIFEST_DIR")).join("../../composer"), + }; + let bin = root.join("bin/composer"); + if bin.is_file() && root.join("vendor/autoload.php").is_file() { + Some(bin) + } else { + None + } +} + +fn copy_dir(from: &Path, to: &Path) { + std::fs::create_dir_all(to).unwrap(); + for entry in std::fs::read_dir(from).unwrap() { + let entry = entry.unwrap(); + let target = to.join(entry.file_name()); + if entry.file_type().unwrap().is_dir() { + copy_dir(&entry.path(), &target); + } else { + std::fs::copy(entry.path(), &target).unwrap(); + } + } +} + +/// Every file under `dir` as relative path => contents. +fn snapshot_tree(dir: &Path) -> IndexMap<String, Vec<u8>> { + let mut files = IndexMap::new(); + fn walk(root: &Path, dir: &Path, files: &mut IndexMap<String, Vec<u8>>) { + for entry in std::fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + if entry.file_type().unwrap().is_dir() { + walk(root, &entry.path(), files); + } else { + let relative = entry + .path() + .strip_prefix(root) + .unwrap() + .to_str() + .unwrap() + .to_string(); + files.insert(relative, std::fs::read(entry.path()).unwrap()); + } + } + } + walk(dir, dir, &mut files); + files +} + +struct InstallRun { + exit_code: i32, + /// The plugin's own IO lines, order preserved (progress rendering differs between the two + /// implementations and is not compared). + plugin_output: Vec<String>, + artifacts: IndexMap<String, Vec<u8>>, +} + +fn run_install(work: &Path, program: &str, args: &[&str]) -> InstallRun { + let project = work.join("project"); + copy_dir(&fixture_dir(), work); + let output = std::process::Command::new(program) + .args(args) + .arg("install") + .current_dir(&project) + .env("COMPOSER_HOME", work.join("home")) + .env("COMPOSER_CACHE_DIR", work.join("cache")) + .env("COMPOSER_NO_INTERACTION", "1") + .output() + .unwrap(); + let mut artifacts = snapshot_tree(&project.join("vendor")); + artifacts.insert( + "composer.lock".to_string(), + std::fs::read(project.join("composer.lock")).unwrap_or_default(), + ); + // Directory walk order is filesystem-dependent; a canonical order keys the comparison. + artifacts.sort_keys(); + let text = format!( + "{}{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let plugin_output = text + .lines() + .filter(|line| line.starts_with("phpstan/extension-installer:") || line.starts_with("> ")) + .map(str::to_string) + .collect(); + // The next run reuses the same path so absolute paths embedded in the artifacts + // (GeneratedConfig.php's install_path) compare byte for byte. + std::fs::remove_dir_all(&project).unwrap(); + InstallRun { + exit_code: output.status.code().unwrap_or(-1), + plugin_output, + artifacts, + } +} + +#[test] +fn test_extension_installer_install_matches_upstream_composer() { + if !php_runtime_available() { + return; + } + let Some(composer_bin) = upstream_composer_bin() else { + return; + }; + // Populated by `fixtures/e2e/fetch` (network, one-off). + if !fixture_dir() + .join("ext/phpstan-extension-installer-1.4.3/src/Plugin.php") + .is_file() + { + return; + } + let _worker = lock_php_worker(); + + let work = TempDir::new().unwrap(); + let composer_bin = composer_bin.to_str().unwrap().to_string(); + let upstream = run_install(work.path(), "php", &[&composer_bin]); + let shirabe = run_install(work.path(), env!("CARGO_BIN_EXE_shirabe"), &[]); + + assert_eq!(0, upstream.exit_code, "upstream Composer must succeed"); + assert_eq!(upstream.exit_code, shirabe.exit_code); + assert_eq!(upstream.plugin_output, shirabe.plugin_output); + assert_eq!( + vec![ + "phpstan/extension-installer: Extensions installed", + "> acme/extension: installed", + "> acme/phpstan-tools: not supported", + ], + upstream.plugin_output + ); + + let upstream_files: Vec<&String> = upstream.artifacts.keys().collect(); + let shirabe_files: Vec<&String> = shirabe.artifacts.keys().collect(); + assert_eq!(upstream_files, shirabe_files); + for (path, contents) in &upstream.artifacts { + assert_eq!( + contents, &shirabe.artifacts[path], + "artifact `{path}` differs between upstream Composer and Shirabe" + ); + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e/fetch b/crates/shirabe/tests/plugin/fixtures/e2e/fetch new file mode 100755 index 00000000..a4179955 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e/fetch @@ -0,0 +1,33 @@ +#!/bin/sh +# Fetches the external plugin the E2E test runs against, into the git-ignored ext/ +# directory. The plugin is pinned to an immutable upstream commit and the extracted +# files are verified by hash, so the test stays deterministic without the third-party +# source ever entering this repository. Requires network once; the E2E test skips +# itself while ext/ is absent. +set -eu + +# phpstan/extension-installer 1.4.3 +commit=85e90b3942d06b2326fba0403ec24fe912372936 +dir="$(dirname "$0")/ext/phpstan-extension-installer-1.4.3" + +if [ -f "$dir/src/Plugin.php" ]; then + echo "already fetched: $dir" + exit 0 +fi + +mkdir -p "$dir" +curl -fsSL "https://codeload.github.com/phpstan/extension-installer/tar.gz/$commit" \ + | tar -xz -C "$dir" --strip-components=1 + +# GitHub archives are content-addressed by the commit, but the archive encoding is not +# guaranteed stable; the extracted files are what the test consumes, so they are what +# gets pinned. +(cd "$dir" && sha256sum -c --quiet) <<'EOF' || { rm -rf "$dir"; echo "hash mismatch; discarded the fetched tree" >&2; exit 1; } +92d8a5f0f9da0ebe198fe466bc4efc1793d90d8da93efd912fdc9dc414a56885 LICENSE +76197285d0c2a8dec7cff9f8562638bda879367829fd982281cba53068c7f153 README.md +71f6125d095522d8b24925d9144d361b2419f41be6174607b026473ae65c2604 composer.json +fc46b5548e9656fab230ed4b97fdf13b54712743d04498374d40ebae2610bac1 src/GeneratedConfig.php +5803a3fb8c272d52848e4aa240707370e301fa081fc632b487fafacfe4985af7 src/Plugin.php +EOF + +echo "fetched: $dir" diff --git a/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-extension/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-extension/composer.json new file mode 100644 index 00000000..066c25e3 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-extension/composer.json @@ -0,0 +1,11 @@ +{ + "name": "acme/extension", + "version": "1.0.0", + "type": "phpstan-extension", + "description": "PHPStan extension fixture handled by phpstan/extension-installer.", + "extra": { + "phpstan": { + "includes": ["extension.neon"] + } + } +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-extension/extension.neon b/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-extension/extension.neon new file mode 100644 index 00000000..e9bca97b --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-extension/extension.neon @@ -0,0 +1,2 @@ +parameters: + acmeExtensionLoaded: true diff --git a/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-phpstan-tools/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-phpstan-tools/composer.json new file mode 100644 index 00000000..074dbe8c --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e/packages/acme-phpstan-tools/composer.json @@ -0,0 +1,5 @@ +{ + "name": "acme/phpstan-tools", + "version": "1.0.0", + "description": "Package whose name mentions phpstan without being an extension; exercises the NOT_INSTALLED path." +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e/packages/phpstan-phpstan/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e/packages/phpstan-phpstan/composer.json new file mode 100644 index 00000000..cbd11818 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e/packages/phpstan-phpstan/composer.json @@ -0,0 +1,5 @@ +{ + "name": "phpstan/phpstan", + "version": "2.1.0", + "description": "Minimal stand-in for phpstan/phpstan used by the extension-installer E2E fixture." +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e/project/composer.json new file mode 100644 index 00000000..d16b5383 --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e/project/composer.json @@ -0,0 +1,71 @@ +{ + "name": "shirabe/e2e-extension-installer", + "description": "E2E fixture project: install phpstan/extension-installer and let it generate its config.", + "repositories": [ + { + "type": "package", + "package": { + "name": "phpstan/extension-installer", + "version": "1.4.3", + "type": "composer-plugin", + "license": [ + "MIT" + ], + "autoload": { + "psr-4": { + "PHPStan\\ExtensionInstaller\\": "src/" + } + }, + "require": { + "php": "^7.2 || ^8.0", + "composer-plugin-api": "^2.0", + "phpstan/phpstan": "^1.9.0 || ^2.0" + }, + "extra": { + "class": "PHPStan\\ExtensionInstaller\\Plugin" + }, + "dist": { + "type": "path", + "url": "../ext/phpstan-extension-installer-1.4.3" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "path", + "url": "../packages/phpstan-phpstan", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "../packages/acme-extension", + "options": { + "symlink": false + } + }, + { + "type": "path", + "url": "../packages/acme-phpstan-tools", + "options": { + "symlink": false + } + }, + { + "packagist.org": false + } + ], + "require": { + "phpstan/extension-installer": "1.4.3", + "acme/extension": "1.0.0", + "acme/phpstan-tools": "1.0.0" + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } + } +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index 3a66bb75..b2631eb7 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -3,5 +3,6 @@ mod async_runtime; #[path = "../common/config_stub.rs"] mod config_stub; +mod e2e_extension_installer_test; mod plugin_installer_test; mod subscriber_test; |
