diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-php-serialize/Cargo.toml | 4 | ||||
| -rw-r--r-- | crates/mozart-php-serialize/src/lib.rs | 172 | ||||
| -rw-r--r-- | crates/mozart-registry/Cargo.toml | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/lib.rs | 1 | ||||
| -rw-r--r-- | crates/mozart-registry/src/path_repository.rs | 243 | ||||
| -rw-r--r-- | crates/mozart-registry/src/resolver.rs | 7 | ||||
| -rw-r--r-- | crates/mozart/src/commands/install.rs | 26 | ||||
| -rw-r--r-- | crates/mozart/src/commands/update.rs | 49 | ||||
| -rw-r--r-- | crates/mozart/tests/installer.rs | 38 |
9 files changed, 527 insertions, 14 deletions
diff --git a/crates/mozart-php-serialize/Cargo.toml b/crates/mozart-php-serialize/Cargo.toml new file mode 100644 index 0000000..e66a5b3 --- /dev/null +++ b/crates/mozart-php-serialize/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "mozart-php-serialize" +version.workspace = true +edition.workspace = true diff --git a/crates/mozart-php-serialize/src/lib.rs b/crates/mozart-php-serialize/src/lib.rs new file mode 100644 index 0000000..6405ea8 --- /dev/null +++ b/crates/mozart-php-serialize/src/lib.rs @@ -0,0 +1,172 @@ +//! Byte-compatible port of PHP's `serialize()` function. +//! +//! Mirrors `php_var_serialize` in PHP's source: each value is rendered to a +//! tagged form like `b:1;`, `i:42;`, `s:3:"foo";`, `a:N:{...}` so the output +//! can be SHA-1'd and compared against PHP-side hashes (e.g. Composer's +//! `PathRepository` reference, which is `sha1($json . serialize($options))`). +//! +//! Only the value forms Mozart needs today are implemented. Floats, objects, +//! and references are deliberately omitted — extend the [`Value`] enum and +//! [`serialize`] writer when a new shape is required, and add a focused test +//! for it (the file_get_contents → hash flow downstream is unforgiving). +//! +//! Lengths are byte counts, not character counts. Array keys are written in +//! insertion order (PHP arrays preserve insertion order). Integer-coercible +//! string keys (e.g. `"1"`) are NOT auto-converted to integers — PHP itself +//! does that during array construction, not at serialization time, so callers +//! that care must construct [`Value::Int`] keys directly. + +use std::fmt::Write; + +/// One PHP value, suitable for `serialize()`. +/// +/// Add variants here as the need arises (e.g. `Float(f64)` → `d:<repr>;`). +/// Keep the variants minimal — every variant we add is a new compatibility +/// surface that has to match PHP byte-for-byte. +#[derive(Debug, Clone)] +pub enum Value { + Null, + Bool(bool), + Int(i64), + /// UTF-8 string. Length prefix is the byte length, matching PHP. + String(String), + /// Associative or indexed array. Order is preserved verbatim — the writer + /// does not normalize integer-coercible keys or sort entries. + Array(Vec<(Value, Value)>), +} + +/// Render `value` as PHP's `serialize()` would. +/// +/// Returns a `String` (not bytes) because every byte we emit is in +/// printable-ASCII or comes from a UTF-8 [`Value::String`] payload, so the +/// result is always valid UTF-8. +pub fn serialize(value: &Value) -> String { + let mut out = String::new(); + write_value(&mut out, value); + out +} + +fn write_value(out: &mut String, value: &Value) { + match value { + Value::Null => out.push_str("N;"), + Value::Bool(b) => { + out.push_str("b:"); + out.push(if *b { '1' } else { '0' }); + out.push(';'); + } + Value::Int(n) => { + write!(out, "i:{};", n).expect("writing to String never fails"); + } + Value::String(s) => { + write!(out, "s:{}:\"{}\";", s.len(), s).expect("writing to String never fails"); + } + Value::Array(entries) => { + write!(out, "a:{}:{{", entries.len()).expect("writing to String never fails"); + for (k, v) in entries { + write_value(out, k); + write_value(out, v); + } + out.push('}'); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Each `expected` string was produced by running the equivalent PHP + // `serialize()` call (`php -r 'echo serialize(...);'`), so the assertions + // pin Mozart's output to actual PHP behaviour rather than the spec we + // think we're following. + + #[test] + fn null() { + assert_eq!(serialize(&Value::Null), "N;"); + } + + #[test] + fn bool_true() { + assert_eq!(serialize(&Value::Bool(true)), "b:1;"); + } + + #[test] + fn bool_false() { + assert_eq!(serialize(&Value::Bool(false)), "b:0;"); + } + + #[test] + fn int_positive() { + assert_eq!(serialize(&Value::Int(42)), "i:42;"); + } + + #[test] + fn int_zero() { + assert_eq!(serialize(&Value::Int(0)), "i:0;"); + } + + #[test] + fn int_negative() { + assert_eq!(serialize(&Value::Int(-7)), "i:-7;"); + } + + #[test] + fn string_ascii() { + assert_eq!(serialize(&Value::String("hi".into())), "s:2:\"hi\";"); + } + + #[test] + fn string_empty() { + assert_eq!(serialize(&Value::String(String::new())), "s:0:\"\";"); + } + + #[test] + fn string_length_is_bytes_not_chars() { + // 「日本」 is 6 bytes in UTF-8 (3 per kanji), 2 chars. PHP measures + // by byte; mirror that. + assert_eq!(serialize(&Value::String("日本".into())), "s:6:\"日本\";"); + } + + #[test] + fn array_empty() { + assert_eq!(serialize(&Value::Array(vec![])), "a:0:{}"); + } + + #[test] + fn array_assoc_single() { + let v = Value::Array(vec![(Value::String("relative".into()), Value::Bool(true))]); + assert_eq!(serialize(&v), "a:1:{s:8:\"relative\";b:1;}"); + } + + #[test] + fn array_assoc_multi_preserves_order() { + let v = Value::Array(vec![ + (Value::String("a".into()), Value::Int(1)), + (Value::String("b".into()), Value::Int(2)), + ]); + assert_eq!(serialize(&v), "a:2:{s:1:\"a\";i:1;s:1:\"b\";i:2;}"); + } + + #[test] + fn array_indexed() { + // PHP `serialize([10, 20])` uses integer keys 0, 1. + let v = Value::Array(vec![ + (Value::Int(0), Value::Int(10)), + (Value::Int(1), Value::Int(20)), + ]); + assert_eq!(serialize(&v), "a:2:{i:0;i:10;i:1;i:20;}"); + } + + #[test] + fn array_nested() { + // PHP: serialize(['outer' => ['inner' => true]]) + let v = Value::Array(vec![( + Value::String("outer".into()), + Value::Array(vec![(Value::String("inner".into()), Value::Bool(true))]), + )]); + assert_eq!( + serialize(&v), + "a:1:{s:5:\"outer\";a:1:{s:5:\"inner\";b:1;}}" + ); + } +} diff --git a/crates/mozart-registry/Cargo.toml b/crates/mozart-registry/Cargo.toml index 578a08c..10eaf3b 100644 --- a/crates/mozart-registry/Cargo.toml +++ b/crates/mozart-registry/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] mozart-core.workspace = true mozart-metadata-minifier.workspace = true +mozart-php-serialize.workspace = true mozart-sat-resolver.workspace = true mozart-semver.workspace = true mozart-vcs.workspace = true diff --git a/crates/mozart-registry/src/lib.rs b/crates/mozart-registry/src/lib.rs index 654252c..73b5b76 100644 --- a/crates/mozart-registry/src/lib.rs +++ b/crates/mozart-registry/src/lib.rs @@ -6,6 +6,7 @@ pub mod installed; pub mod installer_executor; pub mod lockfile; pub mod packagist; +pub mod path_repository; pub mod repository; pub mod repository_filter; pub mod resolver; diff --git a/crates/mozart-registry/src/path_repository.rs b/crates/mozart-registry/src/path_repository.rs new file mode 100644 index 0000000..bf71315 --- /dev/null +++ b/crates/mozart-registry/src/path_repository.rs @@ -0,0 +1,243 @@ +//! Support for `type: path` repositories. +//! +//! Mirrors `Composer\Repository\PathRepository`: a path repo points at a +//! local directory containing a `composer.json`, and the resolver loads the +//! package from that file directly. Mozart does not yet support glob URLs or +//! the `versions` / `reference: none` options — only the bare +//! `{ type: path, url: ... }` form the installer fixtures exercise. +//! +//! Resolution model: a path repo is expanded into a synthetic +//! `type: package` [`RawRepository`] whose payload is the loaded composer.json +//! plus a `dist` block. After this expansion the rest of the registry treats +//! the package the same as any inline `type: package` entry — that is the +//! whole point of doing the work here rather than threading a new repo type +//! through the resolver / lockfile. +//! +//! `dist.reference` matches Composer's `hash('sha1', $json . serialize($options))` +//! where `$options` carries the auto-detected `relative` flag (true when the +//! original URL was not absolute). The same SHA-1 ends up in the lockfile, so +//! consumers comparing references against Composer-produced lockfiles see +//! byte-identical values. + +use std::path::{Path, PathBuf}; + +use mozart_core::package::RawRepository; +use mozart_php_serialize::{Value as PhpValue, serialize as php_serialize}; +use sha1::{Digest, Sha1}; + +/// Translate path repos in `repositories` into synthetic `type: package` +/// entries. Non-path entries are returned unchanged in original order. +/// +/// `base_dir` is the directory used to resolve relative `url` values +/// (Composer's PHP code resolves these against the process cwd; in production +/// that equals the project root, in tests it equals the fixtures anchor). +/// +/// Failures (missing directory, unreadable composer.json, missing +/// `name`/`version`) drop the offending entry silently — the rest of the +/// repository list still applies. This mirrors Composer's lenient +/// PathRepository, which logs a warning and moves on rather than aborting the +/// whole resolve. +pub fn expand_path_repositories( + repositories: &[RawRepository], + base_dir: &Path, +) -> Vec<RawRepository> { + let mut out = Vec::with_capacity(repositories.len()); + for repo in repositories { + if repo.repo_type != "path" { + out.push(repo.clone()); + continue; + } + let Some(url) = repo.url.as_deref() else { + continue; + }; + let Some(synthetic) = load_path_package(url, base_dir) else { + continue; + }; + out.push(synthetic); + } + out +} + +/// Read one path repo's `composer.json` and synthesize the inline-package +/// form. Returns `None` for any I/O or parse failure (Composer behaves the +/// same — `PathRepository::initialize` skips entries whose `composer.json` +/// is missing). +fn load_path_package(url: &str, base_dir: &Path) -> Option<RawRepository> { + let resolved = resolve_path(url, base_dir); + let composer_json_path = resolved.join("composer.json"); + let json = std::fs::read_to_string(&composer_json_path).ok()?; + let mut package: serde_json::Value = serde_json::from_str(&json).ok()?; + let obj = package.as_object_mut()?; + + // `version` is mandatory in the inline-package representation: without it + // the resolver would skip the package. Composer's PathRepository falls + // back to `dev-main` when no version is declared and no VCS is present; + // mirror that so a path repo whose composer.json omits `version` still + // produces a usable entry. + if !obj.contains_key("version") { + obj.insert( + "version".to_string(), + serde_json::Value::String("dev-main".to_string()), + ); + } + + let is_relative = !Path::new(url).is_absolute(); + let reference = compute_path_reference(json.as_bytes(), is_relative); + + obj.insert( + "dist".to_string(), + serde_json::json!({ + "type": "path", + "url": url, + "reference": reference, + }), + ); + // Composer copies `symlink`/`relative` from `options` into + // `transport-options`. We have no `options` to forward today but emit an + // empty object so consumers reading the package see the same shape. + obj.entry("transport-options") + .or_insert_with(|| serde_json::json!({})); + + Some(RawRepository { + repo_type: "package".to_string(), + url: None, + package: Some(serde_json::Value::Array(vec![package])), + only: None, + exclude: None, + canonical: None, + security_advisories: None, + }) +} + +fn resolve_path(url: &str, base_dir: &Path) -> PathBuf { + let p = Path::new(url); + if p.is_absolute() { + p.to_path_buf() + } else { + base_dir.join(p) + } +} + +/// Compose the SHA-1 reference Composer uses for path repos: +/// `sha1($json . serialize(['relative' => $isRelative]))`. The `relative` +/// flag is the only option Composer's auto-detection populates when the user +/// supplied no `options` block. +fn compute_path_reference(json_bytes: &[u8], is_relative: bool) -> String { + let options = PhpValue::Array(vec![( + PhpValue::String("relative".to_string()), + PhpValue::Bool(is_relative), + )]); + let serialized = php_serialize(&options); + let mut hasher = Sha1::new(); + hasher.update(json_bytes); + hasher.update(serialized.as_bytes()); + let bytes = hasher.finalize(); + let mut hex = String::with_capacity(bytes.len() * 2); + for b in bytes { + use std::fmt::Write; + let _ = write!(&mut hex, "{:02x}", b); + } + hex +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn computes_known_reference_for_plugin_a_fixture() { + // Fixture used by partial-update-loads-root-aliases-for-path-repos.test. + // Expected reference (`b133081...`) is what PHP's + // `hash('sha1', file_get_contents($composerJson) . serialize(['relative' => true]))` + // produces for this file — pin it here so reference computation + // changes can't drift silently from Composer. + let composer_json_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../composer/tests/Composer/Test/Fixtures/functional/installed-versions/plugin-a/composer.json"); + let bytes = std::fs::read(&composer_json_path).expect("fixture composer.json must exist"); + let reference = compute_path_reference(&bytes, true); + assert!( + reference.starts_with("b133081"), + "unexpected reference: {reference}" + ); + } + + #[test] + fn relative_url_resolves_against_base_dir_and_emits_synthetic_package_repo() { + let temp = tempfile::tempdir().unwrap(); + std::fs::create_dir_all(temp.path().join("pkg-dir")).unwrap(); + std::fs::write( + temp.path().join("pkg-dir").join("composer.json"), + r#"{"name": "vendor/pkg", "version": "1.2.3"}"#, + ) + .unwrap(); + + let input = vec![RawRepository { + repo_type: "path".to_string(), + url: Some("pkg-dir".to_string()), + package: None, + only: None, + exclude: None, + canonical: None, + security_advisories: None, + }]; + let expanded = expand_path_repositories(&input, temp.path()); + assert_eq!(expanded.len(), 1); + assert_eq!(expanded[0].repo_type, "package"); + + let pkgs = expanded[0] + .package + .as_ref() + .expect("expanded entry must carry a package payload") + .as_array() + .expect("payload should be an array"); + assert_eq!(pkgs.len(), 1); + let pkg = &pkgs[0]; + assert_eq!(pkg["name"], "vendor/pkg"); + assert_eq!(pkg["version"], "1.2.3"); + assert_eq!(pkg["dist"]["type"], "path"); + assert_eq!(pkg["dist"]["url"], "pkg-dir"); + assert!( + pkg["dist"]["reference"] + .as_str() + .map(|s| s.len() == 40) + .unwrap_or(false), + "reference should be a 40-char SHA-1" + ); + } + + #[test] + fn missing_composer_json_drops_the_entry() { + let temp = tempfile::tempdir().unwrap(); + let input = vec![RawRepository { + repo_type: "path".to_string(), + url: Some("does-not-exist".to_string()), + package: None, + only: None, + exclude: None, + canonical: None, + security_advisories: None, + }]; + let expanded = expand_path_repositories(&input, temp.path()); + assert!(expanded.is_empty()); + } + + #[test] + fn non_path_repos_pass_through_unchanged() { + let input = vec![RawRepository { + repo_type: "vcs".to_string(), + url: Some("https://example.com/repo.git".to_string()), + package: None, + only: None, + exclude: None, + canonical: None, + security_advisories: None, + }]; + let expanded = expand_path_repositories(&input, Path::new("/tmp")); + assert_eq!(expanded.len(), 1); + assert_eq!(expanded[0].repo_type, "vcs"); + assert_eq!( + expanded[0].url.as_deref(), + Some("https://example.com/repo.git") + ); + } +} diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs index b323764..d9fe900 100644 --- a/crates/mozart-registry/src/resolver.rs +++ b/crates/mozart-registry/src/resolver.rs @@ -265,7 +265,12 @@ fn normalize_root_alias_atom(atom: &str) -> Option<String> { if let Some(rest) = lower.strip_prefix("dev-") { return Some(format!("dev-{rest}")); } - parse_normalized(trimmed).map(|_| trimmed.to_string()) + // Stable numeric atoms (e.g. `1.1.1`) need to come back in the + // four-segment form `Version::Display` produces, so the alias + // matcher's `input.version != alias.version_normalized` check lines + // up with pool inputs (which carry the 4-segment normalized form). + // Returning the raw input here would silently never match. + parse_normalized(trimmed).map(|v| v.to_string()) } /// A root-level alias declared via the `require: "X as Y"` shorthand on the diff --git a/crates/mozart/src/commands/install.rs b/crates/mozart/src/commands/install.rs index ba9bd8a..a5698ff 100644 --- a/crates/mozart/src/commands/install.rs +++ b/crates/mozart/src/commands/install.rs @@ -1325,7 +1325,15 @@ pub async fn execute( )); let mut executor = FilesystemExecutor::new(mozart_registry::cache::Cache::files(&cache_config)); let working_dir = resolve_working_dir(cli); - run(&working_dir, args, console, repositories, &mut executor).await + run( + &working_dir, + None, + args, + console, + repositories, + &mut executor, + ) + .await } /// Library entry point — pure logic, no `Cli` access. @@ -1334,8 +1342,13 @@ pub async fn execute( /// `'packagist' => false` test config) and a tracing `InstallerExecutor`, /// then call this function directly to exercise the install flow without /// spawning the binary. +/// +/// `path_repo_base_override` is the in-process test escape hatch for relative +/// `type: path` repo URLs — see [`super::update::run`] for the full rationale. +/// Production callers pass `None` to anchor against `working_dir`. pub async fn run( working_dir: &Path, + path_repo_base_override: Option<&Path>, args: &InstallArgs, console: &mozart_core::console::Console, repositories: std::sync::Arc<mozart_registry::repository::RepositorySet>, @@ -1420,8 +1433,15 @@ pub async fn run( }; // Forward the caller's repositories + executor so in-process tests // see their mocks honored across the install→update fallback edge. - return super::update::run(working_dir, &update_args, console, repositories, executor) - .await; + return super::update::run( + working_dir, + path_repo_base_override, + &update_args, + console, + repositories, + executor, + ) + .await; } let lock = lockfile::LockFile::read_from_file(&lock_path)?; diff --git a/crates/mozart/src/commands/update.rs b/crates/mozart/src/commands/update.rs index f065c2a..ac5a685 100644 --- a/crates/mozart/src/commands/update.rs +++ b/crates/mozart/src/commands/update.rs @@ -395,8 +395,15 @@ pub fn apply_partial_update( let name_lower = pkg.name.to_lowercase(); // If this package is NOT in the update set and we have an old locked version, // swap it back to the old version to prevent unintended changes. + // + // Exception: path-repo packages always reload from disk (Composer's + // PoolBuilder treats them as canonical sources, not lock-bound), so + // the resolver-picked version must survive the partial-update + // swap-back. The earlier locked-set construction already excludes + // them from `locked_packages` for the same reason; mirror it here. if !update_set.contains(&name_lower) && let Some(old_pkg) = old_pkg_map.get(&name_lower) + && old_pkg.dist.as_ref().map(|d| d.dist_type.as_str()) != Some("path") { pkg.version = old_pkg.version.clone(); pkg.version_normalized = locked_version_normalized(old_pkg); @@ -964,7 +971,15 @@ pub async fn execute( mozart_registry::cache::Cache::files(&cache_config), ); let working_dir = super::install::resolve_working_dir(cli); - run(&working_dir, args, console, repositories, &mut executor).await + run( + &working_dir, + None, + args, + console, + repositories, + &mut executor, + ) + .await } /// Library entry point — pure logic, no CLI / Cli access. @@ -973,8 +988,17 @@ pub async fn execute( /// (Composer's `'packagist' => false` test config) and a tracing /// `InstallerExecutor`, then call this function directly to exercise the /// update flow without spawning the binary. +/// +/// `path_repo_base_override` is for the in-process test harness only: +/// Composer's PHP test suite `chdir(__DIR__)` so that `type: path` repo URLs +/// like `Fixtures/.../pkg` resolve against the test directory, but the +/// Rust harness writes `composer.json` into a per-test tempdir, so we need a +/// way to anchor relative path-repo URLs somewhere other than `working_dir`. +/// Production callers pass `None` to use `working_dir`, matching Composer's +/// "resolve relative to cwd" behaviour. pub async fn run( working_dir: &std::path::Path, + path_repo_base_override: Option<&std::path::Path>, args: &UpdateArgs, console: &mozart_core::console::Console, repositories: std::sync::Arc<mozart_registry::repository::RepositorySet>, @@ -1012,6 +1036,21 @@ pub async fn run( composer_json.validate_root_does_not_self_require()?; let composer_json_content = std::fs::read_to_string(&composer_json_path)?; + // Expand `type: path` repos into synthetic `type: package` entries so the + // resolver and lockfile see them as ordinary inline packages. The + // original `composer_json.repositories` is preserved for writeback paths + // (e.g. `--bump-after-update` rewrites composer.json) — only the cloned + // `composer_json_expanded` carries the synthetic entries. + let path_repo_base = path_repo_base_override.unwrap_or(working_dir); + let composer_json_expanded = { + let mut clone = composer_json.clone(); + clone.repositories = mozart_registry::path_repository::expand_path_repositories( + &clone.repositories, + path_repo_base, + ); + clone + }; + let lock_path = working_dir.join("composer.lock"); let vendor_dir = working_dir.join("vendor"); @@ -1135,7 +1174,7 @@ pub async fn run( // (line 524: when a propagated package's `require` // points at a `skippedLoad` entry, the dep is unlocked // and re-loaded). - let repo_requires = collect_repo_requires(&composer_json.repositories); + let repo_requires = collect_repo_requires(&composer_json_expanded.repositories); let updated: IndexSet<String> = expand_packages( &raw_packages, Some(&l), @@ -1334,7 +1373,7 @@ pub async fn run( ignore_platform_req_list: args.ignore_platform_req.clone(), repositories: repositories.clone(), temporary_constraints, - raw_repositories: composer_json.repositories.clone(), + raw_repositories: composer_json_expanded.repositories.clone(), root_provide: composer_json .provide .iter() @@ -1428,7 +1467,7 @@ pub async fn run( } Some(lock) => { // 1. Expand wildcards - let repo_requires = collect_repo_requires(&composer_json.repositories); + let repo_requires = collect_repo_requires(&composer_json_expanded.repositories); let mut expanded = expand_packages( &effective_packages, Some(lock), @@ -1509,7 +1548,7 @@ pub async fn run( let mut new_lock = lockfile::generate_lock_file(&lockfile::LockFileGenerationRequest { resolved_packages: resolved, composer_json_content: composer_json_content.clone(), - composer_json: composer_json.clone(), + composer_json: composer_json_expanded.clone(), include_dev: true, repositories: repositories.clone(), previous_lock: old_lock.clone(), diff --git a/crates/mozart/tests/installer.rs b/crates/mozart/tests/installer.rs index 197b00f..d22f2a0 100644 --- a/crates/mozart/tests/installer.rs +++ b/crates/mozart/tests/installer.rs @@ -25,6 +25,17 @@ fn fixtures_dir() -> PathBuf { .join("../../composer/tests/Composer/Test/Fixtures/installer") } +/// Composer's PHPUnit `InstallerTest::setUp()` runs `chdir(__DIR__)` so that +/// relative `type: path` repo URLs (`Fixtures/functional/.../pkg`) resolve +/// against `composer/tests/Composer/Test/`. The Rust harness can't chdir +/// safely (cargo test runs cases in parallel), so it threads the same +/// directory through `install/update::run` as the path-repo resolution base +/// instead. Production callers of `run()` pass `None` and resolve against +/// `working_dir`, matching Composer's "use cwd" behaviour. +fn path_repo_base_for_fixtures() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../composer/tests/Composer/Test") +} + /// Rewrite `file://foobar` URLs in COMPOSER content to absolute fixture /// paths. Mirrors `composer/tests/Composer/Test/InstallerTest.php:540-542`: /// when a fixture's repository entry uses a relative `file://` URL, anchor @@ -101,12 +112,29 @@ async fn run_fixture_in_process(test: &ParsedTest) -> anyhow::Result<InProcessRu let repositories = Arc::new(RepositorySet::empty()); let mut executor = TraceRecorderExecutor::new(); + let path_repo_base = path_repo_base_for_fixtures(); let outcome: anyhow::Result<()> = match &cli.command { Some(Commands::Install(args)) => { - install::run(root, args, &console, repositories, &mut executor).await + install::run( + root, + Some(&path_repo_base), + args, + &console, + repositories, + &mut executor, + ) + .await } Some(Commands::Update(args)) => { - update::run(root, args, &console, repositories, &mut executor).await + update::run( + root, + Some(&path_repo_base), + args, + &console, + repositories, + &mut executor, + ) + .await } other => anyhow::bail!("unsupported run command in fixture: {:?}", other.is_some()), }; @@ -217,8 +245,8 @@ macro_rules! installer_fixture { installer_fixture!(abandoned_listed); installer_fixture!(alias_in_complex_constraints, ignore); -installer_fixture!(alias_in_lock, ignore); -installer_fixture!(alias_in_lock2, ignore); +installer_fixture!(alias_in_lock); +installer_fixture!(alias_in_lock2); installer_fixture!(alias_on_unloadable_package); installer_fixture!(alias_solver_problems); installer_fixture!(alias_solver_problems2); @@ -292,7 +320,7 @@ installer_fixture!(partial_update_from_lock_with_root_alias, ignore); installer_fixture!(partial_update_installs_from_lock_even_missing, ignore); installer_fixture!(partial_update_keeps_older_dep_if_still_required); installer_fixture!(partial_update_keeps_older_dep_if_still_required_with_provide); -installer_fixture!(partial_update_loads_root_aliases_for_path_repos, ignore); +installer_fixture!(partial_update_loads_root_aliases_for_path_repos); installer_fixture!(partial_update_security_advisory_matching_locked_dep); installer_fixture!(partial_update_security_advisory_matching_locked_dep_with_dependencies); installer_fixture!(partial_update_with_dependencies_provide); |
