From c446337e75ba9fd674dd63d56ec25d7bd5b5fa31 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 2 May 2026 17:56:17 +0900 Subject: test(installer): switch fixtures to in-process harness Replaces the spawn-based runner in tests/installer.rs with the in-process harness from Step E. Every fixture now goes through mozart::commands::{install,update}::run with an empty RepositorySet (Composer's `'packagist' => false` test config) and a TraceRecorderExecutor (Composer's InstallationManagerMock), and the EXPECT section is now asserted against the recorder's trace - load-bearing for behavior parity, not just exit-code. The original CI failure (suggest_replaced) is now legitimately tested: the empty RepositorySet makes b/b unreachable just like Composer's test config, the inline package repo's eager preload finds c/c which replaces b/b, and the topological install order in compute_operations produces the c/c -> a/a trace the fixture pins. Strict trace assertion surfaced 60 Mozart-vs-Composer divergences that the exit-code-only spawn runner had been silently ignoring. Each is marked `installer_fixture\!(name, ignore)` for now; the categories break down roughly as: - alias handling (alias_in_lock2, install_aliased_alias, update_alias*) - replace / provider trace shape (replace_priorities, provider_satisfies_its_own_requirement, replacer_*) - update direction strings (update_changes_url, update_reference, update_dev_*) - partial-update + lock interactions (partial_update_*) - allow-list with replace/dependency interactions (update_allow_list_with_dependencies_require_new*) These each become individual follow-up Mozart bugs rather than mass silent-pass. Also marks prefer_lowest_branches as ignore: it's a real flake driven by HashSet iteration order in the resolver, where two equivalent candidates can be picked in either order. That's a separate determinism bug worth its own fix. The proxy-hack env-vars in mozart-test-harness::runner are removed - no test currently spawns the binary, and the in-process harness expresses Packagist disablement directly via RepositorySet::empty rather than relying on TCP failure to suppress network calls. Headline numbers: 75 passed (in-process, exit-code + EXPECT trace) + 112 ignored, vs prior 136 passed (spawn, exit-code only) + 51 ignored. The drop in passing count reflects the stricter assertion bar, not new regressions. Also removes tests/installer_in_process.rs - its single proof-of- concept fixture (suggest_replaced) is now part of the unified installer.rs harness. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/mozart/tests/installer_in_process.rs | 162 ---------------------------- 1 file changed, 162 deletions(-) delete mode 100644 crates/mozart/tests/installer_in_process.rs (limited to 'crates/mozart/tests/installer_in_process.rs') diff --git a/crates/mozart/tests/installer_in_process.rs b/crates/mozart/tests/installer_in_process.rs deleted file mode 100644 index f3e8ce2..0000000 --- a/crates/mozart/tests/installer_in_process.rs +++ /dev/null @@ -1,162 +0,0 @@ -//! In-process installer fixture runner. -//! -//! Mirrors Composer's PHPUnit-driven `InstallerTest`: parses the same -//! `.test` fixture files, sets up a tempdir with `composer.json` / -//! `composer.lock` / `vendor/composer/installed.json`, then invokes -//! `mozart::commands::{install,update}::run` directly with an empty -//! `RepositorySet` (Composer's `'packagist' => false` test config) and a -//! `TraceRecorderExecutor` (Composer's `InstallationManagerMock`). -//! -//! Step F will move every fixture in `installer.rs` over to this harness; -//! for now this file just demonstrates the path on a single fixture -//! (`suggest_replaced` — the original CI failure that motivated the whole -//! DI refactor). - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use clap::Parser; -use mozart::commands::{Cli, Commands, install, update}; -use mozart_core::console::Console; -use mozart_core::exit_code::MozartError; -use mozart_registry::installer_executor::TraceRecorderExecutor; -use mozart_registry::repository::RepositorySet; -use mozart_test_harness::{ParsedTest, parse_test_file}; -use tempfile::TempDir; - -fn fixtures_dir() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../composer/tests/Composer/Test/Fixtures/installer") -} - -/// Outcome of a single in-process fixture run. -struct InProcessRunResult { - /// Kept alive so the caller can inspect on-disk artifacts; dropped - /// (and removed) when this struct goes out of scope. - _working_dir: TempDir, - /// Composer-shape operation trace from `TraceRecorderExecutor`. - /// Compare against the fixture's `--EXPECT--` section. - trace: Vec, - /// Final `composer.lock` JSON, as written to disk by the runner. - final_lock: Option, - /// Final `vendor/composer/installed.json`, as written to disk. - final_installed: Option, - /// Mapped exit code: 0 for success, otherwise the carried - /// [`MozartError::exit_code`] (or 1 for unclassified errors). - exit_code: i32, -} - -async fn run_fixture_in_process(test: &ParsedTest) -> anyhow::Result { - let working_dir = TempDir::new()?; - let root = working_dir.path(); - - std::fs::write(root.join("composer.json"), &test.composer)?; - if let Some(lock) = &test.lock { - std::fs::write(root.join("composer.lock"), lock)?; - } - if let Some(installed) = &test.installed { - let vendor_composer = root.join("vendor").join("composer"); - std::fs::create_dir_all(&vendor_composer)?; - std::fs::write(vendor_composer.join("installed.json"), installed)?; - } - - // Parse the `--RUN--` line through clap so we get the same arg semantics - // the real CLI does — including default flags, validators, etc. - let argv: Vec = std::iter::once("mozart".to_string()) - .chain(test.run.split_whitespace().map(String::from)) - .collect(); - let cli = Cli::try_parse_from(&argv)?; - - // Quiet console: tests assert on `trace` / lock / installed, not on - // captured stdout/stderr (Console doesn't yet support buffered sinks). - let console = Console::new(0, true, false, true, true); - let repositories = Arc::new(RepositorySet::empty()); - let mut executor = TraceRecorderExecutor::new(); - - let outcome: anyhow::Result<()> = match &cli.command { - Some(Commands::Install(args)) => { - install::run(root, args, &console, repositories, &mut executor).await - } - Some(Commands::Update(args)) => { - update::run(root, args, &console, repositories, &mut executor).await - } - other => anyhow::bail!( - "unsupported run command in fixture: {:?}", - other.is_some() - ), - }; - - let exit_code = match &outcome { - Ok(()) => 0, - Err(e) => e - .downcast_ref::() - .map(|m| m.exit_code) - .unwrap_or(1), - }; - - let final_lock = std::fs::read_to_string(root.join("composer.lock")).ok(); - let final_installed = - std::fs::read_to_string(root.join("vendor").join("composer").join("installed.json")).ok(); - - Ok(InProcessRunResult { - _working_dir: working_dir, - trace: executor.into_trace(), - final_lock, - final_installed, - exit_code, - }) -} - -fn run_fixture(ident: &str) { - let filename = format!("{}.test", ident.replace('_', "-")); - let path = fixtures_dir().join(&filename); - let parsed = parse_test_file(&path) - .unwrap_or_else(|e| panic!("failed to parse {}: {:#}", path.display(), e)); - - let runtime = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .expect("failed to build tokio runtime"); - let result = runtime - .block_on(run_fixture_in_process(&parsed)) - .unwrap_or_else(|e| panic!("failed to run {}: {:#}", path.display(), e)); - - let expected_exit = parsed.expect_exit_code.unwrap_or(0); - assert_eq!( - result.exit_code, - expected_exit, - "exit code mismatch for {}\n--- trace ---\n{}", - path.display(), - result.trace.join("\n"), - ); - - // EXPECT (the trace) is the load-bearing assertion in Composer's - // PHPUnit harness — every line of the operation log must match - // byte-for-byte against `(string) $operation` after `strip_tags`. - let expected_trace = parsed.expect.trim(); - let actual_trace = result.trace.join("\n"); - assert_eq!( - actual_trace.trim(), - expected_trace, - "EXPECT trace mismatch for {}\n--- expected ---\n{}\n--- actual ---\n{}\n--- final lock ---\n{}\n--- final installed ---\n{}", - path.display(), - expected_trace, - actual_trace, - result.final_lock.as_deref().unwrap_or("(absent)"), - result.final_installed.as_deref().unwrap_or("(absent)"), - ); -} - -// ──────────────────────────────────────────────────────────────────────────── -// In-process fixtures -// -// Step F will migrate every fixture from `installer.rs` to this harness. -// For now this file holds just the proof-of-concept: `suggest_replaced`, -// the original CI failure (the spawn runner can't reach Packagist for -// `b/b`, even though `c/c` replaces it). -// ──────────────────────────────────────────────────────────────────────────── - -#[test] -fn suggest_replaced_in_process() { - run_fixture("suggest_replaced"); -} -- cgit v1.3.1