diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-26 02:23:58 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-26 02:23:58 +0900 |
| commit | 6c89c720d9e4728c288e3c66ffb7dcb4cba5121c (patch) | |
| tree | 4697f897302425df1b57eb87bf9441308530d0bf /crates/shirabe | |
| parent | 4bd544608e4934fff4463fd6b104d06162a51ba0 (diff) | |
| download | php-shirabe-6c89c720d9e4728c288e3c66ffb7dcb4cba5121c.tar.gz php-shirabe-6c89c720d9e4728c288e3c66ffb7dcb4cba5121c.tar.zst php-shirabe-6c89c720d9e4728c288e3c66ffb7dcb4cba5121c.zip | |
test(command): port show/remove/global/audit/check-platform-reqs/status/self-update/validate tests
Faithfully port the remaining stubbed command test bodies from their PHP
counterparts (expected values verbatim). Newly passing: remove (6), global (2),
check-platform-reqs (1), status (1), audit (1), self-update (1).
Tests whose ported bodies reach a genuine unported src path keep faithful bodies
but stay #[ignore] with precise reasons. Dominant blockers surfaced:
- ShowCommand::configure stub (empty set_definition) blocks all 42 show tests
- Composer-handle RefCell re-entrancy in the Installer (composer.rs:500) and
Factory::create_composer (composer.rs:446) paths
- check_platform_reqs nests InstalledRepository in InstalledRepository
- audit's non-locked branch (audit_command.rs:285) and write_error3 re-entrancy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe')
| -rw-r--r-- | crates/shirabe/tests/command/audit_command_test.rs | 114 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/check_platform_reqs_command_test.rs | 172 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/global_command_test.rs | 369 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/remove_command_test.rs | 957 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/self_update_command_test.rs | 54 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/show_command_test.rs | 1490 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/validate_command_test.rs | 30 |
7 files changed, 3032 insertions, 154 deletions
diff --git a/crates/shirabe/tests/command/audit_command_test.rs b/crates/shirabe/tests/command/audit_command_test.rs index 7d585d9..bccc7c2 100644 --- a/crates/shirabe/tests/command/audit_command_test.rs +++ b/crates/shirabe/tests/command/audit_command_test.rs @@ -1,25 +1,123 @@ //! ref: composer/tests/Composer/Test/Command/AuditCommandTest.php +use crate::test_case::{ + RunOptions, create_composer_lock, create_installed_json, get_application_tester, get_package, + init_temp_composer, +}; +use serial_test::serial; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; + #[test] -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "todo!() at crates/shirabe/src/command/audit_command.rs:285 (AuditCommand::get_packages non-locked branch: installed-repo conversion unported)"] fn test_successful_response_code_when_no_packages_are_required() { - todo!() + let tear_down = init_temp_composer(None, None, None, true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("audit"))], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, app_tester.get_status_code()); + assert_eq!( + "No packages - skipping audit.", + app_tester.get_display().trim() + ); + + drop(tear_down); } #[test] -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[serial] fn test_error_auditing_lock_file_when_it_is_missing() { - todo!() + let tear_down = init_temp_composer(None, None, None, true); + let packages: Vec<PackageInterfaceHandle> = vec![get_package("dummy/pkg", "1.0.0")]; + create_installed_json(&packages, &[], true); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("audit")), + (PhpMixed::from("--locked"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .expect_err("expected UnexpectedValueException"); + assert!( + err.to_string().contains( + "Valid composer.json and composer.lock files are required to run this command with --locked" + ), + "unexpected error: {err}" + ); + + drop(tear_down); } #[test] -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "'RefCell already mutably borrowed' panic at crates/shirabe/src/io/io_interface.rs:215 (write_error3) in the locked-audit advisory-fetch path; not in target file"] fn test_audit_package_with_no_security_vulnerabilities() { - todo!() + let tear_down = init_temp_composer(None, None, None, true); + let packages: Vec<PackageInterfaceHandle> = vec![get_package("dummy/pkg", "1.0.0")]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("audit")), + (PhpMixed::from("--locked"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + + assert!( + app_tester + .get_display() + .trim() + .contains("No security vulnerability advisories found."), + "unexpected display: {}", + app_tester.get_display() + ); + + drop(tear_down); } #[test] -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "todo!() at crates/shirabe/src/command/audit_command.rs:285 (AuditCommand::get_packages non-locked branch: installed-repo conversion unported)"] fn test_audit_package_with_no_dev_option_passed() { - todo!() + let tear_down = init_temp_composer(None, None, None, true); + let dev_package: Vec<PackageInterfaceHandle> = vec![get_package("dummy/pkg", "1.0.0")]; + create_installed_json(&[], &dev_package, true); + create_composer_lock(&[], &dev_package); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("audit")), + (PhpMixed::from("--no-dev"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + + assert!( + app_tester + .get_display() + .trim() + .contains("No packages - skipping audit."), + "unexpected display: {}", + app_tester.get_display() + ); + + drop(tear_down); } diff --git a/crates/shirabe/tests/command/check_platform_reqs_command_test.rs b/crates/shirabe/tests/command/check_platform_reqs_command_test.rs index 47f89b9..1e19259 100644 --- a/crates/shirabe/tests/command/check_platform_reqs_command_test.rs +++ b/crates/shirabe/tests/command/check_platform_reqs_command_test.rs @@ -1,25 +1,173 @@ //! ref: composer/tests/Composer/Test/Command/CheckPlatformReqsCommandTest.php +use crate::test_case::{ + RunOptions, create_composer_lock, create_installed_json, get_application_tester, + get_complete_package, init_temp_composer, +}; +use serial_test::serial; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; + +/// ref: CheckPlatformReqsCommandTest::testPlatformReqsAreSatisfied (data provider rolled into one body). +fn run_platform_reqs_are_satisfied_case( + composer_json: serde_json::Value, + command: &[(&str, PhpMixed)], + expected: &str, + lock: bool, +) { + let tear_down = init_temp_composer(Some(&composer_json), None, None, false); + + let packages: Vec<PackageInterfaceHandle> = + vec![get_complete_package("ext-foobar", "2.3.4").into()]; + let dev_packages: Vec<PackageInterfaceHandle> = + vec![get_complete_package("ext-barbaz", "2.3.4.5").into()]; + + create_installed_json(&packages, &dev_packages, true); + + if lock { + create_composer_lock(&packages, &dev_packages); + } + + let mut input: Vec<(PhpMixed, PhpMixed)> = vec![( + PhpMixed::from("command"), + PhpMixed::from("check-platform-reqs"), + )]; + for (k, v) in command { + input.push((PhpMixed::from(*k), v.clone())); + } + + let mut app_tester = get_application_tester(); + let status_code = app_tester.run(input, RunOptions::default()).unwrap(); + + // assertCommandIsSuccessful + assert_eq!(0, status_code, "{}", app_tester.get_display()); + assert_eq!(expected.trim(), app_tester.get_display().trim()); + + drop(tear_down); +} + #[test] -#[ignore = "needs create_installed_json / create_composer_lock helpers (InstalledFilesystemRepository \ - / Locker::set_lock_data), and require_composer -> Factory reaches ProcessExecutor (git) \ - -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] +#[serial] +#[ignore = "src porting bug: check_platform_reqs_command.rs:288-294 wraps the existing \ + InstalledRepository inside a SECOND InstalledRepository (instead of PHP line 89 \ + `$installedRepo->addRepository(new PlatformRepository(...))`), tripping the \ + add_repository assertion at installed_repository.rs:367 (\"An InstalledRepository can \ + not contain a repository of type installed repo ...\"). Cannot fix from test files."] fn test_platform_reqs_are_satisfied() { - todo!() + // 'Disables checking of require-dev packages requirements.' + run_platform_reqs_are_satisfied_case( + serde_json::json!({ + "require": { "ext-foobar": "^2.0" }, + "require-dev": { "ext-barbaz": "~4.0" }, + }), + &[("--no-dev", PhpMixed::from(true))], + "Checking non-dev platform requirements for packages in the vendor dir +ext-foobar 2.3.4 success", + true, + ); + + // 'Checks requirements only from the lock file, not from installed packages.' + run_platform_reqs_are_satisfied_case( + serde_json::json!({ + "require": { "ext-foobar": "^2.3" }, + "require-dev": { "ext-barbaz": "~2.0" }, + }), + &[("--lock", PhpMixed::from(true))], + "Checking platform requirements using the lock file\next-barbaz 2.3.4.5 success \next-foobar 2.3.4 success", + true, + ); } #[test] -#[ignore = "check-platform-reqs calls require_composer -> Factory, reaching ProcessExecutor (git) \ - -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2)) before \ - the missing-lockfile LogicException can be thrown"] +#[serial] fn test_exception_thrown_if_no_lockfile_found() { - todo!() + let tear_down = init_temp_composer(Some(&serde_json::json!({})), None, None, false); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + vec![( + PhpMixed::from("command"), + PhpMixed::from("check-platform-reqs"), + )], + RunOptions::default(), + ) + .expect_err("expected LogicException for missing lockfile"); + assert!( + err.to_string() + .contains("No lockfile found. Unable to read locked packages"), + "got: {}", + err + ); + + drop(tear_down); } #[test] -#[ignore = "needs create_installed_json / create_composer_lock helpers (InstalledFilesystemRepository \ - / Locker::set_lock_data), and require_composer -> Factory reaches ProcessExecutor (git) \ - -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] +#[serial] +#[ignore = "src porting bug: check_platform_reqs_command.rs:288-294 wraps the existing \ + InstalledRepository inside a SECOND InstalledRepository (instead of PHP line 89 \ + `$installedRepo->addRepository(new PlatformRepository(...))`), tripping the \ + add_repository assertion at installed_repository.rs:367 (\"An InstalledRepository can \ + not contain a repository of type installed repo ...\"). Cannot fix from test files."] fn test_failed_platform_requirement() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": { "ext-foobar": "^0.3" }, + "require-dev": { "ext-barbaz": "^2.3" }, + })), + None, + None, + false, + ); + + let packages: Vec<PackageInterfaceHandle> = + vec![get_complete_package("ext-foobar", "2.3.4").into()]; + let dev_packages: Vec<PackageInterfaceHandle> = + vec![get_complete_package("ext-barbaz", "2.3.4.5").into()]; + + create_installed_json(&packages, &dev_packages, true); + + create_composer_lock(&packages, &dev_packages); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![ + ( + PhpMixed::from("command"), + PhpMixed::from("check-platform-reqs"), + ), + (PhpMixed::from("--format"), PhpMixed::from("json")), + ], + RunOptions::default(), + ) + .unwrap(); + + let expected = r#"Checking platform requirements for packages in the vendor dir +[ + { + "name": "ext-barbaz", + "version": "2.3.4.5", + "status": "success", + "failed_requirement": null, + "provider": null + }, + { + "name": "ext-foobar", + "version": "2.3.4", + "status": "failed", + "failed_requirement": { + "source": "__root__", + "type": "requires", + "target": "ext-foobar", + "constraint": "^0.3" + }, + "provider": null + } +]"#; + + assert_eq!(expected.trim(), app_tester.get_display().trim()); + + drop(tear_down); } diff --git a/crates/shirabe/tests/command/global_command_test.rs b/crates/shirabe/tests/command/global_command_test.rs index 0b62150..6eec017 100644 --- a/crates/shirabe/tests/command/global_command_test.rs +++ b/crates/shirabe/tests/command/global_command_test.rs @@ -1,80 +1,385 @@ //! ref: composer/tests/Composer/Test/Command/GlobalCommandTest.php +use crate::test_case::{ + RunOptions, create_composer_lock, create_installed_json, get_application_tester, + get_complete_package, get_package, init_temp_composer, +}; +use serial_test::serial; use shirabe::util::platform::Platform; +use shirabe_php_shim::PhpMixed; +use std::path::PathBuf; +use tempfile::TempDir; -fn tear_down() { - Platform::clear_env("COMPOSER_HOME"); - Platform::clear_env("COMPOSER"); -} - -struct TearDown; +/// ref: GlobalCommandTest::tearDown (the COMPOSER part; COMPOSER_HOME is restored by the +/// `init_temp_composer` guard). +struct EnvGuard; -impl Drop for TearDown { +impl Drop for EnvGuard { fn drop(&mut self) { - tear_down(); + Platform::clear_env("COMPOSER"); } } +/// ref: TestCase::getUniqueTmpDirectory followed by `chdir($dir)`. +/// +/// Returns the temp dir guard (kept alive by the caller so it is removed at end of scope). +fn chdir_unique_tmp_directory() -> TempDir { + let test_dir = TempDir::new().unwrap(); + std::env::set_current_dir(test_dir.path()).unwrap(); + test_dir +} + #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/getUniqueTmpDirectory harness (not implemented)"] +#[serial] +#[ignore = "GlobalCommand::run proxies to Application::run via todo!() at crates/shirabe/src/command/global_command.rs:190 (pending shared Application handle)"] fn test_global() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let script = r#"@php -r "echo 'COMPOSER SCRIPT OUTPUT: '.getenv('COMPOSER') . PHP_EOL;""#; + let fake_composer = "TMP_COMPOSER.JSON"; + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "scripts": { + "test-script": script, + }, + })), + None, + None, + true, + ); + let composer_home: PathBuf = tear_down.working_dir(); + let composer_home_str = composer_home.to_string_lossy().to_string(); - todo!() + Platform::put_env("COMPOSER_HOME", &composer_home_str); + Platform::put_env("COMPOSER", fake_composer); + + let _test_dir = chdir_unique_tmp_directory(); + + let mut app_tester = get_application_tester(); + let _ = app_tester.run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + ( + PhpMixed::from("command-name"), + PhpMixed::from("test-script"), + ), + (PhpMixed::from("--no-interaction"), PhpMixed::from(true)), + ], + RunOptions::default(), + ); + + let display = app_tester.get_display(); + + assert_eq!( + format!( + "Changed current directory to {}\nCOMPOSER SCRIPT OUTPUT: \n", + composer_home_str + ), + display + ); + + drop(tear_down); } #[test] -#[ignore = "requires ApplicationTester and TestCase::getUniqueTmpDirectory harness (not implemented)"] +#[serial] fn test_cannot_create_home() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let dir = TempDir::new().unwrap(); + let filename = dir.path().join("file"); + std::fs::write(&filename, "").unwrap(); + let filename_str = filename.to_string_lossy().to_string(); - todo!() + Platform::put_env("COMPOSER_HOME", &filename_str); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + ( + PhpMixed::from("command-name"), + PhpMixed::from("test-script"), + ), + (PhpMixed::from("--no-interaction"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .expect_err("expected RuntimeException for a non-directory COMPOSER_HOME"); + + assert!( + err.to_string() + .contains(&format!("{} exists and is not a directory.", filename_str)), + "got: {}", + err + ); + + Platform::clear_env("COMPOSER_HOME"); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createInstalledJson harness (not implemented)"] +#[serial] +#[ignore = "GlobalCommand::run proxies to Application::run via todo!() at crates/shirabe/src/command/global_command.rs:190 (pending shared Application handle)"] fn test_global_show() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/global-tool", "version": "1.0.0"}, + ], + }, + }, + "require": { + "vendor/global-tool": "1.0.0", + }, + })), + None, + None, + true, + ); + let composer_home_str = tear_down.working_dir().to_string_lossy().to_string(); + + let pkg = get_complete_package("vendor/global-tool", "1.0.0"); + pkg.set_description("A globally installed tool".to_string()); + create_installed_json(&[pkg.into()], &[], true); - todo!() + Platform::put_env("COMPOSER_HOME", &composer_home_str); + + let _test_dir = chdir_unique_tmp_directory(); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let _ = app_tester.run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + (PhpMixed::from("command-name"), PhpMixed::from("show")), + ], + RunOptions::default(), + ); + + let output = app_tester.get_display(); + assert!(output.contains("vendor/global-tool"), "got: {}", output); + assert!(output.contains("1.0.0"), "got: {}", output); + + drop(tear_down); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createInstalledJson harness (not implemented)"] +#[serial] +#[ignore = "GlobalCommand::run proxies to Application::run via todo!() at crates/shirabe/src/command/global_command.rs:190 (pending shared Application handle)"] fn test_global_show_without_packages() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let tear_down = init_temp_composer(None, None, None, true); + let composer_home_str = tear_down.working_dir().to_string_lossy().to_string(); + + create_installed_json(&[], &[], true); - todo!() + Platform::put_env("COMPOSER_HOME", &composer_home_str); + + let _test_dir = chdir_unique_tmp_directory(); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + (PhpMixed::from("command-name"), PhpMixed::from("show")), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, status_code); + + drop(tear_down); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] +#[ignore = "GlobalCommand::run proxies to Application::run via todo!() at crates/shirabe/src/command/global_command.rs:190 (pending shared Application handle)"] fn test_global_require() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + { + "name": "vendor/required-pkg", + "version": "2.0.0", + "dist": {"type": "file", "url": file!()}, + }, + ], + }, + }, + })), + None, + None, + true, + ); + let composer_home_str = tear_down.working_dir().to_string_lossy().to_string(); + + Platform::put_env("COMPOSER_HOME", &composer_home_str); - todo!() + let _test_dir = chdir_unique_tmp_directory(); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + (PhpMixed::from("command-name"), PhpMixed::from("require")), + ( + PhpMixed::from("packages"), + PhpMixed::List(vec![PhpMixed::from("vendor/required-pkg:2.0.0")]), + ), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, status_code); + assert!( + app_tester + .get_display() + .contains("Installing vendor/required-pkg"), + "got: {}", + app_tester.get_display() + ); + + drop(tear_down); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createInstalledJson/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "GlobalCommand::run proxies to Application::run via todo!() at crates/shirabe/src/command/global_command.rs:190 (pending shared Application handle)"] fn test_global_update() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/pkg", "version": "1.0.0"}, + ], + }, + }, + "require": { + "vendor/pkg": "1.0.0", + }, + })), + None, + None, + true, + ); + let composer_home_str = tear_down.working_dir().to_string_lossy().to_string(); + + let pkg = get_package("vendor/pkg", "1.0.0"); + create_installed_json(&[pkg.clone()], &[], true); + create_composer_lock(&[pkg], &[]); + + Platform::put_env("COMPOSER_HOME", &composer_home_str); - todo!() + let _test_dir = chdir_unique_tmp_directory(); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + (PhpMixed::from("command-name"), PhpMixed::from("update")), + ], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!(0, status_code); + + drop(tear_down); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] +#[ignore = "GlobalCommand::run proxies to Application::run via todo!() at crates/shirabe/src/command/global_command.rs:190 (pending shared Application handle)"] fn test_global_changes_directory() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "name": "test/global", + })), + None, + None, + true, + ); + let composer_home_str = tear_down.working_dir().to_string_lossy().to_string(); + + Platform::put_env("COMPOSER_HOME", &composer_home_str); + + let _test_dir = chdir_unique_tmp_directory(); - todo!() + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let _ = app_tester.run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("global")), + (PhpMixed::from("command-name"), PhpMixed::from("config")), + (PhpMixed::from("setting-key"), PhpMixed::from("name")), + ], + RunOptions::default(), + ); + + let output = app_tester.get_display(); + assert!( + output.contains(&format!( + "Changed current directory to {}", + composer_home_str + )), + "got: {}", + output + ); + + drop(tear_down); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] fn test_global_missing_command_name() { - let _tear_down = TearDown; + let _env_guard = EnvGuard; + + let tear_down = init_temp_composer(None, None, None, true); + let composer_home_str = tear_down.working_dir().to_string_lossy().to_string(); + + Platform::put_env("COMPOSER_HOME", &composer_home_str); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let err = app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("global"))], + RunOptions::default(), + ) + .expect_err("expected a RuntimeException for the missing command-name argument"); + + assert!( + err.to_string() + .contains("Not enough arguments (missing: \"command-name\")"), + "got: {}", + err + ); - todo!() + drop(tear_down); } diff --git a/crates/shirabe/tests/command/remove_command_test.rs b/crates/shirabe/tests/command/remove_command_test.rs index 2ea8c88..3fc125b 100644 --- a/crates/shirabe/tests/command/remove_command_test.rs +++ b/crates/shirabe/tests/command/remove_command_test.rs @@ -1,97 +1,990 @@ //! ref: composer/tests/Composer/Test/Command/RemoveCommandTest.php -#[ignore = "missing TestCase::get_application_tester (ApplicationTester) infrastructure"] +use crate::test_case::{ + RunOptions, create_composer_lock, create_installed_json, get_application_tester, get_package, + init_temp_composer, +}; +use indexmap::IndexMap; +use serial_test::serial; +use shirabe::json::JsonFile; +use shirabe::package::Link; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; +use shirabe_semver::constraint::{AnyConstraint, MatchAllConstraint}; + +// Symfony\Component\Console\Command\Command exit codes. +const SUCCESS: i32 = 0; +const FAILURE: i32 = 1; +const INVALID: i32 = 2; + +fn input(pairs: Vec<(&str, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + pairs + .into_iter() + .map(|(k, v)| (PhpMixed::from(k), v)) + .collect() +} + +/// Read a JSON file in the CWD and decode it to a `serde_json::Value`. +fn read_json_file(path: &str) -> serde_json::Value { + let mut json = JsonFile::new(path.to_string(), None, None).unwrap(); + let read = json.read().unwrap(); + serde_json::from_str(&JsonFile::encode(&read)).unwrap() +} + +/// Mirrors PHPUnit's assertEmpty for a decoded JSON document (empty object/array/null). +fn is_empty_json(value: &serde_json::Value) -> bool { + match value { + serde_json::Value::Null => true, + serde_json::Value::Object(map) => map.is_empty(), + serde_json::Value::Array(list) => list.is_empty(), + _ => false, + } +} + #[test] +#[serial] fn test_exception_running_with_no_remove_packages() { - todo!() + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + input(vec![("command", PhpMixed::from("remove"))]), + RunOptions::default(), + ) + .expect_err("expected InvalidArgumentException for missing packages argument"); + assert!( + err.to_string() + .contains(r#"Not enough arguments (missing: "packages")."#), + "got: {}", + err + ); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] #[test] +#[serial] fn test_exception_when_running_unused_without_lock_file() { - todo!() + let tear_down = init_temp_composer(None, None, None, true); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("--unused", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .expect_err("expected UnexpectedValueException when --unused without lock file"); + assert!( + err.to_string() + .contains("A valid composer.lock file is required to run this command with --unused"), + "got: {}", + err + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_warning_when_removing_non_existent_package() { - todo!() + let tear_down = init_temp_composer(None, None, None, true); + create_installed_json(&[], &[], true); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ( + "packages", + PhpMixed::List(vec![PhpMixed::from("vendor1/package1")]), + ), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert!( + app_tester.get_display().trim().starts_with( + "<warning>vendor1/package1 is not required in your composer.json and has not been removed</warning>" + ), + "got: {}", + app_tester.get_display() + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] #[test] +#[serial] fn test_warning_when_removing_package_from_wrong_type() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": { "root/req": "1.*" }, + })), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--dev", PhpMixed::from(true)), + ("--no-update", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert_eq!( + "<warning>root/req could not be found in require-dev but it is present in require</warning> +./composer.json has been updated", + app_tester.get_display().trim() + ); + assert_eq!( + serde_json::json!({ "require": { "root/req": "1.*" } }), + read_json_file("./composer.json") + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] #[test] +#[serial] fn test_warning_when_removing_package_with_deprecated_dependencies_flag() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": { "root/req": "1.*" }, + })), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--update-with-dependencies", PhpMixed::from(true)), + ("--no-update", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert_eq!( + "<warning>You are using the deprecated option \"update-with-dependencies\". This is now default behaviour. The --no-update-with-dependencies option can be used to remove a package without its dependencies.</warning> +./composer.json has been updated", + app_tester.get_display().trim() + ); + assert!( + is_empty_json(&read_json_file("./composer.json")), + "expected composer.json to be empty, got: {}", + read_json_file("./composer.json") + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] #[test] +#[serial] fn test_message_output_when_no_unused_packages_to_remove() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "require": {"nested/req": "^1"}}, + {"name": "nested/req", "version": "1.1.0"}, + ], + }, + }, + "require": { "root/req": "1.*" }, + })), + None, + None, + true, + ); + + let required_package = get_package("root/req", "1.0.0"); + let mut requires = IndexMap::new(); + requires.insert( + "nested/req".to_string(), + Link::new( + "root/req".to_string(), + "nested/req".to_string(), + AnyConstraint::MatchAll(MatchAllConstraint::new(None)), + Some(Link::TYPE_REQUIRE.to_string()), + "^1".to_string(), + ), + ); + required_package.__set_requires(requires); + let nested_package = get_package("nested/req", "1.1.0"); + + let packages: Vec<PackageInterfaceHandle> = + vec![required_package.clone(), nested_package.clone()]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("--unused", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert_eq!( + "No unused packages to remove", + app_tester.get_display().trim() + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_unused_package() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0"}, + {"name": "not/req", "version": "1.0.0"}, + ], + }, + }, + "require": { "root/req": "1.*" }, + })), + None, + None, + true, + ); + + let required_package = get_package("root/req", "1.0.0"); + let extraneous_package = get_package("not/req", "1.0.0"); + + create_installed_json(&[required_package.clone()], &[], true); + create_composer_lock(&[required_package.clone(), extraneous_package.clone()], &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("--unused", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert!( + app_tester.get_display().starts_with( + "<warning>not/req is not required in your composer.json and has not been removed</warning>" + ), + "got: {}", + app_tester.get_display() + ); + assert!( + app_tester + .get_display() + .contains("Running composer update not/req"), + "got: {}", + app_tester.get_display() + ); + assert!( + app_tester + .get_display() + .contains("- Removing not/req (1.0.0)"), + "got: {}", + app_tester.get_display() + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_package_by_name() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "type": "metapackage"}, + {"name": "root/another", "version": "1.0.0", "type": "metapackage"}, + ], + }, + }, + "require": { "root/req": "1.*", "root/another": "1.*" }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + let root_another_package = get_package("root/another", "1.0.0"); + // Set as a metapackage so that we can do the whole post-remove update & install process without + // Composer trying to download them (DownloadManager::getDownloaderForPackage). + root_req_package.__set_type("metapackage".to_string()); + root_another_package.__set_type("metapackage".to_string()); + + let packages: Vec<PackageInterfaceHandle> = + vec![root_req_package.clone(), root_another_package.clone()]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + let display = app_tester.get_display(); + let trimmed = display.trim(); + assert!( + trimmed.starts_with("./composer.json has been updated"), + "got: {}", + display + ); + assert!( + trimmed.contains("Running composer update root/req"), + "got: {}", + display + ); + assert!( + trimmed.contains("Lock file operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert!( + trimmed.contains("- Removing root/req (1.0.0)"), + "got: {}", + display + ); + assert!( + trimmed.contains("Package operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert_eq!( + serde_json::json!({ "root/another": "1.*" }), + read_json_file("./composer.json")["require"] + ); + assert_eq!( + serde_json::json!([{"name": "root/another", "version": "1.0.0", "type": "metapackage"}]), + read_json_file("./composer.lock")["packages"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_package_by_name_with_dry_run() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "type": "metapackage"}, + {"name": "root/another", "version": "1.0.0", "type": "metapackage"}, + ], + }, + }, + "require": { "root/req": "1.*", "root/another": "1.*" }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + let root_another_package = get_package("root/another", "1.0.0"); + root_req_package.__set_type("metapackage".to_string()); + root_another_package.__set_type("metapackage".to_string()); + + let packages: Vec<PackageInterfaceHandle> = + vec![root_req_package.clone(), root_another_package.clone()]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + let display = app_tester.get_display(); + let trimmed = display.trim(); + assert!( + trimmed.contains("./composer.json has been updated"), + "got: {}", + display + ); + assert!( + trimmed.contains("Running composer update root/req"), + "got: {}", + display + ); + assert!( + trimmed.contains("Lock file operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert!( + trimmed.contains("- Removing root/req (1.0.0)"), + "got: {}", + display + ); + assert!( + trimmed.contains("Package operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert_eq!( + serde_json::json!({ "root/req": "1.*", "root/another": "1.*" }), + read_json_file("./composer.json")["require"] + ); + assert_eq!( + serde_json::json!([ + {"name": "root/another", "version": "1.0.0", "type": "metapackage"}, + {"name": "root/req", "version": "1.0.0", "type": "metapackage"}, + ]), + read_json_file("./composer.lock")["packages"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_allowed_plugin_package_with_no_other_allowed_plugins() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "type": "metapackage"}, + {"name": "root/another", "version": "1.0.0", "type": "metapackage"}, + ], + }, + }, + "require": { "root/req": "1.*", "root/another": "1.*" }, + "config": { "allow-plugins": { "root/req": true } }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + let root_another_package = get_package("root/another", "1.0.0"); + root_req_package.__set_type("metapackage".to_string()); + root_another_package.__set_type("metapackage".to_string()); + + let packages: Vec<PackageInterfaceHandle> = + vec![root_req_package.clone(), root_another_package.clone()]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert_eq!( + serde_json::json!({ "root/another": "1.*" }), + read_json_file("./composer.json")["require"] + ); + assert!( + is_empty_json(&read_json_file("./composer.json")["config"]), + "expected config to be empty, got: {}", + read_json_file("./composer.json")["config"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_allowed_plugin_package_with_other_allowed_plugins() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "type": "metapackage"}, + {"name": "root/another", "version": "1.0.0", "type": "metapackage"}, + ], + }, + }, + "require": { "root/req": "1.*", "root/another": "1.*" }, + "config": { "allow-plugins": { "root/another": true, "root/req": true } }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + let root_another_package = get_package("root/another", "1.0.0"); + root_req_package.__set_type("metapackage".to_string()); + root_another_package.__set_type("metapackage".to_string()); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert_eq!( + serde_json::json!({ "root/another": "1.*" }), + read_json_file("./composer.json")["require"] + ); + assert_eq!( + serde_json::json!({ "allow-plugins": { "root/another": true } }), + read_json_file("./composer.json")["config"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_packages_by_vendor() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0"}, + {"name": "root/another", "version": "1.0.0"}, + {"name": "another/req", "version": "1.0.0"}, + ], + }, + }, + "require": { "root/req": "1.*", "root/another": "1.*", "another/req": "1.*" }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + let root_another_package = get_package("root/another", "1.0.0"); + let another_req_package = get_package("another/req", "1.0.0"); + + let packages: Vec<PackageInterfaceHandle> = vec![ + root_req_package.clone(), + root_another_package.clone(), + another_req_package.clone(), + ]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/*")])), + ("--no-install", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + let display = app_tester.get_display(); + assert!( + display + .trim() + .starts_with("./composer.json has been updated"), + "got: {}", + display + ); + assert!( + display.contains("Running composer update root/*"), + "got: {}", + display + ); + assert!( + display.contains("- Removing root/another (1.0.0)"), + "got: {}", + display + ); + assert!( + display.contains("- Removing root/req (1.0.0)"), + "got: {}", + display + ); + assert!(display.contains("Writing lock file"), "got: {}", display); + assert_eq!( + serde_json::json!({ "another/req": "1.*" }), + read_json_file("./composer.json")["require"] + ); + assert_eq!( + serde_json::json!([{"name": "another/req", "version": "1.0.0", "type": "library"}]), + read_json_file("./composer.lock")["packages"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_remove_packages_by_vendor_with_dry_run() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0"}, + {"name": "root/another", "version": "1.0.0"}, + {"name": "another/req", "version": "1.0.0"}, + ], + }, + }, + "require": { "root/req": "1.*", "root/another": "1.*", "another/req": "1.*" }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + let root_another_package = get_package("root/another", "1.0.0"); + let another_req_package = get_package("another/req", "1.0.0"); + + let packages: Vec<PackageInterfaceHandle> = vec![ + root_req_package.clone(), + root_another_package.clone(), + another_req_package.clone(), + ]; + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/*")])), + ("--dry-run", PhpMixed::from(true)), + ("--no-install", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, app_tester.get_status_code()); + assert_eq!( + "./composer.json has been updated +Running composer update root/* +Loading composer repositories with package information +Updating dependencies +Lock file operations: 0 installs, 0 updates, 2 removals + - Removing root/another (1.0.0) + - Removing root/req (1.0.0)", + app_tester.get_display().trim() + ); + assert!( + !app_tester.get_display().contains("Writing lock file"), + "got: {}", + app_tester.get_display() + ); + assert_eq!( + serde_json::json!({ "root/req": "1.*", "root/another": "1.*", "another/req": "1.*" }), + read_json_file("./composer.json")["require"] + ); + assert_eq!( + serde_json::json!([ + {"name": "another/req", "version": "1.0.0", "type": "library"}, + {"name": "root/another", "version": "1.0.0", "type": "library"}, + {"name": "root/req", "version": "1.0.0", "type": "library"}, + ]), + read_json_file("./composer.lock")["packages"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] #[test] +#[serial] fn test_warning_when_removing_packages_by_vendor_from_wrong_type() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": { "root/req": "1.*", "root/another": "1.*", "another/req": "1.*" }, + })), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/*")])), + ("--dev", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ("--no-update", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + assert_eq!( + "<warning>root/req could not be found in require-dev but it is present in require</warning> +<warning>root/another could not be found in require-dev but it is present in require</warning> +./composer.json has been updated", + app_tester.get_display().trim() + ); + assert_eq!( + serde_json::json!({ + "require": { "root/req": "1.*", "root/another": "1.*", "another/req": "1.*" } + }), + read_json_file("./composer.json") + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_package_still_present_error_when_no_install_flag_used() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": { "root/req": "1.*" }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + + create_installed_json(&[root_req_package.clone()], &[], true); + create_composer_lock(&[root_req_package.clone()], &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + ("--no-install", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(INVALID, status_code); + let display = app_tester.get_display(); + assert!( + display.contains("./composer.json has been updated"), + "got: {}", + display + ); + assert!( + display.contains("Lock file operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert!( + display.contains("- Removing root/req (1.0.0)"), + "got: {}", + display + ); + assert!(display.contains("Writing lock file"), "got: {}", display); + assert!( + display.contains( + "Removal failed, root/req is still present, it may be required by another package. See `composer why root/req`" + ), + "got: {}", + display + ); + assert!( + is_empty_json(&read_json_file("./composer.json")), + "expected composer.json to be empty, got: {}", + read_json_file("./composer.json") + ); + assert!( + is_empty_json(&read_json_file("./composer.lock")["packages"]), + "expected composer.lock packages to be empty, got: {}", + read_json_file("./composer.lock")["packages"] + ); + assert_eq!( + serde_json::json!([{ + "name": "root/req", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "type": "library", + "install-path": "../root/req" + }]), + read_json_file("./vendor/composer/installed.json")["packages"] + ); + + drop(tear_down); +} + +/// ref: RemoveCommandTest::testUpdateInheritedDependenciesFlagIsPassedToPostRemoveInstaller +/// (data provider provideInheritedDependenciesUpdateFlag). +fn run_update_inherited_dependencies_flag_case( + install_flag_name: &str, + expected_composer_update_command: &str, +) { + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "root/req", "version": "1.0.0", "type": "metapackage"}, + ], + }, + }, + "require": { "root/req": "1.*" }, + })), + None, + None, + true, + ); + let root_req_package = get_package("root/req", "1.0.0"); + root_req_package.__set_type("metapackage".to_string()); + + create_installed_json(&[root_req_package.clone()], &[], true); + create_composer_lock(&[root_req_package.clone()], &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("remove")), + ("packages", PhpMixed::List(vec![PhpMixed::from("root/req")])), + (install_flag_name, PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-interaction", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(SUCCESS, status_code); + let display = app_tester.get_display(); + assert!( + display.contains("./composer.json has been updated"), + "got: {}", + display + ); + assert!( + display.contains(expected_composer_update_command), + "got: {}", + display + ); + assert!( + display.contains("Package operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert!( + display.contains("- Removing root/req (1.0.0)"), + "got: {}", + display + ); + assert!(display.contains("Writing lock file"), "got: {}", display); + assert!( + display.contains("Lock file operations: 0 installs, 0 updates, 1 removal"), + "got: {}", + display + ); + assert!( + is_empty_json(&read_json_file("./composer.lock")["packages"]), + "expected composer.lock packages to be empty, got: {}", + read_json_file("./composer.lock")["packages"] + ); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "remove runs post-remove Installer; panics at crates/shirabe/src/composer.rs:500:36 'RefCell already mutably borrowed' (ComposerHandle::borrow during active borrow_mut) in unported install/update path"] #[test] +#[serial] fn test_update_inherited_dependencies_flag_is_passed_to_post_remove_installer() { - todo!() + // 'update with all dependencies' + run_update_inherited_dependencies_flag_case( + "--update-with-all-dependencies", + "Running composer update root/req --with-all-dependencies", + ); + // 'with all dependencies' + run_update_inherited_dependencies_flag_case( + "--with-all-dependencies", + "Running composer update root/req --with-all-dependencies", + ); + // 'no update with dependencies' + run_update_inherited_dependencies_flag_case( + "--no-update-with-dependencies", + "Running composer update root/req --with-dependencies", + ); } diff --git a/crates/shirabe/tests/command/self_update_command_test.rs b/crates/shirabe/tests/command/self_update_command_test.rs index 8e51580..094eb5e 100644 --- a/crates/shirabe/tests/command/self_update_command_test.rs +++ b/crates/shirabe/tests/command/self_update_command_test.rs @@ -1,39 +1,67 @@ //! ref: composer/tests/Composer/Test/Command/SelfUpdateCommandTest.php -/// Returns the path to the copied composer.phar used by the test bodies. -fn set_up() -> String { - // Depends on initTempComposer and the composer-test.phar fixture, neither ported yet. - todo!() +use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use serial_test::serial; +use shirabe_php_shim::PhpMixed; + +/// ref: SelfUpdateCommandTest::setUp (portable part: initTempComposer; the composer-test.phar copy +/// is omitted because the phar fixture and Symfony Process are not ported). +fn set_up() -> crate::test_case::TearDown { + init_temp_composer(None, None, None, true) } #[test] -#[ignore = "depends on initTempComposer, composer-test.phar fixture, and Symfony Process to spawn the phar; none ported"] +#[serial] +#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update'])` running composer-test.phar \ + over HTTP; requires Symfony Process and the composer-test.phar fixture, neither ported"] fn test_successful_update() { - let _phar = set_up(); + let _tear_down = set_up(); todo!() } #[test] -#[ignore = "depends on initTempComposer, composer-test.phar fixture, and Symfony Process to spawn the phar; none ported"] +#[serial] +#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update', '2.4.0'])` running \ + composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar \ + fixture, neither ported"] fn test_update_to_specific_version() { - let _phar = set_up(); + let _tear_down = set_up(); todo!() } #[test] -#[ignore = "depends on getApplicationTester (ApplicationTester) which is not ported"] +#[serial] fn test_update_with_invalid_option_throws_exception() { - let _phar = set_up(); + let _tear_down = set_up(); - todo!() + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("self-update")), + (PhpMixed::from("invalid-option"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .expect_err("expected InvalidArgumentException for the unknown argument"); + + assert!( + err.to_string() + .contains("The \"invalid-option\" argument does not exist."), + "expected error about unknown argument, got: {:?}", + err, + ); } #[test] -#[ignore = "depends on initTempComposer, composer-test.phar fixture, and Symfony Process to spawn the phar; none ported"] +#[serial] +#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update', $option])` running \ + composer-test.phar over HTTP (data provider: --stable/--preview/--snapshot); requires \ + Symfony Process and the composer-test.phar fixture, neither ported"] fn test_update_to_different_channel() { - let _phar = set_up(); + let _tear_down = set_up(); todo!() } diff --git a/crates/shirabe/tests/command/show_command_test.rs b/crates/shirabe/tests/command/show_command_test.rs index 56e8106..1ba3882 100644 --- a/crates/shirabe/tests/command/show_command_test.rs +++ b/crates/shirabe/tests/command/show_command_test.rs @@ -1,139 +1,1531 @@ //! ref: composer/tests/Composer/Test/Command/ShowCommandTest.php -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +use crate::test_case::{ + RunOptions, get_application_tester, get_complete_package, get_package, get_version_constraint, + init_temp_composer, +}; +use serial_test::serial; +use shirabe::package::Link; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe::repository::PlatformRepository; +use shirabe_php_shim::PhpMixed; + +/// Build a `Vec<(PhpMixed, PhpMixed)>` command input from `(key, value)` pairs. +fn input(pairs: Vec<(&str, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + pairs + .into_iter() + .map(|(k, v)| (PhpMixed::from(k), v)) + .collect() +} + +/// ref: ShowCommandTest::testShow composer.json fixture. +fn show_composer_json(requires: serde_json::Value) -> serde_json::Value { + serde_json::json!({ + "name": "root/pkg", + "version": "1.2.3", + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/package", "description": "generic description", "version": "v1.0.0"}, + + {"name": "outdated/major", "description": "outdated/major v1.0.0 description", "version": "v1.0.0"}, + {"name": "outdated/major", "description": "outdated/major v1.0.1 description", "version": "v1.0.1"}, + {"name": "outdated/major", "description": "outdated/major v1.1.0 description", "version": "v1.1.0"}, + {"name": "outdated/major", "description": "outdated/major v1.1.1 description", "version": "v1.1.1"}, + {"name": "outdated/major", "description": "outdated/major v2.0.0 description", "version": "v2.0.0"}, + + {"name": "outdated/minor", "description": "outdated/minor v1.0.0 description", "version": "1.0.0"}, + {"name": "outdated/minor", "description": "outdated/minor v1.0.1 description", "version": "1.0.1"}, + {"name": "outdated/minor", "description": "outdated/minor v1.1.0 description", "version": "1.1.0"}, + {"name": "outdated/minor", "description": "outdated/minor v1.1.1 description", "version": "1.1.1"}, + + {"name": "outdated/patch", "description": "outdated/patch v1.0.0 description", "version": "1.0.0"}, + {"name": "outdated/patch", "description": "outdated/patch v1.0.1 description", "version": "1.0.1"}, + ], + }, + }, + "require": requires, + }) +} + +/// ref: ShowCommandTest::testShow (one data-provider case). +/// +/// NOTE: PHP sets release dates on the installed packages via `setReleaseDate`, but no such setter +/// is exposed on `PackageInterfaceHandle`, so it is omitted here. Only the "sorting by age" case +/// depends on release dates; every other case produces identical output without them. +fn run_show_case(command: Vec<(PhpMixed, PhpMixed)>, expected: &str, requires: serde_json::Value) { + let _tear_down = init_temp_composer(Some(&show_composer_json(requires)), None, None, true); + + let pkg = get_complete_package("vendor/package", "v1.0.0"); + pkg.set_description("description of installed package".to_string()); + let major = get_complete_package("outdated/major", "v1.0.0"); + let minor = get_complete_package("outdated/minor", "1.0.0"); + let patch = get_complete_package("outdated/patch", "1.0.0"); + + let packages: Vec<PackageInterfaceHandle> = + vec![pkg.into(), major.into(), minor.into(), patch.into()]; + create_installed_json(&packages, &[], true); + + let locked = get_complete_package("vendor/locked", "3.0.0"); + locked.set_description("description of locked package".to_string()); + create_composer_lock(&[locked.into()], &[]); + + let mut app_tester = get_application_tester(); + app_tester.run(command, RunOptions::default()).unwrap(); + assert_eq!(expected.trim(), app_tester.get_display().trim()); +} + +use crate::test_case::{create_composer_lock, create_installed_json}; + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_default_shows_installed_with_version_and_description() { + run_show_case( + input(vec![("command", PhpMixed::from("show"))]), + "outdated/major 1.0.0 +outdated/minor 1.0.0 +outdated/patch 1.0.0 +vendor/package 1.0.0 description of installed package", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_with_installed_and_self() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("show")), + ("--installed", PhpMixed::from(true)), + ("--self", PhpMixed::from(true)), + ]), + "outdated/major 1.0.0 +outdated/minor 1.0.0 +outdated/patch 1.0.0 +root/pkg 1.2.3 +vendor/package 1.0.0 description of installed package", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_with_locked_and_self() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("show")), + ("--locked", PhpMixed::from(true)), + ("--self", PhpMixed::from(true)), + ]), + "root/pkg 1.2.3 +vendor/locked 3.0.0 description of locked package", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_with_available() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("show")), + ("-a", PhpMixed::from(true)), + ]), + "outdated/major outdated/major v2.0.0 description +outdated/minor outdated/minor v1.1.1 description +outdated/patch outdated/patch v1.0.1 description +vendor/package generic description", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_with_direct_shows_nothing_if_no_deps() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("show")), + ("--direct", PhpMixed::from(true)), + ]), + "", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_with_direct_shows_only_root_deps() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("show")), + ("--direct", PhpMixed::from(true)), + ]), + "outdated/major 1.0.0", + serde_json::json!({"outdated/major": "*"}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_outdated_deps() { + run_show_case( + input(vec![("command", PhpMixed::from("outdated"))]), + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +outdated/major 1.0.0 ~ 2.0.0 +outdated/minor 1.0.0 <highlight>! 1.1.1</highlight> +outdated/patch 1.0.0 <highlight>! 1.0.1</highlight>", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition(), so outdated->show fails with an option-does-not-exist error. Additionally, PackageInterfaceHandle exposes no set_release_date setter, so the 'sorting by age' output ('2 years old', '2 weeks old', 'from today') cannot be reproduced even once the definition is ported."] +#[test] +#[serial] +fn test_show_outdated_deps_sorting_by_age() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--sort-by-age", PhpMixed::from(true)), + ]), + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +outdated/minor 1.0.0 <highlight>! 1.1.1</highlight> 2 years old +outdated/patch 1.0.0 <highlight>! 1.0.1</highlight> 2 weeks old +outdated/major 1.0.0 ~ 2.0.0 from today", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] -fn test_show() { - todo!() +#[serial] +fn test_show_outdated_deps_with_direct_only_show_direct_deps_with_updated() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--direct", PhpMixed::from(true)), + ]), + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible +outdated/major 1.0.0 ~ 2.0.0", + serde_json::json!({ + "vendor/package": "*", + "outdated/major": "*", + }), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_outdated_deps_with_direct_show_msg_if_all_up_to_date() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--direct", PhpMixed::from(true)), + ]), + "All your direct dependencies are up to date", + serde_json::json!({"vendor/package": "*"}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_outdated_deps_with_major_only() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--major-only", PhpMixed::from(true)), + ]), + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +outdated/major 1.0.0 ~ 2.0.0", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_show_outdated_deps_with_minor_only() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--minor-only", PhpMixed::from(true)), + ]), + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +outdated/minor 1.0.0 <highlight>! 1.1.1</highlight> + +Transitive dependencies not required in composer.json: +outdated/major 1.0.0 <highlight>! 1.1.1</highlight> +outdated/patch 1.0.0 <highlight>! 1.0.1</highlight>", + serde_json::json!({"outdated/minor": "*"}), + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] +fn test_show_outdated_deps_with_patch_only() { + run_show_case( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--patch-only", PhpMixed::from(true)), + ]), + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +outdated/major 1.0.0 <highlight>! 1.0.1</highlight> +outdated/minor 1.0.0 <highlight>! 1.0.1</highlight> +outdated/patch 1.0.0 <highlight>! 1.0.1</highlight>", + serde_json::json!({}), + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] fn test_outdated_filters_according_to_platform_reqs_and_warns() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/package", "description": "generic description", "version": "1.0.0"}, + {"name": "vendor/package", "description": "generic description", "version": "1.1.0", "require": {"ext-missing": "3"}}, + {"name": "vendor/package", "description": "generic description", "version": "1.2.0", "require": {"ext-missing": "3"}}, + {"name": "vendor/package", "description": "generic description", "version": "1.3.0", "require": {"ext-missing": "3"}}, + ], + }, + }, + })), + None, + None, + true, + ); + + create_installed_json(&[get_package("vendor/package", "1.1.0")], &[], true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![("command", PhpMixed::from("outdated"))]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "<warning>Cannot use vendor/package 1.1.0 as it requires ext-missing 3 which is missing from your platform. +Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +vendor/package 1.1.0 ~ 1.0.0", + app_tester.get_display().trim() + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--verbose", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "<warning>Cannot use vendor/package's latest version 1.3.0 as it requires ext-missing 3 which is missing from your platform. +<warning>Cannot use vendor/package 1.2.0 as it requires ext-missing 3 which is missing from your platform. +<warning>Cannot use vendor/package 1.1.0 as it requires ext-missing 3 which is missing from your platform. +Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +vendor/package 1.1.0 ~ 1.0.0", + app_tester.get_display().trim() + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_outdated_filters_according_to_platform_reqs_without_warning_for_higher_versions() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/package", "description": "generic description", "version": "1.0.0"}, + {"name": "vendor/package", "description": "generic description", "version": "1.1.0"}, + {"name": "vendor/package", "description": "generic description", "version": "1.2.0"}, + {"name": "vendor/package", "description": "generic description", "version": "1.3.0", "require": {"php": "^99"}}, + ], + }, + }, + })), + None, + None, + true, + ); + + create_installed_json(&[get_package("vendor/package", "1.1.0")], &[], true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![("command", PhpMixed::from("outdated"))]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible + +Direct dependencies required in composer.json: +Everything up to date + +Transitive dependencies not required in composer.json: +vendor/package 1.1.0 <highlight>! 1.2.0</highlight>", + app_tester.get_display().trim() + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, configure_links, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_show_direct_with_name_does_not_show_transient_dependencies() { - todo!() + // NOTE: PHP also wires `direct/dependent` -> `vendor/package` via configureLinks, which has no + // public handle API. It is omitted: vendor/package is installed but not a direct root dependency + // either way, so the same InvalidArgumentException is thrown. + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": [], + "require": { + "direct/dependent": "*", + }, + })), + None, + None, + false, + ); + + create_installed_json( + &[ + get_package("direct/dependent", "1.0.0"), + get_package("vendor/package", "1.0.0"), + ], + &[], + true, + ); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--direct", PhpMixed::from(true)), + ("package", PhpMixed::from("vendor/package")), + ]), + RunOptions::default(), + ) + .expect_err("expected an InvalidArgumentException"); + assert!( + err.to_string().contains( + "Package \"vendor/package\" is installed but not a direct dependent of the root package." + ), + "unexpected error: {}", + err + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_show_direct_with_name_only_shows_direct_dependents() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": [], + "require": { + "direct/dependent": "*", + }, + "require-dev": { + "direct/dependent2": "*", + }, + })), + None, + None, + false, + ); + + create_installed_json( + &[ + get_package("direct/dependent", "1.0.0"), + get_package("direct/dependent2", "1.0.0"), + ], + &[], + true, + ); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--direct", PhpMixed::from(true)), + ("package", PhpMixed::from("direct/dependent")), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + assert!( + app_tester + .get_display() + .contains("name : direct/dependent\n") + ); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--direct", PhpMixed::from(true)), + ("package", PhpMixed::from("direct/dependent2")), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + assert!( + app_tester + .get_display() + .contains("name : direct/dependent2\n") + ); +} + +/// Assert every leading-word of each output line is a platform package, replicating +/// `Regex::matchAll('{^(\w+)}m', $output)`. +fn assert_all_platform_packages(output: &str) { + let re = regex::Regex::new(r"(?m)^(\w+)").unwrap(); + for caps in re.captures_iter(output) { + let m = caps.get(1).unwrap().as_str(); + assert!( + PlatformRepository::is_platform_package(m), + "{} is not a platform package", + m + ); + } } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_show_platform_only_shows_platform_packages() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/package", "description": "generic description", "version": "1.0.0"}, + ], + }, + }, + })), + None, + None, + true, + ); + + create_installed_json(&[get_package("vendor/package", "1.0.0")], &[], true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("-p", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + let output = app_tester.get_display(); + assert_all_platform_packages(output.trim()); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_show_platform_works_without_composer_json() { - todo!() + let _tear_down = init_temp_composer(Some(&serde_json::json!({})), None, None, true); + let _ = std::fs::remove_file("./composer.json"); + let _ = std::fs::remove_file("./auth.json"); + + // listing packages + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("-p", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + let output = app_tester.get_display(); + assert_all_platform_packages(output.trim()); + + // getting a single package + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("-p", PhpMixed::from(true)), + ("package", PhpMixed::from("php")), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + + let status_code = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("-p", PhpMixed::from(true)), + ("-f", PhpMixed::from("json")), + ("package", PhpMixed::from("php")), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_outdated_with_zero_major() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "zerozero/major", "description": "generic description", "version": "0.0.1"}, + {"name": "zerozero/major", "description": "generic description", "version": "0.0.2"}, + {"name": "zero/major", "description": "generic description", "version": "0.1.0"}, + {"name": "zero/major", "description": "generic description", "version": "0.2.0"}, + {"name": "zero/minor", "description": "generic description", "version": "0.1.0"}, + {"name": "zero/minor", "description": "generic description", "version": "0.1.2"}, + {"name": "zero/patch", "description": "generic description", "version": "0.1.2"}, + {"name": "zero/patch", "description": "generic description", "version": "0.1.2.1"}, + ], + }, + }, + "require": { + "zerozero/major": "^0.0.1", + "zero/major": "^0.1", + "zero/minor": "^0.1", + "zero/patch": "^0.1", + }, + })), + None, + None, + true, + ); + + create_installed_json( + &[ + get_package("zerozero/major", "0.0.1"), + get_package("zero/major", "0.1.0"), + get_package("zero/minor", "0.1.0"), + get_package("zero/patch", "0.1.2"), + ], + &[], + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--direct", PhpMixed::from(true)), + ("--patch-only", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible +zero/patch 0.1.2 <highlight>! 0.1.2.1</highlight>", + app_tester.get_display().trim() + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--direct", PhpMixed::from(true)), + ("--minor-only", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible +zero/minor 0.1.0 <highlight>! 0.1.2 </highlight> +zero/patch 0.1.2 <highlight>! 0.1.2.1</highlight>", + app_tester.get_display().trim() + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("outdated")), + ("--direct", PhpMixed::from(true)), + ("--major-only", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible +zero/major 0.1.0 ~ 0.2.0 +zerozero/major 0.0.1 ~ 0.0.2", + app_tester.get_display().trim() + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_show_all_shows_all_sections() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + {"name": "vendor/available", "description": "generic description", "version": "1.0.0"}, + ], + }, + }, + })), + None, + None, + true, + ); + + let pkg = get_complete_package("vendor/installed", "2.0.0"); + pkg.set_description("description of installed package".to_string()); + create_installed_json(&[pkg.into()], &[], true); + + let locked = get_complete_package("vendor/locked", "3.0.0"); + locked.set_description("description of locked package".to_string()); + create_composer_lock(&[locked.into()], &[]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--all", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + let output = app_tester.get_display(); + let output = output.trim(); + let re = regex::Regex::new(r"platform:(\n .*)+").unwrap(); + let output = re.replace(output, "platform: wiped"); + + assert_eq!( + "platform: wiped + +locked: + vendor/locked 3.0.0 description of locked package + +available: + vendor/available generic description + +installed: + vendor/installed 2.0.0 description of installed package", + output + ); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_locked_requires_valid_lock_file() { - todo!() + let _tear_down = init_temp_composer(None, None, None, true); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--locked", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .expect_err("expected an exception"); + assert!( + err.to_string().contains( + "A valid composer.json and composer.lock files is required to run this command with --locked" + ), + "unexpected error: {}", + err + ); } -#[ignore = "missing TestCase::init_temp_composer, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_locked_shows_all_locked() { - todo!() + let _tear_down = init_temp_composer(None, None, None, true); + + let pkg = get_complete_package("vendor/locked", "3.0.0"); + pkg.set_description("description of locked package".to_string()); + create_composer_lock(&[pkg.clone().into()], &[]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--locked", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "vendor/locked 3.0.0 description of locked package", + app_tester.get_display().trim() + ); + + let pkg2 = get_complete_package("vendor/locked2", "2.0.0"); + pkg2.set_description("description of locked2 package".to_string()); + create_composer_lock(&[pkg.into(), pkg2.into()], &[]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--locked", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "vendor/locked 3.0.0 description of locked package +vendor/locked2 2.0.0 description of locked2 package", + app_tester.get_display().trim() + ); } -#[ignore = "missing TestCase::get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_invalid_option_combinations() { - todo!() + let combos: Vec<Vec<(&str, PhpMixed)>> = vec![ + vec![ + ("--direct", PhpMixed::from(true)), + ("--all", PhpMixed::from(true)), + ], + vec![ + ("--direct", PhpMixed::from(true)), + ("--available", PhpMixed::from(true)), + ], + vec![ + ("--direct", PhpMixed::from(true)), + ("--platform", PhpMixed::from(true)), + ], + vec![ + ("--tree", PhpMixed::from(true)), + ("--all", PhpMixed::from(true)), + ], + vec![ + ("--tree", PhpMixed::from(true)), + ("--available", PhpMixed::from(true)), + ], + vec![ + ("--tree", PhpMixed::from(true)), + ("--latest", PhpMixed::from(true)), + ], + vec![ + ("--tree", PhpMixed::from(true)), + ("--path", PhpMixed::from(true)), + ], + vec![ + ("--patch-only", PhpMixed::from(true)), + ("--minor-only", PhpMixed::from(true)), + ], + vec![ + ("--patch-only", PhpMixed::from(true)), + ("--major-only", PhpMixed::from(true)), + ], + vec![ + ("--minor-only", PhpMixed::from(true)), + ("--major-only", PhpMixed::from(true)), + ], + vec![ + ("--minor-only", PhpMixed::from(true)), + ("--major-only", PhpMixed::from(true)), + ("--patch-only", PhpMixed::from(true)), + ], + vec![("--format", PhpMixed::from("test"))], + ]; + + for combo in combos { + let mut pairs = vec![("command", PhpMixed::from("show"))]; + pairs.extend(combo.clone()); + let mut app_tester = get_application_tester(); + let status_code = app_tester.run(input(pairs), RunOptions::default()).unwrap(); + assert_eq!(1, status_code, "combo {:?}", combo); + } } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_ignored_option_combinations() { - todo!() + let _tear_down = init_temp_composer(None, None, None, true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--installed", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert!( + app_tester + .get_display() + .contains("You are using the deprecated option \"installed\".") + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ( + "--ignore", + PhpMixed::List(vec![PhpMixed::from("vendor/package")]), + ), + ]), + RunOptions::default(), + ) + .unwrap(); + assert!( + app_tester + .get_display() + .contains("You are using the option \"ignore\"") + ); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_self_and_name_only() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({"name": "vendor/package", "version": "1.2.3"})), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--self", PhpMixed::from(true)), + ("--name-only", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!("vendor/package", app_tester.get_display().trim()); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_self_and_package_combination() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({"name": "vendor/package"})), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + let result = app_tester.run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--self", PhpMixed::from(true)), + ("package", PhpMixed::from("vendor/package")), + ]), + RunOptions::default(), + ); + assert!( + result.is_err(), + "expected an InvalidArgumentException, got {:?}", + result + ); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_self() { - todo!() + let today = chrono::Local::now().format("%Y-%m-%d").to_string(); + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "name": "vendor/package", + "version": "1.2.3", + "time": today, + })), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--self", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + + let expected: Vec<(&str, String)> = vec![ + ("name", "vendor/package".to_string()), + ("descrip.", "".to_string()), + ("keywords", "".to_string()), + ("versions", "* 1.2.3".to_string()), + ("released", format!("{}, today", today)), + ("type", "library".to_string()), + ("homepage", "".to_string()), + ("source", "[] ".to_string()), + ("dist", "[] ".to_string()), + ("path", "".to_string()), + ("names", "vendor/package".to_string()), + ]; + let expected_string = expected + .iter() + .map(|(k, v)| format!("{:<8} : {}", k, v)) + .collect::<Vec<_>>() + .join("\n") + + "\n"; + + assert_eq!(expected_string, app_tester.get_display()); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_not_installed_error() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": {"vendor/package": "1.0.0"}, + "require-dev": {"vendor/package-dev": "1.0.0"}, + })), + None, + None, + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![("command", PhpMixed::from("show"))]), + RunOptions::default(), + ) + .unwrap(); + assert!( + app_tester + .get_display() + .trim() + .contains("No dependencies installed. Try running composer install or update."), + "Should show error message when no dependencies are installed" + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_no_dev_option() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": {"vendor/package": "1.0.0"}, + "require-dev": {"vendor/package-dev": "1.0.0"}, + })), + None, + None, + true, + ); + create_installed_json( + &[ + get_package("vendor/package", "1.0.0"), + get_package("vendor/package-dev", "1.0.0"), + ], + &[], + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--no-dev", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!("vendor/package 1.0.0", app_tester.get_display().trim()); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] fn test_package_filter() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": { + "vendor/package": "1.0.0", + "vendor/other-package": "1.0.0", + "company/package": "1.0.0", + "company/other-package": "1.0.0", + }, + })), + None, + None, + true, + ); + create_installed_json( + &[ + get_package("vendor/package", "1.0.0"), + get_package("vendor/other-package", "1.0.0"), + get_package("company/package", "1.0.0"), + get_package("company/other-package", "1.0.0"), + ], + &[], + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("package", PhpMixed::from("vendor/package")), + ]), + RunOptions::default(), + ) + .unwrap(); + let output = app_tester.get_display(); + let output = output.trim(); + assert!(output.contains("vendor/package")); + assert!(!output.contains("vendor/other-package")); + assert!(!output.contains("company/package")); + assert!(!output.contains("company/other-package")); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("package", PhpMixed::from("company/*")), + ("--name-only", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + let output = app_tester.get_display(); + let output = output.trim(); + assert!(!output.contains("vendor/package")); + assert!(!output.contains("vendor/other-package")); + assert!(output.contains("company/package")); + assert!(output.contains("company/other-package")); +} + +/// ref: ShowCommandTest::testNotExistingPackage (one data-provider case). +fn run_not_existing_package_case(package: &str, options: Vec<(&str, PhpMixed)>, expected: &str) { + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": {"vendor/package": "1.0.0"}, + })), + None, + None, + true, + ); + let pkg = get_package("vendor/package", "1.0.0"); + create_installed_json(&[pkg.clone()], &[], true); + create_composer_lock(&[pkg], &[]); + + let mut pairs = vec![ + ("command", PhpMixed::from("show")), + ("package", PhpMixed::from(package)), + ]; + pairs.extend(options); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run(input(pairs), RunOptions::default()) + .expect_err("expected an exception"); + assert!( + err.to_string().contains(expected), + "expected message starting with {:?}, got: {}", + expected, + err + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] -fn test_not_existing_package() { - todo!() +#[serial] +fn test_not_existing_package_with_no_options() { + run_not_existing_package_case( + "not/existing", + vec![], + "Package \"not/existing\" not found, try using --available (-a) to show all available packages.", + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] +fn test_not_existing_package_with_all_option() { + run_not_existing_package_case( + "not/existing", + vec![("--all", PhpMixed::from(true))], + "Package \"not/existing\" not found.", + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_not_existing_package_with_locked_option() { + run_not_existing_package_case( + "not/existing", + vec![("--locked", PhpMixed::from(true))], + "Package \"not/existing\" not found in lock file, try using --available (-a) to show all available packages.", + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_not_existing_platform_with_platform_option() { + run_not_existing_package_case( + "ext-nonexisting", + vec![("--platform", PhpMixed::from(true))], + "Package \"ext-nonexisting\" not found, try using --available (-a) to show all available packages.", + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_not_existing_platform_without_platform_option() { + run_not_existing_package_case( + "ext-nonexisting", + vec![], + "Package \"ext-nonexisting\" not found, try using --platform (-p) to show platform packages, try using --available (-a) to show all available packages.", + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] fn test_not_existing_package_with_working_dir() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": {"vendor/package": "1.0.0"}, + })), + None, + None, + true, + ); + let dir = tear_down.working_dir(); + create_installed_json(&[get_package("vendor/package", "1.0.0")], &[], true); + + let expected = format!( + "Package \"not/existing\" not found in {}/composer.json, try using --available (-a) to show all available packages.", + dir.display() + ); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("package", PhpMixed::from("not/existing")), + ( + "--working-dir", + PhpMixed::from(dir.display().to_string().as_str()), + ), + ]), + RunOptions::default(), + ) + .expect_err("expected an exception"); + assert!( + err.to_string().contains(&expected), + "expected message starting with {:?}, got: {}", + expected, + err + ); + + drop(tear_down); +} + +/// ref: ShowCommandTest::testSpecificPackageAndTree (one data-provider case). +fn run_specific_package_and_tree_case( + packages: Vec<PackageInterfaceHandle>, + options: Vec<(&str, PhpMixed)>, + expected: &str, +) { + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "require": {"vendor/package": "1.0.0"}, + })), + None, + None, + true, + ); + + create_installed_json(&packages, &[], true); + + let mut pairs = vec![ + ("command", PhpMixed::from("show")), + ("package", PhpMixed::from("vendor/package")), + ("--tree", PhpMixed::from(true)), + ]; + pairs.extend(options); + + let mut app_tester = get_application_tester(); + app_tester.run(input(pairs), RunOptions::default()).unwrap(); + assert_eq!(expected, app_tester.get_display().trim()); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_specific_package_and_tree_just_package() { + let pkg = get_package("vendor/package", "1.0.0"); + run_specific_package_and_tree_case(vec![pkg], vec![], "vendor/package 1.0.0"); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] -fn test_specific_package_and_tree() { - todo!() +#[serial] +fn test_specific_package_and_tree_with_one_package_requirement() { + let pkg = get_complete_package("vendor/package", "1.0.0"); + let mut requires = indexmap::IndexMap::new(); + requires.insert( + "vendor/required-package".to_string(), + Link::new( + "vendor/package".to_string(), + "vendor/required-package".to_string(), + get_version_constraint("=", "1.0.0"), + Some(Link::TYPE_REQUIRE.to_string()), + "1.0.0".to_string(), + ), + ); + pkg.__set_requires(requires); + run_specific_package_and_tree_case( + vec![pkg.into()], + vec![], + "vendor/package 1.0.0 +`--vendor/required-package 1.0.0", + ); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] #[test] +#[serial] +fn test_specific_package_and_tree_with_platform_requirement() { + let pkg = get_complete_package("vendor/package", "1.0.0"); + let mut requires = indexmap::IndexMap::new(); + requires.insert( + "php".to_string(), + Link::new( + "vendor/package".to_string(), + "php".to_string(), + get_version_constraint("=", "8.2.0"), + Some(Link::TYPE_REQUIRE.to_string()), + "8.2.0".to_string(), + ), + ); + pkg.__set_requires(requires); + run_specific_package_and_tree_case( + vec![pkg.into()], + vec![], + "vendor/package 1.0.0 +`--php 8.2.0", + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] +fn test_specific_package_and_tree_with_json_format() { + let pkg = get_package("vendor/package", "1.0.0"); + run_specific_package_and_tree_case( + vec![pkg], + vec![("--format", PhpMixed::from("json"))], + "{ + \"installed\": [ + { + \"name\": \"vendor/package\", + \"version\": \"1.0.0\", + \"description\": null + } + ] +}", + ); +} + +#[ignore = "blocked: ShowCommand::configure() at src/command/show_command.rs:98-100 registers an empty set_definition() (TODO placeholder); no package argument or options are defined, so every show invocation fails with an option/argument-does-not-exist InvalidArgumentException (the outdated command proxies into show and fails identically). Unported src path; cannot fix from test files."] +#[test] +#[serial] fn test_name_only_prints_no_trailing_whitespace() { - todo!() + let _tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + // CAUTION: package names matter - output is sorted, and we want shorter before longer ones + {"name": "vendor/apackage", "description": "generic description", "version": "1.0.0"}, + {"name": "vendor/apackage", "description": "generic description", "version": "1.1.0"}, + {"name": "vendor/longpackagename", "description": "generic description", "version": "1.0.0"}, + {"name": "vendor/longpackagename", "description": "generic description", "version": "1.1.0"}, + {"name": "vendor/somepackage", "description": "generic description", "version": "1.0.0"}, + ], + }, + }, + })), + None, + None, + true, + ); + + create_installed_json( + &[ + get_package("vendor/apackage", "1.0.0"), + get_package("vendor/longpackagename", "1.0.0"), + get_package("vendor/somepackage", "1.0.0"), + ], + &[], + true, + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("-N", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "vendor/apackage +vendor/longpackagename +vendor/somepackage", + app_tester.get_display().trim() + ); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("show")), + ("--outdated", PhpMixed::from(true)), + ("-N", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + assert_eq!( + "Legend: +! patch or minor release available - update recommended +~ major release available - update possible +vendor/apackage +vendor/longpackagename", + app_tester.get_display().trim() + ); } diff --git a/crates/shirabe/tests/command/validate_command_test.rs b/crates/shirabe/tests/command/validate_command_test.rs index 31af4be..5167fc0 100644 --- a/crates/shirabe/tests/command/validate_command_test.rs +++ b/crates/shirabe/tests/command/validate_command_test.rs @@ -1,6 +1,8 @@ //! ref: composer/tests/Composer/Test/Command/ValidateCommandTest.php -use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use crate::test_case::{ + RunOptions, create_composer_lock, get_application_tester, init_temp_composer, +}; use serial_test::serial; use shirabe::util::platform::Platform; use shirabe_php_shim::PhpMixed; @@ -86,9 +88,9 @@ fn provide_validate_tests() -> Vec<ValidateCase> { #[test] #[serial] -#[ignore = "validate creates a Composer instance (create_composer_instance -> Factory) which \ - reaches ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking (stream.rs \ - todo!(), requires fcntl(2))"] +#[ignore = "validate creates a Composer instance (Factory::create_composer) which panics in \ + PartialComposerHandle::borrow_partial at crates/shirabe/src/composer.rs:446 \ + ('RefCell already mutably borrowed'); source-level borrow conflict, not a test issue"] fn test_validate() { for case in provide_validate_tests() { let _tear_down = init_temp_composer(Some(&case.composer_json), None, None, true); @@ -128,11 +130,23 @@ fn test_validate_on_file_issues() { #[test] #[serial] -#[ignore = "validate with a lock file creates a Composer instance and queries the Locker, reaching \ - ProcessExecutor (git) -> shirabe-php-shim stream_set_blocking (stream.rs todo!(), \ - requires fcntl(2))"] +#[ignore = "validate with a lock file creates a Composer instance (Factory::create_composer) which \ + panics in PartialComposerHandle::borrow_partial at crates/shirabe/src/composer.rs:446 \ + ('RefCell already mutably borrowed'); source-level borrow conflict, not a test issue"] fn test_with_composer_lock() { - todo!() + let tear_down = init_temp_composer(Some(&minimal_valid_configuration()), None, None, true); + create_composer_lock(&[], &[]); + + let mut app_tester = get_application_tester(); + app_tester + .run(validate_input(vec![]), RunOptions::default()) + .unwrap(); + + let expected = "<warning>Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version</warning>\n<warning>Composer could not detect the root package (test/suite) version, defaulting to '1.0.0'. See https://getcomposer.org/root-version</warning>\n./composer.json is valid but your composer.lock has some errors\n# Lock file errors\n- Required package \"root/req\" is not present in the lock file.\nThis usually happens when composer files are incorrectly merged or the composer.json file is manually edited.\nRead more about correctly resolving merge conflicts https://getcomposer.org/doc/articles/resolving-merge-conflicts.md\nand prefer using the \"require\" command over editing the composer.json file directly https://getcomposer.org/doc/03-cli.md#require-r"; + + assert_eq!(expected.trim(), app_tester.get_display().trim()); + + drop(tear_down); } #[test] |
