diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-25 17:49:35 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-26 00:20:05 +0900 |
| commit | d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad (patch) | |
| tree | 0a85bd085961fcdfd5570cafc3b44ba5eb8cb152 /crates/shirabe/tests/command | |
| parent | 22d0b327368e5f39de6f381046c081d08efdba15 (diff) | |
| download | php-shirabe-d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad.tar.gz php-shirabe-d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad.tar.zst php-shirabe-d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad.zip | |
test: port 32 command/repository/downloader tests
Add create_installed_json/create_composer_lock test helpers. Port command (8),
repository path/forgejo/perforce/vcs (11), and fossil/hg/download_manager (13)
tests. Fix production porting bugs: root_package_loader/forgejo_url/version_bumper
regex delimiters, repository_manager create_repository_by_class, array_loader
isset, licenses_command RefCell borrow; implement disk_free_space and
touch2/touch3 via libc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/command')
| -rw-r--r-- | crates/shirabe/tests/command/bump_command_test.rs | 206 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/fund_command_test.rs | 198 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/licenses_command_test.rs | 321 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/status_command_test.rs | 41 | ||||
| -rw-r--r-- | crates/shirabe/tests/command/suggests_command_test.rs | 387 |
5 files changed, 1106 insertions, 47 deletions
diff --git a/crates/shirabe/tests/command/bump_command_test.rs b/crates/shirabe/tests/command/bump_command_test.rs index 7accb75..509b42a 100644 --- a/crates/shirabe/tests/command/bump_command_test.rs +++ b/crates/shirabe/tests/command/bump_command_test.rs @@ -1,16 +1,206 @@ //! ref: composer/tests/Composer/Test/Command/BumpCommandTest.php -use crate::test_case::{RunOptions, get_application_tester, init_temp_composer}; +use crate::test_case::{ + RunOptions, create_composer_lock, create_installed_json, get_application_tester, + get_complete_package, init_temp_composer, +}; use serial_test::serial; +use shirabe::json::JsonFile; +use shirabe::package::handle::PackageInterfaceHandle; use shirabe_php_shim::PhpMixed; -#[ignore = "needs create_installed_json / create_composer_lock helpers \ - (InstalledFilesystemRepository / Locker::set_lock_data), and BumpCommand::execute calls \ - require_composer -> Factory, reaching ProcessExecutor (git) -> shirabe-php-shim \ - stream_set_blocking (stream.rs todo!(), requires fcntl(2))"] +/// ref: BumpCommandTest::testBump (data provider rolled into one body). +fn run_bump_case( + composer_json: serde_json::Value, + command: &[(&str, PhpMixed)], + expected: serde_json::Value, + lock: bool, + exit_code: i32, +) { + let tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let packages: Vec<PackageInterfaceHandle> = vec![ + get_complete_package("first/pkg", "2.3.4").into(), + get_complete_package("second/pkg", "3.4.0").into(), + ]; + let dev_packages: Vec<PackageInterfaceHandle> = + vec![get_complete_package("dev/pkg", "2.3.4.5").into()]; + + create_installed_json(&packages, &dev_packages, true); + if lock { + create_composer_lock(&packages, &dev_packages); + } + + let mut input: Vec<(PhpMixed, PhpMixed)> = + vec![(PhpMixed::from("command"), PhpMixed::from("bump"))]; + for (k, v) in command { + input.push((PhpMixed::from(*k), v.clone())); + } + + let mut app_tester = get_application_tester(); + let status_code = app_tester.run(input, RunOptions::default()).unwrap(); + assert_eq!(exit_code, status_code); + + let mut json = JsonFile::new("./composer.json".to_string(), None, None).unwrap(); + let read = json.read().unwrap(); + let actual: serde_json::Value = serde_json::from_str(&JsonFile::encode(&read)).unwrap(); + assert_eq!(expected, actual); + + drop(tear_down); +} + #[test] +#[serial] fn test_bump() { - todo!() + // 'bump all by default' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^v2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + &[], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + true, + 0, + ); + + // 'bump only dev with --dev-only' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + &[("--dev-only", PhpMixed::from(true))], + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + true, + 0, + ); + + // 'bump only non-dev with --no-dev-only' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + &[("--no-dev-only", PhpMixed::from(true))], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + true, + 0, + ); + + // 'bump only listed with packages arg' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + &[( + "packages", + PhpMixed::List(vec![ + PhpMixed::from("first/pkg:3.0.1"), + PhpMixed::from("dev/*"), + ]), + )], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + true, + 0, + ); + + // 'bump works from installed repo without lock file' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + }), + &[], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + }), + false, + 0, + ); + + // 'bump with --dry-run with packages to bump' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + &[("--dry-run", PhpMixed::from(true))], + serde_json::json!({ + "require": { "first/pkg": "^2.0", "second/pkg": "3.*" }, + "require-dev": { "dev/pkg": "~2.0" }, + }), + true, + 1, + ); + + // 'bump with --dry-run without packages to bump' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + &[("--dry-run", PhpMixed::from(true))], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + true, + 0, + ); + + // 'bump works with non-standard package' + run_bump_case( + serde_json::json!({ + "require": { "php": ">=5.3", "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + &[], + serde_json::json!({ + "require": { "php": ">=5.3", "first/pkg": "^2.3.4", "second/pkg": "^3.4" }, + "require-dev": { "dev/pkg": "^2.3.4.5" }, + }), + true, + 0, + ); + + // 'bump works with unknown package' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4", "third/pkg": "^1.2" }, + }), + &[], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4", "third/pkg": "^1.2" }, + }), + true, + 0, + ); + + // 'bump works with aliased package' + run_bump_case( + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "dev-bugfix as 3.4.x-dev" }, + }), + &[], + serde_json::json!({ + "require": { "first/pkg": "^2.3.4", "second/pkg": "dev-bugfix as 3.4.x-dev" }, + }), + true, + 0, + ); } #[test] @@ -44,10 +234,6 @@ fn test_bump_fails_on_non_existing_composer_file() { #[test] #[serial] -#[ignore = "CurlDownloader::new no longer panics (HTTP layer is now reqwest-based), but this path \ - still reaches an unrelated todo!() in shirabe-php-shim stream_set_blocking (stream.rs), \ - which requires fcntl(2). The companion test_bump_fails_on_non_existing_composer_file \ - covers the same error-output capture path without reaching that shim."] fn test_bump_fails_on_write_error_to_composer_file() { if shirabe_php_shim::function_exists("posix_getuid") && shirabe_php_shim::posix_getuid() == 0 { // ref: $this->markTestSkipped('Cannot run as root'); diff --git a/crates/shirabe/tests/command/fund_command_test.rs b/crates/shirabe/tests/command/fund_command_test.rs index f75090c..bbcc589 100644 --- a/crates/shirabe/tests/command/fund_command_test.rs +++ b/crates/shirabe/tests/command/fund_command_test.rs @@ -1,7 +1,201 @@ //! ref: composer/tests/Composer/Test/Command/FundCommandTest.php -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"] +use crate::test_case::{ + RunOptions, create_installed_json, get_application_tester, get_complete_package, + init_temp_composer, +}; +use indexmap::IndexMap; +use serial_test::serial; +use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; +use shirabe_php_shim::PhpMixed; + +fn funding_entry(r#type: &str, url: &str) -> IndexMap<String, PhpMixed> { + let mut m = IndexMap::new(); + m.insert("type".to_string(), PhpMixed::from(r#type)); + m.insert("url".to_string(), PhpMixed::from(url)); + m +} + +/// Runs one `useCaseProvider` case. +fn run_fund_case( + composer_json: serde_json::Value, + command: &[(&str, PhpMixed)], + funding: &[(&str, IndexMap<String, PhpMixed>)], + expected: &str, +) { + let tear_down = init_temp_composer(Some(&composer_json), None, None, true); + + let first = get_complete_package("first/pkg", "2.3.4"); + let stable = get_complete_package("stable/pkg", "1.0.0"); + let dev = get_complete_package("dev/pkg", "2.3.4.5"); + + let by_name: IndexMap<&str, &CompletePackageHandle> = [ + ("first/pkg", &first), + ("stable/pkg", &stable), + ("dev/pkg", &dev), + ] + .into_iter() + .collect(); + + for (pkg_name, info) in funding { + if let Some(pkg) = by_name.get(pkg_name) { + pkg.set_funding(vec![info.clone()]); + } + } + + let packages: Vec<PackageInterfaceHandle> = vec![first.into(), stable.into()]; + let dev_packages: Vec<PackageInterfaceHandle> = vec![dev.into()]; + + create_installed_json(&packages, &dev_packages, true); + + let mut input: Vec<(PhpMixed, PhpMixed)> = + vec![(PhpMixed::from("command"), PhpMixed::from("fund"))]; + for (k, v) in command { + input.push((PhpMixed::from(*k), v.clone())); + } + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + input, + RunOptions { + capture_stderr_separately: true, + ..RunOptions::default() + }, + ) + .unwrap(); + assert_eq!(0, status_code); + assert_eq!(expected.trim(), app_tester.get_display().trim()); + + drop(tear_down); +} + #[test] +#[serial] +#[ignore = "FundCommand queries every repository (incl. the default packagist ComposerRepository) over \ + HTTP for funding metadata before falling back to locally installed data, reaching \ + shirabe-php-shim curl.rs curl_version (todo!()); this path needs real network access"] fn test_fund_command() { - todo!() + // 'no funding links present, locally or remotely' + run_fund_case( + serde_json::json!({ + "repositories": [], + "require": { "first/pkg": "^2.0" }, + "require-dev": { "dev/pkg": "~4.0" }, + }), + &[], + &[], + "No funding links were found in your package dependencies. This doesn't mean they don't need your support!", + ); + + // 'funding links set locally are used as fallback if not found remotely' + run_fund_case( + serde_json::json!({ + "repositories": [], + "require": { "first/pkg": "^2.0" }, + "require-dev": { "dev/pkg": "~4.0" }, + }), + &[], + &[ + ( + "first/pkg", + funding_entry("github", "https://github.com/composer-test-data"), + ), + ( + "dev/pkg", + funding_entry("github", "https://github.com/composer-test-data-dev"), + ), + ], + "The following packages were found in your dependencies which publish funding information: + +dev + pkg + https://github.com/sponsors/composer-test-data-dev + +first + https://github.com/sponsors/composer-test-data + +Please consider following these links and sponsoring the work of package authors! +Thank you!", + ); + + // 'funding links set remotely are used as primary if found' + run_fund_case( + serde_json::json!({ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "first/pkg", "version": "dev-foo", "funding": [{ "type": "github", "url": "https://github.com/test-should-not-be-used" }] }, + { "name": "first/pkg", "version": "dev-main", "default-branch": true, "funding": [{ "type": "custom", "url": "https://example.org" }] }, + { "name": "dev/pkg", "version": "dev-foo", "default-branch": true, "funding": [{ "type": "github", "url": "https://github.com/org" }] }, + { "name": "stable/pkg", "version": "1.0.0", "funding": [{ "type": "github", "url": "org2" }] }, + ], + }, + ], + "require": { "first/pkg": "^2.0", "stable/pkg": "^1.0" }, + "require-dev": { "dev/pkg": "~4.0" }, + }), + &[], + &[ + ( + "first/pkg", + funding_entry("github", "https://github.com/composer-test-data"), + ), + ( + "dev/pkg", + funding_entry("github", "https://github.com/composer-test-data-dev"), + ), + ( + "stable/pkg", + funding_entry("github", "https://github.com/composer-test-data-stable"), + ), + ], + "The following packages were found in your dependencies which publish funding information: + +dev + pkg + https://github.com/sponsors/org + +first + https://example.org + +stable + https://github.com/sponsors/composer-test-data-stable + +Please consider following these links and sponsoring the work of package authors! +Thank you!", + ); + + // 'format funding links as JSON' + run_fund_case( + serde_json::json!({ + "repositories": [], + "require": { "first/pkg": "^2.0" }, + "require-dev": { "dev/pkg": "~4.0" }, + }), + &[("--format", PhpMixed::from("json"))], + &[ + ( + "first/pkg", + funding_entry("github", "https://github.com/composer-test-data"), + ), + ( + "dev/pkg", + funding_entry("github", "https://github.com/composer-test-data-dev"), + ), + ], + r#"{ + "dev": { + "https://github.com/sponsors/composer-test-data-dev": [ + "pkg" + ] + }, + "first": { + "https://github.com/sponsors/composer-test-data": [ + "pkg" + ] + } +}"#, + ); } diff --git a/crates/shirabe/tests/command/licenses_command_test.rs b/crates/shirabe/tests/command/licenses_command_test.rs index 6bfe832..f680621 100644 --- a/crates/shirabe/tests/command/licenses_command_test.rs +++ b/crates/shirabe/tests/command/licenses_command_test.rs @@ -1,71 +1,336 @@ //! ref: composer/tests/Composer/Test/Command/LicensesCommandTest.php -fn set_up() { - // Builds the temp project and installed.json/composer.lock fixtures via - // initTempComposer/createInstalledJson/createComposerLock, none of which are ported yet. - todo!() +use crate::test_case::{ + RunOptions, TearDown, create_composer_lock, create_installed_json, get_application_tester, + get_complete_package, init_temp_composer, +}; +use serial_test::serial; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; + +/// ref: LicensesCommandTest::setUp +fn set_up() -> TearDown { + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "name": "test/pkg", + "version": "1.2.3", + "license": "MIT", + "require": { + "first/pkg": "^2.0", + "second/pkg": "3.*", + "third/pkg": "^1.3", + }, + "require-dev": { + "dev/pkg": "~2.0", + }, + })), + None, + None, + true, + ); + + let first = get_complete_package("first/pkg", "2.3.4"); + first.set_license(vec!["MIT".to_string()]); + + let second = get_complete_package("second/pkg", "3.4.0"); + second.set_license(vec!["LGPL-2.0-only".to_string()]); + second.set_homepage("https://example.org".to_string()); + + let third = get_complete_package("third/pkg", "1.5.4"); + + let dev = get_complete_package("dev/pkg", "2.3.4.5"); + dev.set_license(vec!["MIT".to_string()]); + + let packages: Vec<PackageInterfaceHandle> = vec![first.into(), second.into(), third.into()]; + let dev_packages: Vec<PackageInterfaceHandle> = vec![dev.into()]; + + create_installed_json(&packages, &dev_packages, true); + create_composer_lock(&packages, &dev_packages); + + tear_down +} + +/// Asserts each non-blank line of `display` matches the regex built from `expected[i]` +/// (`implode("\s+", expected[i])` in PHP). +fn assert_lines(display: &str, expected: &[Vec<&str>]) { + let mut i = 0usize; + for line in display.split('\n') { + if line.trim().is_empty() { + i += 1; + continue; + } + assert!(i < expected.len(), "Got more output lines than expected"); + let pattern = expected[i] + .iter() + .map(|s| regex::escape(s)) + .collect::<Vec<_>>() + .join(r"\s+"); + let re = regex::Regex::new(&pattern).unwrap(); + assert!( + re.is_match(line), + "line {} {:?} did not match {:?}", + i, + line, + pattern + ); + i += 1; + } } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \ + PhpMixed::Object carries no runtime class tag, so the Symfony Table helper cannot \ + distinguish TableSeparator/TableCell rows yet"] fn test_basic_run() { - set_up(); + let _tear_down = set_up(); - todo!() + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("license"))], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + + let expected = vec![ + vec!["Name:", "test/pkg"], + vec!["Version:", "1.2.3"], + vec!["Licenses:", "MIT"], + vec!["Dependencies:"], + vec![], + vec!["Name", "Version", "Licenses"], + vec!["dev/pkg", "2.3.4.5", "MIT"], + vec!["first/pkg", "2.3.4", "MIT"], + vec!["second/pkg", "3.4.0", "LGPL-2.0-only"], + vec!["third/pkg", "1.5.4", "none"], + ]; + assert_lines(&app_tester.get_display(), &expected); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \ + see test_basic_run"] fn test_no_dev() { - set_up(); + let _tear_down = set_up(); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--no-dev"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); - todo!() + let expected = vec![ + vec!["Name:", "test/pkg"], + vec!["Version:", "1.2.3"], + vec!["Licenses:", "MIT"], + vec!["Dependencies:"], + vec![], + vec!["Name", "Version", "Licenses"], + vec!["first/pkg", "2.3.4", "MIT"], + vec!["second/pkg", "3.4.0", "LGPL-2.0-only"], + vec!["third/pkg", "1.5.4", "none"], + ]; + assert_lines(&app_tester.get_display(), &expected); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] fn test_format_json() { - set_up(); + let _tear_down = set_up(); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--format"), PhpMixed::from("json")), + ], + RunOptions { + capture_stderr_separately: true, + ..RunOptions::default() + }, + ) + .unwrap(); + assert_eq!(0, status_code); - todo!() + let expected = serde_json::json!({ + "name": "test/pkg", + "version": "1.2.3", + "license": ["MIT"], + "dependencies": { + "dev/pkg": { "version": "2.3.4.5", "license": ["MIT"] }, + "first/pkg": { "version": "2.3.4", "license": ["MIT"] }, + "second/pkg": { "version": "3.4.0", "license": ["LGPL-2.0-only"] }, + "third/pkg": { "version": "1.5.4", "license": [] }, + }, + }); + + let actual: serde_json::Value = serde_json::from_str(&app_tester.get_display()).unwrap(); + assert_eq!(expected, actual); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "renders a Table via SymfonyStyle::table, which reaches shirabe_php_shim::instance_of \ + (var.rs todo!()); see test_basic_run"] fn test_format_summary() { - set_up(); + let _tear_down = set_up(); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--format"), PhpMixed::from("summary")), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); - todo!() + let expected = vec![ + ("-", "-"), + ("License", "Number of dependencies"), + ("-", "-"), + ("MIT", "2"), + ("LGPL-2.0-only", "1"), + ("none", "1"), + ("-", "-"), + ]; + + let display = app_tester.get_display(); + let lines: Vec<&str> = display.split('\n').collect(); + for (i, (key, value)) in expected.iter().enumerate() { + let pattern = format!(r"{}\s+{}", regex::escape(key), regex::escape(value)); + let re = regex::Regex::new(&pattern).unwrap(); + assert!( + re.is_match(lines[i]), + "line {} {:?} did not match {:?}", + i, + lines[i], + pattern + ); + } } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] fn test_format_unknown() { - set_up(); + let _tear_down = set_up(); - todo!() + let mut app_tester = get_application_tester(); + let result = app_tester.run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--format"), PhpMixed::from("unknown")), + ], + RunOptions::default(), + ); + assert!( + result.is_err(), + "expected a RuntimeException, got {:?}", + result + ); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \ + see test_basic_run"] fn test_locked() { - set_up(); + let _tear_down = set_up(); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--locked"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); - todo!() + let expected = vec![ + vec!["Name:", "test/pkg"], + vec!["Version:", "1.2.3"], + vec!["Licenses:", "MIT"], + vec!["Dependencies:"], + vec![], + vec!["Name", "Version", "Licenses"], + vec!["dev/pkg", "2.3.4.5", "MIT"], + vec!["first/pkg", "2.3.4", "MIT"], + vec!["second/pkg", "3.4.0", "LGPL-2.0-only"], + vec!["third/pkg", "1.5.4", "none"], + ]; + assert_lines(&app_tester.get_display(), &expected); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] +#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \ + see test_basic_run"] fn test_locked_no_dev() { - set_up(); + let _tear_down = set_up(); - todo!() + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--locked"), PhpMixed::from(true)), + (PhpMixed::from("--no-dev"), PhpMixed::from(true)), + ], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + + let expected = vec![ + vec!["Name:", "test/pkg"], + vec!["Version:", "1.2.3"], + vec!["Licenses:", "MIT"], + vec!["Dependencies:"], + vec![], + vec!["Name", "Version", "Licenses"], + vec!["first/pkg", "2.3.4", "MIT"], + vec!["second/pkg", "3.4.0", "LGPL-2.0-only"], + vec!["third/pkg", "1.5.4", "none"], + ]; + assert_lines(&app_tester.get_display(), &expected); } #[test] -#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"] +#[serial] fn test_locked_without_lock_file() { - set_up(); + let _tear_down = set_up(); + + // Remove the lock file + let _ = std::fs::remove_file("./composer.lock"); - todo!() + let mut app_tester = get_application_tester(); + let result = app_tester.run( + vec![ + (PhpMixed::from("command"), PhpMixed::from("license")), + (PhpMixed::from("--locked"), PhpMixed::from(true)), + ], + RunOptions::default(), + ); + let err = result.expect_err("expected an UnexpectedValueException"); + assert!( + err.to_string() + .contains("Valid composer.json and composer.lock files are required to run this command with --locked"), + "unexpected error: {}", + err + ); } diff --git a/crates/shirabe/tests/command/status_command_test.rs b/crates/shirabe/tests/command/status_command_test.rs index 6154ea0..aa0ecc6 100644 --- a/crates/shirabe/tests/command/status_command_test.rs +++ b/crates/shirabe/tests/command/status_command_test.rs @@ -1,12 +1,47 @@ //! ref: composer/tests/Composer/Test/Command/StatusCommandTest.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_complete_package, init_temp_composer, +}; +use serial_test::serial; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; + #[test] +#[serial] fn test_no_local_changes() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ "require": { "root/req": "1.*" } })), + None, + None, + true, + ); + + let package = get_complete_package("root/req", "1.0.0"); + package.__set_type("metapackage".to_string()); + + let packages: Vec<PackageInterfaceHandle> = vec![package.into()]; + + create_composer_lock(&packages, &[]); + create_installed_json(&packages, &[], true); + + let mut app_tester = get_application_tester(); + app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("status"))], + RunOptions::default(), + ) + .unwrap(); + + assert_eq!("No local changes", app_tester.get_display().trim()); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"] +#[ignore = "exercises `install` over the network (downloads composer/class-map-generator from a git \ + source or smarty/smarty from a dist zip), then mutates the installed package and runs \ + `status`; the install path needs real network access"] #[test] fn test_locally_modified_packages() { todo!() diff --git a/crates/shirabe/tests/command/suggests_command_test.rs b/crates/shirabe/tests/command/suggests_command_test.rs index c23691b..8bbb950 100644 --- a/crates/shirabe/tests/command/suggests_command_test.rs +++ b/crates/shirabe/tests/command/suggests_command_test.rs @@ -1,13 +1,392 @@ //! ref: composer/tests/Composer/Test/Command/SuggestsCommandTest.php -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"] +use crate::test_case::{ + RunOptions, create_composer_lock, create_installed_json, get_application_tester, + get_complete_package, get_version_constraint, init_temp_composer, +}; +use indexmap::IndexMap; +use serial_test::serial; +use shirabe::package::Link; +use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; +use shirabe_php_shim::PhpMixed; + +/// ref: SuggestsCommandTest::getPackageWithSuggestAndRequires +fn get_package_with_suggest_and_requires( + name: &str, + version: &str, + suggests: &[(&str, &str)], + requires: &[(&str, Link)], + require_devs: &[(&str, Link)], +) -> CompletePackageHandle { + let pkg = get_complete_package(name, version); + let suggests_map: IndexMap<String, String> = suggests + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + pkg.__set_suggests(suggests_map); + let requires_map: IndexMap<String, Link> = requires + .iter() + .map(|(k, v)| (k.to_string(), v.clone())) + .collect(); + pkg.__set_requires(requires_map); + let dev_requires_map: IndexMap<String, Link> = require_devs + .iter() + .map(|(k, v)| (k.to_string(), v.clone())) + .collect(); + pkg.__set_dev_requires(dev_requires_map); + pkg +} + +fn link(source: &str, target: &str, description: &str) -> Link { + Link::new( + source.to_string(), + target.to_string(), + get_version_constraint(">=", "1.0"), + Some(description.to_string()), + "^1.0".to_string(), + ) +} + #[test] +#[serial] fn test_installed_packages_with_no_suggestions() { - todo!() + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + { "name": "vendor1/package1", "version": "1.0.0" }, + { "name": "vendor2/package2", "version": "1.0.0" }, + ], + }, + }, + "require": { + "vendor1/package1": "1.*", + "vendor2/package2": "1.*", + }, + })), + None, + None, + true, + ); + + let packages: Vec<PackageInterfaceHandle> = vec![ + get_complete_package("vendor1/package1", "1.0.0").into(), + get_complete_package("vendor2/package2", "1.0.0").into(), + ]; + + create_installed_json(&packages, &[], true); + create_composer_lock(&packages, &[]); + + let mut app_tester = get_application_tester(); + let status_code = app_tester + .run( + vec![(PhpMixed::from("command"), PhpMixed::from("suggest"))], + RunOptions::default(), + ) + .unwrap(); + assert_eq!(0, status_code); + assert!( + app_tester.get_display().trim().is_empty(), + "expected empty display, got: {:?}", + app_tester.get_display() + ); + + drop(tear_down); +} + +/// Builds the package set shared by every `test_suggest` case. +fn suggest_packages() -> (Vec<PackageInterfaceHandle>, Vec<PackageInterfaceHandle>) { + let packages: Vec<PackageInterfaceHandle> = vec![ + get_package_with_suggest_and_requires( + "vendor1/package1", + "1.0.0", + &[("vendor3/suggested", "helpful for vendor1/package1")], + &[( + "vendor6/package6", + link("vendor1/package1", "vendor6/package6", Link::TYPE_REQUIRE), + )], + &[ + ( + "vendor4/dev-suggested", + link( + "vendor1/package1", + "vendor4/dev-suggested", + Link::TYPE_DEV_REQUIRE, + ), + ), + ( + "vendor3/suggested", + link( + "vendor1/package1", + "vendor3/suggested", + Link::TYPE_DEV_REQUIRE, + ), + ), + ], + ) + .into(), + get_package_with_suggest_and_requires( + "vendor6/package6", + "1.0.0", + &[("vendor7/transitive", "helpful for vendor6/package6")], + &[], + &[], + ) + .into(), + ]; + let dev_packages: Vec<PackageInterfaceHandle> = vec![ + get_package_with_suggest_and_requires( + "vendor2/package2", + "1.0.0", + &[("vendor4/dev-suggested", "helpful for vendor2/package2")], + &[( + "vendor5/dev-package", + link( + "vendor2/package2", + "vendor5/dev-package", + Link::TYPE_REQUIRE, + ), + )], + &[], + ) + .into(), + get_package_with_suggest_and_requires( + "vendor5/dev-package", + "1.0.0", + &[("vendor8/dev-transitive", "helpful for vendor5/dev-package")], + &[], + &[( + "vendor8/dev-transitive", + link( + "vendor5/dev-package", + "vendor8/dev-transitive", + Link::TYPE_DEV_REQUIRE, + ), + )], + ) + .into(), + ]; + (packages, dev_packages) +} + +fn run_suggest_case(has_lock_file: bool, command: &[(&str, PhpMixed)], expected: &str) { + let tear_down = init_temp_composer( + Some(&serde_json::json!({ + "repositories": { + "packages": { + "type": "package", + "package": [ + { "name": "vendor1/package1", "version": "1.0.0", "suggests": { "vendor3/suggested": "helpful for vendor1/package1" }, "require": { "vendor6/package6": "^1.0" }, "require-dev": { "vendor3/suggested": "^1.0", "vendor4/dev-suggested": "^1.0" } }, + { "name": "vendor2/package2", "version": "1.0.0", "suggests": { "vendor4/dev-suggested": "helpful for vendor2/package2" }, "require": { "vendor5/dev-package": "^1.0" } }, + { "name": "vendor5/dev-package", "version": "1.0.0", "suggests": { "vendor8/dev-transitive": "helpful for vendor5/dev-package" }, "require-dev": { "vendor8/dev-transitive": "^1.0" } }, + { "name": "vendor6/package6", "version": "1.0.0", "suggests": { "vendor7/transitive": "helpful for vendor6/package6" } }, + ], + }, + }, + "require": { "vendor1/package1": "^1" }, + "require-dev": { "vendor2/package2": "^1" }, + })), + None, + None, + true, + ); + + let (packages, dev_packages) = suggest_packages(); + create_installed_json(&packages, &dev_packages, true); + if has_lock_file { + create_composer_lock(&packages, &dev_packages); + } + + let mut input: Vec<(PhpMixed, PhpMixed)> = + vec![(PhpMixed::from("command"), PhpMixed::from("suggest"))]; + for (k, v) in command { + input.push((PhpMixed::from(*k), v.clone())); + } + + let mut app_tester = get_application_tester(); + let status_code = app_tester.run(input, RunOptions::default()).unwrap(); + assert_eq!(0, status_code); + assert_eq!(expected.trim(), app_tester.get_display().trim()); + + drop(tear_down); } -#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, get_application_tester (ApplicationTester), and get_version_parser infrastructure"] #[test] +#[serial] fn test_suggest() { - todo!() + let t = PhpMixed::from(true); + let by_package = ("--by-package", t.clone()); + let by_suggestion = ("--by-suggestion", t.clone()); + let no_dev = ("--no-dev", t.clone()); + let all = ("--all", t.clone()); + let list = ("--list", t.clone()); + + // 'with lockfile, show suggested' / 'without lockfile, show suggested' + let basic = "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all"; + run_suggest_case(true, &[], basic); + run_suggest_case(false, &[], basic); + + // 'with lockfile, show suggested (excluding dev)' + run_suggest_case( + true, + &[no_dev.clone()], + "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all", + ); + // 'without lockfile, show suggested (excluding dev)' + run_suggest_case(false, &[no_dev.clone()], basic); + + let all_suggested = "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +vendor5/dev-package suggests: + - vendor8/dev-transitive: helpful for vendor5/dev-package + +vendor6/package6 suggests: + - vendor7/transitive: helpful for vendor6/package6"; + run_suggest_case(true, &[all.clone()], all_suggested); + run_suggest_case(false, &[all.clone()], all_suggested); + + // 'with lockfile, show all suggested (excluding dev)' + run_suggest_case( + true, + &[all.clone(), no_dev.clone()], + "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor6/package6 suggests: + - vendor7/transitive: helpful for vendor6/package6", + ); + run_suggest_case(false, &[all.clone(), no_dev.clone()], all_suggested); + + // grouped by package + run_suggest_case(true, &[by_package.clone()], basic); + run_suggest_case(false, &[by_package.clone()], basic); + run_suggest_case( + true, + &[by_package.clone(), no_dev.clone()], + "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all", + ); + run_suggest_case(false, &[by_package.clone(), no_dev.clone()], basic); + + // grouped by suggestion + let by_suggestion_out = "vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all"; + run_suggest_case(true, &[by_suggestion.clone()], by_suggestion_out); + run_suggest_case(false, &[by_suggestion.clone()], by_suggestion_out); + run_suggest_case( + true, + &[by_suggestion.clone(), no_dev.clone()], + "vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all", + ); + run_suggest_case( + false, + &[by_suggestion.clone(), no_dev.clone()], + by_suggestion_out, + ); + + // grouped by package and suggestion + let by_both = "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2 + +------------------------------------------------------------------------------ +vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +vendor4/dev-suggested is suggested by: + - vendor2/package2: helpful for vendor2/package2 + +2 additional suggestions by transitive dependencies can be shown with --all"; + run_suggest_case(true, &[by_package.clone(), by_suggestion.clone()], by_both); + run_suggest_case(false, &[by_package.clone(), by_suggestion.clone()], by_both); + run_suggest_case( + true, + &[by_package.clone(), by_suggestion.clone(), no_dev.clone()], + "vendor1/package1 suggests: + - vendor3/suggested: helpful for vendor1/package1 + +------------------------------------------------------------------------------ +vendor3/suggested is suggested by: + - vendor1/package1: helpful for vendor1/package1 + +1 additional suggestions by transitive dependencies can be shown with --all", + ); + run_suggest_case( + false, + &[by_package.clone(), by_suggestion.clone(), no_dev.clone()], + by_both, + ); + + // show suggested for package + let for_pkg = "vendor2/package2 suggests: + - vendor4/dev-suggested: helpful for vendor2/package2"; + run_suggest_case( + true, + &[( + "packages", + PhpMixed::List(vec![PhpMixed::from("vendor2/package2")]), + )], + for_pkg, + ); + run_suggest_case( + false, + &[( + "packages", + PhpMixed::List(vec![PhpMixed::from("vendor2/package2")]), + )], + for_pkg, + ); + + // list suggested + let list_out = "vendor3/suggested +vendor4/dev-suggested"; + run_suggest_case(true, &[list.clone()], list_out); + run_suggest_case(false, &[list.clone()], list_out); + run_suggest_case(true, &[list.clone(), no_dev.clone()], "vendor3/suggested"); + run_suggest_case(false, &[list.clone(), no_dev.clone()], list_out); + + let list_all = "vendor3/suggested +vendor4/dev-suggested +vendor7/transitive +vendor8/dev-transitive"; + run_suggest_case(true, &[list.clone(), all.clone()], list_all); + run_suggest_case(false, &[list.clone(), all.clone()], list_all); + run_suggest_case( + true, + &[list.clone(), all.clone(), no_dev.clone()], + "vendor3/suggested +vendor7/transitive", + ); + run_suggest_case( + false, + &[list.clone(), all.clone(), no_dev.clone()], + list_all, + ); } |
