From da602f1cb1d555c7826fa3d026df66b82061cda4 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Wed, 5 Aug 2026 04:07:35 +0900 Subject: test(plugin): pin a composer-normalize fixture and compare its listing The pinned plugin and its real dependency tree (nine packages, fetched by commit and verified by tree hash into the git-ignored ext/) install under both implementations, and the list/help renderings of the plugin-provided normalize command must match upstream byte for byte. The execution comparison is written but ignored: NormalizeCommand builds a second, in-process Composer instance, and the worker's proxy stubs reject native construction of the classes that path instantiates. Stale comments about the missing worker-side application are updated to the current facts. Co-Authored-By: Claude Fable 5 --- crates/shirabe/src/console/application.rs | 12 +- .../tests/command/run_script_command_test.rs | 8 +- crates/shirabe/tests/plugin/e2e_normalize_test.rs | 167 +++++++++++++++ .../tests/plugin/fixtures/e2e-normalize/fetch | 42 ++++ .../fixtures/e2e-normalize/project/composer.json | 228 +++++++++++++++++++++ crates/shirabe/tests/plugin/main.rs | 1 + 6 files changed, 449 insertions(+), 9 deletions(-) create mode 100644 crates/shirabe/tests/plugin/e2e_normalize_test.rs create mode 100755 crates/shirabe/tests/plugin/fixtures/e2e-normalize/fetch create mode 100644 crates/shirabe/tests/plugin/fixtures/e2e-normalize/project/composer.json (limited to 'crates') diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs index 7be18796..62e73aa9 100644 --- a/crates/shirabe/src/console/application.rs +++ b/crates/shirabe/src/console/application.rs @@ -2479,11 +2479,13 @@ impl ApplicationHandle { } // TODO(plugin): `new $dummy($script)` instantiates the // user's PHP command class in-process and registers the - // live object on this Application; hosting a PHP-owned - // command here needs a PHP-side Symfony Application and - // command proxying, neither of which exists yet. The - // shim class_exists above never recognizes user - // classes, so this arm is currently unreachable. + // live object on this Application. The worker-side + // console application and PhpCommandProxy exist now, + // but this arm is not wired to them: the class checks + // above use the shim class_exists, which never + // recognizes user classes, so the arm stays + // unreachable until the checks and the instantiation + // go through the worker. let _ = shirabe_php_shim::instantiate_class( &dummy_str, vec![PhpMixed::String(script.clone())], diff --git a/crates/shirabe/tests/command/run_script_command_test.rs b/crates/shirabe/tests/command/run_script_command_test.rs index 9e213cee..10e171ba 100644 --- a/crates/shirabe/tests/command/run_script_command_test.rs +++ b/crates/shirabe/tests/command/run_script_command_test.rs @@ -122,19 +122,19 @@ fn test_can_define_aliases() { } #[test] -#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a live application command (todo!() in application.rs; needs a PHP-side Symfony Application, which does not exist yet). The EventDispatcher-side Command-class path alone cannot satisfy the direct invocation and its argument definitions"] +#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the command's output would go to the worker's inherited stdio, which the in-process application tester cannot capture"] fn test_execution_of_simple_symfony_command() { // TODO(phase-d): the test invokes the script name as a top-level composer command, which // requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a - // live application command (todo!() in application.rs; needs a PHP-side Symfony Application, which does not exist yet). + // live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the worker writes to inherited stdio the tester cannot capture. todo!() } #[test] -#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php) as a live application command (todo!() in application.rs; needs a PHP-side Symfony Application, which does not exist yet). The EventDispatcher-side Command-class path alone cannot satisfy the direct invocation and its argument definitions"] +#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php) as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the command's output would go to the worker's inherited stdio, which the in-process application tester cannot capture"] fn test_execution_of_symfony_command_with_configuration() { // TODO(phase-d): the test invokes the script name as a top-level composer command, which // requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php) - // as a live application command (todo!() in application.rs; needs a PHP-side Symfony Application, which does not exist yet). + // as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the worker writes to inherited stdio the tester cannot capture. todo!() } diff --git a/crates/shirabe/tests/plugin/e2e_normalize_test.rs b/crates/shirabe/tests/plugin/e2e_normalize_test.rs new file mode 100644 index 00000000..ccfdb596 --- /dev/null +++ b/crates/shirabe/tests/plugin/e2e_normalize_test.rs @@ -0,0 +1,167 @@ +//! ergebnis/composer-normalize E2E compatibility check: upstream Composer and Shirabe each +//! install the pinned plugin (with its real dependency tree) and the `list`/`help` renderings +//! of its command are compared; the execution comparison is present but ignored until the +//! worker can construct a second native Composer instance. +//! +//! Prerequisites: the PHP runtime, the Composer checkout, and the pinned packages in +//! `fixtures/e2e-normalize/ext/` — run `fixtures/e2e-normalize/fetch` once to populate it. +//! The test skips while any of these is missing; test runs themselves are offline. + +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-normalize") +} + +fn fixture_fetched() -> bool { + fixture_dir() + .join("ext/composer-normalize-2.52.0/src/NormalizePlugin.php") + .is_file() +} + +struct CommandRun { + exit_code: i32, + stdout: String, +} + +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), + stdout: String::from_utf8_lossy(&output.stdout).into_owned(), + } +} + +fn prepared_project(program: &str, prefix: &[&str]) -> TempDir { + let work = TempDir::new().unwrap(); + copy_dir(&fixture_dir(), work.path()); + let install = run_command(work.path(), program, prefix, &["install"]); + assert_eq!(0, install.exit_code, "{program}: install must succeed"); + work +} + +fn normalize_lines(stdout: &str) -> Vec<&str> { + stdout + .lines() + .filter(|line| line.contains("normalize")) + .collect() +} + +#[test] +fn test_normalize_listing_matches_upstream_composer() { + if !php_runtime_available() { + return; + } + let Some(composer_bin) = upstream_composer_bin() else { + return; + }; + if !fixture_fetched() { + return; + } + let _worker = lock_php_worker(); + + let composer_bin = composer_bin.to_str().unwrap().to_string(); + let upstream_prefix = [composer_bin.as_str()]; + let u_work = prepared_project("php", &upstream_prefix); + let s_work = prepared_project(env!("CARGO_BIN_EXE_shirabe"), &[]); + + let u_list = run_command( + u_work.path(), + "php", + &upstream_prefix, + &["list", "--no-ansi"], + ); + let s_list = run_command( + s_work.path(), + env!("CARGO_BIN_EXE_shirabe"), + &[], + &["list", "--no-ansi"], + ); + assert_eq!(0, u_list.exit_code); + assert_eq!(u_list.exit_code, s_list.exit_code); + assert_eq!( + normalize_lines(&u_list.stdout), + normalize_lines(&s_list.stdout) + ); + assert!( + !normalize_lines(&s_list.stdout).is_empty(), + "list must mention the plugin-provided normalize command" + ); + + let u_help = run_command( + u_work.path(), + "php", + &upstream_prefix, + &["help", "normalize", "--no-ansi"], + ); + let s_help = run_command( + s_work.path(), + env!("CARGO_BIN_EXE_shirabe"), + &[], + &["help", "normalize", "--no-ansi"], + ); + assert_eq!(0, u_help.exit_code); + assert_eq!(u_help.exit_code, s_help.exit_code); + assert_eq!( + u_help.stdout, s_help.stdout, + "help normalize output differs" + ); +} + +// TODO(plugin): NormalizeCommand::execute builds a second, in-process Composer instance +// (`(new Factory())->createComposer(...)`), and the worker's proxy stub classes reject native +// construction of the FQCNs that path instantiates (Composer\Composer, the event dispatcher, +// the package graph, ...); running the command therefore stops at that explicit error. +#[ignore = "the worker cannot construct a second native Composer instance yet; see the TODO(plugin) above"] +#[test] +fn test_normalize_execution_matches_upstream_composer() { + if !php_runtime_available() { + return; + } + let Some(composer_bin) = upstream_composer_bin() else { + return; + }; + if !fixture_fetched() { + return; + } + let _worker = lock_php_worker(); + + let composer_bin = composer_bin.to_str().unwrap().to_string(); + let upstream_prefix = [composer_bin.as_str()]; + let u_work = prepared_project("php", &upstream_prefix); + let s_work = prepared_project(env!("CARGO_BIN_EXE_shirabe"), &[]); + + let dry = ["normalize", "--dry-run", "--no-ansi"]; + let u_dry = run_command(u_work.path(), "php", &upstream_prefix, &dry); + let s_dry = run_command(s_work.path(), env!("CARGO_BIN_EXE_shirabe"), &[], &dry); + assert_eq!(1, u_dry.exit_code, "upstream dry-run must report a diff"); + assert_eq!(u_dry.exit_code, s_dry.exit_code); + assert_eq!( + u_dry.stdout, s_dry.stdout, + "normalize --dry-run output differs" + ); + + let real = ["normalize", "--no-update-lock", "--no-ansi"]; + let u_run = run_command(u_work.path(), "php", &upstream_prefix, &real); + let s_run = run_command(s_work.path(), env!("CARGO_BIN_EXE_shirabe"), &[], &real); + assert_eq!(0, u_run.exit_code, "upstream normalize must succeed"); + assert_eq!(u_run.exit_code, s_run.exit_code); + assert_eq!( + std::fs::read(u_work.path().join("project/composer.json")).unwrap(), + std::fs::read(s_work.path().join("project/composer.json")).unwrap(), + "normalized composer.json differs" + ); +} diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-normalize/fetch b/crates/shirabe/tests/plugin/fixtures/e2e-normalize/fetch new file mode 100755 index 00000000..7fb7cd5a --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-normalize/fetch @@ -0,0 +1,42 @@ +#!/bin/sh +# Fetches the external packages the composer-normalize E2E test runs against, into the +# git-ignored ext/ directory. Every package is pinned to an immutable upstream commit and the +# extracted tree is verified by a content hash over all files, so the test stays deterministic +# without any third-party source entering this repository. Requires network once; the E2E test +# skips itself while ext/ is absent. +set -eu + +base="$(dirname "$0")/ext" + +tree_hash() { + (cd "$1" && find . -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -d' ' -f1) +} + +# +while read -r dir repo commit hash; do + target="$base/$dir" + if [ -d "$target" ]; then + echo "already fetched: $target" + continue + fi + mkdir -p "$target" + curl -fsSL "https://codeload.github.com/$repo/tar.gz/$commit" \ + | tar -xz -C "$target" --strip-components=1 + actual="$(tree_hash "$target")" + if [ "$actual" != "$hash" ]; then + rm -rf "$target" + echo "tree hash mismatch for $repo@$commit: got $actual; discarded the fetched tree" >&2 + exit 1 + fi + echo "fetched: $target" +done <<'EOF' +composer-normalize-2.52.0 ergebnis/composer-normalize 988f83f5e51a42cdd2337e5fcd935432f8dfa33c 5259a0371a2b571fab3018c8a6a5929883a669f516e1e8a3fb1cfb37e0803e2c +json-1.6.0 ergebnis/json 7b56d2b5d9e897e75b43e2e753075a0904c921b1 faa07a09e62d6a47582f011744fae2d0f220ccdda157465d31b11b3c095c3605 +json-normalizer-4.10.1 ergebnis/json-normalizer 77961faf2c651c3f05977b53c6c68e8434febf62 edea5b84656b1ae172ca4e92ebe481ff3c22828d3637b36c76e65223c38e9dce +json-pointer-3.8.0 ergebnis/json-pointer b58c3c468a7ff109fdf9a255f17de29ecbe5276c 5395f59f684babf62a33d721ef8ab49eaba3d1280786081b45c49c29fdea1562 +json-printer-3.8.1 ergebnis/json-printer 211d73fc7ec6daf98568ee6ed6e6d133dee8503e 72d8e6ca1a168229bb2a50524a00d0c0a3c69be5e65731d177b6b563503e6b9a +json-schema-validator-4.5.1 ergebnis/json-schema-validator b739527a480a9e3651360ad351ea77e7e9019df2 a176d190b6517b0380edfe13e526289864849c8b1788dee19123560658fc0be4 +json-schema-6.10.0 jsonrainbow/json-schema 8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33 8b6b7931f9072aabf599927cf92ecc171f37e6516c4075172b60a45baa276380 +php-enum-4.7.2 marc-mabe/php-enum bb426fcdd65c60fb3638ef741e8782508fda7eef 7bb6a4923625cd6353b95534b322f951acf0aef055054cffd99e97dcbf1778b5 +diff-1.3.0 localheinz/diff 33bd840935970cda6691c23fc7d94ae764c0734c 283c8104b38fc8428e0819d30ab5e94f4e8284293d79b994bba4fbf0e05ca86f +EOF diff --git a/crates/shirabe/tests/plugin/fixtures/e2e-normalize/project/composer.json b/crates/shirabe/tests/plugin/fixtures/e2e-normalize/project/composer.json new file mode 100644 index 00000000..9af30ddb --- /dev/null +++ b/crates/shirabe/tests/plugin/fixtures/e2e-normalize/project/composer.json @@ -0,0 +1,228 @@ +{ + "description": "E2E fixture project: install and run ergebnis/composer-normalize. The top-level keys are deliberately out of schema order so the normalize run has something to do.", + "require": { + "ergebnis/composer-normalize": "2.52.0" + }, + "name": "shirabe/e2e-composer-normalize", + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + } + }, + "repositories": [ + { + "type": "package", + "package": { + "name": "ergebnis/composer-normalize", + "version": "2.52.0", + "type": "composer-plugin", + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "require": { + "ext-json": "*", + "composer-plugin-api": "^2.0.0", + "ergebnis/json": "^1.4.0", + "ergebnis/json-normalizer": "^4.9.0", + "ergebnis/json-printer": "^3.7.0", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0", + "localheinz/diff": "^1.3.0" + }, + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin" + }, + "dist": { + "type": "path", + "url": "../ext/composer-normalize-2.52.0" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "ergebnis/json", + "version": "1.6.0", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "require": { + "ext-json": "*" + }, + "dist": { + "type": "path", + "url": "../ext/json-1.6.0" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "ergebnis/json-normalizer", + "version": "4.10.1", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "require": { + "ext-json": "*", + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ergebnis/json-printer": "^3.5.0", + "ergebnis/json-schema-validator": "^4.2.0", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0" + }, + "dist": { + "type": "path", + "url": "../ext/json-normalizer-4.10.1" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "ergebnis/json-pointer", + "version": "3.8.0", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "dist": { + "type": "path", + "url": "../ext/json-pointer-3.8.0" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "ergebnis/json-printer", + "version": "3.8.1", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*" + }, + "dist": { + "type": "path", + "url": "../ext/json-printer-3.8.1" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "ergebnis/json-schema-validator", + "version": "4.5.1", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "require": { + "ext-json": "*", + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "justinrainbow/json-schema": "^5.2.12 || ^6.0.0" + }, + "dist": { + "type": "path", + "url": "../ext/json-schema-validator-4.5.1" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "justinrainbow/json-schema", + "version": "6.10.0", + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "require": { + "marc-mabe/php-enum": "^4.4" + }, + "dist": { + "type": "path", + "url": "../ext/json-schema-6.10.0" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "marc-mabe/php-enum", + "version": "4.7.2", + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "dist": { + "type": "path", + "url": "../ext/php-enum-4.7.2" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "type": "package", + "package": { + "name": "localheinz/diff", + "version": "1.3.0", + "autoload": { + "classmap": [ + "src/" + ] + }, + "dist": { + "type": "path", + "url": "../ext/diff-1.3.0" + }, + "transport-options": { + "symlink": false + } + } + }, + { + "packagist.org": false + } + ] +} diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs index 633ad790..a65594be 100644 --- a/crates/shirabe/tests/plugin/main.rs +++ b/crates/shirabe/tests/plugin/main.rs @@ -5,5 +5,6 @@ mod config_stub; mod e2e_command_provider_test; mod e2e_extension_installer_test; +mod e2e_normalize_test; mod plugin_installer_test; mod subscriber_test; -- cgit v1.3.1