aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe/tests/command/install_command_test.rs202
-rw-r--r--crates/shirabe/tests/command/reinstall_command_test.rs103
-rw-r--r--crates/shirabe/tests/command/require_command_test.rs393
-rw-r--r--crates/shirabe/tests/command/update_command_test.rs624
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(&regex::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(),
+ );
}