diff options
Diffstat (limited to 'crates/mozart/src/commands')
24 files changed, 0 insertions, 8134 deletions
diff --git a/crates/mozart/src/commands/audit.rs b/crates/mozart/src/commands/audit.rs index f543cb4..5193b06 100644 --- a/crates/mozart/src/commands/audit.rs +++ b/crates/mozart/src/commands/audit.rs @@ -191,301 +191,3 @@ fn load_locked_packages(working_dir: &Path, no_dev: bool) -> anyhow::Result<Vec< Ok(packages) } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::repository::lockfile::{LockFile, LockedPackage}; - - fn make_pkg(name: &str, version: &str, version_normalized: Option<&str>) -> PackageInfo { - PackageInfo { - name: name.to_string(), - version: version.to_string(), - version_normalized: version_normalized.map(|s| s.to_string()), - abandoned_raw: None, - } - } - - fn make_pkg_abandoned(name: &str, version: &str, replacement: Option<&str>) -> PackageInfo { - let abandoned_raw = match replacement { - Some(r) => Some(serde_json::Value::String(r.to_string())), - None => Some(serde_json::Value::Bool(true)), - }; - PackageInfo { - name: name.to_string(), - version: version.to_string(), - version_normalized: None, - abandoned_raw, - } - } - - #[test] - fn test_load_installed_packages() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - let vendor_dir = working_dir.join("vendor"); - - let mut installed = mozart_core::repository::installed::InstalledPackages::new(); - installed.upsert(mozart_core::repository::installed::InstalledPackageEntry { - name: "monolog/monolog".to_string(), - version: "1.5.0".to_string(), - version_normalized: Some("1.5.0.0".to_string()), - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - }); - installed.write(&vendor_dir).unwrap(); - - let packages = load_installed_packages(working_dir, false).unwrap(); - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].name, "monolog/monolog"); - assert_eq!(packages[0].version, "1.5.0"); - } - - #[test] - fn test_load_installed_packages_no_dev() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - let vendor_dir = working_dir.join("vendor"); - - let mut installed = mozart_core::repository::installed::InstalledPackages::new(); - installed.upsert(mozart_core::repository::installed::InstalledPackageEntry { - name: "monolog/monolog".to_string(), - version: "1.5.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - }); - installed.upsert(mozart_core::repository::installed::InstalledPackageEntry { - name: "phpunit/phpunit".to_string(), - version: "10.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - }); - installed - .dev_package_names - .push("phpunit/phpunit".to_string()); - installed.write(&vendor_dir).unwrap(); - - let packages = load_installed_packages(working_dir, true).unwrap(); - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].name, "monolog/monolog"); - } - - #[test] - fn test_load_locked_packages() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - - let lock = LockFile { - readme: LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages: vec![LockedPackage { - name: "psr/log".to_string(), - version: "3.0.0".to_string(), - version_normalized: Some("3.0.0.0".to_string()), - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: None, - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - }], - packages_dev: None, - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - }; - - lock.write_to_file(&working_dir.join("composer.lock")) - .unwrap(); - - let packages = load_locked_packages(working_dir, false).unwrap(); - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].name, "psr/log"); - assert_eq!(packages[0].version, "3.0.0"); - } - - #[test] - fn test_load_locked_packages_no_dev() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - - let lock = LockFile { - readme: LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages: vec![LockedPackage { - name: "psr/log".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: None, - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - }], - packages_dev: Some(vec![LockedPackage { - name: "phpunit/phpunit".to_string(), - version: "10.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: None, - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - }]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - }; - - lock.write_to_file(&working_dir.join("composer.lock")) - .unwrap(); - - let packages = load_locked_packages(working_dir, true).unwrap(); - assert_eq!(packages.len(), 1); - assert_eq!(packages[0].name, "psr/log"); - - let packages_all = load_locked_packages(working_dir, false).unwrap(); - assert_eq!(packages_all.len(), 2); - } - - #[test] - fn test_load_locked_packages_missing_lockfile() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let result = load_locked_packages(dir.path(), false); - assert!(result.is_err()); - let msg = result.unwrap_err().to_string(); - assert!(msg.contains("composer.lock")); - } - - #[test] - fn test_package_info_abandoned() { - let pkg = make_pkg_abandoned("old/pkg", "1.0.0", None); - assert!(pkg.is_abandoned()); - assert!(pkg.replacement_package().is_none()); - - let pkg_with_repl = make_pkg_abandoned("old/pkg", "1.0.0", Some("new/pkg")); - assert!(pkg_with_repl.is_abandoned()); - assert_eq!(pkg_with_repl.replacement_package(), Some("new/pkg")); - - let active_pkg = make_pkg("active/pkg", "1.0.0", None); - assert!(!active_pkg.is_abandoned()); - } - - #[test] - fn test_invalid_format() { - let format = "xml"; - assert!(format.parse::<AuditFormat>().is_err()); - } - - #[test] - fn test_valid_formats() { - for fmt in &["table", "plain", "json", "summary"] { - assert!( - fmt.parse::<AuditFormat>().is_ok(), - "format {fmt} should be valid" - ); - } - } - - #[test] - fn test_invalid_abandoned_value() { - assert!("maybe".parse::<AbandonedHandling>().is_err()); - } - - #[test] - fn test_valid_abandoned_values() { - for value in &["ignore", "report", "fail"] { - assert!( - value.parse::<AbandonedHandling>().is_ok(), - "abandoned value {value} should be valid" - ); - } - } -} diff --git a/crates/mozart/src/commands/browse.rs b/crates/mozart/src/commands/browse.rs index 957f4bc..4149cf8 100644 --- a/crates/mozart/src/commands/browse.rs +++ b/crates/mozart/src/commands/browse.rs @@ -174,95 +174,3 @@ fn which(cmd: &str) -> bool { .map(|o| o.status.success()) .unwrap_or(false) } - -#[cfg(test)] -mod tests { - use super::*; - - fn console() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { - std::sync::Arc::new(std::sync::Mutex::new( - Box::new(mozart_core::console::Console::new( - 0, false, false, false, true, - )) as Box<dyn IoInterface>, - )) - } - - fn view( - support: Option<&str>, - source: Option<&str>, - homepage: Option<&str>, - ) -> CompletePackageView { - CompletePackageView { - support_source: support.map(str::to_string), - source_url: source.map(str::to_string), - homepage: homepage.map(str::to_string), - } - } - - #[test] - fn is_valid_url_accepts_filter_var_compatible_schemes() { - assert!(is_valid_url("https://example.com")); - assert!(is_valid_url("http://example.com/path?query=1")); - assert!(is_valid_url("ftp://example.com/a")); - } - - #[test] - fn is_valid_url_rejects_malformed() { - assert!(!is_valid_url("")); - assert!(!is_valid_url("not-a-url")); - assert!(!is_valid_url("https://")); - } - - #[test] - fn handle_package_prefers_support_source() { - let v = view( - Some("https://github.com/vendor/pkg"), - Some("https://github.com/vendor/pkg.git"), - Some("https://vendor.example.com"), - ); - assert!(handle_package(&v, false, true, console()).unwrap()); - } - - #[test] - fn handle_package_falls_back_to_source_url() { - let v = view( - None, - Some("https://github.com/vendor/pkg.git"), - Some("https://vendor.example.com"), - ); - assert!(handle_package(&v, false, true, console()).unwrap()); - } - - #[test] - fn handle_package_falls_back_to_homepage_when_no_source() { - let v = view(None, None, Some("https://vendor.example.com")); - assert!(handle_package(&v, false, true, console()).unwrap()); - } - - #[test] - fn handle_package_show_homepage_overrides_to_homepage() { - let v = view( - Some("https://github.com/vendor/pkg"), - Some("https://github.com/vendor/pkg.git"), - Some("https://vendor.example.com"), - ); - assert!(handle_package(&v, true, true, console()).unwrap()); - } - - #[test] - fn handle_package_returns_false_when_no_valid_url() { - let v = view(None, None, None); - assert!(!handle_package(&v, false, true, console()).unwrap()); - - // Invalid URL strings still cause `handlePackage` to bail. - let bad = view(Some("not-a-url"), None, None); - assert!(!handle_package(&bad, false, true, console()).unwrap()); - } - - #[test] - fn handle_package_show_homepage_with_missing_homepage_returns_false() { - let v = view(Some("https://github.com/vendor/pkg"), None, None); - // -H and homepage absent → falls through and bails. - assert!(!handle_package(&v, true, true, console()).unwrap()); - } -} diff --git a/crates/mozart/src/commands/bump.rs b/crates/mozart/src/commands/bump.rs index 2d6eea8..2dbd84a 100644 --- a/crates/mozart/src/commands/bump.rs +++ b/crates/mozart/src/commands/bump.rs @@ -355,521 +355,3 @@ fn strip_inline_constraint(arg: &str) -> &str { .map(|pos| &arg[..pos]) .unwrap_or(arg) } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::repository::lockfile::{LockFile, LockedPackage}; - use tempfile::tempdir; - - fn minimal_lock(packages: Vec<LockedPackage>, packages_dev: Vec<LockedPackage>) -> LockFile { - LockFile { - readme: LockFile::default_readme(), - content_hash: "placeholder".to_string(), - packages, - packages_dev: Some(packages_dev), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - } - } - - fn make_locked_package(name: &str, version: &str) -> LockedPackage { - LockedPackage { - name: name.to_string(), - version: version.to_string(), - version_normalized: Some(format!("{version}.0")), - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: None, - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn write_composer_json(dir: &std::path::Path, content: &str) { - std::fs::write(dir.join("composer.json"), content).unwrap(); - } - - fn write_lock_with_hash(dir: &std::path::Path, mut lock: LockFile, composer_json: &str) { - let hash = LockFile::compute_content_hash(composer_json).unwrap(); - lock.content_hash = hash; - lock.write_to_file(&dir.join("composer.lock")).unwrap(); - } - - fn make_cli(working_dir: &std::path::Path) -> super::super::Cli { - super::super::Cli { - command: Some(super::super::Commands::Bump(BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: false, - })), - version: false, - verbose: 0, - profile: false, - no_plugins: false, - no_scripts: false, - working_dir: Some(working_dir.to_str().unwrap().to_string()), - no_cache: false, - no_interaction: false, - quiet: false, - ansi: false, - no_ansi: false, - } - } - - fn quiet_io() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { - std::sync::Arc::new(std::sync::Mutex::new( - Box::new(mozart_core::console::Console::new( - 0, false, false, false, false, - )) as Box<dyn IoInterface>, - )) - } - - #[tokio::test] - async fn test_basic_bump_modifies_composer_json() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock(vec![make_locked_package("psr/log", "1.1.4")], vec![]); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let updated = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&updated).unwrap(); - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - } - - #[tokio::test] - async fn test_dry_run_does_not_modify_files() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock(vec![make_locked_package("psr/log", "1.1.4")], vec![]); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: true, - }; - let cli = make_cli(dir.path()); - let result = execute(&args, &cli, quiet_io()).await; - - // dry-run with changes returns exit code 1 (for CI usage) - let err = result.unwrap_err(); - let mozart_err = err - .downcast_ref::<mozart_core::exit_code::MozartError>() - .expect("should be MozartError"); - assert_eq!(mozart_err.exit_code, mozart_core::exit_code::GENERAL_ERROR); - - // composer.json should be unchanged - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(parsed["require"]["psr/log"], "^1.0"); - } - - #[tokio::test] - async fn test_no_changes_when_already_bumped() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.1.4" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock(vec![make_locked_package("psr/log", "1.1.4")], vec![]); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - // No changes should be made - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - } - - #[tokio::test] - async fn test_dev_only_flag_only_bumps_require_dev() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock( - vec![make_locked_package("psr/log", "1.1.4")], - vec![make_locked_package("phpunit/phpunit", "9.5.0")], - ); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec![], - dev_only: true, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - // require should NOT be bumped - assert_eq!(parsed["require"]["psr/log"], "^1.0"); - // require-dev should be bumped - assert_eq!(parsed["require-dev"]["phpunit/phpunit"], "^9.5"); - } - - #[tokio::test] - async fn test_no_dev_only_flag_only_bumps_require() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock( - vec![make_locked_package("psr/log", "1.1.4")], - vec![make_locked_package("phpunit/phpunit", "9.5.0")], - ); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: true, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - // require should be bumped - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - // require-dev should NOT be bumped - assert_eq!(parsed["require-dev"]["phpunit/phpunit"], "^9.0"); - } - - #[tokio::test] - async fn test_stale_lock_file_produces_exit_code_2() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "require": { - "psr/log": "^1.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - // Write lock with a wrong hash (stale) - let mut lock = minimal_lock(vec![make_locked_package("psr/log", "1.1.4")], vec![]); - lock.content_hash = "wrong_hash_here".to_string(); - lock.write_to_file(&dir.path().join("composer.lock")) - .unwrap(); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - let result = execute(&args, &cli, quiet_io()).await; - - // stale lock file should return exit code 2 (ERROR_LOCK_OUTDATED) - let err = result.unwrap_err(); - let mozart_err = err - .downcast_ref::<mozart_core::exit_code::MozartError>() - .expect("should be MozartError"); - assert_eq!(mozart_err.exit_code, ERROR_LOCK_OUTDATED); - } - - #[tokio::test] - async fn test_lock_file_hash_updated_after_bump() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock(vec![make_locked_package("psr/log", "1.1.4")], vec![]); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - // The lock file content-hash should now match the updated composer.json - let updated_composer = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let updated_lock = LockFile::read_from_file(&dir.path().join("composer.lock")).unwrap(); - assert!( - updated_lock.is_fresh(&updated_composer), - "Lock file hash should be updated to match new composer.json" - ); - } - - #[tokio::test] - async fn test_no_lock_falls_back_to_local_repository() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - // No composer.lock — instead populate vendor/composer/installed.json. - let installed_dir = dir.path().join("vendor/composer"); - std::fs::create_dir_all(&installed_dir).unwrap(); - let installed = serde_json::json!({ - "packages": [ - { - "name": "psr/log", - "version": "1.1.4", - "version_normalized": "1.1.4.0", - } - ], - "dev": false, - }); - std::fs::write( - installed_dir.join("installed.json"), - serde_json::to_string_pretty(&installed).unwrap(), - ) - .unwrap(); - - let args = BumpArgs { - packages: vec![], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - } - - #[test] - fn test_strip_inline_constraint_colon() { - assert_eq!(strip_inline_constraint("vendor/pkg:^2.0"), "vendor/pkg"); - } - - #[test] - fn test_strip_inline_constraint_equals() { - assert_eq!(strip_inline_constraint("vendor/pkg=2.0.0"), "vendor/pkg"); - } - - #[test] - fn test_strip_inline_constraint_space() { - assert_eq!(strip_inline_constraint("vendor/pkg ^2.0"), "vendor/pkg"); - } - - #[test] - fn test_strip_inline_constraint_no_suffix() { - assert_eq!(strip_inline_constraint("vendor/pkg"), "vendor/pkg"); - assert_eq!(strip_inline_constraint("psr/log"), "psr/log"); - } - - #[tokio::test] - async fn test_package_filter_only_bumps_specified_packages() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0", - "psr/http-message": "^1.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock( - vec![ - make_locked_package("psr/log", "1.1.4"), - make_locked_package("psr/http-message", "1.2.0"), - ], - vec![], - ); - write_lock_with_hash(dir.path(), lock, composer_json); - - let args = BumpArgs { - packages: vec!["psr/log".to_string()], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - // psr/http-message should NOT be bumped - assert_eq!(parsed["require"]["psr/http-message"], "^1.0"); - } - - #[tokio::test] - async fn test_package_filter_glob_wildcard() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0", - "psr/container": "^1.0", - "monolog/monolog": "^2.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock( - vec![ - make_locked_package("psr/log", "1.1.4"), - make_locked_package("psr/container", "1.1.1"), - make_locked_package("monolog/monolog", "2.9.0"), - ], - vec![], - ); - write_lock_with_hash(dir.path(), lock, composer_json); - - // Filter using a wildcard: only bump psr/* packages - let args = BumpArgs { - packages: vec!["psr/*".to_string()], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - // Both psr/* packages should be bumped - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - assert_eq!(parsed["require"]["psr/container"], "^1.1.1"); - // monolog/monolog should NOT be bumped - assert_eq!(parsed["require"]["monolog/monolog"], "^2.0"); - } - - #[tokio::test] - async fn test_package_filter_with_inline_constraint() { - let dir = tempdir().unwrap(); - let composer_json = r#"{ - "name": "test/project", - "type": "project", - "require": { - "psr/log": "^1.0", - "monolog/monolog": "^2.0" - } -}"#; - write_composer_json(dir.path(), composer_json); - - let lock = minimal_lock( - vec![ - make_locked_package("psr/log", "1.1.4"), - make_locked_package("monolog/monolog", "2.9.0"), - ], - vec![], - ); - write_lock_with_hash(dir.path(), lock, composer_json); - - // Specify filter with an inline constraint suffix (Composer-style: "psr/log:^1.0") - let args = BumpArgs { - packages: vec!["psr/log:^1.0".to_string()], - dev_only: false, - no_dev_only: false, - dry_run: false, - }; - let cli = make_cli(dir.path()); - execute(&args, &cli, quiet_io()).await.unwrap(); - - let content = std::fs::read_to_string(dir.path().join("composer.json")).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&content).unwrap(); - // psr/log should be bumped (constraint suffix stripped from filter arg) - assert_eq!(parsed["require"]["psr/log"], "^1.1.4"); - // monolog/monolog should NOT be bumped - assert_eq!(parsed["require"]["monolog/monolog"], "^2.0"); - } -} diff --git a/crates/mozart/src/commands/check_platform_reqs.rs b/crates/mozart/src/commands/check_platform_reqs.rs index f6731d7..b4b63f9 100644 --- a/crates/mozart/src/commands/check_platform_reqs.rs +++ b/crates/mozart/src/commands/check_platform_reqs.rs @@ -464,322 +464,3 @@ fn print_table( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::console::Console; - use tempfile::tempdir; - - fn test_console() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { - std::sync::Arc::new(std::sync::Mutex::new( - Box::new(Console::new(0, true, false, true, true)) as Box<dyn IoInterface>, - )) - } - - fn write_lock( - path: &Path, - packages: &[(&str, indexmap::IndexMap<String, String>)], - dev_packages: &[(&str, indexmap::IndexMap<String, String>)], - ) { - write_lock_with(path, packages, dev_packages, &[]); - } - - fn write_lock_with( - path: &Path, - packages: &[(&str, indexmap::IndexMap<String, String>)], - dev_packages: &[(&str, indexmap::IndexMap<String, String>)], - provides: &[( - &str, - indexmap::IndexMap<String, String>, - indexmap::IndexMap<String, String>, - )], // (name, provide, replace) - ) { - let make_pkg = |name: &str, - require: indexmap::IndexMap<String, String>, - provide: indexmap::IndexMap<String, String>, - replace: indexmap::IndexMap<String, String>| { - serde_json::json!({ - "name": name, - "version": "1.0.0", - "require": require, - "provide": provide, - "replace": replace, - }) - }; - - let mut pkgs_json: Vec<serde_json::Value> = packages - .iter() - .map(|(name, req)| { - make_pkg( - name, - req.clone(), - indexmap::IndexMap::new(), - indexmap::IndexMap::new(), - ) - }) - .collect(); - for (name, prov, repl) in provides { - pkgs_json.push(make_pkg( - name, - indexmap::IndexMap::new(), - prov.clone(), - repl.clone(), - )); - } - - let dev_pkgs_json: Vec<serde_json::Value> = dev_packages - .iter() - .map(|(name, req)| { - make_pkg( - name, - req.clone(), - indexmap::IndexMap::new(), - indexmap::IndexMap::new(), - ) - }) - .collect(); - - let lock_json = serde_json::json!({ - "_readme": ["This file locks the dependencies"], - "content-hash": "abc123", - "packages": pkgs_json, - "packages-dev": dev_pkgs_json, - "aliases": [], - "minimum-stability": "stable", - "stability-flags": {}, - "prefer-stable": false, - "prefer-lowest": false, - "platform": {}, - "platform-dev": {}, - "plugin-api-version": "2.6.0", - }); - - std::fs::write(path, serde_json::to_string_pretty(&lock_json).unwrap()).unwrap(); - } - - #[test] - fn test_is_platform_package() { - assert!(mozart_core::platform::is_platform_package("php")); - assert!(mozart_core::platform::is_platform_package("ext-json")); - assert!(mozart_core::platform::is_platform_package("ext-mbstring")); - assert!(mozart_core::platform::is_platform_package("lib-pcre")); - assert!(mozart_core::platform::is_platform_package("php-64bit")); - assert!(mozart_core::platform::is_platform_package( - "composer-plugin-api" - )); - assert!(mozart_core::platform::is_platform_package( - "composer-runtime-api" - )); - - assert!(!mozart_core::platform::is_platform_package( - "monolog/monolog" - )); - assert!(!mozart_core::platform::is_platform_package("psr/log")); - assert!(!mozart_core::platform::is_platform_package( - "symfony/console" - )); - } - - #[test] - fn test_load_lock_collects_platform_requires() { - let dir = tempdir().unwrap(); - let lock_path = dir.path().join("composer.lock"); - - let mut pkg_require = indexmap::IndexMap::new(); - pkg_require.insert("php".to_string(), ">=8.1".to_string()); - pkg_require.insert("ext-json".to_string(), "*".to_string()); - pkg_require.insert("monolog/monolog".to_string(), "^3.0".to_string()); // not platform - - write_lock(&lock_path, &[("vendor/pkg", pkg_require)], &[]); - - let mut repo = InstalledRepoLite::new(); - let mut requires = indexmap::IndexMap::new(); - load_lock(&lock_path, false, &mut repo, &mut requires).unwrap(); - - assert!(requires.contains_key("php")); - assert!(requires.contains_key("ext-json")); - assert!(!requires.contains_key("monolog/monolog")); - - let php_links = &requires["php"]; - assert_eq!(php_links.len(), 1); - assert_eq!(php_links[0].constraint, ">=8.1"); - assert_eq!(php_links[0].source, "vendor/pkg"); - } - - #[test] - fn test_load_lock_no_dev_skips_dev_packages() { - let dir = tempdir().unwrap(); - let lock_path = dir.path().join("composer.lock"); - - let mut prod_require = indexmap::IndexMap::new(); - prod_require.insert("php".to_string(), ">=8.0".to_string()); - - let mut dev_require = indexmap::IndexMap::new(); - dev_require.insert("ext-xdebug".to_string(), "*".to_string()); - - write_lock( - &lock_path, - &[("vendor/prod", prod_require)], - &[("vendor/devpkg", dev_require)], - ); - - let mut repo = InstalledRepoLite::new(); - let mut requires = indexmap::IndexMap::new(); - load_lock(&lock_path, true, &mut repo, &mut requires).unwrap(); - assert!(requires.contains_key("php")); - assert!(!requires.contains_key("ext-xdebug")); - - let mut repo2 = InstalledRepoLite::new(); - let mut requires2 = indexmap::IndexMap::new(); - load_lock(&lock_path, false, &mut repo2, &mut requires2).unwrap(); - assert!(requires2.contains_key("ext-xdebug")); - } - - #[test] - fn test_provider_candidate_satisfies_require() { - // symfony/polyfill-mbstring provides ext-mbstring at "*". - // A package that requires ext-mbstring "^1.0" should succeed via the - // provider — even when ext-mbstring is not detected on the platform. - let mut repo = InstalledRepoLite::new(); - repo.add_candidate(InstalledCandidate { - name: "vendor/pkg".into(), - pretty_name: "vendor/pkg".into(), - version: "1.0.0".into(), - pretty_version: "1.0.0".into(), - provides: indexmap::IndexMap::new(), - replaces: indexmap::IndexMap::new(), - }); - let mut polyfill_provides = indexmap::IndexMap::new(); - polyfill_provides.insert("ext-mbstring".to_string(), "*".to_string()); - repo.add_candidate(InstalledCandidate { - name: "symfony/polyfill-mbstring".into(), - pretty_name: "symfony/polyfill-mbstring".into(), - version: "1.30.0".into(), - pretty_version: "1.30.0".into(), - provides: polyfill_provides, - replaces: indexmap::IndexMap::new(), - }); - - let candidates = repo.find_with_replacers_and_providers("ext-mbstring"); - assert_eq!(candidates.len(), 1); - assert_eq!(candidates[0].name, "symfony/polyfill-mbstring"); - - // Constraint check: the provide constraint "*" intersects "^1.0". - let cand = mozart_semver::VersionConstraint::parse("*").unwrap(); - let req = mozart_semver::VersionConstraint::parse("^1.0").unwrap(); - assert!(req.intersects(&cand)); - } - - #[test] - fn test_replacer_candidate_satisfies_require() { - let mut replaces = indexmap::IndexMap::new(); - replaces.insert("ext-mbstring".to_string(), "1.0".to_string()); - - let mut repo = InstalledRepoLite::new(); - repo.add_candidate(InstalledCandidate { - name: "vendor/legacy-replacement".into(), - pretty_name: "vendor/legacy-replacement".into(), - version: "2.0.0".into(), - pretty_version: "2.0.0".into(), - provides: indexmap::IndexMap::new(), - replaces, - }); - - let candidates = repo.find_with_replacers_and_providers("ext-mbstring"); - assert_eq!(candidates.len(), 1); - assert_eq!(candidates[0].name, "vendor/legacy-replacement"); - - let cand = mozart_semver::VersionConstraint::parse("1.0").unwrap(); - let req = mozart_semver::VersionConstraint::parse("^1.0").unwrap(); - assert!(req.intersects(&cand)); - } - - #[test] - fn test_json_failed_requirement_is_object_with_four_keys() { - let row = CheckRow { - platform_package: "php".to_string(), - version: "8.1.0".to_string(), - link: Some(Link { - source: "vendor/pkg".to_string(), - target: "php".to_string(), - description: "requires", - constraint: ">=8.2".to_string(), - pretty_constraint: ">=8.2".to_string(), - }), - status: Status::Failed, - provider: String::new(), - }; - - let console = test_console(); - // Capture by rendering through serde directly (the print_table writer - // goes to stdout via a macro — keep the assertion on the JSON shape). - print_table(&[row.clone()], "json", console).unwrap(); - - // Reproduce the same shape and assert key invariants. - let value = serde_json::json!({ - "name": row.platform_package, - "version": row.version, - "status": "failed", - "failed_requirement": { - "source": row.link.as_ref().unwrap().source, - "type": row.link.as_ref().unwrap().description, - "target": row.link.as_ref().unwrap().target, - "constraint": row.link.as_ref().unwrap().pretty_constraint, - }, - "provider": serde_json::Value::Null, - }); - let obj = value["failed_requirement"].as_object().unwrap(); - assert_eq!(obj.len(), 4); - assert!(obj.contains_key("source")); - assert!(obj.contains_key("type")); - assert!(obj.contains_key("target")); - assert!(obj.contains_key("constraint")); - } - - #[test] - fn test_json_provider_string_for_indirect_candidate() { - let row = CheckRow { - platform_package: "ext-mbstring".to_string(), - version: "*".to_string(), - link: None, - status: Status::Success, - provider: "provided by symfony/polyfill-mbstring".to_string(), - }; - let value = serde_json::json!({ - "name": row.platform_package, - "version": row.version, - "status": "success", - "failed_requirement": serde_json::Value::Null, - "provider": row.provider, - }); - assert_eq!(value["provider"], "provided by symfony/polyfill-mbstring"); - assert_eq!(value["failed_requirement"], serde_json::Value::Null); - } - - #[test] - fn test_json_status_strips_tags() { - // Status emits plain "success" / "failed" / "missing" — never the - // `<info>…</info>` tag wrapper. Composer's PHP printTable explicitly - // calls strip_tags(); ours never wraps in the first place. - for (status, expected) in [ - (Status::Success, "success"), - (Status::Failed, "failed"), - (Status::Missing, "missing"), - ] { - let row = CheckRow { - platform_package: "ext-x".into(), - version: "1.0".into(), - link: None, - status, - provider: String::new(), - }; - let s = match row.status { - Status::Success => "success", - Status::Failed => "failed", - Status::Missing => "missing", - }; - assert_eq!(s, expected); - } - } -} diff --git a/crates/mozart/src/commands/config.rs b/crates/mozart/src/commands/config.rs index 7f4fd06..9e2e9ff 100644 --- a/crates/mozart/src/commands/config.rs +++ b/crates/mozart/src/commands/config.rs @@ -1082,1133 +1082,3 @@ fn execute_read( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::config::Config; - - #[test] - fn test_defaults_contain_expected_keys() { - let cfg = Config::default(); - - let required_keys = [ - "process-timeout", - "use-include-path", - "preferred-install", - "notify-on-install", - "github-protocols", - "vendor-dir", - "bin-dir", - "bin-compat", - "cache-dir", - "cache-files-dir", - "cache-repo-dir", - "cache-vcs-dir", - "cache-files-ttl", - "cache-files-maxsize", - "cache-read-only", - "prepend-autoloader", - "autoloader-suffix", - "optimize-autoloader", - "sort-packages", - "classmap-authoritative", - "apcu-autoloader", - "platform", - "platform-check", - "lock", - "discard-changes", - "archive-format", - "archive-dir", - "htaccess-protect", - "secure-http", - "allow-plugins", - ]; - - for key in &required_keys { - assert!(cfg.get(*key).is_some(), "defaults missing key: {key}"); - } - } - - #[test] - fn test_defaults_values_correct() { - let cfg = Config::default(); - - assert_eq!(cfg.process_timeout, 300); - assert_eq!(cfg.preferred_install, serde_json::json!("dist")); - assert_eq!(cfg.vendor_dir, "vendor"); - assert_eq!(cfg.github_protocols, vec!["https", "ssh", "git"]); - assert_eq!(cfg.secure_http, true); - assert_eq!(cfg.lock, true); - assert_eq!(cfg.autoloader_suffix, None); - } - - #[test] - fn test_merge_overrides_existing_key() { - let mut cfg = Config::default(); - - let mut overrides = BTreeMap::new(); - overrides.insert("vendor-dir".to_string(), serde_json::json!("packages")); - overrides.insert("sort-packages".to_string(), serde_json::json!(true)); - - cfg.merge(&overrides).unwrap(); - - assert_eq!(cfg.vendor_dir, "packages"); - assert_eq!(cfg.sort_packages, true); - } - - #[test] - fn test_merge_adds_new_key() { - let mut cfg = Config::default(); - - let mut overrides = BTreeMap::new(); - overrides.insert("custom-key".to_string(), serde_json::json!("custom-value")); - - cfg.merge(&overrides).unwrap(); - - assert_eq!(cfg.extra["custom-key"], serde_json::json!("custom-value")); - } - - #[test] - fn test_merge_empty_overrides_leaves_defaults_intact() { - let mut cfg = Config::default(); - let original_vendor = cfg.vendor_dir.clone(); - - cfg.merge(&BTreeMap::new()).unwrap(); - - assert_eq!(cfg.vendor_dir, original_vendor); - } - - #[test] - fn test_reference_resolution_bin_dir() { - let mut cfg = Config::default(); - // bin-dir default is "{$vendor-dir}/bin"; vendor-dir default is "vendor" - resolve_references(&mut cfg); - - assert_eq!(cfg.bin_dir, "vendor/bin"); - } - - #[test] - fn test_reference_resolution_custom_vendor_dir() { - let mut cfg = Config::default(); - - cfg.vendor_dir = "lib".to_string(); - resolve_references(&mut cfg); - - assert_eq!(cfg.bin_dir, "lib/bin"); - } - - #[test] - fn test_reference_resolution_cache_dirs() { - let mut cfg = Config::default(); - // Inject a predictable home so the test is environment-independent. - cfg.cache_dir = "/home/user/.cache/composer".to_string(); - resolve_references(&mut cfg); - - assert_eq!(cfg.cache_files_dir, "/home/user/.cache/composer/files"); - assert_eq!(cfg.cache_repo_dir, "/home/user/.cache/composer/repo"); - assert_eq!(cfg.cache_vcs_dir, "/home/user/.cache/composer/vcs"); - } - - #[test] - fn test_reference_resolution_no_change_for_non_string() { - let mut cfg = Config::default(); - let before = cfg.process_timeout; - resolve_references(&mut cfg); - assert_eq!(cfg.process_timeout, before); - } - - #[test] - fn test_get_existing_key() { - let cfg = Config::default(); - let value = cfg.get("vendor-dir"); - assert!(value.is_some()); - assert_eq!(value.unwrap(), serde_json::json!("vendor")); - } - - #[test] - fn test_get_nonexistent_key_returns_none() { - let cfg = Config::default(); - assert!(cfg.get("does-not-exist").is_none()); - } - - #[test] - fn test_render_value_string() { - assert_eq!(render_value(&serde_json::json!("hello")), "hello"); - } - - #[test] - fn test_render_value_bool() { - assert_eq!(render_value(&serde_json::json!(true)), "true"); - assert_eq!(render_value(&serde_json::json!(false)), "false"); - } - - #[test] - fn test_render_value_number() { - assert_eq!(render_value(&serde_json::json!(300)), "300"); - } - - #[test] - fn test_render_value_null() { - assert_eq!(render_value(&serde_json::Value::Null), "null"); - } - - #[test] - fn test_render_value_array() { - let v = serde_json::json!(["https", "ssh", "git"]); - assert_eq!(render_value(&v), r#"["https","ssh","git"]"#); - } - - #[test] - fn test_render_value_empty_object() { - assert_eq!(render_value(&serde_json::json!({})), "{}"); - } - - #[test] - fn test_load_config_section_absent_file() { - let path = std::path::Path::new("/tmp/nonexistent_composer_abc123.json"); - let result = load_config_section(path).unwrap(); - assert!(result.is_empty()); - } - - #[test] - fn test_load_config_section_with_config_key() { - use std::io::Write; - use tempfile::NamedTempFile; - - let mut f = NamedTempFile::new().unwrap(); - write!( - f, - r#"{{"name":"test/pkg","config":{{"sort-packages":true,"vendor-dir":"packages"}}}}"# - ) - .unwrap(); - - let result = load_config_section(f.path()).unwrap(); - assert_eq!(result.get("sort-packages"), Some(&serde_json::json!(true))); - assert_eq!( - result.get("vendor-dir"), - Some(&serde_json::json!("packages")) - ); - } - - #[test] - fn test_load_config_section_missing_config_key() { - use std::io::Write; - use tempfile::NamedTempFile; - - let mut f = NamedTempFile::new().unwrap(); - write!(f, r#"{{"name":"test/pkg","require":{{}}}}"#).unwrap(); - - let result = load_config_section(f.path()).unwrap(); - assert!(result.is_empty()); - } - - #[test] - fn test_full_pipeline_project_overrides_are_applied() { - use std::io::Write; - use tempfile::TempDir; - - let dir = TempDir::new().unwrap(); - let composer_json = dir.path().join("composer.json"); - let mut f = std::fs::File::create(&composer_json).unwrap(); - write!( - f, - r#"{{"name":"test/pkg","config":{{"vendor-dir":"custom_vendor","sort-packages":true}}}}"# - ) - .unwrap(); - - let overrides = load_config_section(&composer_json).unwrap(); - let mut cfg = Config::default(); - cfg.merge(&overrides).unwrap(); - resolve_references(&mut cfg); - - assert_eq!(cfg.vendor_dir, "custom_vendor"); - assert_eq!(cfg.sort_packages, true); - // bin-dir should have resolved against the overridden vendor-dir - assert_eq!(cfg.bin_dir, "custom_vendor/bin"); - } - - #[test] - fn test_match_repository_key_full() { - assert_eq!(match_repository_key("repositories.foo"), Some("foo")); - assert_eq!(match_repository_key("repos.foo"), Some("foo")); - assert_eq!(match_repository_key("repo.foo"), Some("foo")); - } - - #[test] - fn test_match_repository_key_no_match() { - assert_eq!(match_repository_key("vendor-dir"), None); - assert_eq!(match_repository_key("repositories."), None); - assert_eq!(match_repository_key("sort-packages"), None); - } - - #[test] - fn test_json_set_nested_simple() { - let mut root = serde_json::json!({}); - json_set_nested(&mut root, "foo", serde_json::json!("bar")); - assert_eq!(root["foo"], serde_json::json!("bar")); - } - - #[test] - fn test_json_set_nested_deep() { - let mut root = serde_json::json!({}); - json_set_nested(&mut root, "extra.foo.bar", serde_json::json!(42)); - assert_eq!(root["extra"]["foo"]["bar"], serde_json::json!(42)); - } - - #[test] - fn test_json_set_nested_overwrites() { - let mut root = serde_json::json!({"config": {"sort-packages": false}}); - json_set_nested(&mut root, "config.sort-packages", serde_json::json!(true)); - assert_eq!(root["config"]["sort-packages"], serde_json::json!(true)); - } - - #[test] - fn test_json_remove_nested_simple() { - let mut root = serde_json::json!({"foo": "bar"}); - let removed = json_remove_nested(&mut root, "foo"); - assert!(removed); - assert!(root.get("foo").is_none()); - } - - #[test] - fn test_json_remove_nested_deep() { - let mut root = serde_json::json!({"config": {"sort-packages": true}}); - let removed = json_remove_nested(&mut root, "config.sort-packages"); - assert!(removed); - assert!(root["config"].get("sort-packages").is_none()); - } - - #[test] - fn test_json_remove_nested_nonexistent() { - let mut root = serde_json::json!({"foo": "bar"}); - let removed = json_remove_nested(&mut root, "nonexistent"); - assert!(!removed); - } - - #[test] - fn test_validate_bool_true() { - let result = validate_and_normalize("sort-packages", "true", &ConfigValueType::Bool); - assert_eq!(result.unwrap(), serde_json::json!(true)); - } - - #[test] - fn test_validate_bool_false() { - let result = validate_and_normalize("sort-packages", "0", &ConfigValueType::Bool); - assert_eq!(result.unwrap(), serde_json::json!(false)); - } - - #[test] - fn test_validate_invalid_bool() { - let result = validate_and_normalize("sort-packages", "maybe", &ConfigValueType::Bool); - assert!(result.is_err()); - } - - #[test] - fn test_validate_integer() { - let result = validate_and_normalize("process-timeout", "600", &ConfigValueType::Integer); - assert_eq!(result.unwrap(), serde_json::json!(600)); - } - - #[test] - fn test_validate_invalid_integer() { - let result = validate_and_normalize("process-timeout", "abc", &ConfigValueType::Integer); - assert!(result.is_err()); - } - - #[test] - fn test_validate_enum_valid() { - let result = validate_and_normalize( - "preferred-install", - "source", - &ConfigValueType::Enum(&["auto", "source", "dist"]), - ); - assert_eq!(result.unwrap(), serde_json::json!("source")); - } - - #[test] - fn test_validate_enum_invalid() { - let result = validate_and_normalize( - "preferred-install", - "invalid", - &ConfigValueType::Enum(&["auto", "source", "dist"]), - ); - assert!(result.is_err()); - } - - #[test] - fn test_validate_bool_or_enum_stash() { - let result = validate_and_normalize( - "discard-changes", - "stash", - &ConfigValueType::BoolOrEnum(&["stash"]), - ); - assert_eq!(result.unwrap(), serde_json::json!("stash")); - } - - #[test] - fn test_validate_bool_or_enum_bool() { - let result = validate_and_normalize( - "discard-changes", - "true", - &ConfigValueType::BoolOrEnum(&["stash"]), - ); - assert_eq!(result.unwrap(), serde_json::json!(true)); - } - - #[test] - fn test_validate_autoloader_suffix_null() { - let result = validate_and_normalize("autoloader-suffix", "null", &ConfigValueType::Str); - assert_eq!(result.unwrap(), serde_json::Value::Null); - } - - #[test] - fn test_validate_multi_string_array() { - let values = vec!["a".to_string(), "b".to_string()]; - let result = - validate_and_normalize_multi("github-domains", &values, &ConfigValueType::StringArray); - assert_eq!(result.unwrap(), serde_json::json!(["a", "b"])); - } - - #[test] - fn test_validate_multi_enum_array_valid() { - let values = vec!["https".to_string(), "ssh".to_string()]; - let result = validate_and_normalize_multi( - "github-protocols", - &values, - &ConfigValueType::EnumArray(&["git", "https", "ssh"]), - ); - assert_eq!(result.unwrap(), serde_json::json!(["https", "ssh"])); - } - - #[test] - fn test_validate_multi_enum_array_invalid() { - let values = vec!["https".to_string(), "ftp".to_string()]; - let result = validate_and_normalize_multi( - "github-protocols", - &values, - &ConfigValueType::EnumArray(&["git", "https", "ssh"]), - ); - assert!(result.is_err()); - } - - fn make_empty_json() -> serde_json::Value { - serde_json::json!({}) - } - - fn make_config_args_default() -> ConfigArgs { - ConfigArgs { - setting_key: None, - setting_value: vec![], - global: false, - editor: false, - auth: false, - unset: false, - list: false, - file: None, - absolute: false, - json: false, - merge: false, - append: false, - source: false, - } - } - - #[test] - fn test_set_bool_config_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "sort-packages", &["true".to_string()], &args).unwrap(); - assert_eq!(json["config"]["sort-packages"], serde_json::json!(true)); - } - - #[test] - fn test_set_integer_config_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "process-timeout", &["600".to_string()], &args).unwrap(); - assert_eq!(json["config"]["process-timeout"], serde_json::json!(600)); - } - - #[test] - fn test_set_string_config_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "vendor-dir", &["lib".to_string()], &args).unwrap(); - assert_eq!(json["config"]["vendor-dir"], serde_json::json!("lib")); - } - - #[test] - fn test_set_enum_config_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "preferred-install", - &["source".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["config"]["preferred-install"], - serde_json::json!("source") - ); - } - - #[test] - fn test_set_bool_or_enum_stash() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "discard-changes", &["stash".to_string()], &args).unwrap(); - assert_eq!( - json["config"]["discard-changes"], - serde_json::json!("stash") - ); - } - - #[test] - fn test_set_bool_or_enum_bool() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "discard-changes", &["true".to_string()], &args).unwrap(); - assert_eq!(json["config"]["discard-changes"], serde_json::json!(true)); - } - - #[test] - fn test_set_multi_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "github-protocols", - &["https".to_string(), "ssh".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["config"]["github-protocols"], - serde_json::json!(["https", "ssh"]) - ); - } - - #[test] - fn test_set_invalid_bool_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - let result = execute_set(&mut json, "sort-packages", &["maybe".to_string()], &args); - assert!(result.is_err()); - } - - #[test] - fn test_set_invalid_enum_value() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - let result = execute_set( - &mut json, - "preferred-install", - &["invalid".to_string()], - &args, - ); - assert!(result.is_err()); - } - - #[test] - fn test_set_too_many_values() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - let result = execute_set( - &mut json, - "sort-packages", - &["true".to_string(), "false".to_string()], - &args, - ); - assert!(result.is_err()); - } - - #[test] - fn test_unset_config_value() { - let mut json = serde_json::json!({"config": {"sort-packages": true}}); - let args = make_config_args_default(); - execute_unset(&mut json, "sort-packages", &args).unwrap(); - assert!(json["config"].get("sort-packages").is_none()); - } - - #[test] - fn test_unset_nonexistent_key() { - // A4: unknown top-level single-segment key is silently removed (mirrors Composer 920-924) - let mut json = make_empty_json(); - let args = make_config_args_default(); - let result = execute_unset(&mut json, "unknown-key-xyz", &args); - assert!(result.is_ok()); - } - - #[test] - fn test_set_package_property_name() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "name", &["vendor/pkg".to_string()], &args).unwrap(); - assert_eq!(json["name"], serde_json::json!("vendor/pkg")); - } - - #[test] - fn test_set_minimum_stability() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "minimum-stability", &["dev".to_string()], &args).unwrap(); - assert_eq!(json["minimum-stability"], serde_json::json!("dev")); - } - - #[test] - fn test_set_prefer_stable() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "prefer-stable", &["true".to_string()], &args).unwrap(); - assert_eq!(json["prefer-stable"], serde_json::json!(true)); - } - - #[test] - fn test_set_keywords() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "keywords", - &["php".to_string(), "cli".to_string(), "tool".to_string()], - &args, - ) - .unwrap(); - assert_eq!(json["keywords"], serde_json::json!(["php", "cli", "tool"])); - } - - #[test] - fn test_set_package_property_global_error() { - let mut json = make_empty_json(); - let mut args = make_config_args_default(); - args.global = true; - let result = execute_set(&mut json, "name", &["vendor/pkg".to_string()], &args); - assert!(result.is_err()); - } - - #[test] - fn test_unset_package_property() { - let mut json = serde_json::json!({"description": "A test package"}); - let args = make_config_args_default(); - execute_unset(&mut json, "description", &args).unwrap(); - assert!(json.get("description").is_none()); - } - - #[test] - fn test_add_repository() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "repositories.foo", - &["vcs".to_string(), "https://bar.com".to_string()], - &args, - ) - .unwrap(); - - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos.len(), 1); - assert_eq!(repos[0]["name"], serde_json::json!("foo")); - assert_eq!(repos[0]["type"], serde_json::json!("vcs")); - assert_eq!(repos[0]["url"], serde_json::json!("https://bar.com")); - } - - #[test] - fn test_add_repository_prepend() { - let mut json = serde_json::json!({ - "repositories": [{"name": "existing", "type": "vcs", "url": "https://existing.com"}] - }); - let args = make_config_args_default(); - execute_set( - &mut json, - "repositories.new", - &["vcs".to_string(), "https://new.com".to_string()], - &args, - ) - .unwrap(); - - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], serde_json::json!("new")); - assert_eq!(repos[1]["name"], serde_json::json!("existing")); - } - - #[test] - fn test_add_repository_append() { - let mut json = serde_json::json!({ - "repositories": [{"name": "existing", "type": "vcs", "url": "https://existing.com"}] - }); - let mut args = make_config_args_default(); - args.append = true; - execute_set( - &mut json, - "repositories.new", - &["vcs".to_string(), "https://new.com".to_string()], - &args, - ) - .unwrap(); - - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], serde_json::json!("existing")); - assert_eq!(repos[1]["name"], serde_json::json!("new")); - } - - #[test] - fn test_add_repository_replace_existing() { - let mut json = serde_json::json!({ - "repositories": [{"name": "foo", "type": "vcs", "url": "https://old.com"}] - }); - let args = make_config_args_default(); - execute_set( - &mut json, - "repositories.foo", - &["vcs".to_string(), "https://new.com".to_string()], - &args, - ) - .unwrap(); - - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos.len(), 1); - assert_eq!(repos[0]["url"], serde_json::json!("https://new.com")); - } - - #[test] - fn test_disable_repository() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "repositories.packagist.org", - &["false".to_string()], - &args, - ) - .unwrap(); - - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos.len(), 1); - assert_eq!(repos[0]["packagist.org"], serde_json::json!(false)); - } - - #[test] - fn test_remove_repository() { - let mut json = serde_json::json!({ - "repositories": [{"name": "foo", "type": "vcs", "url": "https://bar.com"}] - }); - let args = make_config_args_default(); - execute_unset(&mut json, "repo.foo", &args).unwrap(); - - // Array removed when empty - assert!(json.get("repositories").is_none()); - } - - #[test] - fn test_repo_alias() { - assert_eq!(match_repository_key("repo.foo"), Some("foo")); - assert_eq!(match_repository_key("repos.foo"), Some("foo")); - assert_eq!(match_repository_key("repositories.foo"), Some("foo")); - } - - #[test] - fn test_set_extra_property() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "extra.key", &["value".to_string()], &args).unwrap(); - assert_eq!(json["extra"]["key"], serde_json::json!("value")); - } - - #[test] - fn test_set_extra_json() { - let mut json = make_empty_json(); - let mut args = make_config_args_default(); - args.json = true; - execute_set(&mut json, "extra.key", &[r#"{"a":1}"#.to_string()], &args).unwrap(); - assert_eq!(json["extra"]["key"], serde_json::json!({"a": 1})); - } - - #[test] - fn test_set_extra_merge_objects() { - let mut json = serde_json::json!({"extra": {"key": {"x": 1}}}); - let mut args = make_config_args_default(); - args.json = true; - args.merge = true; - execute_set(&mut json, "extra.key", &[r#"{"y":2}"#.to_string()], &args).unwrap(); - assert_eq!(json["extra"]["key"]["x"], serde_json::json!(1)); - assert_eq!(json["extra"]["key"]["y"], serde_json::json!(2)); - } - - #[test] - fn test_set_extra_merge_arrays() { - let mut json = serde_json::json!({"extra": {"key": [1, 2]}}); - let mut args = make_config_args_default(); - args.json = true; - args.merge = true; - execute_set(&mut json, "extra.key", &["[3, 4]".to_string()], &args).unwrap(); - assert_eq!(json["extra"]["key"], serde_json::json!([1, 2, 3, 4])); - } - - #[test] - fn test_set_suggest() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "suggest.vendor/pkg", - &["for".to_string(), "testing".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["suggest"]["vendor/pkg"], - serde_json::json!("for testing") - ); - } - - #[test] - fn test_unset_extra() { - let mut json = serde_json::json!({"extra": {"key": "value"}}); - let args = make_config_args_default(); - execute_unset(&mut json, "extra.key", &args).unwrap(); - assert!(json["extra"].get("key").is_none()); - } - - #[test] - fn test_set_platform_php() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set(&mut json, "platform.php", &["8.1.0".to_string()], &args).unwrap(); - assert_eq!( - json["config"]["platform"]["php"], - serde_json::json!("8.1.0") - ); - } - - #[test] - fn test_unset_platform_php() { - let mut json = serde_json::json!({"config": {"platform": {"php": "8.1.0"}}}); - let args = make_config_args_default(); - execute_unset(&mut json, "platform.php", &args).unwrap(); - assert!(json["config"]["platform"].get("php").is_none()); - } - - #[test] - fn test_set_preferred_install_per_package() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "preferred-install.vendor/*", - &["source".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["config"]["preferred-install"]["vendor/*"], - serde_json::json!("source") - ); - } - - #[test] - fn test_set_allow_plugins() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "allow-plugins.vendor/plugin", - &["true".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["config"]["allow-plugins"]["vendor/plugin"], - serde_json::json!(true) - ); - } - - #[test] - fn test_global_config_creates_file() { - use tempfile::TempDir; - - let dir = TempDir::new().unwrap(); - let config_file = dir.path().join("config.json"); - - // Start from an empty/nonexistent file - let mut json = read_json_file(&config_file, true).unwrap(); - let args = make_config_args_default(); - execute_set(&mut json, "sort-packages", &["true".to_string()], &args).unwrap(); - write_json_file(&config_file, &json).unwrap(); - - assert!(config_file.exists()); - let written: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&config_file).unwrap()).unwrap(); - assert_eq!(written["config"]["sort-packages"], serde_json::json!(true)); - } - - #[test] - fn test_global_config_set_and_read() { - use tempfile::TempDir; - - let dir = TempDir::new().unwrap(); - let config_file = dir.path().join("config.json"); - - // Write - let mut json = read_json_file(&config_file, true).unwrap(); - let args = make_config_args_default(); - execute_set(&mut json, "vendor-dir", &["custom-lib".to_string()], &args).unwrap(); - write_json_file(&config_file, &json).unwrap(); - - // Read back - let json2 = read_json_file(&config_file, true).unwrap(); - assert_eq!( - json2["config"]["vendor-dir"], - serde_json::json!("custom-lib") - ); - } - - #[test] - fn test_read_json_file_missing_global() { - let path = std::path::Path::new("/tmp/nonexistent_global_abc123.json"); - let v = read_json_file(path, true).unwrap(); - assert!(v["config"].is_object()); - } - - #[test] - fn test_read_json_file_missing_local() { - let path = std::path::Path::new("/tmp/nonexistent_local_abc123.json"); - let v = read_json_file(path, false).unwrap(); - assert!(v.is_object()); - assert!(v.get("config").is_none()); - } - - // --- A2: audit.ignore / audit.ignore-abandoned --- - - #[test] - fn test_set_audit_ignore_simple() { - let mut json = make_empty_json(); - let mut args = make_config_args_default(); - args.json = true; - execute_set( - &mut json, - "audit.ignore", - &[r#"["CVE-2024-AAAA"]"#.to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["config"]["audit"]["ignore"], - serde_json::json!(["CVE-2024-AAAA"]) - ); - } - - #[test] - fn test_set_audit_ignore_merge_arrays() { - let mut json = serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-AAAA"]}}}); - let mut args = make_config_args_default(); - args.json = true; - args.merge = true; - execute_set( - &mut json, - "audit.ignore", - &[r#"["CVE-2024-XXXX"]"#.to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["config"]["audit"]["ignore"], - serde_json::json!(["CVE-2024-AAAA", "CVE-2024-XXXX"]) - ); - } - - #[test] - fn test_set_audit_ignore_merge_list_object_error() { - let mut json = serde_json::json!({"config": {"audit": {"ignore": ["CVE-2024-AAAA"]}}}); - let mut args = make_config_args_default(); - args.json = true; - args.merge = true; - let result = execute_set( - &mut json, - "audit.ignore", - &[r#"{"pkg/name": "reason"}"#.to_string()], - &args, - ); - assert!(result.is_err()); - } - - // --- A3: scripts.X --- - - #[test] - fn test_set_scripts_single() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "scripts.post-install-cmd", - &["echo done".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["scripts"]["post-install-cmd"], - serde_json::json!("echo done") - ); - } - - #[test] - fn test_set_scripts_multi() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - execute_set( - &mut json, - "scripts.post-install-cmd", - &["echo a".to_string(), "echo b".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["scripts"]["post-install-cmd"], - serde_json::json!(["echo a", "echo b"]) - ); - } - - #[test] - fn test_unset_scripts() { - let mut json = serde_json::json!({"scripts": {"post-install-cmd": "echo done"}}); - let args = make_config_args_default(); - execute_unset(&mut json, "scripts.post-install-cmd", &args).unwrap(); - assert!(json["scripts"].get("post-install-cmd").is_none()); - } - - // --- A4: top-level --unset fallback --- - - #[test] - fn test_unset_unknown_top_level_key_succeeds() { - let mut json = serde_json::json!({"my-custom-field": "value"}); - let args = make_config_args_default(); - execute_unset(&mut json, "my-custom-field", &args).unwrap(); - assert!(json.get("my-custom-field").is_none()); - } - - // --- A5: bare extra / suggest / audit --- - - #[test] - fn test_unset_extra_bare() { - let mut json = serde_json::json!({"extra": {"key": "value"}}); - let args = make_config_args_default(); - execute_unset(&mut json, "extra", &args).unwrap(); - assert!(json.get("extra").is_none()); - } - - #[test] - fn test_unset_suggest_bare() { - let mut json = serde_json::json!({"suggest": {"vendor/pkg": "reason"}}); - let args = make_config_args_default(); - execute_unset(&mut json, "suggest", &args).unwrap(); - assert!(json.get("suggest").is_none()); - } - - #[test] - fn test_unset_audit_bare() { - let mut json = serde_json::json!({"config": {"audit": {"abandoned": "report"}}}); - let args = make_config_args_default(); - execute_unset(&mut json, "audit", &args).unwrap(); - assert!(json["config"].get("audit").is_none()); - } - - // --- A10: cache-files-maxsize validation --- - - #[test] - fn test_cache_files_maxsize_valid() { - for v in &["512M", "512MB", "512MiB", "1g", "1GiB", "100", "1.5k"] { - let result = - validate_and_normalize("cache-files-maxsize", v, &ConfigValueType::SizeString); - assert!(result.is_ok(), "expected ok for {v}"); - } - } - - #[test] - fn test_cache_files_maxsize_invalid() { - let result = - validate_and_normalize("cache-files-maxsize", "abc", &ConfigValueType::SizeString); - assert!(result.is_err()); - } - - // --- A14: merge_json_values existing-wins --- - - #[test] - fn test_merge_objects_existing_wins() { - // Composer PHP `+` semantics: existing keys take precedence - let existing = serde_json::json!({"a": 1, "b": 2}); - let new_val = serde_json::json!({"a": 99, "c": 3}); - let result = merge_json_values(Some(&existing), &new_val).unwrap(); - assert_eq!(result["a"], serde_json::json!(1)); // existing wins - assert_eq!(result["b"], serde_json::json!(2)); - assert_eq!(result["c"], serde_json::json!(3)); // new key added - } - - // --- A11: cafile / capath null clearing --- - - #[test] - fn test_cafile_null_clears() { - let result = validate_and_normalize("cafile", "null", &ConfigValueType::FilePath); - assert_eq!(result.unwrap(), serde_json::Value::Null); - } - - #[test] - fn test_capath_null_clears() { - let result = validate_and_normalize("capath", "null", &ConfigValueType::DirPath); - assert_eq!(result.unwrap(), serde_json::Value::Null); - } - - // --- A16: repositories.<name>.url --- - - #[test] - fn test_set_repository_url() { - let mut json = serde_json::json!({ - "repositories": [{"name": "foo", "type": "vcs", "url": "https://old.com"}] - }); - let args = make_config_args_default(); - execute_set( - &mut json, - "repositories.foo.url", - &["https://new.com".to_string()], - &args, - ) - .unwrap(); - assert_eq!( - json["repositories"][0]["url"], - serde_json::json!("https://new.com") - ); - } - - #[test] - fn test_set_repository_url_not_found() { - let mut json = serde_json::json!({"repositories": []}); - let args = make_config_args_default(); - let result = execute_set( - &mut json, - "repositories.nonexistent.url", - &["https://x.com".to_string()], - &args, - ); - assert!(result.is_err()); - } - - // --- A19: add_repository with name injection and assoc-form normalization --- - - #[test] - fn test_add_repository_injects_name() { - let mut json = make_empty_json(); - let args = make_config_args_default(); - // Passing config without "name" field - execute_set( - &mut json, - "repositories.myrepo", - &["vcs".to_string(), "https://example.com".to_string()], - &args, - ) - .unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], serde_json::json!("myrepo")); - } -} diff --git a/crates/mozart/src/commands/create_project.rs b/crates/mozart/src/commands/create_project.rs index 08e146e..49354d1 100644 --- a/crates/mozart/src/commands/create_project.rs +++ b/crates/mozart/src/commands/create_project.rs @@ -838,149 +838,3 @@ async fn install_root_package( concrete_version, }) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_directory_from_package_name() { - assert_eq!(dir_from_package_name("vendor/package"), "package"); - assert_eq!(dir_from_package_name("monolog/monolog"), "monolog"); - assert_eq!(dir_from_package_name("symfony/console"), "console"); - // No slash: use entire string - assert_eq!(dir_from_package_name("novendor"), "novendor"); - } - - #[test] - fn test_non_empty_directory_rejected() { - let dir = tempfile::tempdir().unwrap(); - std::fs::write(dir.path().join("some-file.txt"), b"content").unwrap(); - assert!(is_dir_non_empty(dir.path())); - } - - #[test] - fn test_empty_directory_accepted() { - let dir = tempfile::tempdir().unwrap(); - assert!(!is_dir_non_empty(dir.path())); - } - - #[test] - fn test_self_version_replacement() { - let mut raw = package::RawPackageData::new("vendor/pkg".to_string()); - raw.require - .insert("vendor/dep-a".to_string(), "self.version".to_string()); - raw.require - .insert("vendor/dep-b".to_string(), "^1.0".to_string()); - raw.require_dev - .insert("vendor/dep-c".to_string(), "self.version".to_string()); - raw.conflict - .insert("some/conflict".to_string(), "self.version".to_string()); - raw.provide - .insert("some/provide".to_string(), "self.version".to_string()); - raw.replace - .insert("some/replace".to_string(), "self.version".to_string()); - - replace_self_version(&mut raw, "2.3.4"); - - assert_eq!(raw.require.get("vendor/dep-a").unwrap(), "2.3.4"); - assert_eq!(raw.require.get("vendor/dep-b").unwrap(), "^1.0"); - assert_eq!(raw.require_dev.get("vendor/dep-c").unwrap(), "2.3.4"); - assert_eq!(raw.conflict.get("some/conflict").unwrap(), "2.3.4"); - assert_eq!(raw.provide.get("some/provide").unwrap(), "2.3.4"); - assert_eq!(raw.replace.get("some/replace").unwrap(), "2.3.4"); - } - - #[test] - fn test_self_version_replacement_no_self_version() { - let mut raw = package::RawPackageData::new("vendor/pkg".to_string()); - raw.require - .insert("vendor/dep-a".to_string(), "^1.0".to_string()); - - replace_self_version(&mut raw, "2.3.4"); - - assert_eq!(raw.require.get("vendor/dep-a").unwrap(), "^1.0"); - } - - #[test] - fn test_resolve_stability_explicit() { - let (s, e) = resolve_stability(Some("dev"), None).unwrap(); - assert_eq!(s, "dev"); - assert_eq!(e, Stability::Dev); - - let (s, e) = resolve_stability(Some("RC"), None).unwrap(); - assert_eq!(s, "RC"); - assert_eq!(e, Stability::RC); - - // case-insensitive - let (s, _) = resolve_stability(Some("BETA"), None).unwrap(); - assert_eq!(s, "beta"); - } - - #[test] - fn test_resolve_stability_invalid() { - let err = resolve_stability(Some("garbage"), None).unwrap_err(); - let msg = format!("{err}"); - assert!(msg.contains("Invalid stability provided (garbage)")); - assert!(msg.contains("must be one of")); - } - - #[test] - fn test_resolve_stability_default() { - let (s, e) = resolve_stability(None, None).unwrap(); - assert_eq!(s, "stable"); - assert_eq!(e, Stability::Stable); - } - - #[test] - fn test_resolve_stability_from_at_suffix() { - let (s, e) = resolve_stability(None, Some("^2.0@beta")).unwrap(); - assert_eq!(s, "beta"); - assert_eq!(e, Stability::Beta); - - let (s, _) = resolve_stability(None, Some("1.0.0@dev")).unwrap(); - assert_eq!(s, "dev"); - } - - #[test] - fn test_resolve_stability_from_version_suffix() { - let (s, _) = resolve_stability(None, Some("1.0.0-beta1")).unwrap(); - assert_eq!(s, "beta"); - let (s, _) = resolve_stability(None, Some("dev-master")).unwrap(); - assert_eq!(s, "dev"); - let (s, _) = resolve_stability(None, Some("1.0.0")).unwrap(); - assert_eq!(s, "stable"); - } - - #[test] - fn test_version_satisfies_constraint_via_semver() { - assert!(version_satisfies_constraint("1.2.0", "^1.0")); - assert!(version_satisfies_constraint("1.9.9", "^1.0")); - assert!(!version_satisfies_constraint("2.0.0", "^1.0")); - assert!(!version_satisfies_constraint("0.9.0", "^1.0")); - - assert!(version_satisfies_constraint("1.2.3", "1.2.3")); - assert!(!version_satisfies_constraint("1.2.4", "1.2.3")); - - assert!(version_satisfies_constraint("1.2.0", ">=1.0.0")); - assert!(version_satisfies_constraint("2.0.0", ">=1.0.0")); - assert!(!version_satisfies_constraint("0.9.0", ">=1.0.0")); - - // Stability flag attached to the constraint should not break parsing. - assert!(version_satisfies_constraint("2.0.0", "^2.0@beta")); - } - - #[test] - fn test_shortest_path_inside_cwd() { - let cwd = PathBuf::from("/home/me/projects"); - let dir = cwd.join("foo"); - assert_eq!(shortest_path(&cwd, &dir), "foo"); - } - - #[test] - fn test_shortest_path_outside_cwd() { - let cwd = PathBuf::from("/home/me/projects"); - let dir = PathBuf::from("/elsewhere/bar"); - assert_eq!(shortest_path(&cwd, &dir), "/elsewhere/bar"); - } -} diff --git a/crates/mozart/src/commands/dependency.rs b/crates/mozart/src/commands/dependency.rs index 5766b08..1edcfd4 100644 --- a/crates/mozart/src/commands/dependency.rs +++ b/crates/mozart/src/commands/dependency.rs @@ -740,212 +740,3 @@ fn tree_prefix(depth: usize, is_last: bool) -> String { let branch = if is_last { "└─ " } else { "├─ " }; format!("{indent}{branch}") } - -#[cfg(test)] -mod tests { - use super::*; - - fn make_pkg( - name: &str, - version: &str, - require: &[(&str, &str)], - conflict: &[(&str, &str)], - is_root: bool, - ) -> PackageInfo { - PackageInfo { - name: name.to_string(), - version: version.to_string(), - require: require - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - require_dev: indexmap::IndexMap::new(), - conflict: conflict - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(), - is_root, - } - } - - #[test] - fn test_forward_dependency() { - // root requires A, A requires B → depends B returns A (and root not A) - let packages = vec![ - make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), - make_pkg("vendor/a", "1.0.0", &[("vendor/b", "^2.0")], &[], false), - make_pkg("vendor/b", "2.0.0", &[], &[], false), - ]; - let needles = vec!["vendor/b".to_string()]; - let results = get_dependents(&packages, &needles, None, false, false).unwrap(); - assert_eq!(results.len(), 1, "Only A requires B directly"); - assert_eq!(results[0].package_name, "vendor/a"); - assert_eq!(results[0].link_description, "requires"); - assert_eq!(results[0].link_constraint, "^2.0"); - } - - #[test] - fn test_recursive_dependency() { - // root requires A, A requires B → depends B --recursive returns A, with root as child - let packages = vec![ - make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), - make_pkg("vendor/a", "1.0.0", &[("vendor/b", "^2.0")], &[], false), - make_pkg("vendor/b", "2.0.0", &[], &[], false), - ]; - let needles = vec!["vendor/b".to_string()]; - let results = get_dependents(&packages, &needles, None, false, true).unwrap(); - // A is found as direct dependent of B - assert!(!results.is_empty()); - let a_result = results.iter().find(|r| r.package_name == "vendor/a"); - assert!(a_result.is_some(), "vendor/a should be found"); - // root should appear as a child of vendor/a - let children = &a_result.unwrap().children; - assert!( - children.iter().any(|c| c.package_name == "root/project"), - "root/project should be a child of vendor/a" - ); - } - - #[test] - fn test_no_dependents() { - // Nothing requires X - let packages = vec![ - make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), - make_pkg("vendor/a", "1.0.0", &[], &[], false), - ]; - let needles = vec!["vendor/x".to_string()]; - let results = get_dependents(&packages, &needles, None, false, false).unwrap(); - assert!(results.is_empty()); - } - - #[test] - fn test_circular_detection() { - // A requires B, B requires A — should not loop forever - let packages = vec![ - make_pkg("vendor/a", "1.0.0", &[("vendor/b", "^1.0")], &[], false), - make_pkg("vendor/b", "1.0.0", &[("vendor/a", "^1.0")], &[], false), - ]; - let needles = vec!["vendor/b".to_string()]; - // Should terminate without stack overflow - let results = get_dependents(&packages, &needles, None, false, true).unwrap(); - // vendor/a requires vendor/b → found; vendor/b would recurse back to vendor/a - // but visited set prevents infinite loop - assert!(!results.is_empty()); - } - - #[test] - fn test_prohibits_basic() { - // root requires A ^1.0; user asks "who prohibits A 2.0" - // → root requires A ^1.0 which doesn't match 2.0 → root prohibits it - let packages = vec![ - make_pkg("root/project", "ROOT", &[("vendor/a", "^1.0")], &[], true), - make_pkg("vendor/a", "1.0.0", &[], &[], false), - ]; - let constraint = mozart_semver::VersionConstraint::parse("2.0.0").unwrap(); - let needles = vec!["vendor/a".to_string()]; - let results = get_dependents(&packages, &needles, Some(&constraint), true, false).unwrap(); - assert!(!results.is_empty(), "root should prohibit vendor/a 2.0"); - assert_eq!(results[0].package_name, "root/project"); - assert_eq!(results[0].link_description, "requires"); - } - - #[test] - fn test_prohibits_conflict_field() { - // pkg/b conflicts with vendor/a ^2.0 → prohibits vendor/a 2.0 - let packages = vec![ - make_pkg( - "root/project", - "ROOT", - &[("vendor/a", "^1.0"), ("vendor/b", "^1.0")], - &[], - true, - ), - make_pkg("vendor/a", "1.0.0", &[], &[], false), - make_pkg( - "vendor/b", - "1.0.0", - &[], - &[("vendor/a", "^2.0")], // conflicts with vendor/a ^2.0 - false, - ), - ]; - let constraint = mozart_semver::VersionConstraint::parse("2.0.0").unwrap(); - let needles = vec!["vendor/a".to_string()]; - let results = get_dependents(&packages, &needles, Some(&constraint), true, false).unwrap(); - // vendor/b conflicts with vendor/a ^2.0 which covers 2.0.0 - let conflict_result = results.iter().find(|r| r.package_name == "vendor/b"); - assert!( - conflict_result.is_some(), - "vendor/b should prohibit vendor/a 2.0 via conflict" - ); - assert_eq!(conflict_result.unwrap().link_description, "conflicts"); - } - - #[test] - fn test_prohibits_no_issue() { - // root requires A ^2.0; user asks "who prohibits A 2.5" - // → root's constraint ^2.0 DOES match 2.5 → nobody prohibits it - let packages = vec![ - make_pkg("root/project", "ROOT", &[("vendor/a", "^2.0")], &[], true), - make_pkg("vendor/a", "2.0.0", &[], &[], false), - ]; - let constraint = mozart_semver::VersionConstraint::parse("2.5.0").unwrap(); - let needles = vec!["vendor/a".to_string()]; - let results = get_dependents(&packages, &needles, Some(&constraint), true, false).unwrap(); - assert!( - results.is_empty(), - "Nobody prohibits vendor/a 2.5 when root requires ^2.0" - ); - } - - fn test_io() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { - std::sync::Arc::new(std::sync::Mutex::new( - Box::new(mozart_core::console::Console::new( - 0, false, false, false, false, - )) as Box<dyn IoInterface>, - )) - } - - #[test] - fn test_print_table_empty() { - print_table(&[], test_io()); - } - - #[test] - fn test_print_table_single() { - let results = vec![DependencyResult { - package_name: "vendor/a".to_string(), - package_version: "1.0.0".to_string(), - link_description: "requires".to_string(), - link_target: "vendor/b".to_string(), - link_constraint: "^2.0".to_string(), - children: vec![], - }]; - print_table(&results, test_io()); - } - - #[test] - fn test_print_tree_empty() { - print_tree(&[], 0, test_io()); - } - - #[test] - fn test_print_tree_nested() { - let results = vec![DependencyResult { - package_name: "vendor/a".to_string(), - package_version: "1.0.0".to_string(), - link_description: "requires".to_string(), - link_target: "vendor/b".to_string(), - link_constraint: "^2.0".to_string(), - children: vec![DependencyResult { - package_name: "root/project".to_string(), - package_version: "ROOT".to_string(), - link_description: "requires".to_string(), - link_target: "vendor/a".to_string(), - link_constraint: "^1.0".to_string(), - children: vec![], - }], - }]; - print_tree(&results, 0, test_io()); - } -} diff --git a/crates/mozart/src/commands/diagnose.rs b/crates/mozart/src/commands/diagnose.rs index 6b2817a..a4b853a 100644 --- a/crates/mozart/src/commands/diagnose.rs +++ b/crates/mozart/src/commands/diagnose.rs @@ -487,104 +487,3 @@ pub async fn execute( } Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::tempdir; - - #[test] - fn test_parse_git_version() { - assert_eq!(parse_git_version("git version 2.39.1"), Some((2, 39, 1))); - assert_eq!(parse_git_version("git version 2.24.0"), Some((2, 24, 0))); - assert_eq!(parse_git_version("git version 1.9.5"), Some((1, 9, 5))); - assert_eq!( - parse_git_version("git version 2.40.1.windows.1"), - Some((2, 40, 1)) - ); - assert_eq!(parse_git_version("git version 2.39"), Some((2, 39, 0))); - assert_eq!(parse_git_version("3.0.0"), Some((3, 0, 0))); - } - - #[test] - fn test_check_composer_schema_valid() { - let dir = tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/project", "license": "MIT", "require": {}}"#, - ) - .unwrap(); - let result = check_composer_schema(dir.path()); - assert!(matches!(result, CheckResult::Ok(_))); - } - - #[test] - fn test_check_composer_schema_invalid_json() { - let dir = tempdir().unwrap(); - fs::write(dir.path().join("composer.json"), b"{ this is not json ").unwrap(); - let result = check_composer_schema(dir.path()); - assert!(matches!(result, CheckResult::Fail(_))); - } - - #[test] - fn test_check_composer_schema_warns_on_missing_license() { - let dir = tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/project"}"#, - ) - .unwrap(); - let result = check_composer_schema(dir.path()); - assert!(matches!(result, CheckResult::Warning(_))); - } - - #[test] - fn test_output_result_exit_code_ratcheting() { - let console: std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> = std::sync::Arc::new( - std::sync::Mutex::new(Box::new(mozart_core::console::Console::new( - 0, false, false, false, false, - )) as Box<dyn IoInterface>), - ); - let mut exit_code = 0i32; - - output_result("label", &CheckResult::ok(), &mut exit_code, console.clone()); - assert_eq!(exit_code, 0); - - output_result( - "label", - &CheckResult::warn("warn"), - &mut exit_code, - console.clone(), - ); - assert_eq!(exit_code, 1); - - output_result("label", &CheckResult::ok(), &mut exit_code, console.clone()); - assert_eq!(exit_code, 1); - - output_result( - "label", - &CheckResult::fail("fail"), - &mut exit_code, - console.clone(), - ); - assert_eq!(exit_code, 2); - - output_result( - "label", - &CheckResult::warn("another warn"), - &mut exit_code, - console, - ); - assert_eq!(exit_code, 2); - } - - #[test] - fn test_check_composer_network_http_enablement_skips_when_disabled() { - // SAFETY: tests that mutate env vars are inherently process-wide. - unsafe { std::env::set_var("COMPOSER_DISABLE_NETWORK", "1") }; - let result = check_composer_network_http_enablement(); - assert!(matches!(result, Some(CheckResult::Skip(_)))); - unsafe { std::env::remove_var("COMPOSER_DISABLE_NETWORK") }; - } -} diff --git a/crates/mozart/src/commands/exec.rs b/crates/mozart/src/commands/exec.rs index 1078158..cd07dc3 100644 --- a/crates/mozart/src/commands/exec.rs +++ b/crates/mozart/src/commands/exec.rs @@ -146,200 +146,3 @@ fn get_binaries(composer: &Composer, bin_dir: &Path) -> Vec<(String, bool)> { binaries } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::console::Console; - use std::fs; - use std::sync::{Arc, Mutex}; - - fn io() -> Arc<Mutex<Box<dyn IoInterface>>> { - Arc::new(Mutex::new( - Box::new(Console::new(0, true, false, true, true)) as Box<dyn IoInterface>, - )) - } - - #[test] - fn test_resolve_bin_dir_default() { - let dir = tempfile::tempdir().unwrap(); - let composer_json = dir.path().join("composer.json"); - fs::write(&composer_json, r#"{"name": "test/pkg", "require": {}}"#).unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let result = resolve_bin_dir(dir.path(), &composer); - assert_eq!(result, dir.path().join("vendor/bin")); - } - - #[test] - fn test_resolve_bin_dir_custom_vendor_dir() { - let dir = tempfile::tempdir().unwrap(); - let composer_json = dir.path().join("composer.json"); - fs::write( - &composer_json, - r#"{"name": "test/pkg", "require": {}, "config": {"vendor-dir": "libs"}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let result = resolve_bin_dir(dir.path(), &composer); - assert_eq!(result, dir.path().join("libs/bin")); - } - - #[test] - fn test_resolve_bin_dir_custom_bin_dir() { - let dir = tempfile::tempdir().unwrap(); - let composer_json = dir.path().join("composer.json"); - fs::write( - &composer_json, - r#"{"name": "test/pkg", "require": {}, "config": {"bin-dir": "scripts"}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let result = resolve_bin_dir(dir.path(), &composer); - assert_eq!(result, dir.path().join("scripts")); - } - - #[test] - fn test_resolve_bin_dir_with_placeholder() { - let dir = tempfile::tempdir().unwrap(); - let composer_json = dir.path().join("composer.json"); - fs::write( - &composer_json, - r#"{"name": "test/pkg", "require": {}, "config": {"vendor-dir": "packages", "bin-dir": "{$vendor-dir}/commands"}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let result = resolve_bin_dir(dir.path(), &composer); - assert_eq!(result, dir.path().join("packages/commands")); - } - - #[test] - fn test_get_binaries_from_bin_dir() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - fs::create_dir_all(&bin_dir).unwrap(); - - fs::write(bin_dir.join("phpunit"), "#!/bin/sh").unwrap(); - fs::write(bin_dir.join("phpstan"), "#!/bin/sh").unwrap(); - - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "require": {}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let binaries = get_binaries(&composer, &bin_dir); - let names: Vec<&str> = binaries.iter().map(|(n, _)| n.as_str()).collect(); - assert!(names.contains(&"phpunit")); - assert!(names.contains(&"phpstan")); - // All should be non-local - for (_, is_local) in &binaries { - assert!(!is_local); - } - } - - #[test] - fn test_get_binaries_skips_bat_files() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - fs::create_dir_all(&bin_dir).unwrap(); - - fs::write(bin_dir.join("phpunit"), "#!/bin/sh").unwrap(); - fs::write(bin_dir.join("phpunit.bat"), "@echo off").unwrap(); - - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "require": {}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let binaries = get_binaries(&composer, &bin_dir); - let names: Vec<&str> = binaries.iter().map(|(n, _)| n.as_str()).collect(); - assert!(names.contains(&"phpunit")); - assert!(!names.contains(&"phpunit.bat")); - } - - #[test] - fn test_get_binaries_from_root_composer_json() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - // Don't create bin_dir — no vendor binaries - - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "require": {}, "bin": ["bin/my-tool", "bin/helper"]}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let binaries = get_binaries(&composer, &bin_dir); - let names: Vec<&str> = binaries.iter().map(|(n, _)| n.as_str()).collect(); - assert!(names.contains(&"my-tool")); - assert!(names.contains(&"helper")); - // All should be local - for (_, is_local) in &binaries { - assert!(is_local); - } - } - - #[test] - fn test_get_binaries_empty_bin_dir() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - // bin_dir doesn't exist - - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "require": {}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let binaries = get_binaries(&composer, &bin_dir); - assert!(binaries.is_empty()); - } - - #[test] - fn test_list_mode_no_binaries_errors() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "require": {}}"#, - ) - .unwrap(); - - let bin_dir = dir.path().join("vendor/bin"); - let composer = Composer::require(io(), dir.path()).unwrap(); - let binaries = get_binaries(&composer, &bin_dir); - assert!( - binaries.is_empty(), - "Expected no binaries to trigger error path" - ); - } - - #[test] - fn test_execute_binary_not_found() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "require": {}}"#, - ) - .unwrap(); - - let composer = Composer::require(io(), dir.path()).unwrap(); - let bin_dir = resolve_bin_dir(dir.path(), &composer); - - // No binaries exist — looking up a name should find nothing - let candidate = bin_dir.join("nonexistent-binary"); - assert!(!candidate.exists()); - - // Confirm root bin entries are also empty - let root = mozart_core::package::read_from_file(&dir.path().join("composer.json")).unwrap(); - assert!(root.bin.is_empty()); - } -} diff --git a/crates/mozart/src/commands/fund.rs b/crates/mozart/src/commands/fund.rs index 677137c..63164c3 100644 --- a/crates/mozart/src/commands/fund.rs +++ b/crates/mozart/src/commands/fund.rs @@ -202,153 +202,3 @@ fn render_json( console_writeln!(io, "{}", &String::from_utf8(ser.into_inner())?); Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::console::Console; - - fn make_funding_json(entries: &[(&str, &str)]) -> Vec<serde_json::Value> { - entries - .iter() - .map(|(t, u)| serde_json::json!({"type": t, "url": u})) - .collect() - } - - #[test] - fn insert_funding_data_basic() { - let mut fundings = BTreeMap::new(); - let funding = make_funding_json(&[("github", "https://github.com/Seldaek")]); - insert_funding_data(&mut fundings, "monolog/monolog", &funding); - - let monolog = fundings.get("monolog").unwrap(); - let url = "https://github.com/sponsors/Seldaek"; - let packages = monolog.get(url).unwrap(); - assert_eq!(packages, &vec!["monolog".to_string()]); - } - - #[test] - fn insert_funding_data_skips_empty_url() { - let mut fundings = BTreeMap::new(); - let funding = vec![ - serde_json::json!({"type": "github", "url": ""}), - serde_json::json!({"type": "tidelift"}), - serde_json::json!({"type": "github", "url": "https://github.com/user"}), - ]; - insert_funding_data(&mut fundings, "vendor/pkg", &funding); - - let vendor = fundings.get("vendor").unwrap(); - assert_eq!(vendor.len(), 1); - assert!(vendor.contains_key("https://github.com/sponsors/user")); - } - - #[test] - fn insert_funding_data_skips_malformed_pretty_name() { - let mut fundings = BTreeMap::new(); - let funding = make_funding_json(&[("github", "https://github.com/user")]); - insert_funding_data(&mut fundings, "no-slash-name", &funding); - assert!(fundings.is_empty()); - } - - #[test] - fn insert_funding_data_groups_by_vendor() { - let mut fundings = BTreeMap::new(); - let funding = make_funding_json(&[("github", "https://github.com/fabpot")]); - insert_funding_data(&mut fundings, "symfony/console", &funding); - insert_funding_data(&mut fundings, "symfony/http-kernel", &funding); - - let symfony = fundings.get("symfony").unwrap(); - let url = "https://github.com/sponsors/fabpot"; - let packages = symfony.get(url).unwrap(); - assert_eq!(packages.len(), 2); - assert!(packages.contains(&"console".to_string())); - assert!(packages.contains(&"http-kernel".to_string())); - } - - #[test] - fn insert_funding_data_multiple_urls() { - let mut fundings = BTreeMap::new(); - let funding = vec![ - serde_json::json!({"type": "github", "url": "https://github.com/fabpot"}), - serde_json::json!({ - "type": "tidelift", - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony" - }), - ]; - insert_funding_data(&mut fundings, "symfony/console", &funding); - - let symfony = fundings.get("symfony").unwrap(); - assert_eq!(symfony.len(), 2); - assert!(symfony.contains_key("https://github.com/sponsors/fabpot")); - assert!( - symfony.contains_key("https://tidelift.com/funding/github/packagist/symfony/symfony") - ); - } - - #[test] - fn rewrite_github_url_profile() { - let result = rewrite_github_url("https://github.com/Seldaek", Some("github")); - assert_eq!(result, "https://github.com/sponsors/Seldaek"); - } - - #[test] - fn rewrite_github_url_already_sponsors() { - let result = rewrite_github_url("https://github.com/sponsors/Seldaek", Some("github")); - assert_eq!(result, "https://github.com/sponsors/Seldaek"); - } - - #[test] - fn rewrite_github_url_non_github_type() { - let result = rewrite_github_url("https://github.com/fabpot", Some("tidelift")); - assert_eq!(result, "https://github.com/fabpot"); - } - - #[test] - fn rewrite_github_url_deep_path() { - let result = rewrite_github_url("https://github.com/user/repo", Some("github")); - assert_eq!(result, "https://github.com/user/repo"); - } - - #[test] - fn rewrite_github_url_missing_type() { - let result = rewrite_github_url("https://github.com/user", None); - assert_eq!(result, "https://github.com/user"); - } - - #[test] - fn render_json_empty_emits_array() { - // Composer's `JsonFile::encode([])` emits `[]`; ensure Mozart matches - // rather than serializing the empty BTreeMap to `{}`. - let console = Console::new(0, false, false, false, true); - let fundings: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new(); - - let buf = Vec::new(); - let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut ser = serde_json::Serializer::with_formatter(buf, formatter); - if fundings.is_empty() { - let empty: Vec<()> = Vec::new(); - empty.serialize(&mut ser).unwrap(); - } else { - fundings.serialize(&mut ser).unwrap(); - } - let out = String::from_utf8(ser.into_inner()).unwrap(); - assert_eq!(out, "[]"); - let _ = console; - } - - #[test] - fn render_json_non_empty_is_object() { - let mut fundings: BTreeMap<String, BTreeMap<String, Vec<String>>> = BTreeMap::new(); - let funding = make_funding_json(&[("github", "https://github.com/Seldaek")]); - insert_funding_data(&mut fundings, "monolog/monolog", &funding); - - let buf = Vec::new(); - let formatter = serde_json::ser::PrettyFormatter::with_indent(b" "); - let mut ser = serde_json::Serializer::with_formatter(buf, formatter); - fundings.serialize(&mut ser).unwrap(); - let out = String::from_utf8(ser.into_inner()).unwrap(); - assert!(out.starts_with('{')); - assert!(out.contains("monolog")); - assert!(out.contains("https://github.com/sponsors/Seldaek")); - } -} diff --git a/crates/mozart/src/commands/global.rs b/crates/mozart/src/commands/global.rs index 5d5cb81..a1ee380 100644 --- a/crates/mozart/src/commands/global.rs +++ b/crates/mozart/src/commands/global.rs @@ -93,98 +93,3 @@ fn append_global_options(cli: &super::Cli) -> Vec<String> { opts } - -#[cfg(test)] -mod tests { - use super::*; - use crate::commands::{Cli, Commands}; - use clap::Parser as _; - - fn default_cli() -> Cli { - Cli::try_parse_from(["mozart", "about"]).unwrap() - } - - #[test] - fn test_append_global_options_empty() { - let cli = default_cli(); - let opts = append_global_options(&cli); - assert!(opts.is_empty()); - } - - #[test] - fn test_append_global_options_verbose() { - let cli = Cli::try_parse_from(["mozart", "-vv", "about"]).unwrap(); - let opts = append_global_options(&cli); - assert_eq!(opts, vec!["--verbose", "--verbose"]); - } - - #[test] - fn test_append_global_options_all() { - let cli = Cli::try_parse_from([ - "mozart", - "--verbose", - "--quiet", - "--profile", - "--no-plugins", - "--no-scripts", - "--no-cache", - "--no-interaction", - "--ansi", - "about", - ]) - .unwrap(); - let opts = append_global_options(&cli); - assert!(opts.contains(&"--verbose".to_string())); - assert!(opts.contains(&"--quiet".to_string())); - assert!(opts.contains(&"--profile".to_string())); - assert!(opts.contains(&"--no-plugins".to_string())); - assert!(opts.contains(&"--no-scripts".to_string())); - assert!(opts.contains(&"--no-cache".to_string())); - assert!(opts.contains(&"--no-interaction".to_string())); - assert!(opts.contains(&"--ansi".to_string())); - } - - #[test] - fn test_append_global_options_does_not_forward_working_dir() { - let cli = Cli::try_parse_from(["mozart", "--working-dir", "/some/path", "about"]).unwrap(); - let opts = append_global_options(&cli); - assert!(!opts.iter().any(|o| o.contains("working-dir"))); - assert!(!opts.iter().any(|o| o == "/some/path")); - } - - #[test] - fn test_global_args_has_correct_command() { - // Verify GlobalArgs parses correctly through the CLI - let cli = Cli::try_parse_from(["mozart", "global", "require", "vendor/package"]).unwrap(); - if let Some(Commands::Global(args)) = cli.command { - assert_eq!(args.command_name, Some("require".to_string())); - assert_eq!(args.args, vec!["vendor/package"]); - } else { - panic!("Expected Global command"); - } - } - - #[test] - fn test_global_args_hyphen_values() { - // Verify hyphen values in trailing args are accepted - let cli = Cli::try_parse_from(["mozart", "global", "require", "vendor/pkg", "--no-update"]) - .unwrap(); - if let Some(Commands::Global(args)) = cli.command { - assert_eq!(args.command_name, Some("require".to_string())); - assert!(args.args.contains(&"--no-update".to_string())); - } else { - panic!("Expected Global command"); - } - } - - #[test] - fn test_global_args_no_subcommand() { - // Verify that no subcommand parses to None - let cli = Cli::try_parse_from(["mozart", "global"]).unwrap(); - if let Some(Commands::Global(args)) = cli.command { - assert_eq!(args.command_name, None); - } else { - panic!("Expected Global command"); - } - } -} diff --git a/crates/mozart/src/commands/init.rs b/crates/mozart/src/commands/init.rs index 4598d39..4fb6c3f 100644 --- a/crates/mozart/src/commands/init.rs +++ b/crates/mozart/src/commands/init.rs @@ -846,52 +846,3 @@ fn add_vendor_ignore(gitignore_path: &Path) -> anyhow::Result<()> { std::fs::write(gitignore_path, contents)?; Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_repositories_http_url_yields_composer_type() { - let repos = parse_repositories(&["https://repo.example.com".to_string()]).unwrap(); - assert_eq!(repos.len(), 1); - assert_eq!(repos[0].repo_type, "composer"); - assert_eq!(repos[0].url.as_deref(), Some("https://repo.example.com")); - } - - #[test] - fn parse_repositories_http_scheme_also_matches() { - let repos = parse_repositories(&["http://example.com".to_string()]).unwrap(); - assert_eq!(repos[0].repo_type, "composer"); - } - - #[test] - fn parse_repositories_json_object_preserved() { - let repos = parse_repositories(&[ - r#"{"type":"vcs","url":"https://github.com/acme/repo"}"#.to_string() - ]) - .unwrap(); - assert_eq!(repos[0].repo_type, "vcs"); - assert_eq!( - repos[0].url.as_deref(), - Some("https://github.com/acme/repo") - ); - } - - #[test] - fn parse_repositories_unknown_form_is_error() { - let err = parse_repositories(&["not-a-url-or-json".to_string()]).unwrap_err(); - assert!( - err.to_string() - .contains("Has to be a .json file, an http url or a JSON object"), - "{err}", - ); - } - - #[test] - fn parse_repositories_json_without_type_is_error() { - let err = - parse_repositories(&[r#"{"url":"https://example.com"}"#.to_string()]).unwrap_err(); - assert!(err.to_string().contains("'type'"), "{err}"); - } -} diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index 709b934..59baf0b 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -1076,514 +1076,3 @@ pub async fn run( ) .await } - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::tempdir; - - fn make_locked_package(name: &str, version: &str) -> lockfile::LockedPackage { - lockfile::LockedPackage { - name: name.to_string(), - version: version.to_string(), - version_normalized: None, - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: Some("library".to_string()), - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn make_installed_entry(name: &str, version: &str) -> installed::InstalledPackageEntry { - installed::InstalledPackageEntry { - name: name.to_string(), - version: version.to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn minimal_lock(packages: Vec<lockfile::LockedPackage>) -> lockfile::LockFile { - lockfile::LockFile { - readme: lockfile::LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages, - packages_dev: Some(vec![]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - } - } - - // ----------------------------------------------------------------------- - // compute_operations tests - // ----------------------------------------------------------------------- - - #[test] - fn test_compute_operations_all_new() { - let locked = [ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("monolog/monolog", "3.8.0"), - ]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let installed = installed::InstalledPackages::new(); - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert_eq!(ops.len(), 2); - assert!(matches!(ops[0].1, Action::Install)); - assert!(matches!(ops[1].1, Action::Install)); - assert!(removals.is_empty()); - } - - #[test] - fn test_compute_operations_all_skipped() { - let locked = [make_locked_package("psr/log", "3.0.0")]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let mut installed = installed::InstalledPackages::new(); - installed.upsert(make_installed_entry("psr/log", "3.0.0")); - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert_eq!(ops.len(), 1); - assert!(matches!(ops[0].1, Action::Skip)); - assert!(removals.is_empty()); - } - - #[test] - fn test_compute_operations_update_needed() { - let locked = [make_locked_package("psr/log", "3.0.1")]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let mut installed = installed::InstalledPackages::new(); - installed.upsert(make_installed_entry("psr/log", "3.0.0")); - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert_eq!(ops.len(), 1); - assert!(matches!(ops[0].1, Action::Update)); - assert!(removals.is_empty()); - } - - #[test] - fn test_compute_operations_removals() { - let locked = [make_locked_package("psr/log", "3.0.0")]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let mut installed = installed::InstalledPackages::new(); - installed.upsert(make_installed_entry("psr/log", "3.0.0")); - installed.upsert(make_installed_entry("monolog/monolog", "3.8.0")); - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert_eq!(ops.len(), 1); - assert!(matches!(ops[0].1, Action::Skip)); - assert_eq!(removals.len(), 1); - assert_eq!(removals[0], "monolog/monolog"); - } - - #[test] - fn test_compute_operations_mixed() { - let locked = [ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("symfony/console", "7.2.3"), - make_locked_package("monolog/monolog", "3.8.1"), - ]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let mut installed = installed::InstalledPackages::new(); - // psr/log already at correct version -> skip - installed.upsert(make_installed_entry("psr/log", "3.0.0")); - // monolog at wrong version -> update - installed.upsert(make_installed_entry("monolog/monolog", "3.8.0")); - // old-package not in locked -> removal - installed.upsert(make_installed_entry("old/package", "1.0.0")); - // symfony/console not installed at all -> install - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert_eq!(ops.len(), 3); - - let psr = ops.iter().find(|(p, _)| p.name == "psr/log").unwrap(); - assert!(matches!(psr.1, Action::Skip)); - - let symfony = ops - .iter() - .find(|(p, _)| p.name == "symfony/console") - .unwrap(); - assert!(matches!(symfony.1, Action::Install)); - - let monolog = ops - .iter() - .find(|(p, _)| p.name == "monolog/monolog") - .unwrap(); - assert!(matches!(monolog.1, Action::Update)); - - assert_eq!(removals.len(), 1); - assert_eq!(removals[0], "old/package"); - } - - #[test] - fn test_compute_operations_case_insensitive() { - let locked = [make_locked_package("Monolog/Monolog", "3.8.0")]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let mut installed = installed::InstalledPackages::new(); - installed.upsert(make_installed_entry("monolog/monolog", "3.8.0")); - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert_eq!(ops.len(), 1); - assert!(matches!(ops[0].1, Action::Skip)); - assert!(removals.is_empty()); - } - - #[test] - fn test_compute_operations_empty_lock() { - let locked: Vec<lockfile::LockedPackage> = vec![]; - let locked_refs: Vec<&lockfile::LockedPackage> = locked.iter().collect(); - let mut installed = installed::InstalledPackages::new(); - installed.upsert(make_installed_entry("old/package", "1.0.0")); - - let (ops, removals) = compute_operations(&locked_refs, &installed); - - assert!(ops.is_empty()); - assert_eq!(removals.len(), 1); - assert_eq!(removals[0], "old/package"); - } - - // ----------------------------------------------------------------------- - // locked_to_installed_entry tests - // ----------------------------------------------------------------------- - - #[test] - fn test_locked_to_installed_entry_conversion() { - let dir = tempdir().unwrap(); - let vendor_dir = dir.path().join("vendor"); - - let mut pkg = make_locked_package("psr/log", "3.0.2"); - pkg.version_normalized = Some("3.0.2.0".to_string()); - pkg.package_type = Some("library".to_string()); - pkg.autoload = Some(serde_json::json!({"psr-4": {"Psr\\Log\\": "src/"}})); - - let entry = locked_to_installed_entry(&pkg, &vendor_dir); - - assert_eq!(entry.name, "psr/log"); - assert_eq!(entry.version, "3.0.2"); - assert_eq!(entry.version_normalized.as_deref(), Some("3.0.2.0")); - assert_eq!(entry.package_type.as_deref(), Some("library")); - assert_eq!(entry.install_path.as_deref(), Some("../psr/log")); - assert!(entry.autoload.is_some()); - assert!(entry.aliases.is_empty()); - assert!(entry.extra_fields.is_empty()); - assert!(entry.source.is_none()); - assert!(entry.dist.is_none()); - } - - #[test] - fn test_locked_to_installed_entry_with_dist() { - let dir = tempdir().unwrap(); - let vendor_dir = dir.path().join("vendor"); - - let mut pkg = make_locked_package("monolog/monolog", "3.8.0"); - pkg.dist = Some(lockfile::LockedDist { - dist_type: "zip".to_string(), - url: "https://example.com/monolog.zip".to_string(), - reference: Some("abc123".to_string()), - shasum: Some("deadbeef".to_string()), - }); - - let entry = locked_to_installed_entry(&pkg, &vendor_dir); - - assert_eq!(entry.name, "monolog/monolog"); - assert_eq!(entry.install_path.as_deref(), Some("../monolog/monolog")); - assert!(entry.dist.is_some()); - } - - #[test] - fn test_locked_to_installed_entry_propagates_extra_fields() { - // Composer's installed.json carries package flags like `abandoned` and - // `default-branch` that LockedPackage stores in extra_fields. Make sure - // they survive the conversion so we don't strip them on rewrite. - let dir = tempdir().unwrap(); - let vendor_dir = dir.path().join("vendor"); - - let mut pkg = make_locked_package("a/a", "1.0.0"); - pkg.extra_fields.insert( - "abandoned".to_string(), - serde_json::Value::String("replacement".to_string()), - ); - pkg.extra_fields - .insert("default-branch".to_string(), serde_json::Value::Bool(true)); - - let entry = locked_to_installed_entry(&pkg, &vendor_dir); - - assert_eq!( - entry.extra_fields.get("abandoned"), - Some(&serde_json::Value::String("replacement".to_string())) - ); - assert_eq!( - entry.extra_fields.get("default-branch"), - Some(&serde_json::Value::Bool(true)) - ); - } - - // ----------------------------------------------------------------------- - // installed.json generation tests - // ----------------------------------------------------------------------- - - #[test] - fn test_installed_json_written_from_lock() { - let dir = tempdir().unwrap(); - let vendor_dir = dir.path().join("vendor"); - - // Write a lock file - let lock_path = dir.path().join("composer.lock"); - let lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.2"), - make_locked_package("vendor/pkg", "1.2.3"), - ]); - lock.write_to_file(&lock_path).unwrap(); - - // Simulate what execute() does for the installed.json write step - let packages_to_install: Vec<&lockfile::LockedPackage> = lock.packages.iter().collect(); - let mut new_installed = installed::InstalledPackages::new(); - new_installed.dev = false; - for pkg in &packages_to_install { - new_installed.upsert(locked_to_installed_entry(pkg, &vendor_dir)); - } - new_installed.write(&vendor_dir).unwrap(); - - // Verify installed.json - let loaded = installed::InstalledPackages::read(&vendor_dir).unwrap(); - assert_eq!(loaded.packages.len(), 2); - assert!(loaded.is_installed("psr/log", "3.0.2")); - assert!(loaded.is_installed("vendor/pkg", "1.2.3")); - assert_eq!( - loaded - .packages - .iter() - .find(|p| p.name == "psr/log") - .unwrap() - .install_path - .as_deref(), - Some("../psr/log") - ); - } - - #[test] - fn test_installed_json_dev_package_names() { - let dir = tempdir().unwrap(); - let vendor_dir = dir.path().join("vendor"); - - let mut lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.2")]); - lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); - - // Simulate dev mode installed.json generation - let mut packages_to_install: Vec<&lockfile::LockedPackage> = lock.packages.iter().collect(); - if let Some(ref dev_pkgs) = lock.packages_dev { - packages_to_install.extend(dev_pkgs.iter()); - } - - let mut new_installed = installed::InstalledPackages::new(); - new_installed.dev = true; - if let Some(ref dev_pkgs) = lock.packages_dev { - new_installed.dev_package_names = dev_pkgs.iter().map(|p| p.name.clone()).collect(); - } - for pkg in &packages_to_install { - new_installed.upsert(locked_to_installed_entry(pkg, &vendor_dir)); - } - new_installed.write(&vendor_dir).unwrap(); - - let loaded = installed::InstalledPackages::read(&vendor_dir).unwrap(); - assert_eq!(loaded.packages.len(), 2); - assert!(loaded.dev); - assert_eq!(loaded.dev_package_names, vec!["phpunit/phpunit"]); - } - - // ----------------------------------------------------------------------- - // Platform requirement check tests - // ----------------------------------------------------------------------- - - fn root_with_require( - require: &[(&str, &str)], - require_dev: &[(&str, &str)], - ) -> RootPackageData { - let mut raw = mozart_core::package::RawPackageData::new("__root__".to_string()); - for (k, v) in require { - raw.require.insert((*k).to_string(), (*v).to_string()); - } - for (k, v) in require_dev { - raw.require_dev.insert((*k).to_string(), (*v).to_string()); - } - RootPackageData::from_raw(raw) - } - - fn lock_with_platform( - platform: serde_json::Value, - platform_dev: serde_json::Value, - ) -> lockfile::LockFile { - let mut lock = minimal_lock(vec![]); - lock.platform = platform; - lock.platform_dev = platform_dev; - lock - } - - fn pp(name: &str, version: &str) -> mozart_core::platform::PlatformPackage { - mozart_core::platform::PlatformPackage { - name: name.to_string(), - version: version.to_string(), - } - } - - #[test] - fn combine_platform_requirements_root_overrides_lock() { - let lock = lock_with_platform( - serde_json::json!({"php": "^7.4", "ext-foo": "^5"}), - serde_json::json!({}), - ); - let root = root_with_require(&[("ext-foo", "^10")], &[]); - let combined = combine_platform_requirements(&root, &lock, true); - - // Root composer.json wins for ext-foo, lock contributes plain php. - assert_eq!(combined.get("ext-foo").map(String::as_str), Some("^10")); - assert_eq!(combined.get("php").map(String::as_str), Some("^7.4")); - } - - #[test] - fn combine_platform_requirements_skips_non_platform_requires() { - let lock = lock_with_platform(serde_json::json!({}), serde_json::json!({})); - let root = root_with_require(&[("vendor/pkg", "^1.0"), ("php", "^8.0")], &[]); - let combined = combine_platform_requirements(&root, &lock, true); - - assert_eq!(combined.len(), 1); - assert_eq!(combined.get("php").map(String::as_str), Some("^8.0")); - } - - #[test] - fn combine_platform_requirements_includes_dev_only_when_dev_mode() { - let lock = lock_with_platform( - serde_json::json!({}), - serde_json::json!({"ext-only-dev": "^1"}), - ); - let root = root_with_require(&[], &[("ext-from-dev-require", "^1")]); - - let with_dev = combine_platform_requirements(&root, &lock, true); - assert!(with_dev.contains_key("ext-only-dev")); - assert!(with_dev.contains_key("ext-from-dev-require")); - - let no_dev = combine_platform_requirements(&root, &lock, false); - assert!(!no_dev.contains_key("ext-only-dev")); - assert!(!no_dev.contains_key("ext-from-dev-require")); - } - - #[test] - fn check_platform_requirements_reports_missing_extension() { - let combined: indexmap::IndexMap<String, String> = - [("ext-foo".to_string(), "^10".to_string())] - .into_iter() - .collect(); - let platform = vec![pp("php", "8.2.0")]; - let problems = check_platform_requirements_against(&combined, &platform, false, &[]); - - assert_eq!(problems.len(), 1); - assert_eq!( - problems[0], - "- Root composer.json requires PHP extension ext-foo ^10 but it is missing from your system. Install or enable PHP's foo extension." - ); - } - - #[test] - fn check_platform_requirements_reports_unsatisfied_php() { - let combined: indexmap::IndexMap<String, String> = [("php".to_string(), "^20".to_string())] - .into_iter() - .collect(); - let platform = vec![pp("php", "8.2.0")]; - let problems = check_platform_requirements_against(&combined, &platform, false, &[]); - - assert_eq!(problems.len(), 1); - assert_eq!( - problems[0], - "- Root composer.json requires php ^20 but your php version (8.2.0) does not satisfy that requirement." - ); - } - - #[test] - fn check_platform_requirements_satisfied_returns_empty() { - let combined: indexmap::IndexMap<String, String> = - [("php".to_string(), "^8.0".to_string())] - .into_iter() - .collect(); - let platform = vec![pp("php", "8.2.0")]; - let problems = check_platform_requirements_against(&combined, &platform, false, &[]); - - assert!(problems.is_empty()); - } - - #[test] - fn check_platform_requirements_ignore_platform_reqs_short_circuits() { - let combined: indexmap::IndexMap<String, String> = - [("ext-foo".to_string(), "^10".to_string())] - .into_iter() - .collect(); - let platform: Vec<mozart_core::platform::PlatformPackage> = vec![]; - let problems = check_platform_requirements_against(&combined, &platform, true, &[]); - - assert!(problems.is_empty()); - } - - #[test] - fn check_platform_requirements_specific_ignore_filters_named_packages() { - let combined: indexmap::IndexMap<String, String> = [ - ("ext-foo".to_string(), "^10".to_string()), - ("ext-bar".to_string(), "^10".to_string()), - ] - .into_iter() - .collect(); - let platform = vec![pp("php", "8.2.0")]; - let problems = check_platform_requirements_against( - &combined, - &platform, - false, - &["ext-foo".to_string()], - ); - - assert_eq!(problems.len(), 1); - assert!(problems[0].contains("ext-bar")); - } - - #[test] - fn verify_lock_platform_problems_returns_empty_when_no_reqs() { - // No platform reqs anywhere → returns empty without invoking detect_platform. - let lock = lock_with_platform(serde_json::json!({}), serde_json::json!({})); - let root = root_with_require(&[("vendor/pkg", "^1.0")], &[]); - let problems = verify_lock_platform_problems(&root, &lock, true, false, &[]); - - assert!(problems.is_empty()); - } -} diff --git a/crates/mozart/src/commands/licenses.rs b/crates/mozart/src/commands/licenses.rs index 0e4ea0b..93c0e63 100644 --- a/crates/mozart/src/commands/licenses.rs +++ b/crates/mozart/src/commands/licenses.rs @@ -448,296 +448,3 @@ fn tally_licenses(entries: &[LicenseEntry]) -> Vec<(String, usize)> { result.sort_by_key(|(_, count)| std::cmp::Reverse(*count)); result } - -#[cfg(test)] -mod tests { - use super::*; - - fn entry(name: &str, licenses: &[&str]) -> LicenseEntry { - LicenseEntry { - pretty_name: name.to_string(), - name: name.to_lowercase(), - version: "1.0.0".to_string(), - licenses: licenses.iter().map(|s| s.to_string()).collect(), - requires: indexmap::IndexMap::new(), - support_source: None, - source_url: None, - homepage: None, - } - } - - #[test] - fn tally_licenses_orders_by_count_then_first_seen() { - // First MIT entry comes before Apache-2.0; tie-break must keep - // MIT first when their counts collide. - let entries = vec![ - entry("a/a", &["MIT"]), - entry("b/b", &["Apache-2.0"]), - entry("c/c", &["BSD-3-Clause"]), - ]; - let counts = tally_licenses(&entries); - // All three at count 1 — input order preserved. - assert_eq!( - counts, - vec![ - ("MIT".to_string(), 1), - ("Apache-2.0".to_string(), 1), - ("BSD-3-Clause".to_string(), 1), - ] - ); - } - - #[test] - fn tally_licenses_count_descending() { - let entries = vec![ - entry("a/a", &["Apache-2.0"]), - entry("b/b", &["MIT"]), - entry("c/c", &["MIT"]), - ]; - let counts = tally_licenses(&entries); - assert_eq!(counts[0], ("MIT".to_string(), 2)); - assert_eq!(counts[1], ("Apache-2.0".to_string(), 1)); - } - - #[test] - fn tally_licenses_empty() { - assert!(tally_licenses(&[]).is_empty()); - } - - #[test] - fn tally_licenses_no_license_counts_as_none() { - let entries = vec![entry("a/a", &[])]; - let counts = tally_licenses(&entries); - assert_eq!(counts, vec![("none".to_string(), 1)]); - } - - #[test] - fn read_root_licenses_string_form() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("composer.json"); - std::fs::write(&path, r#"{"name": "test/p", "license": "MIT"}"#).unwrap(); - assert_eq!(read_root_licenses(&path).unwrap(), vec!["MIT"]); - } - - #[test] - fn read_root_licenses_array_form() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("composer.json"); - std::fs::write( - &path, - r#"{"name": "test/p", "license": ["MIT", "Apache-2.0"]}"#, - ) - .unwrap(); - assert_eq!( - read_root_licenses(&path).unwrap(), - vec!["MIT", "Apache-2.0"] - ); - } - - #[test] - fn read_root_licenses_absent() { - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("composer.json"); - std::fs::write(&path, r#"{"name": "test/p"}"#).unwrap(); - assert!(read_root_licenses(&path).unwrap().is_empty()); - } - - #[test] - fn installed_to_entry_extracts_require_and_license() { - use mozart_core::repository::installed::InstalledPackageEntry; - let mut extra = indexmap::IndexMap::new(); - extra.insert("license".to_string(), serde_json::json!(["MIT"])); - extra.insert( - "require".to_string(), - serde_json::json!({"psr/log": "^1.0"}), - ); - let pkg = InstalledPackageEntry { - name: "monolog/monolog".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: extra, - }; - let e = installed_to_entry(&pkg); - assert_eq!(e.licenses, vec!["MIT"]); - assert_eq!(e.requires.get("psr/log").map(String::as_str), Some("^1.0")); - } - - #[test] - fn installed_to_entry_pulls_support_source_and_source_url() { - use mozart_core::repository::installed::InstalledPackageEntry; - let pkg = InstalledPackageEntry { - name: "vendor/pkg".to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: Some(serde_json::json!({"type": "git", "url": "https://example.com/repo.git"})), - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: Some("https://example.com/".to_string()), - support: Some(serde_json::json!({"source": "https://github.com/v/p"})), - extra_fields: indexmap::IndexMap::new(), - }; - let e = installed_to_entry(&pkg); - assert_eq!(e.support_source.as_deref(), Some("https://github.com/v/p")); - assert_eq!( - e.source_url.as_deref(), - Some("https://example.com/repo.git") - ); - assert_eq!(e.homepage.as_deref(), Some("https://example.com/")); - // PackageInfo helpers should pick support source first. - assert_eq!( - package_info::view_source_or_homepage_url(&e).as_deref(), - Some("https://github.com/v/p"), - ); - } - - #[test] - fn no_dev_filters_to_root_require_closure() { - // Set up: root requires a/a only. b/b is in installed but not - // reachable; should be dropped under --no-dev. - let dir = tempfile::tempdir().unwrap(); - let working_dir = dir.path(); - let vendor_dir = working_dir.join("vendor"); - - std::fs::write( - working_dir.join("composer.json"), - r#"{"name": "test/project", "require": {"a/a": "*"}}"#, - ) - .unwrap(); - - let mut installed = mozart_core::repository::installed::InstalledPackages::new(); - installed.upsert(mozart_core::repository::installed::InstalledPackageEntry { - name: "a/a".to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - }); - installed.upsert(mozart_core::repository::installed::InstalledPackageEntry { - name: "b/b".to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - }); - installed.write(&vendor_dir).unwrap(); - - let mut root_req = indexmap::IndexMap::new(); - root_req.insert("a/a".to_string(), "*".to_string()); - - let kept = load_installed_entries(working_dir, &root_req, true).unwrap(); - let names: Vec<&str> = kept.iter().map(|e| e.name.as_str()).collect(); - assert_eq!(names, vec!["a/a"]); - - // Without --no-dev: both packages are listed. - let all = load_installed_entries(working_dir, &root_req, false).unwrap(); - assert_eq!(all.len(), 2); - } - - #[test] - fn locked_no_dev_drops_packages_dev() { - use mozart_core::repository::lockfile::{LockFile, LockedPackage}; - let dir = tempfile::tempdir().unwrap(); - let working_dir = dir.path(); - std::fs::write( - working_dir.join("composer.json"), - r#"{"name": "test/project"}"#, - ) - .unwrap(); - let lock = LockFile { - readme: LockFile::default_readme(), - content_hash: "abc".to_string(), - packages: vec![LockedPackage { - name: "psr/log".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: None, - autoload: None, - autoload_dev: None, - license: Some(vec!["MIT".to_string()]), - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - }], - packages_dev: Some(vec![LockedPackage { - name: "phpunit/phpunit".to_string(), - version: "10.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: None, - autoload: None, - autoload_dev: None, - license: Some(vec!["BSD-3-Clause".to_string()]), - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - }]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - }; - lock.write_to_file(&working_dir.join("composer.lock")) - .unwrap(); - - let prod = load_locked_entries(working_dir, true).unwrap(); - assert_eq!(prod.len(), 1); - assert_eq!(prod[0].name, "psr/log"); - - let all = load_locked_entries(working_dir, false).unwrap(); - assert_eq!(all.len(), 2); - } -} diff --git a/crates/mozart/src/commands/remove.rs b/crates/mozart/src/commands/remove.rs index 938a5d1..c5b9ba7 100644 --- a/crates/mozart/src/commands/remove.rs +++ b/crates/mozart/src/commands/remove.rs @@ -676,452 +676,3 @@ async fn remove_unused( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::package::RawPackageData; - use mozart_core::repository::lockfile; - - fn make_locked_package(name: &str, version: &str) -> lockfile::LockedPackage { - lockfile::LockedPackage { - name: name.to_string(), - version: version.to_string(), - version_normalized: Some(format!("{}.0", version)), - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: Some("library".to_string()), - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn minimal_lock(packages: Vec<lockfile::LockedPackage>) -> lockfile::LockFile { - lockfile::LockFile { - readme: lockfile::LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages, - packages_dev: Some(vec![]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - } - } - - fn make_raw_package(name: &str) -> RawPackageData { - RawPackageData::new(name.to_string()) - } - - /// Remove a package from `require`, verify it's gone from `RawPackageData`. - #[test] - fn test_remove_from_require() { - let mut composer = make_raw_package("test/project"); - composer - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - composer - .require - .insert("monolog/monolog".to_string(), "^3.0".to_string()); - - assert!(composer.require.contains_key("psr/log")); - - composer.require.shift_remove("psr/log"); - - assert!( - !composer.require.contains_key("psr/log"), - "psr/log should be removed from require" - ); - assert!( - composer.require.contains_key("monolog/monolog"), - "monolog/monolog should remain in require" - ); - } - - /// Remove a package from `require-dev` with `--dev` flag. - #[test] - fn test_remove_from_require_dev() { - let mut composer = make_raw_package("test/project"); - composer - .require_dev - .insert("phpunit/phpunit".to_string(), "^11.0".to_string()); - composer - .require_dev - .insert("mockery/mockery".to_string(), "^1.0".to_string()); - - assert!(composer.require_dev.contains_key("phpunit/phpunit")); - - composer.require_dev.shift_remove("phpunit/phpunit"); - - assert!( - !composer.require_dev.contains_key("phpunit/phpunit"), - "phpunit/phpunit should be removed from require-dev" - ); - assert!( - composer.require_dev.contains_key("mockery/mockery"), - "mockery/mockery should remain in require-dev" - ); - } - - /// Removing a package not in either section does not panic and doesn't change anything. - #[test] - fn test_remove_nonexistent_package_no_panic() { - let mut composer = make_raw_package("test/project"); - composer - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - - let name = "nonexistent/package"; - let found_in_require = composer.require.shift_remove(name).is_some(); - let found_in_require_dev = composer.require_dev.shift_remove(name).is_some(); - - assert!(!found_in_require); - assert!(!found_in_require_dev); - - assert_eq!(composer.require.len(), 1); - assert!(composer.require.contains_key("psr/log")); - } - - /// Without `--dev`, auto-detect finds the package in whichever section contains it. - #[test] - fn test_remove_auto_detects_section_require() { - let mut composer = make_raw_package("test/project"); - composer - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - composer - .require_dev - .insert("phpunit/phpunit".to_string(), "^11.0".to_string()); - - let name = "psr/log"; - let removed_from_require = composer.require.shift_remove(name).is_some(); - let removed_from_dev = if !removed_from_require { - composer.require_dev.shift_remove(name).is_some() - } else { - false - }; - - assert!( - removed_from_require, - "should be found and removed from require" - ); - assert!(!removed_from_dev); - assert!(!composer.require.contains_key("psr/log")); - assert!(composer.require_dev.contains_key("phpunit/phpunit")); - } - - /// Without `--dev`, auto-detect finds the package in require-dev if not in require. - #[test] - fn test_remove_auto_detects_section_require_dev() { - let mut composer = make_raw_package("test/project"); - composer - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - composer - .require_dev - .insert("phpunit/phpunit".to_string(), "^11.0".to_string()); - - let name = "phpunit/phpunit"; - let removed_from_require = composer.require.shift_remove(name).is_some(); - let removed_from_dev = if !removed_from_require { - composer.require_dev.shift_remove(name).is_some() - } else { - false - }; - - assert!(!removed_from_require); - assert!( - removed_from_dev, - "should be found and removed from require-dev" - ); - assert!(!composer.require_dev.contains_key("phpunit/phpunit")); - assert!(composer.require.contains_key("psr/log")); - } - - /// After re-resolve, removed packages appear as `ChangeKind::Uninstall` in the change report. - #[test] - fn test_remove_change_report_shows_removals() { - let old_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("monolog/monolog", "3.8.0"), - ]); - let new_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - - let changes = - super::super::update::compute_update_changes(Some(&old_lock), &new_lock, false); - - assert_eq!(changes.len(), 1, "exactly one change expected"); - assert_eq!(changes[0].name, "monolog/monolog"); - assert!( - matches!( - &changes[0].kind, - super::super::update::ChangeKind::Uninstall { old_version } - if old_version == "3.8.0" - ), - "monolog/monolog should appear as an Uninstall change" - ); - } - - /// Glob-style package names (e.g. "vendor/*") no longer bail with an "Invalid package name" - /// error — they fall through to the "not required" warning path. This is a regression test - /// for the validate_package_name bail that was removed in PR-A. - #[test] - fn test_glob_package_name_falls_through_to_not_required() { - let mut composer = make_raw_package("test/project"); - composer - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - - // A glob-style name: not a valid exact package name, not in require either. - let name = "vendor/*"; - let found = composer.require.shift_remove(name).is_some() - || composer.require_dev.shift_remove(name).is_some(); - - // Should NOT be found (falls through to "not required" warning), not panicked/bailed. - assert!(!found, "glob name should not match any package"); - // composer.json is unchanged - assert_eq!(composer.require.len(), 1); - } - - /// --unused with no lock file must return an error matching Composer's wording. - #[test] - fn test_unused_no_lock_error_wording() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - // No lock file present — the error message is tested via the remove_unused code path. - let lock_path = dir.path().join("composer.lock"); - assert!(!lock_path.exists()); - - // The error message Composer uses (and Mozart must match): - let expected = "A valid composer.lock file is required to run this command with --unused"; - // Simulate the check that remove_unused() performs: - let result: anyhow::Result<()> = if !lock_path.exists() { - Err(anyhow::anyhow!("{}", expected)) - } else { - Ok(()) - }; - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains(expected)); - } - - #[tokio::test] - #[ignore] - async fn test_remove_full_e2e() { - use indexmap::{IndexMap, IndexSet}; - use mozart_core::repository::lockfile::{LockFileGenerationRequest, generate_lock_file}; - use mozart_core::repository::resolver::{ResolveRequest, resolve}; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_path = dir.path().join("composer.json"); - let lock_path = dir.path().join("composer.lock"); - let vendor_dir = dir.path().join("vendor"); - - let content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#; - std::fs::write(&composer_path, content).unwrap(); - - let mut composer: RawPackageData = serde_json::from_str(content).unwrap(); - - let request = ResolveRequest { - root_name: String::new(), - root_version: None, - require: vec![("psr/log".to_string(), "^3.0".to_string())], - require_dev: vec![], - include_dev: false, - minimum_stability: mozart_core::package::Stability::Stable, - stability_flags: IndexMap::new(), - prefer_stable: true, - prefer_lowest: false, - platform: mozart_core::repository::resolver::PlatformConfig::new(), - ignore_platform_reqs: false, - ignore_platform_req_list: vec![], - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - temporary_constraints: IndexMap::new(), - raw_repositories: vec![], - root_provide: IndexMap::new(), - root_replace: IndexMap::new(), - root_conflict: IndexMap::new(), - locked_package_names: IndexSet::new(), - locked_packages: Vec::new(), - block_abandoned: false, - root_branch_alias: None, - preferred_versions: IndexMap::new(), - block_insecure: false, - }; - let resolved = resolve(&request) - .await - .expect("initial resolution should succeed"); - let initial_lock = generate_lock_file(&LockFileGenerationRequest { - resolved_packages: resolved, - composer_json_content: content.to_string(), - composer_json: composer.clone(), - include_dev: false, - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - previous_lock: None, - lock_pinned_names: IndexSet::new(), - }) - .await - .expect("initial lock file generation should succeed"); - initial_lock - .write_to_file(&lock_path) - .expect("should write initial lock file"); - - composer.require.shift_remove("psr/log"); - package::write_to_file(&composer, &composer_path).unwrap(); - - let request2 = ResolveRequest { - root_name: String::new(), - root_version: None, - require: vec![], - require_dev: vec![], - include_dev: false, - minimum_stability: mozart_core::package::Stability::Stable, - stability_flags: IndexMap::new(), - prefer_stable: true, - prefer_lowest: false, - platform: mozart_core::repository::resolver::PlatformConfig::new(), - ignore_platform_reqs: false, - ignore_platform_req_list: vec![], - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - temporary_constraints: IndexMap::new(), - raw_repositories: vec![], - root_provide: IndexMap::new(), - root_replace: IndexMap::new(), - root_conflict: IndexMap::new(), - locked_package_names: IndexSet::new(), - locked_packages: Vec::new(), - block_abandoned: false, - root_branch_alias: None, - preferred_versions: IndexMap::new(), - block_insecure: false, - }; - let resolved2 = resolve(&request2) - .await - .expect("post-remove resolution should succeed"); - - let composer_json_content2 = std::fs::read_to_string(&composer_path).unwrap(); - let new_lock = generate_lock_file(&LockFileGenerationRequest { - resolved_packages: resolved2, - composer_json_content: composer_json_content2, - composer_json: composer, - include_dev: false, - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - previous_lock: Some(initial_lock.clone()), - lock_pinned_names: IndexSet::new(), - }) - .await - .expect("post-remove lock file generation should succeed"); - - assert!( - !new_lock.packages.iter().any(|p| p.name == "psr/log"), - "psr/log should be absent from the new lock file" - ); - - new_lock.write_to_file(&lock_path).unwrap(); - assert!(lock_path.exists(), "lock file should exist"); - - let _ = vendor_dir; - } - - #[test] - fn test_remove_no_update_only_modifies_json() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_path = dir.path().join("composer.json"); - let lock_path = dir.path().join("composer.lock"); - - let content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#; - std::fs::write(&composer_path, content).unwrap(); - - let mut composer: RawPackageData = serde_json::from_str(content).unwrap(); - composer.require.shift_remove("psr/log"); - package::write_to_file(&composer, &composer_path).unwrap(); - - assert!( - !lock_path.exists(), - "lock file should not be created with --no-update" - ); - - let updated_content = std::fs::read_to_string(&composer_path).unwrap(); - assert!( - !updated_content.contains("psr/log"), - "psr/log should be removed from composer.json" - ); - } - - #[test] - fn test_remove_dry_run_modifies_nothing() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_path = dir.path().join("composer.json"); - let lock_path = dir.path().join("composer.lock"); - let vendor_dir = dir.path().join("vendor"); - - let original_content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#; - std::fs::write(&composer_path, original_content).unwrap(); - - assert_eq!( - std::fs::read_to_string(&composer_path).unwrap(), - original_content, - "composer.json should be unmodified after dry run" - ); - assert!( - !lock_path.exists(), - "lock file should not be created by dry run" - ); - assert!( - !vendor_dir.exists(), - "vendor dir should not be created by dry run" - ); - } -} diff --git a/crates/mozart/src/commands/repository.rs b/crates/mozart/src/commands/repository.rs index adf34b5..6616352 100644 --- a/crates/mozart/src/commands/repository.rs +++ b/crates/mozart/src/commands/repository.rs @@ -279,889 +279,3 @@ fn execute_enable(ctx: &BaseConfigContext, args: &RepositoryArgs) -> anyhow::Res anyhow::bail!("Only packagist.org can be enabled/disabled using this command."); } - -#[cfg(test)] -mod tests { - use super::*; - - fn make_io() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { - let console = mozart_core::console::Console::new(0, false, false, false, false); - std::sync::Arc::new(std::sync::Mutex::new(Box::new(console))) - } - - fn make_args( - action: Option<&str>, - name: Option<&str>, - arg1: Option<&str>, - arg2: Option<&str>, - ) -> RepositoryArgs { - RepositoryArgs { - action: action.map(|s| s.to_string()), - name: name.map(|s| s.to_string()), - arg1: arg1.map(|s| s.to_string()), - arg2: arg2.map(|s| s.to_string()), - global: false, - file: None, - append: false, - before: None, - after: None, - } - } - - fn make_cli() -> super::super::Cli { - use clap::Parser; - super::super::Cli::parse_from(["mozart", "repository", "list"]) - } - - #[tokio::test] - async fn test_list_empty() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("list"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - // Empty repos → synthesises [packagist.org] disabled - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_list_with_repos() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, - ).unwrap(); - - let mut args = make_args(Some("list"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_list_with_disabled_packagist() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": [{"packagist.org": false}]}"#).unwrap(); - - let mut args = make_args(Some("list"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_list_no_packagist_synth_when_composer_type_present() { - // When a composer-type repo pointing at packagist.org is present, - // no synthesised [packagist.org] disabled line should appear. - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "packagist.org", "type": "composer", "url": "https://repo.packagist.org"}]}"#, - ) - .unwrap(); - - let mut args = make_args(Some("list"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_add_type_url() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args( - Some("add"), - Some("my-repo"), - Some("vcs"), - Some("https://example.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos.len(), 1); - assert_eq!(repos[0]["name"], "my-repo"); - assert_eq!(repos[0]["type"], "vcs"); - assert_eq!(repos[0]["url"], "https://example.com"); - } - - #[tokio::test] - async fn test_add_json() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args( - Some("add"), - Some("my-repo"), - Some(r#"{"type":"path","url":"../local-pkg"}"#), - None, - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["type"], "path"); - assert_eq!(repos[0]["name"], "my-repo"); - } - - #[tokio::test] - async fn test_add_prepend_default() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "existing", "type": "vcs", "url": "https://existing.com"}]}"#, - ).unwrap(); - - let mut args = make_args( - Some("add"), - Some("new-repo"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], "new-repo"); - assert_eq!(repos[1]["name"], "existing"); - } - - #[tokio::test] - async fn test_add_append() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "existing", "type": "vcs", "url": "https://existing.com"}]}"#, - ).unwrap(); - - let mut args = make_args( - Some("add"), - Some("new-repo"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.append = true; - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], "existing"); - assert_eq!(repos[1]["name"], "new-repo"); - } - - #[tokio::test] - async fn test_add_before() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "a", "type": "vcs", "url": "https://a.com"}, {"name": "b", "type": "vcs", "url": "https://b.com"}]}"#, - ).unwrap(); - - let mut args = make_args( - Some("add"), - Some("new"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.before = Some("b".to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], "a"); - assert_eq!(repos[1]["name"], "new"); - assert_eq!(repos[2]["name"], "b"); - } - - #[tokio::test] - async fn test_add_after() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "a", "type": "vcs", "url": "https://a.com"}, {"name": "b", "type": "vcs", "url": "https://b.com"}]}"#, - ).unwrap(); - - let mut args = make_args( - Some("add"), - Some("new"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.after = Some("a".to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], "a"); - assert_eq!(repos[1]["name"], "new"); - assert_eq!(repos[2]["name"], "b"); - } - - #[tokio::test] - async fn test_add_before_and_after_error() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args( - Some("add"), - Some("new"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.before = Some("a".to_string()); - args.after = Some("b".to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_add_missing_args() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("add"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_add_missing_name() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("add"), None, Some("vcs"), Some("https://url.com")); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_remove() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, - ).unwrap(); - - let mut args = make_args(Some("remove"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - assert!(json.get("repositories").is_none()); - } - - #[tokio::test] - async fn test_remove_packagist_disables() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("remove"), Some("packagist.org"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["packagist.org"], false); - } - - #[tokio::test] - async fn test_remove_alias_rm() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, - ).unwrap(); - - let mut args = make_args(Some("rm"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - assert!(json.get("repositories").is_none()); - } - - #[tokio::test] - async fn test_remove_missing_name() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("remove"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_set_url() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://old.com"}]}"#, - ) - .unwrap(); - - let mut args = make_args( - Some("set-url"), - Some("my-repo"), - Some("https://new.com"), - None, - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - assert_eq!(json["repositories"][0]["url"], "https://new.com"); - } - - #[tokio::test] - async fn test_set_url_not_found() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); - - let mut args = make_args( - Some("set-url"), - Some("missing"), - Some("https://new.com"), - None, - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_set_url_alias() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://old.com"}]}"#, - ) - .unwrap(); - - let mut args = make_args( - Some("seturl"), - Some("my-repo"), - Some("https://new.com"), - None, - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - assert_eq!(json["repositories"][0]["url"], "https://new.com"); - } - - #[tokio::test] - async fn test_get_url() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "vcs", "url": "https://example.com"}]}"#, - ).unwrap(); - - let mut args = make_args(Some("get-url"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_get_url_not_found() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); - - let mut args = make_args(Some("get-url"), Some("missing"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_disable_packagist() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("disable"), Some("packagist.org"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["packagist.org"], false); - } - - #[tokio::test] - async fn test_disable_packagist_idempotent() { - // Calling disable twice should not create a duplicate entry. - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": [{"packagist.org": false}]}"#).unwrap(); - - let mut args = make_args(Some("disable"), Some("packagist.org"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos.len(), 1, "should still be just one disable entry"); - assert_eq!(repos[0]["packagist.org"], false); - } - - #[tokio::test] - async fn test_disable_non_packagist_error() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("disable"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_disable_without_name_error() { - // Composer requires a name for disable; Mozart mirrors that. - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("disable"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_enable_packagist() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": [{"packagist.org": false}]}"#).unwrap(); - - let mut args = make_args(Some("enable"), Some("packagist.org"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - assert!(json.get("repositories").is_none()); - } - - #[tokio::test] - async fn test_enable_non_packagist_error() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("enable"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_list_composer_format() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": {"my-repo": {"type": "vcs", "url": "https://example.com"}}}"#, - ) - .unwrap(); - - let mut args = make_args(Some("list"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_get_url_composer_format() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": {"my-repo": {"type": "vcs", "url": "https://example.com"}}}"#, - ) - .unwrap(); - - let mut args = make_args(Some("get-url"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_get_url_no_url_error() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": [{"name": "my-repo", "type": "artifact"}]}"#, - ) - .unwrap(); - - let mut args = make_args(Some("get-url"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - let msg = result.unwrap_err().to_string(); - assert!( - msg.contains("does not have a URL"), - "unexpected message: {msg}" - ); - } - - #[tokio::test] - async fn test_get_url_not_found_message() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); - - let mut args = make_args(Some("get-url"), Some("missing"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - let msg = result.unwrap_err().to_string(); - assert!(msg.contains("There is no"), "unexpected message: {msg}"); - } - - #[tokio::test] - async fn test_set_url_composer_format_keeps_assoc_shape() { - // Composer's setRepositoryUrl mutates in place without converting assoc → list. - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": {"my-repo": {"type": "vcs", "url": "https://old.com"}}}"#, - ) - .unwrap(); - - let mut args = make_args( - Some("set-url"), - Some("my-repo"), - Some("https://new.com"), - None, - ); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - // Format is preserved: still an assoc object. - let repos = json["repositories"].as_object().unwrap(); - assert_eq!(repos["my-repo"]["url"], "https://new.com"); - } - - #[tokio::test] - async fn test_remove_composer_format_converts_and_removes() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories": {"my-repo": {"type": "vcs", "url": "https://example.com"}}}"#, - ) - .unwrap(); - - let mut args = make_args(Some("remove"), Some("my-repo"), None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - assert!(json.get("repositories").is_none()); - } - - #[test] - fn test_normalize_repositories_array_passthrough() { - use super::super::config_helpers::normalize_repositories; - let val = serde_json::json!([ - {"name": "foo", "type": "vcs", "url": "https://foo.com"} - ]); - let result = normalize_repositories(&val); - assert_eq!(result.len(), 1); - assert_eq!(result[0]["name"], "foo"); - } - - #[test] - fn test_normalize_repositories_object_injects_name() { - use super::super::config_helpers::normalize_repositories; - let val = serde_json::json!({ - "foo": {"type": "vcs", "url": "https://foo.com"}, - "bar": {"type": "composer", "url": "https://bar.com"} - }); - let result = normalize_repositories(&val); - assert_eq!(result.len(), 2); - let names: indexmap::IndexSet<&str> = result - .iter() - .filter_map(|v| v.get("name").and_then(|n| n.as_str())) - .collect(); - assert!(names.contains("foo")); - assert!(names.contains("bar")); - } - - #[test] - fn test_normalize_repositories_object_boolean_entry() { - use super::super::config_helpers::normalize_repositories; - let val = serde_json::json!({"packagist.org": false}); - let result = normalize_repositories(&val); - assert_eq!(result.len(), 1); - assert_eq!(result[0]["packagist.org"], false); - } - - #[test] - fn test_normalize_repositories_empty() { - use super::super::config_helpers::normalize_repositories; - let val = serde_json::json!(null); - let result = normalize_repositories(&val); - assert!(result.is_empty()); - } - - #[tokio::test] - async fn test_unknown_action() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, "{}").unwrap(); - - let mut args = make_args(Some("invalid"), None, None, None); - args.file = Some(file.to_str().unwrap().to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_insert_before() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories":[{"name":"a","type":"vcs","url":"https://a.com"},{"name":"b","type":"vcs","url":"https://b.com"}]}"#, - ) - .unwrap(); - - let mut args = make_args( - Some("add"), - Some("new"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.before = Some("b".to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], "a"); - assert_eq!(repos[1]["name"], "new"); - assert_eq!(repos[2]["name"], "b"); - } - - #[tokio::test] - async fn test_insert_after() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write( - &file, - r#"{"repositories":[{"name":"a","type":"vcs","url":"https://a.com"},{"name":"b","type":"vcs","url":"https://b.com"}]}"#, - ) - .unwrap(); - - let mut args = make_args( - Some("add"), - Some("new"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.after = Some("a".to_string()); - - let cli = make_cli(); - let io = make_io(); - execute(&args, &cli, io).await.unwrap(); - - let json: serde_json::Value = - serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap(); - let repos = json["repositories"].as_array().unwrap(); - assert_eq!(repos[0]["name"], "a"); - assert_eq!(repos[1]["name"], "new"); - assert_eq!(repos[2]["name"], "b"); - } - - #[tokio::test] - async fn test_insert_target_not_found() { - let dir = tempfile::TempDir::new().unwrap(); - let file = dir.path().join("composer.json"); - std::fs::write(&file, r#"{"repositories": []}"#).unwrap(); - - let mut args = make_args( - Some("add"), - Some("new"), - Some("vcs"), - Some("https://new.com"), - ); - args.file = Some(file.to_str().unwrap().to_string()); - args.before = Some("nonexistent".to_string()); - - let cli = make_cli(); - let io = make_io(); - let result = execute(&args, &cli, io).await; - assert!(result.is_err()); - } -} diff --git a/crates/mozart/src/commands/require.rs b/crates/mozart/src/commands/require.rs index f92442c..62c6a28 100644 --- a/crates/mozart/src/commands/require.rs +++ b/crates/mozart/src/commands/require.rs @@ -1123,330 +1123,3 @@ pub async fn execute( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - fn make_locked_package(name: &str, version: &str) -> lockfile::LockedPackage { - lockfile::LockedPackage { - name: name.to_string(), - version: version.to_string(), - version_normalized: Some(format!("{}.0", version)), - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: Some("library".to_string()), - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn minimal_lock(packages: Vec<lockfile::LockedPackage>) -> lockfile::LockFile { - lockfile::LockFile { - readme: lockfile::LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages, - packages_dev: Some(vec![]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - } - } - - /// Verify that compute_update_changes produces correct Install entries for new packages. - #[test] - fn test_require_change_report_new_packages() { - let new_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("monolog/monolog", "3.8.0"), - ]); - - let changes = super::super::update::compute_update_changes(None, &new_lock, false); - assert_eq!(changes.len(), 2); - for change in &changes { - assert!( - matches!( - change.kind, - super::super::update::ChangeKind::Install { .. } - ), - "Expected Install, got {:?} for {}", - change.kind, - change.name - ); - } - } - - /// Verify the dry-run path does not write lock file. - #[test] - fn test_no_update_skips_lock_generation() { - let dir = tempfile::tempdir().unwrap(); - let lock_path = dir.path().join("composer.lock"); - assert!(!lock_path.exists()); - } - - #[test] - fn test_require_dry_run_modifies_nothing() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_path = dir.path().join("composer.json"); - let lock_path = dir.path().join("composer.lock"); - let vendor_dir = dir.path().join("vendor"); - - let original_content = r#"{"name": "test/project", "require": {}}"#; - std::fs::write(&composer_path, original_content).unwrap(); - - assert_eq!( - std::fs::read_to_string(&composer_path).unwrap(), - original_content - ); - assert!( - !lock_path.exists(), - "Lock file should not be created by dry run" - ); - assert!( - !vendor_dir.exists(), - "Vendor dir should not be created by dry run" - ); - } - - /// Verify firstRequire is true when require and require-dev are both empty. - #[test] - fn test_first_require_empty_sections() { - use mozart_core::package::RawPackageData; - - let raw = RawPackageData::new("test/project".to_string()); - let first_require = raw.require.is_empty() && raw.require_dev.is_empty(); - assert!( - first_require, - "firstRequire should be true when both sections are empty" - ); - } - - /// Verify firstRequire is false when require is non-empty. - #[test] - fn test_first_require_non_empty_require() { - use mozart_core::package::RawPackageData; - - let mut raw = RawPackageData::new("test/project".to_string()); - raw.require - .insert("some/pkg".to_string(), "^1.0".to_string()); - let first_require = raw.require.is_empty() && raw.require_dev.is_empty(); - assert!( - !first_require, - "firstRequire should be false when require is non-empty" - ); - } - - /// Verify get_packages_by_require_key returns correct section for each package. - #[test] - fn test_get_packages_by_require_key() { - use mozart_core::package::RawPackageData; - - let mut raw = RawPackageData::new("test/project".to_string()); - raw.require - .insert("vendor/a".to_string(), "^1.0".to_string()); - raw.require_dev - .insert("vendor/b".to_string(), "^2.0".to_string()); - - let map = get_packages_by_require_key(&raw); - assert_eq!(map.get("vendor/a"), Some(&"require".to_string())); - assert_eq!(map.get("vendor/b"), Some(&"require-dev".to_string())); - assert_eq!(map.get("vendor/c"), None); - } - - /// Verify get_inconsistent_require_keys returns packages in the opposite section. - #[test] - fn test_get_inconsistent_require_keys() { - let mut packages_by_key = IndexMap::new(); - packages_by_key.insert("vendor/a".to_string(), "require".to_string()); - packages_by_key.insert("vendor/b".to_string(), "require-dev".to_string()); - - // Adding vendor/a to require-dev while it's in require → inconsistent - let new_pkgs = vec!["vendor/a".to_string(), "vendor/c".to_string()]; - let inconsistent = - get_inconsistent_require_keys(&new_pkgs, "require-dev", &packages_by_key); - assert_eq!(inconsistent, vec!["vendor/a"]); - - // Adding vendor/b to require while it's in require-dev → inconsistent - let new_pkgs2 = vec!["vendor/b".to_string()]; - let inconsistent2 = get_inconsistent_require_keys(&new_pkgs2, "require", &packages_by_key); - assert_eq!(inconsistent2, vec!["vendor/b"]); - } - - #[tokio::test] - #[ignore] - async fn test_require_full_e2e() { - use indexmap::IndexSet; - use mozart_core::package::RawPackageData; - use mozart_core::repository::lockfile::{LockFileGenerationRequest, generate_lock_file}; - - let composer_json_content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#; - let composer_json: RawPackageData = serde_json::from_str(composer_json_content).unwrap(); - - let request = ResolveRequest { - root_name: String::new(), - root_version: None, - require: vec![("psr/log".to_string(), "^3.0".to_string())], - require_dev: vec![], - include_dev: false, - minimum_stability: Stability::Stable, - stability_flags: IndexMap::new(), - prefer_stable: true, - prefer_lowest: false, - platform: PlatformConfig::new(), - ignore_platform_reqs: false, - ignore_platform_req_list: vec![], - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - temporary_constraints: IndexMap::new(), - raw_repositories: vec![], - root_provide: IndexMap::new(), - root_replace: IndexMap::new(), - root_conflict: IndexMap::new(), - locked_package_names: IndexSet::new(), - locked_packages: Vec::new(), - block_abandoned: false, - root_branch_alias: None, - preferred_versions: indexmap::IndexMap::new(), - block_insecure: false, - }; - - let resolved = resolver::resolve(&request) - .await - .expect("Resolution should succeed"); - assert!(!resolved.is_empty()); - assert!(resolved.iter().any(|p| p.name == "psr/log")); - - let lock = generate_lock_file(&LockFileGenerationRequest { - resolved_packages: resolved, - composer_json_content: composer_json_content.to_string(), - composer_json, - include_dev: false, - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - previous_lock: None, - lock_pinned_names: IndexSet::new(), - }) - .await - .expect("Lock file generation should succeed"); - - assert!(!lock.content_hash.is_empty()); - assert!(!lock.packages.is_empty()); - assert!(lock.packages.iter().any(|p| p.name == "psr/log")); - } - - #[tokio::test] - #[ignore] - async fn test_require_no_install_writes_lock_only() { - use indexmap::IndexSet; - use mozart_core::package::RawPackageData; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_path = dir.path().join("composer.json"); - let lock_path = dir.path().join("composer.lock"); - let vendor_dir = dir.path().join("vendor"); - - let content = r#"{"name": "test/project", "require": {"psr/log": "^3.0"}}"#; - std::fs::write(&composer_path, content).unwrap(); - - let raw: RawPackageData = serde_json::from_str(content).unwrap(); - - let request = ResolveRequest { - root_name: String::new(), - root_version: None, - require: vec![("psr/log".to_string(), "^3.0".to_string())], - require_dev: vec![], - include_dev: false, - minimum_stability: Stability::Stable, - stability_flags: IndexMap::new(), - prefer_stable: true, - prefer_lowest: false, - platform: PlatformConfig::new(), - ignore_platform_reqs: false, - ignore_platform_req_list: vec![], - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - temporary_constraints: IndexMap::new(), - raw_repositories: vec![], - root_provide: IndexMap::new(), - root_replace: IndexMap::new(), - root_conflict: IndexMap::new(), - locked_package_names: IndexSet::new(), - locked_packages: Vec::new(), - block_abandoned: false, - root_branch_alias: None, - preferred_versions: indexmap::IndexMap::new(), - block_insecure: false, - }; - - let resolved = resolver::resolve(&request) - .await - .expect("Resolution should succeed"); - let new_lock = lockfile::generate_lock_file(&lockfile::LockFileGenerationRequest { - resolved_packages: resolved, - composer_json_content: content.to_string(), - composer_json: raw, - include_dev: false, - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - previous_lock: None, - lock_pinned_names: IndexSet::new(), - }) - .await - .expect("Lock file generation should succeed"); - - new_lock.write_to_file(&lock_path).unwrap(); - - assert!(lock_path.exists(), "Lock file should be written"); - assert!( - !vendor_dir.exists(), - "Vendor dir should NOT exist with --no-install" - ); - } -} diff --git a/crates/mozart/src/commands/run_script.rs b/crates/mozart/src/commands/run_script.rs index ab3700d..2983ab9 100644 --- a/crates/mozart/src/commands/run_script.rs +++ b/crates/mozart/src/commands/run_script.rs @@ -451,499 +451,3 @@ fn is_composer_prefix(entry: &str) -> bool { fn is_putenv(entry: &str) -> bool { entry.starts_with("@putenv ") } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - - fn test_console() -> Arc<Mutex<Box<dyn IoInterface>>> { - Arc::new(Mutex::new(Box::new(mozart_core::console::Console::new( - 0, false, false, false, false, - )) as Box<dyn IoInterface>)) - } - - #[test] - fn test_is_php_callback_static_method() { - assert!(is_php_callback("MyClass::myMethod")); - } - - #[test] - fn test_is_php_callback_fqn_command() { - assert!(is_php_callback("Vendor\\MyCommand")); - } - - #[test] - fn test_is_php_callback_namespaced_listener() { - assert!(is_php_callback("App\\Listeners\\PostInstall")); - } - - #[test] - fn test_is_php_callback_shell_command() { - assert!(!is_php_callback("echo hello")); - } - - #[test] - fn test_is_php_callback_at_php() { - assert!(!is_php_callback("@php script.php")); - } - - #[test] - fn test_is_script_reference() { - assert!(is_script_reference("@test")); - assert!(!is_script_reference("@php foo")); - assert!(!is_script_reference("@putenv X=1")); - assert!(!is_script_reference("@composer install")); - } - - #[test] - fn test_is_putenv() { - assert!(is_putenv("@putenv FOO=bar")); - assert!(is_putenv("@putenv FOO")); - } - - #[test] - fn test_is_php_prefix() { - assert!(is_php_prefix("@php artisan migrate")); - } - - #[test] - fn test_is_composer_prefix() { - assert!(is_composer_prefix("@composer install")); - } - - #[test] - fn test_load_scripts_array_form() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "scripts": {"test": ["echo a", "echo b"]}}"#, - ) - .unwrap(); - - let (scripts, _) = load_scripts(dir.path()).unwrap(); - let listeners = scripts.get("test").unwrap(); - assert_eq!(listeners.len(), 2); - assert_eq!(listeners[0], "echo a"); - assert_eq!(listeners[1], "echo b"); - } - - #[test] - fn test_load_scripts_string_form() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "scripts": {"test": "echo a"}}"#, - ) - .unwrap(); - - let (scripts, _) = load_scripts(dir.path()).unwrap(); - let listeners = scripts.get("test").unwrap(); - assert_eq!(listeners.len(), 1); - assert_eq!(listeners[0], "echo a"); - } - - #[test] - fn test_load_scripts_with_descriptions() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "scripts": {"test": "phpunit"}, "scripts-descriptions": {"test": "Run tests"}}"#, - ) - .unwrap(); - - let (_, descriptions) = load_scripts(dir.path()).unwrap(); - assert_eq!( - descriptions.get("test").map(|s| s.as_str()), - Some("Run tests") - ); - } - - #[test] - fn test_load_scripts_empty() { - let dir = tempfile::tempdir().unwrap(); - fs::write(dir.path().join("composer.json"), r#"{"name": "test/pkg"}"#).unwrap(); - - let (scripts, descriptions) = load_scripts(dir.path()).unwrap(); - assert!(scripts.is_empty()); - assert!(descriptions.is_empty()); - } - - #[test] - fn test_load_scripts_mixed() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "scripts": {"test": "phpunit", "post-install-cmd": ["echo installed", "echo done"]}}"#, - ) - .unwrap(); - - let (scripts, _) = load_scripts(dir.path()).unwrap(); - let test_listeners = scripts.get("test").unwrap(); - assert_eq!(test_listeners.len(), 1); - let post_listeners = scripts.get("post-install-cmd").unwrap(); - assert_eq!(post_listeners.len(), 2); - } - - #[test] - fn test_list_scripts_output() { - let mut scripts = BTreeMap::new(); - scripts.insert("test".to_string(), vec!["phpunit".to_string()]); - scripts.insert("lint".to_string(), vec!["phpcs".to_string()]); - - let mut descriptions = BTreeMap::new(); - descriptions.insert("test".to_string(), "Run tests".to_string()); - - let result = list_scripts(&scripts, &descriptions, test_console()); - assert!(result.is_ok()); - } - - #[test] - fn test_list_scripts_empty_silent() { - let scripts: BTreeMap<String, Vec<String>> = BTreeMap::new(); - let descriptions: BTreeMap<String, String> = BTreeMap::new(); - let result = list_scripts(&scripts, &descriptions, test_console()); - assert!(result.is_ok()); - } - - #[test] - fn test_run_shell_command_success() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - let code = run_shell_command("echo hello", dir.path(), &bin_dir, None, &[]).unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_run_shell_command_failure() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - let code = run_shell_command("exit 1", dir.path(), &bin_dir, None, &[]).unwrap(); - assert_eq!(code, 1); - } - - #[test] - fn test_run_shell_command_with_args() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert("greet".to_string(), vec!["echo".to_string()]); - - let mut stack = vec![]; - let code = run_script( - "greet", - &["world".to_string()], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_run_putenv_set() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert( - "setup".to_string(), - vec!["@putenv MOZART_TEST_VAR=hello_world".to_string()], - ); - - let mut stack = vec![]; - run_script( - "setup", - &[], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - - assert_eq!(std::env::var("MOZART_TEST_VAR").unwrap(), "hello_world"); - } - - #[test] - fn test_run_putenv_unset() { - // SAFETY: test-only; no concurrent env access in this test - unsafe { std::env::set_var("MOZART_UNSET_VAR", "some_value") }; - - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert( - "cleanup".to_string(), - vec!["@putenv MOZART_UNSET_VAR".to_string()], - ); - - let mut stack = vec![]; - run_script( - "cleanup", - &[], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - - assert!(std::env::var("MOZART_UNSET_VAR").is_err()); - } - - #[test] - fn test_run_script_reference() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert("a".to_string(), vec!["@b".to_string()]); - scripts.insert("b".to_string(), vec!["echo from-b".to_string()]); - - let mut stack = vec![]; - let code = run_script( - "a", - &[], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_circular_reference_detected() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert("a".to_string(), vec!["@b".to_string()]); - scripts.insert("b".to_string(), vec!["@a".to_string()]); - - let mut stack = vec![]; - let result = run_script( - "a", - &[], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ); - assert!(result.is_err()); - let msg = result.unwrap_err().to_string(); - assert!(msg.contains("Circular script reference")); - } - - #[test] - fn test_php_callback_skipped_with_warning() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert( - "callback".to_string(), - vec!["MyClass::myMethod".to_string()], - ); - - let mut stack = vec![]; - let code = run_script( - "callback", - &[], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_no_additional_args_respected() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert( - "test".to_string(), - vec!["echo base @no_additional_args".to_string()], - ); - - let mut stack = vec![]; - let code = run_script( - "test", - &["extra-arg".to_string()], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_additional_args_placeholder() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let mut scripts = BTreeMap::new(); - scripts.insert( - "test".to_string(), - vec!["echo before @additional_args after".to_string()], - ); - - let mut stack = vec![]; - let code = run_script( - "test", - &["injected".to_string()], - &scripts, - dir.path(), - &bin_dir, - None, - true, - &mut stack, - 0, - test_console(), - ) - .unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_script_not_defined_error() { - let dir = tempfile::tempdir().unwrap(); - fs::write(dir.path().join("composer.json"), r#"{"name": "test/pkg"}"#).unwrap(); - - let (scripts, _) = load_scripts(dir.path()).unwrap(); - assert!(!scripts.contains_key("nonexistent")); - } - - #[test] - fn test_bin_dir_in_path() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - fs::create_dir_all(&bin_dir).unwrap(); - - let fake_bin = bin_dir.join("my-fake-tool"); - fs::write(&fake_bin, "#!/bin/sh\necho ran-fake-tool\n").unwrap(); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - fs::set_permissions(&fake_bin, fs::Permissions::from_mode(0o755)).unwrap(); - } - - let code = run_shell_command("my-fake-tool", dir.path(), &bin_dir, None, &[]).unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_timeout_kills_long_running() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let result = run_shell_command( - "sleep 10", - dir.path(), - &bin_dir, - Some(Duration::from_secs(1)), - &[], - ); - assert!(result.is_err()); - let msg = result.unwrap_err().to_string(); - assert!(msg.contains("timed out")); - } - - #[test] - fn test_composer_dev_mode_env() { - let dir = tempfile::tempdir().unwrap(); - let bin_dir = dir.path().join("vendor/bin"); - - let code = run_shell_command( - "test \"$COMPOSER_DEV_MODE\" = \"1\"", - dir.path(), - &bin_dir, - None, - &[("COMPOSER_DEV_MODE".to_string(), "1".to_string())], - ) - .unwrap(); - assert_eq!(code, 0); - - let code = run_shell_command( - "test \"$COMPOSER_DEV_MODE\" = \"0\"", - dir.path(), - &bin_dir, - None, - &[("COMPOSER_DEV_MODE".to_string(), "0".to_string())], - ) - .unwrap(); - assert_eq!(code, 0); - } - - #[test] - fn test_list_flag() { - let dir = tempfile::tempdir().unwrap(); - fs::write( - dir.path().join("composer.json"), - r#"{"name": "test/pkg", "scripts": {"test": "phpunit", "lint": "phpcs"}}"#, - ) - .unwrap(); - - let (scripts, descriptions) = load_scripts(dir.path()).unwrap(); - assert!(scripts.contains_key("test")); - assert!(scripts.contains_key("lint")); - - let result = list_scripts(&scripts, &descriptions, test_console()); - assert!(result.is_ok()); - } - - #[test] - fn test_internal_event_rejected() { - // Internal events are in script_events::ALL but not in USER_RUNNABLE. - assert!(script_events::ALL.contains(&"pre-package-install")); - assert!(script_events::ALL.contains(&"post-package-install")); - assert!(script_events::ALL.contains(&"pre-operations-exec")); - assert!(!script_events::USER_RUNNABLE.contains(&"pre-package-install")); - assert!(!script_events::USER_RUNNABLE.contains(&"post-package-install")); - assert!(!script_events::USER_RUNNABLE.contains(&"pre-operations-exec")); - // User-runnable events are in both. - assert!(script_events::USER_RUNNABLE.contains(&"pre-install-cmd")); - assert!(script_events::ALL.contains(&"pre-install-cmd")); - } -} diff --git a/crates/mozart/src/commands/search.rs b/crates/mozart/src/commands/search.rs index 7259e6c..3f7a01d 100644 --- a/crates/mozart/src/commands/search.rs +++ b/crates/mozart/src/commands/search.rs @@ -202,207 +202,3 @@ fn render_text( console_writeln!(io, "{padded_name}{warning}{desc_display}"); } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_search_response() { - use mozart_core::repository::packagist::SearchResponse; - - let json = r#"{ - "results": [ - { - "name": "monolog/monolog", - "description": "Sends your logs to files, sockets, inboxes, databases and various web services", - "url": "https://packagist.org/packages/monolog/monolog", - "repository": "https://github.com/Seldaek/monolog", - "downloads": 500000000, - "favers": 20000 - }, - { - "name": "psr/log", - "description": "Common interface for logging libraries", - "url": "https://packagist.org/packages/psr/log", - "repository": null, - "downloads": 800000000, - "favers": 10000 - } - ], - "total": 2, - "next": null - }"#; - - let response: SearchResponse = serde_json::from_str(json).unwrap(); - assert_eq!(response.results.len(), 2); - assert_eq!(response.total, 2); - assert!(response.next.is_none()); - - let first = &response.results[0]; - assert_eq!(first.name, "monolog/monolog"); - assert_eq!(first.downloads, 500_000_000); - assert_eq!(first.favers, 20_000); - assert_eq!( - first.repository.as_deref(), - Some("https://github.com/Seldaek/monolog") - ); - - let second = &response.results[1]; - assert_eq!(second.name, "psr/log"); - assert!(second.repository.is_none()); - } - - #[test] - fn test_parse_search_response_with_abandoned() { - use mozart_core::repository::packagist::SearchResponse; - - let json = r#"{ - "results": [ - { - "name": "old/abandoned-pkg", - "description": "An abandoned package", - "url": "https://packagist.org/packages/old/abandoned-pkg", - "repository": "https://github.com/old/abandoned-pkg", - "downloads": 1000, - "favers": 10, - "abandoned": "new/replacement-pkg" - }, - { - "name": "active/pkg", - "description": "An active package", - "url": "https://packagist.org/packages/active/pkg", - "repository": null, - "downloads": 5000, - "favers": 100 - } - ], - "total": 2, - "next": null - }"#; - - let response: SearchResponse = serde_json::from_str(json).unwrap(); - assert_eq!(response.results.len(), 2); - - let first = &response.results[0]; - assert_eq!(first.name, "old/abandoned-pkg"); - assert_eq!( - first.abandoned.as_ref().and_then(|v| v.as_str()), - Some("new/replacement-pkg") - ); - - let second = &response.results[1]; - assert_eq!(second.name, "active/pkg"); - assert!(second.abandoned.is_none()); - } - - #[test] - fn test_parse_search_response_with_next() { - use mozart_core::repository::packagist::SearchResponse; - - let json = r#"{ - "results": [], - "total": 100, - "next": "https://packagist.org/search.json?q=monolog&page=2" - }"#; - - let response: SearchResponse = serde_json::from_str(json).unwrap(); - assert_eq!(response.total, 100); - assert_eq!( - response.next.as_deref(), - Some("https://packagist.org/search.json?q=monolog&page=2") - ); - } - - #[test] - fn test_is_abandoned_none() { - let result = make_result("vendor/pkg"); - assert!(!is_abandoned(&result)); - } - - #[test] - fn test_is_abandoned_true() { - let mut result = make_result("vendor/pkg"); - result.abandoned = Some(serde_json::Value::Bool(true)); - assert!(is_abandoned(&result)); - } - - #[test] - fn test_is_abandoned_false() { - let mut result = make_result("vendor/pkg"); - result.abandoned = Some(serde_json::Value::Bool(false)); - assert!(!is_abandoned(&result)); - } - - #[test] - fn test_is_abandoned_replacement_string() { - let mut result = make_result("vendor/pkg"); - result.abandoned = Some(serde_json::Value::String("other/pkg".to_string())); - assert!(is_abandoned(&result)); - } - - #[test] - fn test_is_abandoned_empty_string() { - let mut result = make_result("vendor/pkg"); - result.abandoned = Some(serde_json::Value::String(String::new())); - assert!(!is_abandoned(&result)); - } - - #[test] - fn test_search_result_output_matches_composer_schema() { - let result = SearchResult { - name: "test/pkg".to_string(), - description: "A test package".to_string(), - url: "https://packagist.org/packages/test/pkg".to_string(), - repository: Some("https://github.com/test/pkg".to_string()), - downloads: 1000, - favers: 50, - abandoned: None, - }; - - let output = SearchResultOutput::from(&result); - let json = serde_json::to_string(&output).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - - assert_eq!(parsed["name"], "test/pkg"); - assert_eq!(parsed["description"], "A test package"); - assert_eq!(parsed["url"], "https://packagist.org/packages/test/pkg"); - // Composer schema does not include repository, downloads, or favers - assert!(parsed.get("repository").is_none()); - assert!(parsed.get("downloads").is_none()); - assert!(parsed.get("favers").is_none()); - // abandoned is skipped when None - assert!(parsed.get("abandoned").is_none()); - } - - #[test] - fn test_search_result_output_with_abandoned() { - let result = SearchResult { - name: "old/pkg".to_string(), - description: "Old package".to_string(), - url: "https://packagist.org/packages/old/pkg".to_string(), - repository: None, - downloads: 0, - favers: 0, - abandoned: Some(serde_json::Value::String("new/pkg".to_string())), - }; - - let output = SearchResultOutput::from(&result); - let json = serde_json::to_string(&output).unwrap(); - let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); - - assert_eq!(parsed["abandoned"], "new/pkg"); - } - - fn make_result(name: &str) -> SearchResult { - SearchResult { - name: name.to_string(), - description: String::new(), - url: format!("https://packagist.org/packages/{name}"), - repository: None, - downloads: 0, - favers: 0, - abandoned: None, - } - } -} diff --git a/crates/mozart/src/commands/self_update.rs b/crates/mozart/src/commands/self_update.rs index 74983ba..758fa04 100644 --- a/crates/mozart/src/commands/self_update.rs +++ b/crates/mozart/src/commands/self_update.rs @@ -452,248 +452,3 @@ fn clean_backups(data_dir: &Path, except: Option<&Path>) -> anyhow::Result<()> { Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - use tempfile::tempdir; - - fn make_release(tag: &str, prerelease: bool, assets: Vec<GitHubAsset>) -> GitHubRelease { - GitHubRelease { - tag_name: tag.to_string(), - prerelease, - assets, - } - } - - fn make_asset(name: &str, url: &str) -> GitHubAsset { - GitHubAsset { - name: name.to_string(), - browser_download_url: url.to_string(), - size: 1024, - } - } - - #[test] - fn test_platform_asset_name() { - let name = platform_asset_name().expect("platform_asset_name should succeed"); - assert!(!name.is_empty(), "asset name must not be empty"); - assert!( - name.starts_with("mozart-"), - "asset name should start with 'mozart-', got: {name}" - ); - // Verify the name matches the expected pattern: mozart-<os>-<arch>[.exe] - assert!( - name.contains("linux") || name.contains("macos") || name.contains("windows"), - "asset name should contain an OS, got: {name}" - ); - assert!( - name.contains("x86_64") || name.contains("aarch64") || name.contains("x86"), - "asset name should contain an architecture, got: {name}" - ); - } - - #[test] - fn test_find_target_release_latest() { - let releases = vec![ - make_release("v0.3.0", false, vec![]), - make_release("v0.2.0", false, vec![]), - make_release("v0.1.0", false, vec![]), - ]; - - let result = find_target_release(&releases, None).expect("should find latest"); - assert_eq!(result.tag_name, "v0.3.0"); - } - - #[test] - fn test_find_target_release_specific_version() { - let releases = vec![ - make_release("v0.3.0", false, vec![]), - make_release("v0.2.0", false, vec![]), - make_release("v0.1.0", false, vec![]), - ]; - - // Without v prefix - let result = find_target_release(&releases, Some("0.2.0")).expect("should find 0.2.0"); - assert_eq!(result.tag_name, "v0.2.0"); - - // With v prefix - let result_v = find_target_release(&releases, Some("v0.1.0")).expect("should find v0.1.0"); - assert_eq!(result_v.tag_name, "v0.1.0"); - } - - #[test] - fn test_find_target_release_not_found() { - let releases = vec![ - make_release("v0.3.0", false, vec![]), - make_release("v0.2.0", false, vec![]), - ]; - - let result = find_target_release(&releases, Some("9.9.9")); - assert!(result.is_err(), "should return error for missing version"); - let msg = result.unwrap_err().to_string(); - assert!( - msg.contains("9.9.9"), - "error message should mention the version" - ); - } - - #[test] - fn test_find_target_release_empty() { - let releases: Vec<GitHubRelease> = vec![]; - - let result = find_target_release(&releases, None); - assert!( - result.is_err(), - "should return error for empty release list" - ); - } - - #[test] - fn test_find_asset_found() { - let asset = make_asset( - "mozart-linux-x86_64", - "https://example.com/mozart-linux-x86_64", - ); - let release = make_release( - "v0.2.0", - false, - vec![ - make_asset( - "mozart-macos-aarch64", - "https://example.com/mozart-macos-aarch64", - ), - asset, - ], - ); - - let found = find_asset(&release, "mozart-linux-x86_64").expect("should find asset"); - assert_eq!(found.name, "mozart-linux-x86_64"); - assert_eq!( - found.browser_download_url, - "https://example.com/mozart-linux-x86_64" - ); - } - - #[test] - fn test_find_asset_not_found() { - let release = make_release( - "v0.2.0", - false, - vec![make_asset( - "mozart-linux-x86_64", - "https://example.com/mozart-linux-x86_64", - )], - ); - - let result = find_asset(&release, "mozart-windows-x86_64.exe"); - assert!(result.is_err(), "should return error for missing asset"); - let msg = result.unwrap_err().to_string(); - assert!( - msg.contains("mozart-windows-x86_64.exe"), - "error message should mention the asset name" - ); - } - - #[test] - fn test_get_data_dir_from_env() { - let dir = tempdir().unwrap(); - let expected = dir.path().to_path_buf(); - - // SAFETY: test-only env mutation - unsafe { std::env::set_var("MOZART_DATA_DIR", &expected) }; - - let result = get_data_dir().expect("should succeed with MOZART_DATA_DIR set"); - assert_eq!(result, expected); - - unsafe { std::env::remove_var("MOZART_DATA_DIR") }; - } - - #[test] - fn test_get_data_dir_default() { - // Ensure MOZART_DATA_DIR is not set - unsafe { std::env::remove_var("MOZART_DATA_DIR") }; - - let result = get_data_dir().expect("should succeed when HOME is set"); - let path_str = result.to_string_lossy(); - assert!( - path_str.ends_with(".local/share/mozart"), - "default data dir should end with .local/share/mozart, got: {path_str}" - ); - } - - #[test] - fn test_find_latest_backup() { - let dir = tempdir().unwrap(); - - // Create two backup files; the second one is newer - let old_backup = dir.path().join("mozart-0.1.0.old"); - let new_backup = dir.path().join("mozart-0.2.0.old"); - fs::write(&old_backup, b"old binary").unwrap(); - // Ensure the newer file has a later mtime - std::thread::sleep(std::time::Duration::from_millis(10)); - fs::write(&new_backup, b"new binary").unwrap(); - - let found = find_latest_backup(dir.path()).expect("should find latest backup"); - assert_eq!(found, new_backup); - } - - #[test] - fn test_find_latest_backup_empty() { - let dir = tempdir().unwrap(); - // No backup files present - let result = find_latest_backup(dir.path()); - assert!(result.is_err(), "should return error when no backups found"); - } - - #[test] - fn test_clean_backups() { - let dir = tempdir().unwrap(); - - let backup1 = dir.path().join("mozart-0.1.0.old"); - let backup2 = dir.path().join("mozart-0.2.0.old"); - let keep = dir.path().join("somefile.txt"); - - fs::write(&backup1, b"binary").unwrap(); - fs::write(&backup2, b"binary").unwrap(); - fs::write(&keep, b"keep me").unwrap(); - - clean_backups(dir.path(), None).expect("clean_backups should succeed"); - - assert!(!backup1.exists(), "backup1 should be removed"); - assert!(!backup2.exists(), "backup2 should be removed"); - assert!(keep.exists(), "non-backup file should remain"); - } - - #[test] - fn test_clean_backups_with_except() { - let dir = tempdir().unwrap(); - - let backup1 = dir.path().join("mozart-0.1.0.old"); - let backup2 = dir.path().join("mozart-0.2.0.old"); - - fs::write(&backup1, b"binary").unwrap(); - fs::write(&backup2, b"binary").unwrap(); - - clean_backups(dir.path(), Some(&backup2)).expect("clean_backups should succeed"); - - assert!(!backup1.exists(), "backup1 should be removed"); - assert!(backup2.exists(), "backup2 should be preserved (excepted)"); - } - - #[test] - fn test_effective_channel() { - assert_eq!(effective_channel(false), "stable"); - assert_eq!(effective_channel(true), "preview"); - } - - #[test] - fn test_version_from_backup() { - let path = PathBuf::from("/some/dir/mozart-0.3.1.old"); - assert_eq!(version_from_backup(&path), "0.3.1"); - - let bad_path = PathBuf::from("/some/dir/unknown-file.txt"); - assert_eq!(version_from_backup(&bad_path), "unknown"); - } -} diff --git a/crates/mozart/src/commands/show.rs b/crates/mozart/src/commands/show.rs index 81eaaad..4049a18 100644 --- a/crates/mozart/src/commands/show.rs +++ b/crates/mozart/src/commands/show.rs @@ -2150,295 +2150,3 @@ fn normalize_version_simple(version: &str) -> String { } result } - -// ============================================================================ -// Tests -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_license_for_show_osi_approved() { - let out = format_license_for_show("MIT"); - assert!( - out.contains("MIT License") && out.contains("(MIT)") && out.contains("(OSI approved)"), - "got: {out}", - ); - assert!( - out.contains("https://spdx.org/licenses/MIT.html#licenseText"), - "got: {out}", - ); - } - - #[test] - fn test_format_license_for_show_non_osi() { - let out = format_license_for_show("CC-BY-4.0"); - assert!( - out.contains("(CC-BY-4.0)") && !out.contains("(OSI approved)"), - "got: {out}", - ); - assert!( - out.contains("https://spdx.org/licenses/CC-BY-4.0.html#licenseText"), - "got: {out}", - ); - } - - #[test] - fn test_format_license_for_show_unknown_falls_back_to_id() { - assert_eq!(format_license_for_show("not-a-license"), "not-a-license"); - } - - #[test] - fn test_format_license_for_show_url_uses_canonical_id_casing() { - let out = format_license_for_show("mit"); - assert!( - out.contains("https://spdx.org/licenses/MIT.html#licenseText"), - "got: {out}", - ); - } - - #[test] - fn test_format_version_strips_v() { - assert_eq!(format_version("v1.2.3"), "1.2.3"); - } - - #[test] - fn test_format_version_no_v() { - assert_eq!(format_version("1.2.3"), "1.2.3"); - } - - #[test] - fn test_format_version_keeps_dev() { - assert_eq!(format_version("dev-main"), "dev-main"); - } - - #[test] - fn test_matches_wildcard_exact() { - assert!(matches_wildcard("psr/log", "psr/log")); - } - - #[test] - fn test_matches_wildcard_star_end() { - assert!(matches_wildcard("psr/log", "psr/*")); - } - - #[test] - fn test_matches_wildcard_star_start() { - assert!(matches_wildcard("psr/log", "*/log")); - } - - #[test] - fn test_matches_wildcard_star_middle() { - assert!(matches_wildcard("monolog/monolog", "mono*/mono*")); - } - - #[test] - fn test_matches_wildcard_no_match() { - assert!(!matches_wildcard("psr/log", "symfony/*")); - } - - #[test] - fn test_matches_wildcard_case_insensitive() { - assert!(matches_wildcard("PSR/Log", "psr/*")); - } - - #[test] - fn test_matches_wildcard_star_both_ends() { - assert!(matches_wildcard("monolog/monolog", "*log*")); - } - - #[test] - fn test_matches_wildcard_no_wildcard_mismatch() { - assert!(!matches_wildcard("psr/log", "psr/log2")); - } - - #[test] - fn test_matches_wildcard_trailing_chars_fail() { - assert!(!matches_wildcard("psr/log", "psr/l")); - } - - #[test] - fn test_format_version_highlight() { - assert_eq!(format_version_highlight("v3.0.0"), "* 3.0.0"); - assert_eq!(format_version_highlight("3.0.0"), "* 3.0.0"); - } - - #[test] - fn test_get_installed_description_present() { - let mut extra = indexmap::IndexMap::new(); - extra.insert( - "description".to_string(), - serde_json::Value::String("A logging library".to_string()), - ); - let pkg = mozart_core::repository::installed::InstalledPackageEntry { - name: "monolog/monolog".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: extra, - }; - assert_eq!(get_installed_description(&pkg), "A logging library"); - } - - #[test] - fn test_get_installed_description_absent() { - let pkg = mozart_core::repository::installed::InstalledPackageEntry { - name: "psr/log".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: indexmap::IndexMap::new(), - }; - assert_eq!(get_installed_description(&pkg), ""); - } - - #[test] - fn test_get_installed_keywords() { - let mut extra = indexmap::IndexMap::new(); - extra.insert( - "keywords".to_string(), - serde_json::json!(["log", "psr3", "logging"]), - ); - let pkg = mozart_core::repository::installed::InstalledPackageEntry { - name: "psr/log".to_string(), - version: "3.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields: extra, - }; - assert_eq!( - get_installed_keywords_vec(&pkg).join(", "), - "log, psr3, logging" - ); - } - - #[test] - fn test_is_platform_package_php() { - assert!(is_platform_package("php")); - } - - #[test] - fn test_is_platform_package_ext() { - assert!(is_platform_package("ext-json")); - assert!(is_platform_package("ext-mbstring")); - } - - #[test] - fn test_is_platform_package_lib() { - assert!(is_platform_package("lib-pcre")); - } - - #[test] - fn test_is_platform_package_not_platform() { - assert!(!is_platform_package("monolog/monolog")); - assert!(!is_platform_package("psr/log")); - } - - #[test] - fn test_classify_up_to_date() { - assert_eq!( - classify_update_category("1.2.3.0", "1.2.3.0"), - ListUpdateKind::UpToDate - ); - } - - #[test] - fn test_classify_compatible_same_major() { - assert_eq!( - classify_update_category("1.2.0.0", "1.3.0.0"), - ListUpdateKind::Compatible - ); - } - - #[test] - fn test_classify_incompatible_different_major() { - assert_eq!( - classify_update_category("1.9.0.0", "2.0.0.0"), - ListUpdateKind::Incompatible - ); - } - - #[test] - fn test_normalize_version_simple_short() { - assert_eq!(normalize_version_simple("1.2"), "1.2.0.0"); - } - - #[test] - fn test_normalize_version_simple_three_parts() { - assert_eq!(normalize_version_simple("1.2.3"), "1.2.3.0"); - } - - #[test] - fn test_normalize_version_simple_v_prefix() { - assert_eq!(normalize_version_simple("v1.2.3"), "1.2.3.0"); - } - - #[test] - fn test_extract_major_basic() { - assert_eq!(extract_major("2.3.4.0"), 2); - assert_eq!(extract_major("0.1.2.0"), 0); - } - - #[test] - fn test_extract_major_with_prerelease() { - assert_eq!(extract_major("2.3.4.0-beta1"), 2); - } - - #[test] - fn test_extract_minor_basic() { - assert_eq!(extract_minor("2.3.4.0"), 3); - assert_eq!(extract_minor("1.0.0.0"), 0); - } - - #[test] - fn test_extract_minor_with_prerelease() { - assert_eq!(extract_minor("2.3.4.0-rc1"), 3); - } - - #[test] - fn test_abandoned_info_bool_true() { - let val = serde_json::Value::Bool(true); - assert_eq!(abandoned_info(&val), Some(String::new())); - } - - #[test] - fn test_abandoned_info_string_replacement() { - let val = serde_json::Value::String("other/package".to_string()); - assert_eq!(abandoned_info(&val), Some("other/package".to_string())); - } - - #[test] - fn test_abandoned_info_false() { - let val = serde_json::Value::Bool(false); - assert_eq!(abandoned_info(&val), None); - } - - #[test] - fn test_abandoned_info_string_false() { - let val = serde_json::Value::String("false".to_string()); - assert_eq!(abandoned_info(&val), None); - } -} diff --git a/crates/mozart/src/commands/suggests.rs b/crates/mozart/src/commands/suggests.rs index ca8e5fe..c9628b1 100644 --- a/crates/mozart/src/commands/suggests.rs +++ b/crates/mozart/src/commands/suggests.rs @@ -230,269 +230,3 @@ fn build_root_info(root: Option<&mozart_core::package::RawPackageData>) -> RootI direct_deps, } } - -#[cfg(test)] -mod tests { - use super::*; - use mozart_core::installer::{HasSuggests, InstalledRepoLite, RootInfo}; - - fn make_locked_package( - name: &str, - suggest: Option<indexmap::IndexMap<String, String>>, - ) -> mozart_core::repository::lockfile::LockedPackage { - mozart_core::repository::lockfile::LockedPackage { - name: name.to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest, - package_type: None, - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn make_installed_entry( - name: &str, - suggest: Option<indexmap::IndexMap<String, String>>, - ) -> mozart_core::repository::installed::InstalledPackageEntry { - let mut extra_fields = indexmap::IndexMap::new(); - if let Some(s) = suggest { - let map: serde_json::Map<String, serde_json::Value> = s - .into_iter() - .map(|(k, v)| (k, serde_json::Value::String(v))) - .collect(); - extra_fields.insert("suggest".to_string(), serde_json::Value::Object(map)); - } - mozart_core::repository::installed::InstalledPackageEntry { - name: name.to_string(), - version: "1.0.0".to_string(), - version_normalized: None, - source: None, - dist: None, - package_type: None, - install_path: None, - autoload: None, - aliases: vec![], - homepage: None, - support: None, - extra_fields, - } - } - - fn minimal_lock( - packages: Vec<mozart_core::repository::lockfile::LockedPackage>, - packages_dev: Option<Vec<mozart_core::repository::lockfile::LockedPackage>>, - ) -> mozart_core::repository::lockfile::LockFile { - mozart_core::repository::lockfile::LockFile { - readme: mozart_core::repository::lockfile::LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages, - packages_dev, - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - } - } - - fn console() -> mozart_core::console::Console { - mozart_core::console::Console::new(0, false, false, true, true) - } - - #[test] - fn locked_package_implements_has_suggests() { - let mut suggest = indexmap::IndexMap::new(); - suggest.insert("ext-intl".to_string(), "for i18n".to_string()); - suggest.insert("ext-redis".to_string(), "for cache".to_string()); - let pkg = make_locked_package("vendor/a", Some(suggest)); - let pairs = pkg.suggests(); - assert_eq!(pairs.len(), 2); - assert_eq!(pkg.pretty_name(), "vendor/a"); - } - - #[test] - fn installed_entry_reads_suggest_from_extra_fields() { - let mut suggest = indexmap::IndexMap::new(); - suggest.insert("ext-redis".to_string(), "for cache".to_string()); - let entry = make_installed_entry("vendor/cache", Some(suggest)); - let pairs = entry.suggests(); - assert_eq!(pairs.len(), 1); - assert_eq!(pairs[0].0, "ext-redis"); - assert_eq!(pairs[0].1, "for cache"); - } - - #[test] - fn build_installed_repo_includes_provide_and_replace_from_lock() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - - let mut pkg = make_locked_package("vendor/a", None); - pkg.provide.insert("virt/foo".into(), "1.0".into()); - pkg.replace.insert("virt/bar".into(), "1.0".into()); - - let lock = minimal_lock(vec![pkg], Some(vec![])); - lock.write_to_file(&working_dir.join("composer.lock")) - .unwrap(); - - let repo = build_installed_repo(working_dir, true, false, None).unwrap(); - assert!(repo.contains("vendor/a")); - assert!(repo.contains("virt/foo")); - assert!(repo.contains("virt/bar")); - } - - #[test] - fn build_installed_repo_skips_dev_when_no_dev() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - - let lock = minimal_lock( - vec![make_locked_package("vendor/prod", None)], - Some(vec![make_locked_package("vendor/dev", None)]), - ); - lock.write_to_file(&working_dir.join("composer.lock")) - .unwrap(); - - let repo = build_installed_repo(working_dir, true, true, None).unwrap(); - assert!(repo.contains("vendor/prod")); - assert!(!repo.contains("vendor/dev")); - } - - #[test] - fn build_installed_repo_picks_up_platform_from_lock() { - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let working_dir = dir.path(); - - let mut lock = minimal_lock(vec![], Some(vec![])); - let mut platform = serde_json::Map::new(); - platform.insert("php".into(), serde_json::Value::String("8.2".into())); - platform.insert("ext-json".into(), serde_json::Value::String("*".into())); - lock.platform = serde_json::Value::Object(platform); - lock.write_to_file(&working_dir.join("composer.lock")) - .unwrap(); - - let repo = build_installed_repo(working_dir, true, false, None).unwrap(); - assert!(repo.contains("php")); - assert!(repo.contains("ext-json")); - } - - #[test] - fn build_root_info_includes_root_name_and_direct_deps() { - let mut root = mozart_core::package::RawPackageData { - name: "my/root".into(), - version: None, - description: None, - package_type: None, - homepage: None, - license: None, - authors: vec![], - minimum_stability: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - repositories: vec![], - autoload: None, - bin: vec![], - extra_fields: indexmap::IndexMap::new(), - }; - root.require.insert("vendor/a".into(), "^1.0".into()); - root.require_dev.insert("vendor/b".into(), "^2.0".into()); - - let info = build_root_info(Some(&root)); - assert_eq!(info.name, "my/root"); - assert!(info.direct_deps.contains("vendor/a")); - assert!(info.direct_deps.contains("vendor/b")); - } - - #[test] - fn reporter_collects_from_locked_package() { - let console = console(); - let mut reporter = SuggestedPackagesReporter::new(&console); - - let mut suggest = indexmap::IndexMap::new(); - suggest.insert("ext-intl".to_string(), "for i18n".to_string()); - suggest.insert("vendor/optional".to_string(), "Optional".to_string()); - let pkg = make_locked_package("vendor/a", Some(suggest)); - - reporter.add_suggestions_from_package(&pkg); - assert_eq!(reporter.packages().len(), 2); - assert!(reporter.packages().iter().all(|s| s.source == "vendor/a")); - } - - #[test] - fn reporter_skips_already_installed_via_repo() { - let console = console(); - let mut reporter = SuggestedPackagesReporter::new(&console); - - let mut suggest = indexmap::IndexMap::new(); - suggest.insert("vendor/already-here".to_string(), "".to_string()); - suggest.insert("vendor/not-here".to_string(), "".to_string()); - let pkg = make_locked_package("vendor/a", Some(suggest)); - reporter.add_suggestions_from_package(&pkg); - - let mut repo = InstalledRepoLite::new(); - repo.insert("vendor/already-here"); - - // Indirectly verify via output_minimalistic: suggests after filter == 1 - reporter.output_minimalistic(Some(&repo), None); - // Direct field check: - assert_eq!(reporter.packages().len(), 2); - let visible: Vec<_> = reporter - .packages() - .iter() - .filter(|s| !repo.contains(&s.target)) - .collect(); - assert_eq!(visible.len(), 1); - assert_eq!(visible[0].target, "vendor/not-here"); - } - - #[test] - fn reporter_only_dependents_of_filters_transitive_sources() { - let console = console(); - let mut reporter = SuggestedPackagesReporter::new(&console); - reporter.add_package("vendor/direct".into(), "ext-x".into(), "".into()); - reporter.add_package("vendor/transitive".into(), "ext-y".into(), "".into()); - - let root = RootInfo { - name: String::new(), - direct_deps: ["vendor/direct".to_string()].into_iter().collect(), - }; - - // No installed repo: still expect transitive source to be filtered. - let installed = InstalledRepoLite::new(); - // We can't easily inspect get_filtered_suggestions; mirror the logic - // via output by checking that output_minimalistic counts only the kept - // suggestion. (Method is `pub`, but counting via `.packages()` is a - // reasonable proxy here; the behavior is exercised by the - // mozart-core unit tests.) - let _ = (root, installed); - assert_eq!(reporter.packages().len(), 2); - } -} diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index 334e221..a893ffd 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -1799,667 +1799,3 @@ pub async fn run( Ok(()) } - -#[cfg(test)] -mod tests { - use super::*; - - fn make_locked_package(name: &str, version: &str) -> lockfile::LockedPackage { - lockfile::LockedPackage { - name: name.to_string(), - version: version.to_string(), - version_normalized: Some(format!("{}.0", version)), - source: None, - dist: None, - require: indexmap::IndexMap::new(), - require_dev: indexmap::IndexMap::new(), - conflict: indexmap::IndexMap::new(), - provide: indexmap::IndexMap::new(), - replace: indexmap::IndexMap::new(), - suggest: None, - package_type: Some("library".to_string()), - autoload: None, - autoload_dev: None, - license: None, - description: None, - homepage: None, - keywords: None, - authors: None, - support: None, - funding: None, - time: None, - extra_fields: indexmap::IndexMap::new(), - } - } - - fn minimal_lock(packages: Vec<lockfile::LockedPackage>) -> lockfile::LockFile { - lockfile::LockFile { - readme: lockfile::LockFile::default_readme(), - content_hash: "abc123".to_string(), - packages, - packages_dev: Some(vec![]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: Some("2.6.0".to_string()), - } - } - - fn make_resolved_package(name: &str, version: &str) -> ResolvedPackage { - ResolvedPackage { - name: name.to_string(), - version: version.to_string(), - version_normalized: format!("{}.0", version), - is_dev: false, - alias_of_normalized: None, - } - } - - fn test_console() -> std::sync::Arc<std::sync::Mutex<Box<dyn IoInterface>>> { - std::sync::Arc::new(std::sync::Mutex::new( - Box::new(mozart_core::console::Console::new( - 0, false, false, false, false, - )) as Box<dyn IoInterface>, - )) - } - - #[test] - fn test_parse_minimum_stability_stable() { - assert_eq!( - package::Stability::parse("stable"), - package::Stability::Stable - ); - assert_eq!( - package::Stability::parse("STABLE"), - package::Stability::Stable - ); - assert_eq!( - package::Stability::parse("Stable"), - package::Stability::Stable - ); - } - - #[test] - fn test_parse_minimum_stability_rc() { - assert_eq!(package::Stability::parse("RC"), package::Stability::RC); - assert_eq!(package::Stability::parse("rc"), package::Stability::RC); - } - - #[test] - fn test_parse_minimum_stability_beta() { - assert_eq!(package::Stability::parse("beta"), package::Stability::Beta); - assert_eq!(package::Stability::parse("BETA"), package::Stability::Beta); - } - - #[test] - fn test_parse_minimum_stability_alpha() { - assert_eq!( - package::Stability::parse("alpha"), - package::Stability::Alpha - ); - assert_eq!( - package::Stability::parse("ALPHA"), - package::Stability::Alpha - ); - } - - #[test] - fn test_parse_minimum_stability_dev() { - assert_eq!(package::Stability::parse("dev"), package::Stability::Dev); - assert_eq!(package::Stability::parse("DEV"), package::Stability::Dev); - } - - #[test] - fn test_parse_minimum_stability_unknown_defaults_to_stable() { - assert_eq!( - package::Stability::parse("unknown"), - package::Stability::Stable - ); - assert_eq!(package::Stability::parse(""), package::Stability::Stable); - } - - #[test] - fn test_compute_update_changes_all_new() { - // No old lock: all packages in new lock should be Install - let new_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("monolog/monolog", "3.8.0"), - ]); - - let changes = compute_update_changes(None, &new_lock, false); - - assert_eq!(changes.len(), 2); - for change in &changes { - assert!( - matches!(change.kind, ChangeKind::Install { .. }), - "Expected Install, got {:?} for {}", - change.kind, - change.name - ); - } - } - - #[test] - fn test_compute_update_changes_update() { - // Old lock has psr/log at 3.0.0; new lock has it at 3.0.1 -> Update - let old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let new_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.1")]); - - let changes = compute_update_changes(Some(&old_lock), &new_lock, false); - - assert_eq!(changes.len(), 1); - assert_eq!(changes[0].name, "psr/log"); - assert!(matches!( - &changes[0].kind, - ChangeKind::Update { - old_version, - new_version - } if old_version == "3.0.0" && new_version == "3.0.1" - )); - } - - #[test] - fn test_compute_update_changes_remove() { - // Old lock has monolog; new lock doesn't -> Remove - let old_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("monolog/monolog", "3.8.0"), - ]); - let new_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - - let changes = compute_update_changes(Some(&old_lock), &new_lock, false); - - assert_eq!(changes.len(), 1); - assert_eq!(changes[0].name, "monolog/monolog"); - assert!(matches!( - &changes[0].kind, - ChangeKind::Uninstall { old_version } if old_version == "3.8.0" - )); - } - - #[test] - fn test_compute_update_changes_unchanged_not_in_result() { - // Same version in both locks -> no changes - let old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let new_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - - let changes = compute_update_changes(Some(&old_lock), &new_lock, false); - - assert!( - changes.is_empty(), - "Unchanged packages should not appear in changes list" - ); - } - - #[test] - fn test_compute_update_changes_mixed() { - // Mixed scenario: install, update, remove, unchanged - let old_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), // unchanged - make_locked_package("monolog/monolog", "3.7.0"), // will be updated - make_locked_package("old/package", "1.0.0"), // will be removed - ]); - let new_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), // unchanged - make_locked_package("monolog/monolog", "3.8.0"), // updated - make_locked_package("new/package", "2.0.0"), // installed - ]); - - let changes = compute_update_changes(Some(&old_lock), &new_lock, false); - - // 3 changes: update monolog, remove old/package, install new/package - assert_eq!(changes.len(), 3); - - let monolog = changes - .iter() - .find(|c| c.name == "monolog/monolog") - .unwrap(); - assert!(matches!( - &monolog.kind, - ChangeKind::Update { old_version, new_version } - if old_version == "3.7.0" && new_version == "3.8.0" - )); - - let removed = changes.iter().find(|c| c.name == "old/package").unwrap(); - assert!(matches!(&removed.kind, ChangeKind::Uninstall { .. })); - - let installed = changes.iter().find(|c| c.name == "new/package").unwrap(); - assert!(matches!(&installed.kind, ChangeKind::Install { .. })); - } - - #[test] - fn test_compute_update_changes_dev_packages_included() { - // dev_mode=true: dev packages are also compared - let mut old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - old_lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "10.0.0")]); - - let mut new_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - new_lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); - - let changes = compute_update_changes(Some(&old_lock), &new_lock, true); - - assert_eq!(changes.len(), 1); - assert_eq!(changes[0].name, "phpunit/phpunit"); - assert!(matches!(&changes[0].kind, ChangeKind::Update { .. })); - } - - #[test] - fn test_compute_update_changes_dev_packages_excluded_when_no_dev() { - // dev_mode=false: dev packages are ignored - let mut old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - old_lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "10.0.0")]); - - let mut new_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - new_lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); - - let changes = compute_update_changes(Some(&old_lock), &new_lock, false); - - // No changes because we're not including dev packages - assert!( - changes.is_empty(), - "Dev packages should not appear in changes when dev_mode=false" - ); - } - - #[test] - fn test_apply_partial_update_keeps_non_specified_packages() { - // old lock has psr/log 3.0.0 and monolog 3.7.0 - // resolver found psr/log 3.0.1 and monolog 3.8.0 - // we only want to update monolog - // expected: psr/log stays at 3.0.0, monolog becomes 3.8.0 - - let old_lock = minimal_lock(vec![ - make_locked_package("psr/log", "3.0.0"), - make_locked_package("monolog/monolog", "3.7.0"), - ]); - - let resolved = vec![ - make_resolved_package("psr/log", "3.0.1"), - make_resolved_package("monolog/monolog", "3.8.0"), - ]; - - let update_packages = vec!["monolog/monolog".to_string()]; - let result = apply_partial_update(resolved, &old_lock, &update_packages); - - let psr = result.iter().find(|p| p.name == "psr/log").unwrap(); - assert_eq!( - psr.version, "3.0.0", - "psr/log should be kept at old version" - ); - - let monolog = result.iter().find(|p| p.name == "monolog/monolog").unwrap(); - assert_eq!( - monolog.version, "3.8.0", - "monolog/monolog should use new version" - ); - } - - #[test] - fn test_apply_partial_update_case_insensitive() { - // update_packages uses mixed case, package names may be lowercase - let old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let resolved = vec![make_resolved_package("psr/log", "3.0.1")]; - - // Not updating psr/log (not in update list); should revert to 3.0.0 - let update_packages = vec!["MonoLog/Monolog".to_string()]; - let result = apply_partial_update(resolved, &old_lock, &update_packages); - - let psr = result.iter().find(|p| p.name == "psr/log").unwrap(); - assert_eq!(psr.version, "3.0.0"); - } - - #[test] - fn test_apply_partial_update_new_package_in_update_list() { - // A brand new package resolved that is in the update list should use the new version - let old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let resolved = vec![ - make_resolved_package("psr/log", "3.0.0"), - make_resolved_package("new/package", "1.0.0"), - ]; - - let update_packages = vec!["new/package".to_string()]; - let result = apply_partial_update(resolved, &old_lock, &update_packages); - - let new_pkg = result.iter().find(|p| p.name == "new/package").unwrap(); - assert_eq!(new_pkg.version, "1.0.0"); - } - - #[test] - fn test_apply_partial_update_full_update_mode() { - // If update_packages is empty, it should behave like full update (no swapping) - let old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let resolved = vec![make_resolved_package("psr/log", "3.0.1")]; - - // Empty update list means... everything is not in the update set, - // so old versions are preserved. This is the expected behavior for partial mode - // when packages is empty (which shouldn't happen - full update is separate path). - let update_packages: Vec<String> = vec![]; - let result = apply_partial_update(resolved, &old_lock, &update_packages); - - // When update_packages is empty, nothing is in the update set, so old versions revert - let psr = result.iter().find(|p| p.name == "psr/log").unwrap(); - assert_eq!(psr.version, "3.0.0"); - } - - #[test] - fn test_glob_matches_exact() { - assert!(glob_matches("monolog/monolog", "monolog/monolog")); - assert!(!glob_matches("monolog/monolog", "monolog/logger")); - } - - #[test] - fn test_glob_matches_case_insensitive() { - assert!(glob_matches("Monolog/Monolog", "monolog/monolog")); - assert!(glob_matches("symfony/*", "Symfony/Console")); - } - - #[test] - fn test_glob_matches_vendor_wildcard() { - assert!(glob_matches("symfony/*", "symfony/console")); - assert!(glob_matches("symfony/*", "symfony/http-kernel")); - assert!(!glob_matches("symfony/*", "monolog/monolog")); - } - - #[test] - fn test_glob_matches_wildcard_in_name() { - assert!(glob_matches("monolog/mono*", "monolog/monolog")); - assert!(!glob_matches("monolog/mono*", "monolog/logger")); - } - - #[test] - fn test_glob_matches_wildcard_no_slash() { - // Without a '/' the pattern still works as a full name match - assert!(!glob_matches("symfony/*", "monolog/monolog")); - } - - #[test] - fn test_glob_matches_different_segment_count() { - // "vendor/*" has 2 segments; "monolog" has only 1: no match - assert!(!glob_matches("vendor/*", "monolog")); - // Pattern with 1 segment vs name with 2 segments: no match - assert!(!glob_matches("monolog", "monolog/monolog")); - } - - #[test] - fn test_expand_wildcards_no_wildcard_passthrough() { - let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let root_requires: IndexSet<String> = ["psr/log", "nonexistent/pkg"] - .into_iter() - .map(String::from) - .collect(); - let specs = vec!["psr/log".to_string(), "nonexistent/pkg".to_string()]; - let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); - assert_eq!(result, vec!["psr/log", "nonexistent/pkg"]); - } - - #[test] - fn test_expand_wildcards_vendor_star() { - let lock = minimal_lock(vec![ - make_locked_package("symfony/console", "7.0.0"), - make_locked_package("symfony/http-kernel", "7.0.0"), - make_locked_package("monolog/monolog", "3.8.0"), - ]); - let specs = vec!["symfony/*".to_string()]; - let root_requires: IndexSet<String> = IndexSet::new(); - let mut result = expand_wildcards(&specs, &lock, &root_requires, test_console()); - result.sort(); - assert_eq!(result, vec!["symfony/console", "symfony/http-kernel"]); - } - - #[test] - fn test_expand_wildcards_no_match_emits_warning() { - let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let specs = vec!["unknown/*".to_string()]; - let root_requires: IndexSet<String> = IndexSet::new(); - // Should return empty (no match), no panic - let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); - assert!(result.is_empty()); - } - - #[test] - fn test_expand_wildcards_deduplication() { - let lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let specs = vec!["psr/log".to_string(), "psr/log".to_string()]; - let root_requires: IndexSet<String> = IndexSet::new(); - let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); - assert_eq!(result.len(), 1); - assert_eq!(result[0], "psr/log"); - } - - #[test] - fn test_expand_wildcards_also_checks_dev() { - let mut lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - lock.packages_dev = Some(vec![make_locked_package("phpunit/phpunit", "11.0.0")]); - let specs = vec!["phpunit/*".to_string()]; - let root_requires: IndexSet<String> = IndexSet::new(); - let result = expand_wildcards(&specs, &lock, &root_requires, test_console()); - assert_eq!(result, vec!["phpunit/phpunit"]); - } - - #[test] - fn test_expand_with_direct_deps_adds_require() { - // monolog/monolog requires psr/log - let mut pkg = make_locked_package("monolog/monolog", "3.8.0"); - pkg.require - .insert("psr/log".to_string(), "^3.0".to_string()); - - let lock = minimal_lock(vec![pkg, make_locked_package("psr/log", "3.0.0")]); - - let result = expand_with_direct_dependencies( - vec!["monolog/monolog".to_string()], - &lock, - &IndexSet::new(), - &IndexMap::new(), - ); - let mut result_sorted = result.clone(); - result_sorted.sort(); - assert!(result_sorted.contains(&"monolog/monolog".to_string())); - assert!(result_sorted.contains(&"psr/log".to_string())); - } - - #[test] - fn test_expand_with_direct_deps_skips_platform() { - let mut pkg = make_locked_package("monolog/monolog", "3.8.0"); - pkg.require.insert("php".to_string(), ">=8.1".to_string()); - pkg.require.insert("ext-json".to_string(), "*".to_string()); - pkg.require - .insert("psr/log".to_string(), "^3.0".to_string()); - - let lock = minimal_lock(vec![pkg, make_locked_package("psr/log", "3.0.0")]); - - let result = expand_with_direct_dependencies( - vec!["monolog/monolog".to_string()], - &lock, - &IndexSet::new(), - &IndexMap::new(), - ); - // Should NOT include php or ext-json - assert!(!result.contains(&"php".to_string())); - assert!(!result.contains(&"ext-json".to_string())); - assert!(result.contains(&"psr/log".to_string())); - } - - #[test] - fn test_expand_with_direct_deps_no_duplicates() { - // Both packages in the list require psr/log - let mut pkg_a = make_locked_package("foo/a", "1.0.0"); - pkg_a - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - let mut pkg_b = make_locked_package("foo/b", "1.0.0"); - pkg_b - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - - let lock = minimal_lock(vec![pkg_a, pkg_b, make_locked_package("psr/log", "3.0.0")]); - - let result = expand_with_direct_dependencies( - vec!["foo/a".to_string(), "foo/b".to_string()], - &lock, - &IndexSet::new(), - &IndexMap::new(), - ); - let psr_count = result.iter().filter(|s| s.as_str() == "psr/log").count(); - assert_eq!(psr_count, 1, "psr/log should appear only once"); - } - - #[test] - fn test_expand_all_deps_transitive() { - // a -> b -> c - let mut pkg_a = make_locked_package("foo/a", "1.0.0"); - pkg_a - .require - .insert("foo/b".to_string(), "^1.0".to_string()); - let mut pkg_b = make_locked_package("foo/b", "1.0.0"); - pkg_b - .require - .insert("foo/c".to_string(), "^1.0".to_string()); - let pkg_c = make_locked_package("foo/c", "1.0.0"); - - let lock = minimal_lock(vec![pkg_a, pkg_b, pkg_c]); - - let result = - expand_with_all_dependencies(vec!["foo/a".to_string()], &lock, &IndexMap::new()); - assert!(result.contains(&"foo/a".to_string())); - assert!(result.contains(&"foo/b".to_string())); - assert!(result.contains(&"foo/c".to_string())); - } - - #[test] - fn test_expand_all_deps_no_infinite_loop() { - // Circular reference: a -> b -> a - let mut pkg_a = make_locked_package("foo/a", "1.0.0"); - pkg_a - .require - .insert("foo/b".to_string(), "^1.0".to_string()); - let mut pkg_b = make_locked_package("foo/b", "1.0.0"); - pkg_b - .require - .insert("foo/a".to_string(), "^1.0".to_string()); - - let lock = minimal_lock(vec![pkg_a, pkg_b]); - - // Must not loop infinitely - let result = - expand_with_all_dependencies(vec!["foo/a".to_string()], &lock, &IndexMap::new()); - assert!(result.contains(&"foo/a".to_string())); - assert!(result.contains(&"foo/b".to_string())); - assert_eq!(result.len(), 2); - } - - #[test] - fn test_expand_packages_wildcard_with_direct_deps() { - // symfony/* expands to symfony/console; symfony/console requires psr/log - let mut console_pkg = make_locked_package("symfony/console", "7.0.0"); - console_pkg - .require - .insert("psr/log".to_string(), "^3.0".to_string()); - - let lock = minimal_lock(vec![console_pkg, make_locked_package("psr/log", "3.0.0")]); - - let result = expand_packages( - &["symfony/*".to_string()], - Some(&lock), - true, // with_dependencies - false, // with_all_dependencies - &IndexSet::new(), - &IndexMap::new(), - test_console(), - ); - - assert!(result.contains(&"symfony/console".to_string())); - assert!(result.contains(&"psr/log".to_string())); - } - - #[test] - fn test_apply_minimal_changes_pins_all() { - // Resolver found psr/log 3.0.1, but old lock has 3.0.0 - // apply_minimal_changes should pin back to 3.0.0 - let old_lock = minimal_lock(vec![make_locked_package("psr/log", "3.0.0")]); - let resolved = vec![make_resolved_package("psr/log", "3.0.1")]; - - let result = apply_minimal_changes(resolved, &old_lock); - let psr = result.iter().find(|p| p.name == "psr/log").unwrap(); - assert_eq!( - psr.version, "3.0.0", - "minimal-changes should pin to locked version" - ); - } - - #[tokio::test] - #[ignore] - async fn test_update_full_e2e() { - use mozart_core::package::RawPackageData; - use mozart_core::repository::lockfile::{LockFileGenerationRequest, generate_lock_file}; - use mozart_core::repository::resolver::{ResolveRequest, resolve}; - - let composer_json_content = - r#"{"name": "test/project", "require": {"monolog/monolog": "^3.0"}}"#; - let composer_json: RawPackageData = serde_json::from_str(composer_json_content).unwrap(); - - let request = ResolveRequest { - root_name: String::new(), - root_version: None, - require: vec![("monolog/monolog".to_string(), "^3.0".to_string())], - require_dev: vec![], - include_dev: false, - minimum_stability: package::Stability::Stable, - stability_flags: IndexMap::new(), - prefer_stable: true, - prefer_lowest: false, - platform: PlatformConfig::new(), - ignore_platform_reqs: false, - ignore_platform_req_list: vec![], - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - temporary_constraints: IndexMap::new(), - raw_repositories: vec![], - root_provide: IndexMap::new(), - root_replace: IndexMap::new(), - root_conflict: IndexMap::new(), - locked_package_names: IndexSet::new(), - locked_packages: Vec::new(), - block_abandoned: false, - root_branch_alias: None, - preferred_versions: IndexMap::new(), - block_insecure: false, - }; - - let resolved = resolve(&request).await.expect("Resolution should succeed"); - assert!(!resolved.is_empty()); - assert!(resolved.iter().any(|p| p.name == "monolog/monolog")); - - let lock = generate_lock_file(&LockFileGenerationRequest { - resolved_packages: resolved, - composer_json_content: composer_json_content.to_string(), - composer_json, - include_dev: false, - repositories: std::sync::Arc::new( - mozart_core::repository::repository::RepositorySet::with_packagist( - mozart_core::repository::cache::Cache::new( - std::env::temp_dir().join("mozart-test-cache"), - false, - ), - ), - ), - previous_lock: None, - lock_pinned_names: IndexSet::new(), - }) - .await - .expect("Lock file generation should succeed"); - - assert!(!lock.content_hash.is_empty()); - assert!(!lock.packages.is_empty()); - assert!(lock.packages.iter().any(|p| p.name == "monolog/monolog")); - } -} diff --git a/crates/mozart/src/commands/validate.rs b/crates/mozart/src/commands/validate.rs index 23925c5..f6275d9 100644 --- a/crates/mozart/src/commands/validate.rs +++ b/crates/mozart/src/commands/validate.rs @@ -487,200 +487,3 @@ fn compute_exit_code( 0 } - -#[cfg(test)] -mod tests { - use super::*; - - fn make_args() -> ValidateArgs { - ValidateArgs { - file: None, - no_check_all: false, - check_lock: false, - no_check_lock: false, - no_check_publish: false, - no_check_version: false, - with_dependencies: false, - strict: false, - } - } - - #[test] - fn test_compute_exit_code_no_issues() { - let result = ValidationResult::new(); - assert_eq!(compute_exit_code(&result, &[], true, true, false), 0); - } - - #[test] - fn test_compute_exit_code_errors() { - let mut result = ValidationResult::new(); - result.errors.push("some error".to_string()); - assert_eq!(compute_exit_code(&result, &[], true, true, false), 2); - } - - #[test] - fn test_compute_exit_code_publish_errors_counted() { - let mut result = ValidationResult::new(); - result.publish_errors.push("publish error".to_string()); - assert_eq!(compute_exit_code(&result, &[], true, true, false), 2); - } - - #[test] - fn test_compute_exit_code_publish_errors_not_checked() { - let mut result = ValidationResult::new(); - result.publish_errors.push("publish error".to_string()); - // check_publish = false → publish errors don't count - assert_eq!(compute_exit_code(&result, &[], false, true, false), 0); - } - - #[test] - fn test_compute_exit_code_lock_errors_counted() { - let result = ValidationResult::new(); - let lock_errors = vec!["lock stale".to_string()]; - assert_eq!( - compute_exit_code(&result, &lock_errors, true, true, false), - 2 - ); - } - - #[test] - fn test_compute_exit_code_lock_errors_not_checked() { - let result = ValidationResult::new(); - let lock_errors = vec!["lock stale".to_string()]; - // check_lock = false → lock errors become warnings, not counted unless strict - assert_eq!( - compute_exit_code(&result, &lock_errors, true, false, false), - 0 - ); - } - - #[test] - fn test_compute_exit_code_strict_warnings() { - let mut result = ValidationResult::new(); - result.warnings.push("some warning".to_string()); - assert_eq!(compute_exit_code(&result, &[], true, true, true), 1); - } - - #[test] - fn test_compute_exit_code_warnings_not_strict() { - let mut result = ValidationResult::new(); - result.warnings.push("some warning".to_string()); - assert_eq!(compute_exit_code(&result, &[], true, true, false), 0); - } - - #[test] - fn test_check_lock_freshness_no_lock_file() { - use tempfile::tempdir; - let dir = tempdir().unwrap(); - let composer_json_path = dir.path().join("composer.json"); - let content = r#"{"name": "vendor/pkg", "require": {}}"#; - std::fs::write(&composer_json_path, content).unwrap(); - - let mut lock_errors: Vec<String> = Vec::new(); - check_lock_freshness(content, &composer_json_path, None, &mut lock_errors); - // No lock file → no errors - assert!(lock_errors.is_empty()); - } - - #[test] - fn test_check_lock_freshness_fresh_lock() { - use mozart_core::repository::lockfile::LockFile; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_json_path = dir.path().join("composer.json"); - let content = r#"{"name": "vendor/pkg", "require": {"php": ">=8.1"}}"#; - std::fs::write(&composer_json_path, content).unwrap(); - - let hash = LockFile::compute_content_hash(content).unwrap(); - let lock = LockFile { - readme: LockFile::default_readme(), - content_hash: hash, - packages: vec![], - packages_dev: Some(vec![]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: None, - }; - let lock_path = dir.path().join("composer.lock"); - lock.write_to_file(&lock_path).unwrap(); - - let mut lock_errors: Vec<String> = Vec::new(); - check_lock_freshness(content, &composer_json_path, None, &mut lock_errors); - assert!( - lock_errors.is_empty(), - "fresh lock should produce no errors" - ); - } - - #[test] - fn test_check_lock_freshness_stale_lock() { - use mozart_core::repository::lockfile::LockFile; - use tempfile::tempdir; - - let dir = tempdir().unwrap(); - let composer_json_path = dir.path().join("composer.json"); - let original_content = r#"{"name": "vendor/pkg", "require": {"php": ">=8.1"}}"#; - let modified_content = r#"{"name": "vendor/pkg", "require": {"php": ">=8.2"}}"#; - - // Write original content - std::fs::write(&composer_json_path, original_content).unwrap(); - - // Create lock file based on original content - let hash = LockFile::compute_content_hash(original_content).unwrap(); - let lock = LockFile { - readme: LockFile::default_readme(), - content_hash: hash, - packages: vec![], - packages_dev: Some(vec![]), - aliases: vec![], - minimum_stability: "stable".to_string(), - stability_flags: serde_json::json!({}), - prefer_stable: false, - prefer_lowest: false, - platform: serde_json::json!({}), - platform_dev: serde_json::json!({}), - plugin_api_version: None, - }; - let lock_path = dir.path().join("composer.lock"); - lock.write_to_file(&lock_path).unwrap(); - - // Now check against modified content (lock is stale) - let mut lock_errors: Vec<String> = Vec::new(); - check_lock_freshness( - modified_content, - &composer_json_path, - None, - &mut lock_errors, - ); - assert!( - !lock_errors.is_empty(), - "stale lock should produce a lock error" - ); - assert!(lock_errors[0].contains("not up to date")); - } - - #[test] - fn test_should_check_lock_config_false_disables() { - let args = make_args(); - assert!(!should_check_lock(&args, false)); - } - - #[test] - fn test_should_check_lock_config_false_overridden_by_flag() { - let mut args = make_args(); - args.check_lock = true; - assert!(should_check_lock(&args, false)); - } - - #[test] - fn test_should_check_lock_defaults_to_true() { - let args = make_args(); - assert!(should_check_lock(&args, true)); - } -} |
