diff options
Diffstat (limited to 'crates/shirabe/tests')
| -rw-r--r-- | crates/shirabe/tests/command/install_command_test.rs | 202 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/reinstall_command_test.rs | 103 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/require_command_test.rs | 393 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/update_command_test.rs | 624 |
4 files changed, 1290 insertions, 32 deletions
diff --git a/crates/shirabe/tests/command/install_command_test.rs b/crates/shirabe/tests/command/install_command_test.rs index a0586ab..c495b12 100644 --- a/crates/shirabe/tests/command/install_command_test.rs +++ b/crates/shirabe/tests/command/install_command_test.rs @@ -1,25 +1,211 @@ //! ref: composer/tests/Composer/Test/Command/InstallCommandTest.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_php_shim::PhpMixed; + +fn input(pairs: Vec<(&str, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + pairs + .into_iter() + .map(|(k, v)| (PhpMixed::from(k), v)) + .collect() +} + +/// ref: InstallCommandTest::errorCaseProvider +fn error_cases() -> Vec<( + &'static str, + serde_json::Value, + Vec<(&'static str, PhpMixed)>, + &'static str, +)> { + vec![ + ( + "it writes an error when the dev flag is passed", + serde_json::json!({ "repositories": [] }), + vec![("--dev", PhpMixed::from(true))], + r#"<warning>You are using the deprecated option "--dev". It has no effect and will break in Composer 3.</warning> +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Nothing to install, update or remove +Generating autoload files"#, + ), + ( + "it writes an error when no-suggest flag passed", + serde_json::json!({ "repositories": [] }), + vec![("--no-suggest", PhpMixed::from(true))], + r#"<warning>You are using the deprecated option "--no-suggest". It has no effect and will break in Composer 3.</warning> +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Nothing to install, update or remove +Generating autoload files"#, + ), + ( + "it writes an error when packages passed", + serde_json::json!({ "repositories": [] }), + vec![( + "packages", + PhpMixed::List(vec![PhpMixed::from("vendor/package")]), + )], + r#"Invalid argument vendor/package. Use "composer require vendor/package" instead to add packages to your composer.json."#, + ), + ( + "it writes an error when no-install flag is passed", + serde_json::json!({ "repositories": [] }), + vec![("--no-install", PhpMixed::from(true))], + r#"Invalid option "--no-install". Use "composer update --no-install" instead if you are trying to update the composer.lock file."#, + ), + ] +} + #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock/createInstalledJson harness (not implemented)"] +#[serial] fn test_install_command_errors() { - todo!() + for (label, composer_json, command, expected) in error_cases() { + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let packages = [get_package("vendor/package", "1.2.3")]; + let dev_packages = [get_package("vendor/devpackage", "2.3.4")]; + + create_composer_lock(&packages, &dev_packages); + create_installed_json(&packages, &dev_packages, true); + + let mut app_tester = get_application_tester(); + let mut args = vec![("command", PhpMixed::from("install"))]; + args.extend(command); + let _ = app_tester.run(input(args), RunOptions::default()); + + assert_eq!( + expected.trim(), + app_tester.get_display().trim(), + "case: {label}" + ); + } } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "Factory::create_default_installers is a Phase-C stub that registers no installers (pending the composer construction-ordering / shared-ownership rework), so the install step fails with \"Unknown installer type: metapackage\""] fn test_install_from_empty_vendor() { - todo!() + let composer_json = serde_json::json!({ + "require": { "root/req": "1.*" }, + "require-dev": { "root/another": "1.*" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let root_req_package = get_package("root/req", "1.0.0"); + let 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()); + another_package.__set_type("metapackage".to_string()); + + create_composer_lock(&[root_req_package], &[another_package]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("install")), + ("--no-progress", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + + assert_eq!( + "Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Package operations: 2 installs, 0 updates, 0 removals + - Installing root/another (1.0.0) + - Installing root/req (1.0.0) +Generating autoload files", + app_tester.get_display().trim() + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "Factory::create_default_installers is a Phase-C stub that registers no installers (pending the composer construction-ordering / shared-ownership rework), so the install step fails with \"Unknown installer type: metapackage\""] fn test_install_from_empty_vendor_no_dev() { - todo!() + let composer_json = serde_json::json!({ + "require": { "root/req": "1.*" }, + "require-dev": { "root/another": "1.*" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let root_req_package = get_package("root/req", "1.0.0"); + let 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()); + another_package.__set_type("metapackage".to_string()); + + create_composer_lock(&[root_req_package], &[another_package]); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("install")), + ("--no-progress", PhpMixed::from(true)), + ("--no-dev", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + + assert_eq!( + "Installing dependencies from lock file +Verifying lock file contents can be installed on current platform. +Package operations: 1 install, 0 updates, 0 removals + - Installing root/req (1.0.0) +Generating autoload files", + app_tester.get_display().trim() + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock/createInstalledJson harness (not implemented)"] +#[serial] +#[ignore = "Factory::create_default_installers is a Phase-C stub that registers no installers (pending the composer construction-ordering / shared-ownership rework), so the install step fails with \"Unknown installer type: metapackage\""] fn test_install_new_packages_with_existing_partial_vendor() { - todo!() + let composer_json = serde_json::json!({ + "require": { + "root/req": "1.*", + "root/another": "1.*", + }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let root_req_package = get_package("root/req", "1.0.0"); + let 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()); + another_package.__set_type("metapackage".to_string()); + + create_composer_lock(&[root_req_package.clone(), another_package], &[]); + create_installed_json(&[root_req_package], &[], true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("install")), + ("--no-progress", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + + assert_eq!( + "Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Package operations: 1 install, 0 updates, 0 removals + - Installing root/another (1.0.0) +Generating autoload files", + app_tester.get_display().trim() + ); } diff --git a/crates/shirabe/tests/command/reinstall_command_test.rs b/crates/shirabe/tests/command/reinstall_command_test.rs index b420fd5..ba3d39a 100644 --- a/crates/shirabe/tests/command/reinstall_command_test.rs +++ b/crates/shirabe/tests/command/reinstall_command_test.rs @@ -1,7 +1,106 @@ //! ref: composer/tests/Composer/Test/Command/ReinstallCommandTest.php -#[ignore = "missing TestCase::init_temp_composer, create_composer_lock, create_installed_json, and 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 serial_test::serial; +use shirabe_php_shim::PhpMixed; + +fn input(pairs: Vec<(&str, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + pairs + .into_iter() + .map(|(k, v)| (PhpMixed::from(k), v)) + .collect() +} + +/// ref: ReinstallCommandTest::caseProvider +fn cases() -> Vec<(&'static str, Vec<(&'static str, PhpMixed)>, &'static str)> { + vec![ + ( + "reinstall a package by name", + vec![( + "packages", + PhpMixed::List(vec![ + PhpMixed::from("root/req"), + PhpMixed::from("root/anotherreq*"), + ]), + )], + "- Removing root/req (1.0.0) + - Removing root/anotherreq2 (1.0.0) + - Removing root/anotherreq (1.0.0) + - Installing root/anotherreq (1.0.0) + - Installing root/anotherreq2 (1.0.0) + - Installing root/req (1.0.0)", + ), + ( + "reinstall packages by type", + vec![( + "--type", + PhpMixed::List(vec![PhpMixed::from("metapackage")]), + )], + "- Removing root/req (1.0.0) + - Removing root/lala (1.0.0) + - Removing root/anotherreq2 (1.0.0) + - Removing root/anotherreq (1.0.0) + - Installing root/anotherreq (1.0.0) + - Installing root/anotherreq2 (1.0.0) + - Installing root/lala (1.0.0) + - Installing root/req (1.0.0)", + ), + ( + "reinstall a package that is not installed", + vec![( + "packages", + PhpMixed::List(vec![PhpMixed::from("root/unknownreq")]), + )], + r#"<warning>Pattern "root/unknownreq" does not match any currently installed packages.</warning> +<warning>Found no packages to reinstall, aborting.</warning>"#, + ), + ] +} + #[test] +#[serial] +#[ignore = "ReinstallCommand::execute defers the two InstallationManager::execute(...) calls (Phase-C: needs a &mut InstalledRepositoryInterface view of local_repo, and InstallationManager::execute is itself todo!()), so no Removing/Installing lines are emitted and the output is empty. Only the \"not installed\" case, which aborts with warnings before reaching that code, would pass"] fn test_reinstall_command() { - todo!() + for (label, options, expected) in cases() { + let composer_json = serde_json::json!({ + "require": { "root/req": "1.*" }, + "require-dev": { + "root/anotherreq": "2.*", + "root/anotherreq2": "2.*", + "root/lala": "2.*", + }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let root_req_package = get_package("root/req", "1.0.0"); + let another_req_package = get_package("root/anotherreq", "1.0.0"); + let another_req_package2 = get_package("root/anotherreq2", "1.0.0"); + let another_req_package3 = get_package("root/lala", "1.0.0"); + root_req_package.__set_type("metapackage".to_string()); + another_req_package.__set_type("metapackage".to_string()); + another_req_package2.__set_type("metapackage".to_string()); + another_req_package3.__set_type("metapackage".to_string()); + + let dev = [ + another_req_package.clone(), + another_req_package2.clone(), + another_req_package3.clone(), + ]; + create_composer_lock(std::slice::from_ref(&root_req_package), &dev); + create_installed_json(std::slice::from_ref(&root_req_package), &dev, true); + + let mut app_tester = get_application_tester(); + let mut args = vec![ + ("command", PhpMixed::from("reinstall")), + ("--no-progress", PhpMixed::from(true)), + ("--no-plugins", PhpMixed::from(true)), + ]; + args.extend(options); + app_tester.run(input(args), RunOptions::default()).unwrap(); + + assert_eq!(expected, app_tester.get_display().trim(), "case: {label}"); + } } diff --git a/crates/shirabe/tests/command/require_command_test.rs b/crates/shirabe/tests/command/require_command_test.rs index 0f43342..2464f3d 100644 --- a/crates/shirabe/tests/command/require_command_test.rs +++ b/crates/shirabe/tests/command/require_command_test.rs @@ -1,25 +1,402 @@ //! ref: composer/tests/Composer/Test/Command/RequireCommandTest.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::json::JsonFile; +use shirabe_php_shim::PhpMixed; + +fn input(pairs: Vec<(&str, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + pairs + .into_iter() + .map(|(k, v)| (PhpMixed::from(k), v)) + .collect() +} + #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/getApplicationTester harness (not implemented)"] +#[serial] +#[ignore = "shirabe_php_shim::phpversion(extension) is a todo!() (per-extension version strings not modeled); reached while checking the ext-foobar platform requirement during version selection"] fn test_require_throws_if_none_matches() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + { "name": "required/pkg", "version": "1.0.0", "require": { "ext-foobar": "^1" } }, + ], + }, + }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let mut app_tester = get_application_tester(); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("require")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ( + "packages", + PhpMixed::List(vec![PhpMixed::from("required/pkg")]), + ), + ]), + RunOptions::default(), + ) + .expect_err("expected InvalidArgumentException for incompatible platform requirements"); + + assert!( + err.to_string().contains( + "Package required/pkg has requirements incompatible with your PHP version, PHP extensions and Composer version:" + ) && err + .to_string() + .contains("- required/pkg 1.0.0 requires ext-foobar ^1 but it is not present."), + "got: {err}" + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/getApplicationTester/setInputs harness (not implemented)"] +#[serial] +#[ignore = "Phase-C re-entrancy: EventDispatcher::make_autoloader calls Composer borrow_mut while the Composer is already borrowed up the installer-event-dispatch stack, panicking with \"RefCell already borrowed\" (composer.rs:507)"] fn test_require_warns_if_resolved_to_feature_branch() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + { "name": "required/pkg", "version": "2.0.0", "require": { "common/dep": "^1" } }, + { "name": "required/pkg", "version": "dev-foo-bar", "require": { "common/dep": "^2" } }, + { "name": "common/dep", "version": "2.0.0" }, + ], + }, + }, + "require": { "common/dep": "^2.0" }, + "minimum-stability": "dev", + "prefer-stable": true, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["n".to_string()]); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("require")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ( + "packages", + PhpMixed::List(vec![PhpMixed::from("required/pkg")]), + ), + ]), + RunOptions { + interactive: Some(true), + ..RunOptions::default() + }, + ) + .unwrap(); + + assert_eq!( + "./composer.json has been updated +Running composer update required/pkg +Loading composer repositories with package information +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking common/dep (2.0.0) + - Locking required/pkg (dev-foo-bar) +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals + - Installing common/dep (2.0.0) + - Installing required/pkg (dev-foo-bar) +Using version dev-foo-bar for required/pkg +<warning>Version dev-foo-bar looks like it may be a feature branch which is unlikely to keep working in the long run and may be in an unstable state</warning> +Are you sure you want to use this constraint (y) or would you rather abort (n) the whole operation [y,n]? . +Installation failed, reverting ./composer.json to its original content. +", + app_tester.get_display() + ); +} + +/// ref: RequireCommandTest::provideRequire +fn provide_require() -> Vec<( + &'static str, + serde_json::Value, + Vec<(&'static str, PhpMixed)>, + &'static str, +)> { + vec![ + ( + "warn once for missing ext but a lower package matches", + serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "required/pkg", "version": "1.2.0", "require": { "ext-foobar": "^1" } }, + { "name": "required/pkg", "version": "1.1.0", "require": { "ext-foobar": "^1" } }, + { "name": "required/pkg", "version": "1.0.0" }, + ] } }, + }), + vec![("packages", PhpMixed::List(vec![PhpMixed::from("required/pkg")]))], + "<warning>Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform. +./composer.json has been updated +Running composer update required/pkg +Loading composer repositories with package information +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking required/pkg (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 0 updates, 0 removals + - Installing required/pkg (1.0.0) +Using version ^1.0 for required/pkg", + ), + ( + "warn multiple times when verbose", + serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "required/pkg", "version": "1.2.0", "require": { "ext-foobar": "^1" } }, + { "name": "required/pkg", "version": "1.1.0", "require": { "ext-foobar": "^1" } }, + { "name": "required/pkg", "version": "1.0.0" }, + ] } }, + }), + vec![ + ("packages", PhpMixed::List(vec![PhpMixed::from("required/pkg")])), + ("--no-install", PhpMixed::from(true)), + ("-v", PhpMixed::from(true)), + ], + "<warning>Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform. +<warning>Cannot use required/pkg 1.1.0 as it requires ext-foobar ^1 which is missing from your platform. +./composer.json has been updated +Running composer update required/pkg +Loading composer repositories with package information +Updating dependencies +Dependency resolution completed in %d seconds +Analyzed %d packages to resolve dependencies +Analyzed %d rules to resolve dependencies +Lock file operations: 1 install, 0 updates, 0 removals +Installs: required/pkg:1.0.0 + - Locking required/pkg (1.0.0) +Using version ^1.0 for required/pkg", + ), + ( + "warn for not satisfied req which is satisfied by lower version", + serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "required/pkg", "version": "1.1.0", "require": { "php": "^20" } }, + { "name": "required/pkg", "version": "1.0.0", "require": { "php": ">=7" } }, + ] } }, + }), + vec![ + ("packages", PhpMixed::List(vec![PhpMixed::from("required/pkg")])), + ("--no-install", PhpMixed::from(true)), + ], + "<warning>Cannot use required/pkg's latest version 1.1.0 as it requires php ^20 which is not satisfied by your platform. +./composer.json has been updated +Running composer update required/pkg +Loading composer repositories with package information +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking required/pkg (1.0.0) +Using version ^1.0 for required/pkg", + ), + ( + "version selection happens early even if not completely accurate if no update is requested", + serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "required/pkg", "version": "1.1.0", "require": { "php": "^20" } }, + { "name": "required/pkg", "version": "1.0.0", "require": { "php": ">=7" } }, + ] } }, + }), + vec![ + ("packages", PhpMixed::List(vec![PhpMixed::from("required/pkg")])), + ("--no-update", PhpMixed::from(true)), + ], + "<warning>Cannot use required/pkg's latest version 1.1.0 as it requires php ^20 which is not satisfied by your platform. +Using version ^1.0 for required/pkg +./composer.json has been updated", + ), + ( + "pick best matching version when not provided", + serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "existing/dep", "version": "1.1.0", "require": { "required/pkg": "^1" } }, + { "name": "required/pkg", "version": "2.0.0" }, + { "name": "required/pkg", "version": "1.1.0" }, + { "name": "required/pkg", "version": "1.0.0" }, + ] } }, + "require": { "existing/dep": "^1" }, + }), + vec![ + ("packages", PhpMixed::List(vec![PhpMixed::from("required/pkg")])), + ("--no-install", PhpMixed::from(true)), + ], + "./composer.json has been updated +Running composer update required/pkg +Loading composer repositories with package information +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking existing/dep (1.1.0) + - Locking required/pkg (1.1.0) +Using version ^1.1 for required/pkg", + ), + ( + "use exact constraint with --fixed", + serde_json::json!({ + "type": "project", + "repositories": { "packages": { "type": "package", "package": [ + { "name": "required/pkg", "version": "1.1.0" }, + ] } }, + }), + vec![ + ("packages", PhpMixed::List(vec![PhpMixed::from("required/pkg")])), + ("--no-install", PhpMixed::from(true)), + ("--fixed", PhpMixed::from(true)), + ], + "./composer.json has been updated +Running composer update required/pkg +Loading composer repositories with package information +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking required/pkg (1.1.0) +Using version 1.1.0 for required/pkg", + ), + ] } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/getApplicationTester harness (not implemented)"] +#[serial] +#[ignore = "Phase-C re-entrancy: EventDispatcher::make_autoloader calls Composer borrow_mut while the Composer is already borrowed up the installer-event-dispatch stack, panicking with \"RefCell already borrowed\" (composer.rs:507)"] fn test_require() { - todo!() + for (label, composer_json, command, expected) in provide_require() { + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let mut app_tester = get_application_tester(); + let mut args = vec![ + ("command", PhpMixed::from("require")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ]; + args.extend(command); + app_tester.run(input(args), RunOptions::default()).unwrap(); + + let display = app_tester.get_display(); + let display = display.trim(); + if expected.contains("%d") { + // The PHP test builds a regex by quoting the expected and replacing %d with [0-9.]+. + // Compare line-by-line, treating "%d" as a numeric wildcard. + let exp_lines: Vec<&str> = expected.trim().lines().collect(); + let got_lines: Vec<&str> = display.lines().collect(); + assert_eq!( + exp_lines.len(), + got_lines.len(), + "case: {label}\n--- expected ---\n{expected}\n--- got ---\n{display}" + ); + for (exp, got) in exp_lines.iter().zip(got_lines.iter()) { + if exp.contains("%d") { + let prefix = &exp[..exp.find("%d").unwrap()]; + assert!( + got.starts_with(prefix), + "case: {label}\nexpected line prefix {prefix:?}, got {got:?}" + ); + } else { + assert_eq!(exp, got, "case: {label}"); + } + } + } else { + assert_eq!(expected.trim(), display, "case: {label}"); + } + } +} + +/// ref: RequireCommandTest::provideInconsistentRequireKeys +fn provide_inconsistent_require_keys() -> Vec<(bool, bool, &'static str)> { + vec![ + ( + true, + false, + "<warning>required/pkg is currently present in the require key and you ran the command with the --dev flag, which will move it to the require-dev key.</warning>", + ), + ( + false, + false, + "<warning>required/pkg is currently present in the require-dev key and you ran the command without the --dev flag, which will move it to the require key.</warning>", + ), + ( + true, + true, + "<warning>required/pkg is currently present in the require key and you ran the command with the --dev flag, which will move it to the require-dev key.</warning>", + ), + ( + false, + true, + "<warning>required/pkg is currently present in the require-dev key and you ran the command without the --dev flag, which will move it to the require key.</warning>", + ), + ] } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock/createInstalledJson/getApplicationTester harness (not implemented)"] +#[serial] +#[ignore = "update/solver pipeline incomplete: the require run fails with \"Fixed package __root__ 1.0.0+no-version-set was not added to solver pool.\" (the root package is not seeded into the solver pool)"] fn test_inconsistent_require_keys() { - todo!() + for (is_dev, is_interactive, expected_warning) in provide_inconsistent_require_keys() { + let current_key = if is_dev { "require" } else { "require-dev" }; + let other_key = if is_dev { "require-dev" } else { "require" }; + + let composer_json = serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "required/pkg", "version": "1.0.0" }, + ] } }, + current_key: { "required/pkg": "^1.0" }, + }); + let tear_down = init_temp_composer(Some(&composer_json), None, None, true); + let dir = tear_down.working_dir(); + + let package = get_package("required/pkg", "1.0.0"); + if is_dev { + create_composer_lock(&[], std::slice::from_ref(&package)); + create_installed_json(&[], std::slice::from_ref(&package), true); + } else { + create_composer_lock(std::slice::from_ref(&package), &[]); + create_installed_json(std::slice::from_ref(&package), &[], false); + } + + let mut app_tester = get_application_tester(); + let mut command = vec![ + ("command", PhpMixed::from("require")), + ("--no-audit", PhpMixed::from(true)), + ("--dev", PhpMixed::from(is_dev)), + ("--no-install", PhpMixed::from(true)), + ( + "packages", + PhpMixed::List(vec![PhpMixed::from("required/pkg")]), + ), + ]; + if is_interactive { + app_tester.set_inputs(vec!["yes".to_string()]); + } else { + command.push(("--no-interaction", PhpMixed::from(true))); + } + + app_tester + .run(input(command), RunOptions::default()) + .unwrap(); + + assert!( + app_tester.get_display().contains(expected_warning), + "case is_dev={is_dev} is_interactive={is_interactive}\ngot: {}", + app_tester.get_display() + ); + + let mut composer_content = + JsonFile::new(format!("{}/composer.json", dir.display()), None, None).unwrap(); + let content = composer_content.read().unwrap(); + let content: serde_json::Value = serde_json::from_str(&JsonFile::encode(&content)).unwrap(); + assert!( + content.get(other_key).is_some(), + "expected key {other_key} present" + ); + assert!( + content.get(current_key).is_none(), + "expected key {current_key} absent" + ); + } } diff --git a/crates/shirabe/tests/command/update_command_test.rs b/crates/shirabe/tests/command/update_command_test.rs index 95b6962..6cc36bd 100644 --- a/crates/shirabe/tests/command/update_command_test.rs +++ b/crates/shirabe/tests/command/update_command_test.rs @@ -1,43 +1,639 @@ //! ref: composer/tests/Composer/Test/Command/UpdateCommandTest.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::Link; +use shirabe_php_shim::PhpMixed; +use shirabe_semver::constraint::{AnyConstraint, MatchAllConstraint}; + +fn input(pairs: Vec<(&str, PhpMixed)>) -> Vec<(PhpMixed, PhpMixed)> { + pairs + .into_iter() + .map(|(k, v)| (PhpMixed::from(k), v)) + .collect() +} + +/// Minimal port of PHPUnit's `assertStringMatchesFormat` covering the placeholders these fixtures +/// use: `%d` (integer), `%f` (float), `%s` (non-newline run), `%a` (any run incl. newlines). +fn assert_string_matches_format(expected: &str, actual: &str) { + let mut pattern = String::from("(?s)^"); + let mut chars = expected.chars().peekable(); + while let Some(c) = chars.next() { + if c == '%' + && let Some(&next) = chars.peek() + { + match next { + 'd' => { + chars.next(); + pattern.push_str(r"[0-9]+"); + continue; + } + 'f' => { + chars.next(); + pattern.push_str(r"[+-]?[0-9]*\.?[0-9]+([eE][+-]?[0-9]+)?"); + continue; + } + 's' => { + chars.next(); + pattern.push_str(r"[^\r\n]+"); + continue; + } + 'a' => { + chars.next(); + pattern.push_str(r".+"); + continue; + } + _ => {} + } + } + pattern.push_str(®ex::escape(&c.to_string())); + } + pattern.push('$'); + let re = regex::Regex::new(&pattern).unwrap(); + assert!( + re.is_match(actual), + "string did not match format.\n--- expected format ---\n{expected}\n--- actual ---\n{actual}" + ); +} + +fn root_dep_and_transitive_dep() -> serde_json::Value { + serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "root/req", "version": "1.0.0", "require": { "dep/pkg": "^1" } }, + { "name": "dep/pkg", "version": "1.0.0", "replace": { "replaced/pkg": "1.0.0" } }, + { "name": "dep/pkg", "version": "1.0.1", "replace": { "replaced/pkg": "1.0.1" } }, + { "name": "dep/pkg", "version": "1.0.2", "replace": { "replaced/pkg": "1.0.2" } }, + ] } }, + "require": { "root/req": "1.*" }, + }) +} + +/// ref: UpdateCommandTest::provideUpdates +fn provide_updates() -> Vec<( + &'static str, + serde_json::Value, + Vec<(&'static str, PhpMixed)>, + &'static str, + bool, +)> { + vec![ + ( + "simple update", + root_dep_and_transitive_dep(), + vec![], + "Loading composer repositories with package information +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking dep/pkg (1.0.2) + - Locking root/req (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals + - Installing dep/pkg (1.0.2) + - Installing root/req (1.0.0)", + false, + ), + ( + "simple update with very verbose output", + root_dep_and_transitive_dep(), + vec![("-vv", PhpMixed::from(true))], + "Loading composer repositories with package information +Updating dependencies +Dependency resolution completed in %f seconds +Analyzed %d packages to resolve dependencies +Analyzed %d rules to resolve dependencies +Lock file operations: 2 installs, 0 updates, 0 removals +Installs: dep/pkg:1.0.2, root/req:1.0.0 + - Locking dep/pkg (1.0.2) from package repo (defining 4 packages) + - Locking root/req (1.0.0) from package repo (defining 4 packages) +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals +Installs: dep/pkg:1.0.2, root/req:1.0.0 + - Installing dep/pkg (1.0.2) + - Installing root/req (1.0.0)", + false, + ), + ( + "update with temporary constraint + --no-install", + root_dep_and_transitive_dep(), + vec![ + ("--with", PhpMixed::List(vec![PhpMixed::from("dep/pkg:1.0.0")])), + ("--no-install", PhpMixed::from(true)), + ], + "Loading composer repositories with package information +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking dep/pkg (1.0.0) + - Locking root/req (1.0.0)", + false, + ), + ( + "update with temporary constraint failing resolution", + root_dep_and_transitive_dep(), + vec![("--with", PhpMixed::List(vec![PhpMixed::from("dep/pkg:^2")]))], + "Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires root/req 1.* -> satisfiable by root/req[1.0.0]. + - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2).", + false, + ), + ( + "update with temporary constraint failing resolution on root package", + root_dep_and_transitive_dep(), + vec![("--with", PhpMixed::List(vec![PhpMixed::from("root/req:^2")]))], + "The temporary constraint \"^2\" for \"root/req\" must be a subset of the constraint in your composer.json (1.*) +Run `composer require root/req` or `composer require root/req:^2` instead to replace the constraint", + false, + ), + ( + "update & bump", + root_dep_and_transitive_dep(), + vec![("--bump-after-update", PhpMixed::from(true))], + "Loading composer repositories with package information +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking dep/pkg (1.0.2) + - Locking root/req (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals + - Installing dep/pkg (1.0.2) + - Installing root/req (1.0.0) +Bumping dependencies +<warning>Warning: Bumping dependency constraints is not recommended for libraries as it will narrow down your dependencies and may cause problems for your users.</warning> +<warning>If your package is not a library, you can explicitly specify the \"type\" by using \"composer config type project\".</warning> +<warning>Alternatively you can use --bump-after-update=dev to only bump dependencies within \"require-dev\".</warning> +No requirements to update in ./composer.json.", + true, + ), + ( + "update & bump with lock", + root_dep_and_transitive_dep(), + vec![ + ("--bump-after-update", PhpMixed::from(true)), + ("--lock", PhpMixed::from(true)), + ], + "Loading composer repositories with package information +Updating dependencies +Nothing to modify in lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove", + true, + ), + ( + "update & bump dev only", + root_dep_and_transitive_dep(), + vec![("--bump-after-update", PhpMixed::from("dev"))], + "Loading composer repositories with package information +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Locking dep/pkg (1.0.2) + - Locking root/req (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals + - Installing dep/pkg (1.0.2) + - Installing root/req (1.0.0) +Bumping dependencies +No requirements to update in ./composer.json.", + true, + ), + ( + "update & dump with failing update", + root_dep_and_transitive_dep(), + vec![ + ("--with", PhpMixed::List(vec![PhpMixed::from("dep/pkg:^2")])), + ("--bump-after-update", PhpMixed::from(true)), + ], + "Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires root/req 1.* -> satisfiable by root/req[1.0.0]. + - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (dep/pkg:^2).", + false, + ), + ( + "update with replaced name filter fails to resolve", + root_dep_and_transitive_dep(), + vec![("--with", PhpMixed::List(vec![PhpMixed::from("replaced/pkg:^2")]))], + "Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires root/req 1.* -> satisfiable by root/req[1.0.0]. + - root/req 1.0.0 requires dep/pkg ^1 -> found dep/pkg[1.0.0, 1.0.1, 1.0.2] but it conflicts with your temporary update constraint (replaced/pkg:^2).", + false, + ), + ] +} + #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_update() { - todo!() + for (label, composer_json, command, expected, create_lock) in provide_updates() { + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + if create_lock { + create_composer_lock(&[], &[]); + } + + let mut app_tester = get_application_tester(); + let mut args = vec![ + ("command", PhpMixed::from("update")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ]; + args.extend(command); + app_tester.run(input(args), RunOptions::default()).unwrap(); + + assert_string_matches_format(expected.trim(), app_tester.get_display().trim()); + let _ = label; + } } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_update_with_patch_only() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "root/req", "version": "1.0.0" }, + { "name": "root/req", "version": "1.0.1" }, + { "name": "root/req", "version": "1.1.0" }, + { "name": "root/req2", "version": "1.0.0" }, + { "name": "root/req2", "version": "1.0.1" }, + { "name": "root/req2", "version": "1.1.0" }, + { "name": "root/req3", "version": "1.0.0" }, + { "name": "root/req3", "version": "1.0.1" }, + { "name": "root/req3", "version": "1.1.0" }, + ] } }, + "require": { "root/req": "1.*", "root/req2": "1.*", "root/req3": "1.*" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let package = get_package("root/req", "1.0.0"); + let package2 = get_package("root/req2", "1.0.0"); + let package3 = get_package("root/req3", "1.0.0"); + create_composer_lock(&[package, package2, package3], &[]); + + let mut app_tester = get_application_tester(); + // root/req fails because of incompatible --with requirement + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-install", PhpMixed::from(true)), + ("--patch-only", PhpMixed::from(true)), + ( + "--with", + PhpMixed::List(vec![PhpMixed::from("root/req:^1.1")]), + ), + ]), + RunOptions::default(), + ) + .unwrap(); + + assert_string_matches_format( + "Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires root/req 1.*, found root/req[1.0.0, 1.0.1, 1.1.0] but it conflicts with your temporary update constraint (root/req:[[>= 1.1.0.0-dev < 2.0.0.0-dev] [>= 1.0.0.0-dev < 1.1.0.0-dev]]).", + app_tester.get_display().trim(), + ); + + let mut app_tester = get_application_tester(); + // root/req upgrades to 1.0.1 as that is compatible with the --with requirement now + // root/req2 upgrades to 1.0.1 only due to --patch-only + // root/req3 does not update as it is not in the allowlist + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-install", PhpMixed::from(true)), + ("--patch-only", PhpMixed::from(true)), + ( + "--with", + PhpMixed::List(vec![PhpMixed::from("root/req:^1.0.1")]), + ), + ( + "packages", + PhpMixed::List(vec![ + PhpMixed::from("root/req"), + PhpMixed::from("root/req2"), + ]), + ), + ]), + RunOptions::default(), + ) + .unwrap(); + + assert_string_matches_format( + "Loading composer repositories with package information +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Upgrading root/req (1.0.0 => 1.0.1) + - Upgrading root/req2 (1.0.0 => 1.0.1)", + app_tester.get_display().trim(), + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_interactive_mode_throws_if_no_package_to_update() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "root/req", "version": "1.0.0" }, + ] } }, + "require": { "root/req": "1.*" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + create_composer_lock(&[get_package("root/req", "1.0.0")], &[]); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--interactive", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .expect_err("expected an exception when there is no package to update"); + assert!( + err.to_string() + .contains("Could not find any package with new versions available"), + "got: {err}" + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_interactive_mode_throws_if_no_package_entered() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "root/req", "version": "1.0.0" }, + { "name": "root/req", "version": "1.0.1" }, + ] } }, + "require": { "root/req": "1.*" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + create_composer_lock(&[get_package("root/req", "1.0.0")], &[]); + + let mut app_tester = get_application_tester(); + app_tester.set_inputs(vec!["".to_string()]); + let err = app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--interactive", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .expect_err("expected an exception when no package name is entered"); + assert!( + err.to_string() + .contains(r#"No package named "" is installed."#), + "got: {err}" + ); +} + +/// ref: UpdateCommandTest::provideInteractiveUpdates +fn provide_interactive_updates() -> Vec<(Vec<(&'static str, &'static str)>, &'static str)> { + vec![ + ( + vec![("dep/pkg", "1.0.1")], + "Lock file operations: 1 install, 1 update, 0 removals + - Locking another-dep/pkg (1.0.2) + - Upgrading dep/pkg (1.0.1 => 1.0.2) +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 1 update, 0 removals + - Upgrading dep/pkg (1.0.1 => 1.0.2) + - Installing another-dep/pkg (1.0.2)", + ), + ( + vec![("dep/pkg", "1.0.1"), ("another-dep/pkg", "1.0.2")], + "Lock file operations: 0 installs, 1 update, 0 removals + - Upgrading dep/pkg (1.0.1 => 1.0.2) +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 1 update, 0 removals + - Upgrading dep/pkg (1.0.1 => 1.0.2)", + ), + ] } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer/createComposerLock/createInstalledJson harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_interactive_tmp() { - todo!() + for (package_names, expected) in provide_interactive_updates() { + let composer_json = serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "root/req", "version": "1.0.0", "require": { "dep/pkg": "^1" } }, + { "name": "dep/pkg", "version": "1.0.0" }, + { "name": "dep/pkg", "version": "1.0.1" }, + { "name": "dep/pkg", "version": "1.0.2" }, + { "name": "another-dep/pkg", "version": "1.0.2" }, + ] } }, + "require": { "root/req": "1.*" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let root_package = get_package("root/req", "1.0.0"); + let mut packages = vec![root_package.clone()]; + for (pkg, ver) in &package_names { + packages.push(get_package(pkg, ver)); + } + + let mut requires = indexmap::IndexMap::new(); + requires.insert( + "dep/pkg".to_string(), + Link::new( + "root/req".to_string(), + "dep/pkg".to_string(), + AnyConstraint::MatchAll(MatchAllConstraint::new(None)), + Some(Link::TYPE_REQUIRE.to_string()), + "^1".to_string(), + ), + ); + requires.insert( + "another-dep/pkg".to_string(), + Link::new( + "root/req".to_string(), + "another-dep/pkg".to_string(), + AnyConstraint::MatchAll(MatchAllConstraint::new(None)), + Some(Link::TYPE_REQUIRE.to_string()), + "^1".to_string(), + ), + ); + root_package.__set_requires(requires); + + create_composer_lock(&packages, &[]); + create_installed_json(&packages, &[], true); + + let mut app_tester = get_application_tester(); + let mut inputs: Vec<String> = package_names + .iter() + .map(|(pkg, _)| pkg.to_string()) + .collect(); + inputs.push("".to_string()); + inputs.push("yes".to_string()); + app_tester.set_inputs(inputs); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--interactive", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--dry-run", PhpMixed::from(true)), + ]), + RunOptions { + interactive: Some(true), + ..RunOptions::default() + }, + ) + .unwrap(); + + assert!( + app_tester.get_display().trim().ends_with(expected.trim()), + "expected display to end with:\n{expected}\n--- got ---\n{}", + app_tester.get_display() + ); + } } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_no_security_blocking_allows_insecure_packages() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { "packages": { + "type": "package", + "package": [ + { "name": "vulnerable/pkg", "version": "1.0.0" }, + { "name": "vulnerable/pkg", "version": "1.1.0" }, + ], + "security-advisories": { + "vulnerable/pkg": [ + { + "advisoryId": "PKSA-test-001", + "packageName": "vulnerable/pkg", + "remoteId": "CVE-2024-1234", + "title": "Test Security Vulnerability", + "link": "https://example.com/advisory", + "cve": "CVE-2024-1234", + "affectedVersions": ">=1.1.0,<2.0.0", + "source": "test", + "reportedAt": "2024-01-01 00:00:00", + "composerRepository": "Package Repository", + "severity": "high", + "sources": [ { "name": "test", "remoteId": "CVE-2024-1234" } ], + }, + ], + }, + } }, + "require": { "vulnerable/pkg": "^1.0" }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + // Test 1: Without --no-security-blocking, the vulnerable version 1.1.0 should be filtered out + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-install", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + let display = app_tester.get_display(); + assert!( + display.contains("Locking vulnerable/pkg (1.0.0)"), + "got: {display}" + ); + assert!( + !display.contains("Locking vulnerable/pkg (1.1.0)"), + "got: {display}" + ); + + // Test 2: With --no-security-blocking, the vulnerable version 1.1.0 should be allowed + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--no-install", PhpMixed::from(true)), + ("--no-security-blocking", PhpMixed::from(true)), + ]), + RunOptions::default(), + ) + .unwrap(); + let display = app_tester.get_display(); + assert!( + display.contains("Locking vulnerable/pkg (1.1.0)"), + "got: {display}" + ); + assert!( + !display.contains("Locking vulnerable/pkg (1.0.0)"), + "got: {display}" + ); } #[test] -#[ignore = "requires ApplicationTester and TestCase::initTempComposer harness (not implemented)"] +#[serial] +#[ignore = "UpdateCommand::configure sets an empty InputDefinition (Phase-C: InputArgument/InputOption modeling pending), so the command rejects its own options, failing with e.g. \"The \\\"--dry-run\\\" option does not exist.\""] fn test_bump_after_update_without_lockfile() { - todo!() + let composer_json = serde_json::json!({ + "repositories": { "packages": { "type": "package", "package": [ + { "name": "root/a", "version": "1.0.0" }, + { "name": "root/a", "version": "1.1.0" }, + ] } }, + "require-dev": { "root/a": "^1.0.0" }, + "config": { "lock": false }, + }); + let _tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + input(vec![ + ("command", PhpMixed::from("update")), + ("--dry-run", PhpMixed::from(true)), + ("--no-audit", PhpMixed::from(true)), + ("--bump-after-update", PhpMixed::from("dev")), + ]), + RunOptions::default(), + ) + .unwrap(); + + assert_string_matches_format( + "Loading composer repositories with package information +Updating dependencies +Package operations: 1 install, 0 updates, 0 removals + - Installing root/a (1.1.0) +Bumping dependencies +./composer.json would be updated with: + - require-dev.root/a: ^1.1.0", + app_tester.get_display().trim(), + ); } |
