aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-02 11:41:49 +0900
committernsfisis <nsfisis@gmail.com>2026-08-02 11:41:49 +0900
commit69b35b0c4d8cb583b389973b096f9dd2dee695c6 (patch)
tree7e9b994e0a4d290ff45c8ae44dd05ad56af77ea8
parentf1dee6362f9b25e78f7e6417e957c0ca34356ecb (diff)
downloadphp-shirabe-69b35b0c4d8cb583b389973b096f9dd2dee695c6.tar.gz
php-shirabe-69b35b0c4d8cb583b389973b096f9dd2dee695c6.tar.zst
php-shirabe-69b35b0c4d8cb583b389973b096f9dd2dee695c6.zip
test(completion): port CommandCompletionTester and CompletionFunctionalTest
Adds the Symfony console test helper (Tester/CommandCompletionTester) and ports every CompletionFunctionalTest data-provider entry as an individual test. The tests reproduce the PHP environment by chdir'ing into the vendored composer/ checkout (its composer.json/lock provide the installed packages, scripts and package properties the expectations reference); the Packagist-backed entries query the live repository exactly like the PHP test does. Only `exec ` is ignored: its expectations require the dev checkout's fully installed vendor/bin, which the vendored checkout does not ship. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-rw-r--r--crates/shirabe-external-packages/src/symfony/console.rs1
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/tester.rs3
-rw-r--r--crates/shirabe-external-packages/src/symfony/console/tester/command_completion_tester.rs54
-rw-r--r--crates/shirabe/tests/completion_functional_test.rs499
4 files changed, 545 insertions, 12 deletions
diff --git a/crates/shirabe-external-packages/src/symfony/console.rs b/crates/shirabe-external-packages/src/symfony/console.rs
index 986c9ca8..814e53a3 100644
--- a/crates/shirabe-external-packages/src/symfony/console.rs
+++ b/crates/shirabe-external-packages/src/symfony/console.rs
@@ -15,6 +15,7 @@ pub mod question;
pub mod signal_registry;
pub mod style;
pub mod terminal;
+pub mod tester;
pub use application::*;
pub use attribute::*;
diff --git a/crates/shirabe-external-packages/src/symfony/console/tester.rs b/crates/shirabe-external-packages/src/symfony/console/tester.rs
new file mode 100644
index 00000000..1a7f6d0c
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/tester.rs
@@ -0,0 +1,3 @@
+pub mod command_completion_tester;
+
+pub use command_completion_tester::*;
diff --git a/crates/shirabe-external-packages/src/symfony/console/tester/command_completion_tester.rs b/crates/shirabe-external-packages/src/symfony/console/tester/command_completion_tester.rs
new file mode 100644
index 00000000..35da1b9d
--- /dev/null
+++ b/crates/shirabe-external-packages/src/symfony/console/tester/command_completion_tester.rs
@@ -0,0 +1,54 @@
+//! ref: composer/vendor/symfony/console/Tester/CommandCompletionTester.php
+
+use crate::symfony::console::command::command::Command;
+use crate::symfony::console::completion::completion_input::CompletionInput;
+use crate::symfony::console::completion::completion_suggestions::CompletionSuggestions;
+
+/// Eases the testing of command completion.
+#[derive(Debug)]
+pub struct CommandCompletionTester {
+ command: std::rc::Rc<std::cell::RefCell<dyn Command>>,
+}
+
+impl CommandCompletionTester {
+ pub fn new(command: std::rc::Rc<std::cell::RefCell<dyn Command>>) -> Self {
+ Self { command }
+ }
+
+ /// Create completion suggestions from input tokens.
+ pub fn complete(&self, input: &[&str]) -> anyhow::Result<Vec<String>> {
+ let mut input: Vec<String> = input.iter().map(|s| s.to_string()).collect();
+ let current_index = input.len() as i64;
+ if input.last().map(String::as_str) == Some("") {
+ input.pop();
+ }
+ // array_unshift($input, $this->command->getName())
+ input.insert(0, self.command.borrow().get_name().unwrap_or_default());
+
+ let mut completion_input = CompletionInput::from_tokens(input, current_index)?;
+ {
+ let command_ref = self.command.borrow();
+ let definition = command_ref.get_definition();
+ completion_input.bind(&definition)?;
+ }
+ let mut suggestions = CompletionSuggestions::new();
+
+ self.command
+ .borrow()
+ .complete(&completion_input, &mut suggestions)?;
+
+ let mut result: Vec<String> = suggestions
+ .get_option_suggestions()
+ .iter()
+ .map(|option| format!("--{}", option.get_name()))
+ .collect();
+ // array_map('strval', ... $suggestions->getValueSuggestions())
+ result.extend(
+ suggestions
+ .get_value_suggestions()
+ .iter()
+ .map(|suggestion| suggestion.to_string()),
+ );
+ Ok(result)
+ }
+}
diff --git a/crates/shirabe/tests/completion_functional_test.rs b/crates/shirabe/tests/completion_functional_test.rs
index 860df546..89478d9d 100644
--- a/crates/shirabe/tests/completion_functional_test.rs
+++ b/crates/shirabe/tests/completion_functional_test.rs
@@ -1,16 +1,491 @@
//! ref: composer/tests/Composer/Test/CompletionFunctionalTest.php
+//!
+//! Validate autocompletion for all commands.
+//!
+//! The PHP test runs inside the Composer dev checkout and takes its expectations from that
+//! environment: the checkout's own composer.json/composer.lock (installed packages, scripts,
+//! package properties) and live Packagist queries for available-package suggestions. Each
+//! data-provider entry is ported as its own test so the entries that cannot run here can be
+//! ignored individually (currently only `exec `, whose expectations require a fully
+//! installed vendor/bin).
+#[path = "common/bootstrap.rs"]
+mod bootstrap;
+
+use serial_test::serial;
+use shirabe::console::application::{Application, ApplicationHandle};
+use shirabe_external_packages::symfony::console::tester::command_completion_tester::CommandCompletionTester;
+
+struct RestoreCwd(std::path::PathBuf);
+
+impl Drop for RestoreCwd {
+ fn drop(&mut self) {
+ let _ = std::env::set_current_dir(&self.0);
+ }
+}
+
+/// PHP runs from the Composer dev checkout; chdir into the vendored checkout to reproduce
+/// that environment (restored on drop, so the tests are `#[serial]`).
+fn chdir_composer_checkout() -> RestoreCwd {
+ let prev = std::env::current_dir().unwrap();
+ let checkout = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../composer");
+ std::env::set_current_dir(checkout).unwrap();
+ RestoreCwd(prev)
+}
+
+/// ref: CompletionFunctionalTest::testComplete
+///
+/// `expected_suggestions` are sample expected suggestions (a subset). None if nothing is
+/// expected.
+fn assert_complete(input: &str, expected_suggestions: Option<&[&str]>) {
+ bootstrap::bootstrap();
+ let _guard = chdir_composer_checkout();
+
+ let mut input: Vec<&str> = input.split(' ').collect();
+ let command_name = input.remove(0);
+ // PHP: $this->getApplication()->get($commandName)
+ let application = ApplicationHandle::new("Composer".to_string(), "".to_string()).unwrap();
+ let base = application.__base_application();
+ let command = {
+ let mut app_ref = base.borrow_mut();
+ let app_dyn: &mut dyn shirabe_external_packages::symfony::console::application::Application =
+ &mut *app_ref;
+ let app = app_dyn
+ .as_any_mut()
+ .downcast_mut::<Application>()
+ .expect("ApplicationHandle wraps the shirabe Application");
+ app.get(command_name).unwrap()
+ };
+
+ let tester = CommandCompletionTester::new(command);
+ let suggestions = tester.complete(&input).unwrap();
+
+ let Some(expected_suggestions) = expected_suggestions else {
+ assert!(
+ suggestions.is_empty(),
+ "Expected no suggestions. Got \"{}\".",
+ suggestions.join("\", \"")
+ );
+ return;
+ };
+
+ let diff: Vec<&str> = expected_suggestions
+ .iter()
+ .copied()
+ .filter(|expected| !suggestions.iter().any(|s| s == expected))
+ .collect();
+ assert!(
+ diff.is_empty(),
+ "Suggestions must contain \"{}\". Got \"{}\".",
+ diff.join("\", \""),
+ suggestions.join("\", \"")
+ );
+}
+
+// PHP: $randomVendor = 'a/'; $installedPackages = [...]; $preferInstall = [...]
+const RANDOM_VENDOR: &str = "a/";
+const INSTALLED_PACKAGES: &[&str] = &["composer/semver", "psr/log"];
+const PREFER_INSTALL: &[&str] = &["dist", "source", "auto"];
+
+#[test]
+#[serial]
+fn test_complete_archive() {
+ assert_complete("archive ", Some(&[RANDOM_VENDOR]));
+}
+
+#[test]
+#[serial]
+fn test_complete_archive_package_prefix() {
+ assert_complete(
+ "archive symfony/http-",
+ Some(&["symfony/http-kernel", "symfony/http-foundation"]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_archive_format() {
+ assert_complete("archive --format ", Some(&["tar", "zip"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_create_project() {
+ assert_complete("create-project ", Some(&[RANDOM_VENDOR]));
+}
+
+#[test]
+#[serial]
+fn test_complete_create_project_prefer_install() {
+ assert_complete(
+ "create-project symfony/skeleton --prefer-install ",
+ Some(PREFER_INSTALL),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_depends() {
+ assert_complete("depends ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_why() {
+ assert_complete("why ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+#[ignore = "expects the Composer dev checkout's fully installed vendor/bin (phpstan, simple-phpunit, ...); the vendored checkout ships only jsonlint and validate-json"]
+fn test_complete_exec() {
+ assert_complete(
+ "exec ",
+ Some(&[
+ "composer",
+ "jsonlint",
+ "phpstan",
+ "phpstan.phar",
+ "simple-phpunit",
+ "validate-json",
+ ]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_browse() {
+ assert_complete("browse ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_home_h() {
+ assert_complete("home -H ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_init_require() {
+ assert_complete("init --require ", Some(&[RANDOM_VENDOR]));
+}
+
+#[test]
+#[serial]
+fn test_complete_init_require_dev() {
+ assert_complete(
+ "init --require-dev foo/bar --require-dev ",
+ Some(&[RANDOM_VENDOR]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_install_prefer_install() {
+ assert_complete("install --prefer-install ", Some(PREFER_INSTALL));
+}
+
+#[test]
+#[serial]
+fn test_complete_install() {
+ assert_complete("install ", None);
+}
+
+#[test]
+#[serial]
+fn test_complete_outdated() {
+ assert_complete("outdated ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_prohibits() {
+ assert_complete("prohibits ", Some(&[RANDOM_VENDOR]));
+}
+
+#[test]
+#[serial]
+fn test_complete_why_not() {
+ assert_complete("why-not symfony/http-ker", Some(&["symfony/http-kernel"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_reinstall_prefer_install() {
+ assert_complete("reinstall --prefer-install ", Some(PREFER_INSTALL));
+}
+
+#[test]
+#[serial]
+fn test_complete_reinstall() {
+ assert_complete("reinstall ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_remove() {
+ assert_complete("remove ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_require_prefer_install() {
+ assert_complete("require --prefer-install ", Some(PREFER_INSTALL));
+}
+
+#[test]
+#[serial]
+fn test_complete_require() {
+ assert_complete("require ", Some(&[RANDOM_VENDOR]));
+}
+
+#[test]
+#[serial]
+fn test_complete_require_dev_package_prefix() {
+ assert_complete(
+ "require --dev symfony/http-",
+ Some(&["symfony/http-kernel", "symfony/http-foundation"]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_run_script() {
+ assert_complete("run-script ", Some(&["compile", "test", "phpstan"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_run_script_args() {
+ assert_complete("run-script test ", None);
+}
+
+#[test]
+#[serial]
+fn test_complete_search_format() {
+ assert_complete("search --format ", Some(&["text", "json"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_show_format() {
+ assert_complete("show --format ", Some(&["text", "json"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_info() {
+ assert_complete("info ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_suggests() {
+ assert_complete("suggests ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_update_prefer_install() {
+ assert_complete("update --prefer-install ", Some(PREFER_INSTALL));
+}
+
+#[test]
+#[serial]
+fn test_complete_update() {
+ assert_complete("update ", Some(INSTALLED_PACKAGES));
+}
+
+#[test]
+#[serial]
+fn test_complete_config_list() {
+ assert_complete("config --list ", None);
+}
+
+#[test]
+#[serial]
+fn test_complete_config_editor() {
+ assert_complete("config --editor ", None);
+}
+
+#[test]
+#[serial]
+fn test_complete_config_auth() {
+ assert_complete("config --auth ", None);
+}
+
+#[test]
+#[serial]
+fn test_complete_config() {
+ assert_complete(
+ "config ",
+ Some(&[
+ "bin-compat",
+ "extra",
+ "extra.branch-alias",
+ "home",
+ "name",
+ "repositories",
+ "repositories.packagist.org",
+ "suggest",
+ "suggest.ext-zip",
+ "type",
+ "version",
+ ]),
+ );
+}
+
+// global setting
+#[test]
+#[serial]
+fn test_complete_config_bin() {
+ assert_complete("config bin", Some(&["bin-dir"]));
+}
+
+// existing package-property
+#[test]
+#[serial]
+fn test_complete_config_nam() {
+ assert_complete("config nam", Some(&["name"]));
+}
+
+// non-existing package-property
+#[test]
+#[serial]
+fn test_complete_config_ver() {
+ assert_complete("config ver", Some(&["version"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_config_repo() {
+ assert_complete(
+ "config repo",
+ Some(&["repositories", "repositories.packagist.org"]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_config_repositories_dot() {
+ assert_complete(
+ "config repositories.",
+ Some(&["repositories.packagist.org"]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_config_sug() {
+ assert_complete("config sug", Some(&["suggest", "suggest.ext-zip"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_config_suggest_ext() {
+ assert_complete("config suggest.ext-", Some(&["suggest.ext-zip"]));
+}
+
+#[test]
+#[serial]
+fn test_complete_config_ext() {
+ assert_complete(
+ "config ext",
+ Some(&["extra", "extra.branch-alias", "extra.branch-alias.dev-main"]),
+ );
+}
+
+// as this test does not use a fixture (yet?), the completion
+// of setting authentication settings can have varying results
+// yield ['config http-basic.', […]];
+
+#[test]
+#[serial]
+fn test_complete_config_unset() {
+ assert_complete(
+ "config --unset ",
+ Some(&[
+ "extra",
+ "extra.branch-alias",
+ "extra.branch-alias.dev-main",
+ "name",
+ "suggest",
+ "suggest.ext-zip",
+ "type",
+ ]),
+ );
+}
+
+// global setting
#[test]
-#[ignore = "CommandCompletionTester (Symfony Console test helper) is not ported, and the test only works inside the Composer dev checkout (its own composer.json/lock + installed vendor dir, plus live Packagist queries for package-name suggestions)"]
-fn test_complete() {
- // TODO(phase-d): two blockers. (1) CommandCompletionTester (Symfony Console test helper
- // driving the `|_complete` command) is not implemented in the shirabe-external-packages
- // console port, so the data-provider-driven completion test has no harness to run against.
- // (2) The PHP test's expected suggestions come from the environment it runs in: the Composer
- // dev checkout's own composer.json/composer.lock and installed vendor packages (e.g.
- // `depends ` -> composer/semver, psr/log; `run-script ` -> compile/test/phpstan) and live
- // Packagist API queries for package-name completion (e.g. `archive symfony/http-`). The port
- // has no equivalent fixture environment, so even with a tester the data sets could not be
- // reproduced without altering expected values.
- todo!()
+#[serial]
+fn test_complete_config_unset_bin_dir() {
+ assert_complete("config --unset bin-dir", None);
}
+
+// existing package-property
+#[test]
+#[serial]
+fn test_complete_config_unset_nam() {
+ assert_complete("config --unset nam", Some(&["name"]));
+}
+
+// non-existing package-property
+#[test]
+#[serial]
+fn test_complete_config_unset_version() {
+ assert_complete("config --unset version", None);
+}
+
+#[test]
+#[serial]
+fn test_complete_config_unset_extra_dot() {
+ assert_complete(
+ "config --unset extra.",
+ Some(&["extra.branch-alias", "extra.branch-alias.dev-main"]),
+ );
+}
+
+// as this test does not use a fixture (yet?), the completion
+// of unsetting authentication settings can have varying results
+// yield ['config --unset http-basic.', […]];
+
+#[test]
+#[serial]
+fn test_complete_config_global() {
+ assert_complete(
+ "config --global ",
+ Some(&[
+ "bin-compat",
+ "home",
+ "repositories",
+ "repositories.packagist.org",
+ ]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_config_global_repo() {
+ assert_complete(
+ "config --global repo",
+ Some(&["repositories", "repositories.packagist.org"]),
+ );
+}
+
+#[test]
+#[serial]
+fn test_complete_config_global_repositories_dot() {
+ assert_complete(
+ "config --global repositories.",
+ Some(&["repositories.packagist.org"]),
+ );
+}
+
+// as this test does not use a fixture (yet?), the completion
+// of unsetting global settings can have varying results
+// yield ['config --global --unset ', null];
+
+// as this test does not use a fixture (yet?), the completion of
+// unsetting global authentication settings can have varying results
+// yield ['config --global --unset http-basic.', […]];