aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-06 02:40:17 +0900
committernsfisis <nsfisis@gmail.com>2026-08-06 02:40:17 +0900
commit1df28c4f702045a33865d29d00ef430d8138579b (patch)
tree2a058de677892ad78115d59c1b9794dca4a7e5f7 /crates/shirabe
parent90764c477f24e4d67d3e0cae943c33fd2b8ae220 (diff)
downloadphp-shirabe-1df28c4f702045a33865d29d00ef430d8138579b.tar.gz
php-shirabe-1df28c4f702045a33865d29d00ef430d8138579b.tar.zst
php-shirabe-1df28c4f702045a33865d29d00ef430d8138579b.zip
test(plugin): compare a composer/installers run against upstream Composer
The fixture project pins composer/installers 2.3.0 and requires two packages of framework-specific types, so the plugin's LibraryInstaller subclass decides where they land. Upstream Composer and Shirabe install the same project and the whole resulting tree is compared. The plugin tarball is fetched by `fixtures/e2e-installers/fetch` into a git-ignored directory and pinned by a digest over the extracted files, so the third-party source never enters this repository and the test skips itself while the directory is absent. The tree is staged and only moved into place once verified, so an unverified tree is never observable under the name the test looks for.
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/tests/plugin/e2e_installers_test.rs113
-rwxr-xr-xcrates/shirabe/tests/plugin/fixtures/e2e-installers/fetch42
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/composer.json6
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/src/hello.txt1
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/composer.json6
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/src/hello.txt1
-rw-r--r--crates/shirabe/tests/plugin/fixtures/e2e-installers/project/composer.json57
-rw-r--r--crates/shirabe/tests/plugin/main.rs1
8 files changed, 227 insertions, 0 deletions
diff --git a/crates/shirabe/tests/plugin/e2e_installers_test.rs b/crates/shirabe/tests/plugin/e2e_installers_test.rs
new file mode 100644
index 00000000..36e99d77
--- /dev/null
+++ b/crates/shirabe/tests/plugin/e2e_installers_test.rs
@@ -0,0 +1,113 @@
+//! composer/installers E2E compatibility check: upstream Composer and Shirabe each install a
+//! project whose packages are placed by the plugin's `LibraryInstaller` subclass, and the
+//! resulting project trees are compared.
+//!
+//! The plugin is fetched by `fixtures/e2e-installers/fetch` into a git-ignored directory; the
+//! test skips itself while that directory, 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-installers")
+}
+
+fn plugin_fetched() -> bool {
+ fixture_dir()
+ .join("ext/composer-installers-2.3.0/src/Composer/Installers/Installer.php")
+ .is_file()
+}
+
+struct CommandRun {
+ exit_code: i32,
+}
+
+fn run_command(work: &Path, program: &str, prefix_args: &[&str], args: &[&str]) -> CommandRun {
+ let output = std::process::Command::new(program)
+ .args(prefix_args)
+ .args(args)
+ .current_dir(work.join("project"))
+ .env("COMPOSER_HOME", work.join("home"))
+ .env("COMPOSER_CACHE_DIR", work.join("cache"))
+ .env("COMPOSER_NO_INTERACTION", "1")
+ .env("COLUMNS", "120")
+ .env("LINES", "30")
+ .output()
+ .unwrap();
+ CommandRun {
+ exit_code: output.status.code().unwrap_or(-1),
+ }
+}
+
+/// Every file under `dir` as (relative path, contents), so two project trees compare as a whole.
+fn tree(dir: &Path) -> Vec<(String, String)> {
+ let mut files = Vec::new();
+ collect(dir, dir, &mut files);
+ files.sort();
+ files
+}
+
+fn collect(root: &Path, dir: &Path, files: &mut Vec<(String, String)>) {
+ for entry in std::fs::read_dir(dir).unwrap() {
+ let entry = entry.unwrap();
+ let path = entry.path();
+ if entry.file_type().unwrap().is_dir() {
+ collect(root, &path, files);
+ } else {
+ let relative = path
+ .strip_prefix(root)
+ .unwrap()
+ .to_str()
+ .unwrap()
+ .to_string();
+ files.push((relative, std::fs::read_to_string(&path).unwrap_or_default()));
+ }
+ }
+}
+
+/// Runs `install` in a fresh copy of the fixture and returns the run plus the resulting tree.
+fn install(program: &str, prefix: &[&str]) -> (CommandRun, Vec<(String, String)>, TempDir) {
+ let work = TempDir::new().unwrap();
+ copy_dir(&fixture_dir(), work.path());
+ let run = run_command(work.path(), program, prefix, &["install"]);
+ let tree = tree(&work.path().join("project"));
+ (run, tree, work)
+}
+
+#[test]
+fn test_composer_installers_matches_upstream_composer() {
+ if !php_runtime_available() || !plugin_fetched() {
+ 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, upstream_tree, _upstream_work) = install("php", &[composer_bin.as_str()]);
+ let (shirabe, shirabe_tree, _shirabe_work) = install(env!("CARGO_BIN_EXE_shirabe"), &[]);
+
+ assert_eq!(0, upstream.exit_code, "upstream install must succeed");
+ assert_eq!(upstream.exit_code, shirabe.exit_code);
+
+ // The paths alone carry the plugin's whole contribution: `acme/hello-module` and
+ // `acme/hello-theme` land under modules/ and themes/ rather than vendor/, which only the
+ // plugin-provided installer's getInstallPath can decide.
+ let paths = |files: &[(String, String)]| -> Vec<String> {
+ files
+ .iter()
+ .map(|(path, _)| path.clone())
+ .filter(|path| !path.starts_with("vendor/composer/installers/"))
+ .collect()
+ };
+ assert!(
+ paths(&shirabe_tree).contains(&"modules/hello-module/composer.json".to_string()),
+ "the plugin-provided installer must place the module outside vendor/: {:?}",
+ paths(&shirabe_tree)
+ );
+ assert_eq!(paths(&upstream_tree), paths(&shirabe_tree));
+ assert_eq!(upstream_tree, shirabe_tree);
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-installers/fetch b/crates/shirabe/tests/plugin/fixtures/e2e-installers/fetch
new file mode 100755
index 00000000..a00d9a07
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-installers/fetch
@@ -0,0 +1,42 @@
+#!/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
+
+# composer/installers v2.3.0
+commit=12fb2dfe5e16183de69e784a7b84046c43d97e8e
+# 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. This tree holds a hundred installer classes, so the pin is one digest over
+# the sorted per-file digests rather than a line per file.
+expected=2f9bb837ed1374985909cf33eb1e6e33d0dc3bf1eb0c55599c7326a886bf4f50
+
+ext="$(dirname "$0")/ext"
+dir="$ext/composer-installers-2.3.0"
+
+if [ -f "$dir/src/Composer/Installers/Installer.php" ]; then
+ echo "already fetched: $dir"
+ exit 0
+fi
+
+# Staged next to the destination and only moved into place once verified, so a mismatched
+# tree is never observable under the name the test looks for.
+staging="$ext/.staging-$commit"
+mkdir -p "$staging"
+# The CI workflows are not part of the installed package and would only add noise to the
+# digest.
+curl -fsSL "https://codeload.github.com/composer/installers/tar.gz/$commit" \
+ | tar -xz -C "$staging" --strip-components=1 --exclude='.github'
+
+actual=$(cd "$staging" && find . -type f | sort | xargs sha256sum | sha256sum | cut -d' ' -f1)
+if [ "$actual" != "$expected" ]; then
+ echo "hash mismatch: expected $expected, got $actual" >&2
+ echo "the fetched tree was left in $staging for inspection; delete it before retrying" >&2
+ exit 1
+fi
+
+mv "$staging" "$dir"
+echo "fetched: $dir"
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/composer.json
new file mode 100644
index 00000000..7f55c074
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/composer.json
@@ -0,0 +1,6 @@
+{
+ "name": "acme/hello-module",
+ "version": "1.0.0",
+ "type": "drupal-module",
+ "description": "Fixture package placed by composer/installers under modules/."
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/src/hello.txt b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/src/hello.txt
new file mode 100644
index 00000000..c872e5a8
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-module/src/hello.txt
@@ -0,0 +1 @@
+hello module payload
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/composer.json
new file mode 100644
index 00000000..ea23f0f4
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/composer.json
@@ -0,0 +1,6 @@
+{
+ "name": "acme/hello-theme",
+ "version": "1.0.0",
+ "type": "drupal-theme",
+ "description": "Fixture package placed by composer/installers under themes/."
+}
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/src/hello.txt b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/src/hello.txt
new file mode 100644
index 00000000..b2c6f073
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-installers/packages/hello-theme/src/hello.txt
@@ -0,0 +1 @@
+hello theme payload
diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-installers/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-installers/project/composer.json
new file mode 100644
index 00000000..c2793f71
--- /dev/null
+++ b/crates/shirabe/tests/plugin/fixtures/e2e-installers/project/composer.json
@@ -0,0 +1,57 @@
+{
+ "name": "shirabe/e2e-installers",
+ "description": "E2E fixture project: let composer/installers place packages of a framework-specific type.",
+ "repositories": [
+ {
+ "type": "package",
+ "package": {
+ "name": "composer/installers",
+ "version": "2.3.0",
+ "type": "composer-plugin",
+ "license": [
+ "MIT"
+ ],
+ "autoload": {
+ "psr-4": {
+ "Composer\\Installers\\": "src/Composer/Installers"
+ }
+ },
+ "require": {
+ "php": "^7.2 || ^8.0",
+ "composer-plugin-api": "^1.0 || ^2.0"
+ },
+ "extra": {
+ "class": "Composer\\Installers\\Plugin",
+ "plugin-modifies-install-path": true
+ },
+ "dist": {
+ "type": "path",
+ "url": "../ext/composer-installers-2.3.0"
+ },
+ "transport-options": {
+ "symlink": false
+ }
+ }
+ },
+ {
+ "type": "path",
+ "url": "../packages/*",
+ "options": {
+ "symlink": false
+ }
+ },
+ {
+ "packagist.org": false
+ }
+ ],
+ "require": {
+ "composer/installers": "2.3.0",
+ "acme/hello-module": "1.0.0",
+ "acme/hello-theme": "1.0.0"
+ },
+ "config": {
+ "allow-plugins": {
+ "composer/installers": true
+ }
+ }
+}
diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs
index 02db6b03..0b404ee9 100644
--- a/crates/shirabe/tests/plugin/main.rs
+++ b/crates/shirabe/tests/plugin/main.rs
@@ -6,6 +6,7 @@ mod config_stub;
mod e2e_command_provider_test;
mod e2e_extension_installer_test;
mod e2e_installer_test;
+mod e2e_installers_test;
mod e2e_normalize_test;
mod plugin_installer_test;
mod subscriber_test;