diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-06 02:40:17 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-06 02:40:17 +0900 |
| commit | 1df28c4f702045a33865d29d00ef430d8138579b (patch) | |
| tree | 2a058de677892ad78115d59c1b9794dca4a7e5f7 /crates/shirabe/tests/plugin/e2e_installers_test.rs | |
| parent | 90764c477f24e4d67d3e0cae943c33fd2b8ae220 (diff) | |
| download | php-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/tests/plugin/e2e_installers_test.rs')
| -rw-r--r-- | crates/shirabe/tests/plugin/e2e_installers_test.rs | 113 |
1 files changed, 113 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); +} |
