aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart')
-rw-r--r--crates/mozart/src/commands/audit.rs298
-rw-r--r--crates/mozart/src/commands/browse.rs92
-rw-r--r--crates/mozart/src/commands/bump.rs518
-rw-r--r--crates/mozart/src/commands/check_platform_reqs.rs319
-rw-r--r--crates/mozart/src/commands/config.rs1130
-rw-r--r--crates/mozart/src/commands/create_project.rs146
-rw-r--r--crates/mozart/src/commands/dependency.rs209
-rw-r--r--crates/mozart/src/commands/diagnose.rs101
-rw-r--r--crates/mozart/src/commands/exec.rs197
-rw-r--r--crates/mozart/src/commands/fund.rs150
-rw-r--r--crates/mozart/src/commands/global.rs95
-rw-r--r--crates/mozart/src/commands/init.rs49
-rw-r--r--crates/mozart/src/commands/install.rs511
-rw-r--r--crates/mozart/src/commands/licenses.rs293
-rw-r--r--crates/mozart/src/commands/remove.rs449
-rw-r--r--crates/mozart/src/commands/repository.rs886
-rw-r--r--crates/mozart/src/commands/require.rs327
-rw-r--r--crates/mozart/src/commands/run_script.rs496
-rw-r--r--crates/mozart/src/commands/search.rs204
-rw-r--r--crates/mozart/src/commands/self_update.rs245
-rw-r--r--crates/mozart/src/commands/show.rs292
-rw-r--r--crates/mozart/src/commands/suggests.rs266
-rw-r--r--crates/mozart/src/commands/update.rs664
-rw-r--r--crates/mozart/src/commands/validate.rs197
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));
- }
-}