From fe210391f59d5fe19d5c3d56d369ab7918117de1 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Fri, 1 May 2026 20:42:56 +0900 Subject: feat(registry): accept v1 (bare array) installed.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Composer's FilesystemRepository::initialize branches on isset($data['packages']) — object form is v2, bare array is v1 — and treats dev-package-names/dev as optional. Mirror that in InstalledPackages::read so Mozart consumes shared .test fixtures (which use v1) without harness preprocessing, and so installs over v1-era vendor directories keep working. Drop the v1→v2 wrapper that was added to mozart-test-harness for the same reason. Removes #[ignore] from update_to_empty_from_locked (2/187 green). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart-registry/src/installed.rs | 107 ++++++++++++++++++++++++++++++- crates/mozart-test-harness/src/runner.rs | 5 ++ crates/mozart/tests/installer.rs | 5 +- 3 files changed, 111 insertions(+), 6 deletions(-) (limited to 'crates') diff --git a/crates/mozart-registry/src/installed.rs b/crates/mozart-registry/src/installed.rs index 7543b0e..6e8e0ac 100644 --- a/crates/mozart-registry/src/installed.rs +++ b/crates/mozart-registry/src/installed.rs @@ -70,14 +70,67 @@ impl InstalledPackages { /// Read installed.json from `vendor/composer/installed.json`. /// If the file does not exist, returns an empty registry. + /// + /// Accepts both Composer formats, mirroring `FilesystemRepository::initialize`: + /// - **v2** — object with a `packages` array, plus optional `dev-package-names`/`dev` + /// (the shape Composer 2.x writes). + /// - **v1** — bare array of package entries (older shape; still legal input). pub fn read(vendor_dir: &Path) -> anyhow::Result { let path = vendor_dir.join("composer/installed.json"); if !path.exists() { return Ok(InstalledPackages::new()); } let content = fs::read_to_string(&path)?; - let installed: InstalledPackages = serde_json::from_str(&content)?; - Ok(installed) + Self::from_json_str(&content) + } + + /// Parse an installed.json document. See [`Self::read`] for the accepted shapes. + pub fn from_json_str(content: &str) -> anyhow::Result { + use anyhow::{Context, anyhow}; + + let value: serde_json::Value = + serde_json::from_str(content).context("invalid installed.json")?; + + match value { + serde_json::Value::Object(mut obj) => { + let packages_value = obj.remove("packages").ok_or_else(|| { + anyhow!("Could not parse package list from installed.json (missing `packages`)") + })?; + let packages: Vec = + serde_json::from_value(packages_value) + .context("invalid `packages` array in installed.json")?; + + let dev_package_names: Vec = match obj.remove("dev-package-names") { + Some(v) => serde_json::from_value(v) + .context("invalid `dev-package-names` in installed.json")?, + None => Vec::new(), + }; + let dev: bool = match obj.remove("dev") { + Some(v) => { + serde_json::from_value(v).context("invalid `dev` flag in installed.json")? + } + None => true, + }; + + Ok(InstalledPackages { + packages, + dev_package_names, + dev, + }) + } + serde_json::Value::Array(_) => { + let packages: Vec = serde_json::from_value(value) + .context("invalid v1 installed.json package array")?; + Ok(InstalledPackages { + packages, + dev_package_names: Vec::new(), + dev: true, + }) + } + _ => Err(anyhow!( + "Could not parse package list from installed.json (expected object or array)" + )), + } } /// Write installed.json to `vendor/composer/installed.json`. @@ -205,6 +258,56 @@ mod tests { assert!(installed.dev_package_names.is_empty()); } + #[test] + fn test_reads_v2_object_form() { + let json = r#"{ + "packages": [ + {"name": "a/a", "version": "1.0.0"} + ], + "dev-package-names": ["a/a"], + "dev": false + }"#; + let installed = InstalledPackages::from_json_str(json).unwrap(); + assert_eq!(installed.packages.len(), 1); + assert_eq!(installed.packages[0].name, "a/a"); + assert_eq!(installed.dev_package_names, vec!["a/a".to_string()]); + assert!(!installed.dev); + } + + #[test] + fn test_reads_v1_array_form() { + // Composer 1.x / fixture-style: bare array of packages. + // FilesystemRepository::initialize accepts this; so must Mozart. + let json = r#"[ + {"name": "a/a", "version": "1.0.0"}, + {"name": "b/b", "version": "2.0.0"} + ]"#; + let installed = InstalledPackages::from_json_str(json).unwrap(); + assert_eq!(installed.packages.len(), 2); + assert_eq!(installed.packages[0].name, "a/a"); + assert_eq!(installed.packages[1].name, "b/b"); + assert!(installed.dev_package_names.is_empty()); + assert!(installed.dev); + } + + #[test] + fn test_v2_defaults_when_optional_fields_missing() { + let json = r#"{"packages": []}"#; + let installed = InstalledPackages::from_json_str(json).unwrap(); + assert!(installed.packages.is_empty()); + assert!(installed.dev_package_names.is_empty()); + assert!(installed.dev); + } + + #[test] + fn test_rejects_non_object_non_array() { + let err = InstalledPackages::from_json_str("\"oops\"").unwrap_err(); + assert!( + err.to_string().contains("expected object or array"), + "{err}" + ); + } + #[test] fn test_is_installed_case_insensitive() { let mut installed = InstalledPackages::new(); diff --git a/crates/mozart-test-harness/src/runner.rs b/crates/mozart-test-harness/src/runner.rs index e041cd7..cefd50f 100644 --- a/crates/mozart-test-harness/src/runner.rs +++ b/crates/mozart-test-harness/src/runner.rs @@ -20,6 +20,11 @@ pub struct RunResult { /// Set up a temp project from the parsed test, invoke `mozart` with the /// `--RUN--` command, and capture the result. +/// +/// `--INSTALLED--` is written verbatim to `vendor/composer/installed.json`. +/// Composer's fixtures use the v1 plain-array shape (`[{...}]`), which +/// `FilesystemRepository::initialize` accepts alongside the v2 object shape; +/// Mozart's reader is expected to do the same. Do not pre-wrap here. pub fn run_test(test: &ParsedTest, mozart_bin: &Path) -> Result { let working_dir = TempDir::new().context("failed to create tempdir")?; let root = working_dir.path(); diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index a0e3f25..0520108 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -766,10 +766,7 @@ installer_fixture!( ignore = "mozart binary cannot yet run this fixture" ); installer_fixture!(update_to_empty_from_blank); -installer_fixture!( - update_to_empty_from_locked, - ignore = "mozart binary cannot yet run this fixture" -); +installer_fixture!(update_to_empty_from_locked); installer_fixture!( update_with_all_dependencies, ignore = "mozart binary cannot yet run this fixture" -- cgit v1.3.1