aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/tests/plugin')
-rw-r--r--crates/shirabe/tests/plugin/e2e_installers_test.rs198
1 files changed, 169 insertions, 29 deletions
diff --git a/crates/shirabe/tests/plugin/e2e_installers_test.rs b/crates/shirabe/tests/plugin/e2e_installers_test.rs
index 36e99d77..67c51b5c 100644
--- a/crates/shirabe/tests/plugin/e2e_installers_test.rs
+++ b/crates/shirabe/tests/plugin/e2e_installers_test.rs
@@ -1,6 +1,6 @@
-//! 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.
+//! composer/installers E2E compatibility check: upstream Composer and Shirabe each run the same
+//! command sequences on a project whose packages are placed by the plugin's `LibraryInstaller`
+//! subclass, and the resulting output and 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.
@@ -20,8 +20,17 @@ fn plugin_fetched() -> bool {
.is_file()
}
+/// The two implementations under comparison: upstream Composer first, Shirabe second.
+fn implementations(composer_bin: &str) -> [(&str, Vec<&str>); 2] {
+ [
+ ("php", vec![composer_bin]),
+ (env!("CARGO_BIN_EXE_shirabe"), vec![]),
+ ]
+}
+
struct CommandRun {
exit_code: i32,
+ stdout: Vec<String>,
}
fn run_command(work: &Path, program: &str, prefix_args: &[&str], args: &[&str]) -> CommandRun {
@@ -38,9 +47,26 @@ fn run_command(work: &Path, program: &str, prefix_args: &[&str], args: &[&str])
.unwrap();
CommandRun {
exit_code: output.status.code().unwrap_or(-1),
+ stdout: normalize_output(&String::from_utf8_lossy(&output.stdout), work),
}
}
+/// Output lines the two implementations must agree on: the working directory (which the plugin
+/// prints absolutely when it deletes a package) is replaced by a placeholder, and progress bar
+/// frames are dropped — Shirabe renders those differently from upstream for every install,
+/// including projects with no plugin at all, so they say nothing about this plugin.
+fn normalize_output(stdout: &str, work: &Path) -> Vec<String> {
+ let work = work.to_str().unwrap();
+ stdout
+ .lines()
+ .filter(|line| {
+ let trimmed = line.trim_start();
+ !(trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains('['))
+ })
+ .map(|line| line.replace(work, "<work>"))
+ .collect()
+}
+
/// 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();
@@ -67,13 +93,10 @@ fn collect(root: &Path, dir: &Path, files: &mut Vec<(String, String)>) {
}
}
-/// 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)
+fn edit(path: &Path, from: &str, to: &str) {
+ let text = std::fs::read_to_string(path).unwrap();
+ assert!(text.contains(from), "{path:?} does not contain {from:?}");
+ std::fs::write(path, text.replace(from, to)).unwrap();
}
#[test]
@@ -87,27 +110,144 @@ fn test_composer_installers_matches_upstream_composer() {
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"), &[]);
+ let mut results = Vec::new();
+ for (program, prefix) in &implementations(&composer_bin) {
+ 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"));
+ results.push((run, tree, work));
+ }
+ let [upstream, shirabe] = <[_; 2]>::try_from(results).ok().unwrap();
- assert_eq!(0, upstream.exit_code, "upstream install must succeed");
- assert_eq!(upstream.exit_code, shirabe.exit_code);
+ assert_eq!(0, upstream.0.exit_code, "upstream install must succeed");
+ assert_eq!(upstream.0.exit_code, shirabe.0.exit_code);
+ assert_eq!(upstream.0.stdout, shirabe.0.stdout);
- // 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()
- };
+ // `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.
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)
+ shirabe
+ .1
+ .iter()
+ .any(|(path, _)| path == "modules/hello-module/composer.json"),
+ "the plugin-provided installer must place the module outside vendor/"
);
- assert_eq!(paths(&upstream_tree), paths(&shirabe_tree));
- assert_eq!(upstream_tree, shirabe_tree);
+ assert_eq!(upstream.1, shirabe.1);
+}
+
+/// The rest of the installer contract: `update` reinstalls a package in place, a second `install`
+/// runs over an already-installed tree, and `remove` reaches the plugin's own `uninstall()`
+/// override — the one that chains onto the promise `LibraryInstaller::uninstall` returns.
+#[test]
+fn test_composer_installers_update_and_remove_match_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 mut results = Vec::new();
+ for (program, prefix) in &implementations(&composer_bin) {
+ let work = TempDir::new().unwrap();
+ copy_dir(&fixture_dir(), work.path());
+ let install = run_command(work.path(), program, prefix, &["install"]);
+
+ let module = work.path().join("packages/hello-module");
+ edit(
+ &module.join("composer.json"),
+ "\"version\": \"1.0.0\"",
+ "\"version\": \"1.1.0\"",
+ );
+ std::fs::write(module.join("src/hello.txt"), "hello module payload v1.1\n").unwrap();
+ edit(
+ &work.path().join("project/composer.json"),
+ "\"acme/hello-module\": \"1.0.0\"",
+ "\"acme/hello-module\": \"1.1.0\"",
+ );
+
+ let update = run_command(
+ work.path(),
+ program,
+ prefix,
+ &["update", "acme/hello-module"],
+ );
+ let reinstall = run_command(work.path(), program, prefix, &["install"]);
+ let remove = run_command(
+ work.path(),
+ program,
+ prefix,
+ &["remove", "acme/hello-theme"],
+ );
+ let tree = tree(&work.path().join("project"));
+ results.push((install, update, reinstall, remove, tree, work));
+ }
+ let [upstream, shirabe] = <[_; 2]>::try_from(results).ok().unwrap();
+
+ for (label, upstream, shirabe) in [
+ ("install", &upstream.0, &shirabe.0),
+ ("update", &upstream.1, &shirabe.1),
+ ("reinstall", &upstream.2, &shirabe.2),
+ ("remove", &upstream.3, &shirabe.3),
+ ] {
+ assert_eq!(0, upstream.exit_code, "upstream {label} must succeed");
+ assert_eq!(upstream.exit_code, shirabe.exit_code, "{label} exit code");
+ assert_eq!(upstream.stdout, shirabe.stdout, "{label} output");
+ }
+ assert_eq!(upstream.4, shirabe.4);
+}
+
+/// The plugin's configuration surface: `installer-paths` in the root package's extra (both the
+/// `type:` and the package-name matcher, with `{$name}` templating) and `installer-name` in the
+/// installed package's own extra. Both are read back through the package proxy.
+#[test]
+fn test_composer_installers_custom_paths_match_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 mut results = Vec::new();
+ for (program, prefix) in &implementations(&composer_bin) {
+ let work = TempDir::new().unwrap();
+ copy_dir(&fixture_dir(), work.path());
+ edit(
+ &work.path().join("project/composer.json"),
+ " \"require\": {",
+ " \"extra\": {\n \"installer-paths\": {\n \
+ \"web/custom/{$name}/\": [\"type:drupal-theme\"],\n \
+ \"web/mods/{$name}/\": [\"acme/hello-module\"]\n }\n },\n \"require\": {",
+ );
+ edit(
+ &work.path().join("packages/hello-module/composer.json"),
+ " \"type\": \"drupal-module\",",
+ " \"type\": \"drupal-module\",\n \"extra\": {\"installer-name\": \"renamed-mod\"},",
+ );
+ let run = run_command(work.path(), program, prefix, &["install"]);
+ let tree = tree(&work.path().join("project"));
+ results.push((run, tree, work));
+ }
+ let [upstream, shirabe] = <[_; 2]>::try_from(results).ok().unwrap();
+
+ assert_eq!(0, upstream.0.exit_code, "upstream install must succeed");
+ assert_eq!(upstream.0.exit_code, shirabe.0.exit_code);
+ assert_eq!(upstream.0.stdout, shirabe.0.stdout);
+ for expected in [
+ "web/custom/hello-theme/composer.json",
+ "web/mods/renamed-mod/composer.json",
+ ] {
+ assert!(
+ shirabe.1.iter().any(|(path, _)| path == expected),
+ "expected {expected} in {:?}",
+ shirabe.1.iter().map(|(path, _)| path).collect::<Vec<_>>()
+ );
+ }
+ assert_eq!(upstream.1, shirabe.1);
}