From 69b35b0c4d8cb583b389973b096f9dd2dee695c6 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 2 Aug 2026 11:41:49 +0900 Subject: 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 --- crates/shirabe/tests/completion_functional_test.rs | 499 ++++++++++++++++++++- 1 file changed, 487 insertions(+), 12 deletions(-) (limited to 'crates/shirabe/tests/completion_functional_test.rs') 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::() + .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.', […]]; -- cgit v1.3.1