aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart/tests
diff options
context:
space:
mode:
Diffstat (limited to 'crates/mozart/tests')
-rw-r--r--crates/mozart/tests/installer_in_process.rs162
1 files changed, 162 insertions, 0 deletions
diff --git a/crates/mozart/tests/installer_in_process.rs b/crates/mozart/tests/installer_in_process.rs
new file mode 100644
index 0000000..f3e8ce2
--- /dev/null
+++ b/crates/mozart/tests/installer_in_process.rs
@@ -0,0 +1,162 @@
+//! 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<String>,
+ /// Final `composer.lock` JSON, as written to disk by the runner.
+ final_lock: Option<String>,
+ /// Final `vendor/composer/installed.json`, as written to disk.
+ final_installed: Option<String>,
+ /// 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<InProcessRunResult> {
+ 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<String> = 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::<MozartError>()
+ .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");
+}