diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/mozart-registry/Cargo.toml | 3 | ||||
| -rw-r--r-- | crates/mozart-registry/tests/poolbuilder.rs | 80 | ||||
| -rw-r--r-- | crates/mozart-test-harness/src/lib.rs | 4 | ||||
| -rw-r--r-- | crates/mozart-test-harness/src/parser.rs | 14 | ||||
| -rw-r--r-- | crates/mozart-test-harness/src/pool_builder_parser.rs | 178 |
5 files changed, 276 insertions, 3 deletions
diff --git a/crates/mozart-registry/Cargo.toml b/crates/mozart-registry/Cargo.toml index 10eaf3b..6816e8d 100644 --- a/crates/mozart-registry/Cargo.toml +++ b/crates/mozart-registry/Cargo.toml @@ -26,3 +26,6 @@ tempfile.workspace = true tokio.workspace = true tracing.workspace = true zip.workspace = true + +[dev-dependencies] +mozart-test-harness.workspace = true diff --git a/crates/mozart-registry/tests/poolbuilder.rs b/crates/mozart-registry/tests/poolbuilder.rs new file mode 100644 index 0000000..d8511e4 --- /dev/null +++ b/crates/mozart-registry/tests/poolbuilder.rs @@ -0,0 +1,80 @@ +//! Pool-builder fixture suite, ported from +//! `composer/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php`. +//! +//! Composer drives this suite through a `@dataProvider`; each `.test` file +//! becomes one parameterized case. Mirrored here as one `#[test]` per +//! fixture so the count surfaces in `cargo test` output and individual +//! cases can be re-enabled as the runner is fleshed out. +//! +//! Every test is currently `#[ignore]` because the runner is a stub: the +//! orchestration that takes a `RepositorySet` + `Request` and produces a +//! populated `Pool` lives inline in `mozart_registry::resolver::resolve`, +//! not as an extracted entry point. Wiring those up — alias handling, +//! stability flags, fixed/locked packages, the optimizer pass — is the +//! follow-up work this scaffolding exists to track. + +use std::path::{Path, PathBuf}; + +use mozart_test_harness::{ParsedPoolBuilderTest, parse_pool_builder_test_file}; + +fn fixtures_dir() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../composer/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder") +} + +fn run_poolbuilder_fixture(ident: &str) { + let filename = format!("{}.test", ident.replace('_', "-")); + let path = fixtures_dir().join(&filename); + let _parsed: ParsedPoolBuilderTest = parse_pool_builder_test_file(&path) + .unwrap_or_else(|e| panic!("failed to parse {}: {:#}", path.display(), e)); + + // Runner is intentionally not implemented yet — see module docs. + // Removing `#[ignore]` from a case will surface this `unimplemented!` + // and force the missing pool-builder entry point into existence. + unimplemented!( + "PoolBuilderTest runner not yet wired up; cannot execute {}", + path.display() + ); +} + +macro_rules! poolbuilder_fixture { + ($name:ident) => { + #[test] + #[ignore] + fn $name() { + run_poolbuilder_fixture(stringify!($name)); + } + }; +} + +poolbuilder_fixture!(alias_priority_conflicting); +poolbuilder_fixture!(alias_with_reference); +poolbuilder_fixture!(constraint_expansion_works_with_exact_versions); +poolbuilder_fixture!(filter_impossible_packages); +poolbuilder_fixture!(filter_impossible_packages_locked_replacer); +poolbuilder_fixture!(filter_impossible_packages_only_required); +poolbuilder_fixture!(filter_impossible_packages_only_required_provides); +poolbuilder_fixture!(filter_impossible_packages_only_required_replaces); +poolbuilder_fixture!(filter_impossible_packages_provides); +poolbuilder_fixture!(filter_impossible_packages_replaces); +poolbuilder_fixture!(fixed_packages_do_not_load_from_repos); +poolbuilder_fixture!(fixed_packages_replaced_do_not_load_from_repos); +poolbuilder_fixture!(load_replaced_package_if_replacer_dropped); +poolbuilder_fixture!(load_replaced_root_package_if_replacer_dropped); +poolbuilder_fixture!(multi_repo_replace); +poolbuilder_fixture!(multi_repo_replace_partial_update_all); +poolbuilder_fixture!(must_expand_root_reqs); +poolbuilder_fixture!(package_versions_are_not_loaded_if_not_required_expansion); +poolbuilder_fixture!(package_versions_are_not_loaded_if_not_required_recursive); +poolbuilder_fixture!(packages_that_do_not_exist); +poolbuilder_fixture!(partial_update); +poolbuilder_fixture!(partial_update_transitive_deps_no_root_unfix); +poolbuilder_fixture!(partial_update_transitive_deps_unfix); +poolbuilder_fixture!(partial_update_unfixes_path_repo_replacer_with_transitive_deps); +poolbuilder_fixture!(partial_update_unfixes_path_repos_always_but_not_their_transitive_deps); +poolbuilder_fixture!(partial_update_unfixing_locked_deps); +poolbuilder_fixture!(partial_update_unfixing_replacers); +poolbuilder_fixture!(partial_update_unfixing_with_replacers); +poolbuilder_fixture!(partial_update_unfixing_with_replacers_providers); +poolbuilder_fixture!(root_requirements_avoid_loading_further_versions); +poolbuilder_fixture!(stability_flags_take_over_minimum_stability_and_filter_packages); diff --git a/crates/mozart-test-harness/src/lib.rs b/crates/mozart-test-harness/src/lib.rs index f8fa2b3..ea125cc 100644 --- a/crates/mozart-test-harness/src/lib.rs +++ b/crates/mozart-test-harness/src/lib.rs @@ -6,7 +6,11 @@ //! runner; actual `.test` fixtures and tests live elsewhere. mod parser; +mod pool_builder_parser; mod runner; pub use parser::{ParsedTest, parse_test_file, parse_test_str}; +pub use pool_builder_parser::{ + ParsedPoolBuilderTest, parse_pool_builder_test_file, parse_pool_builder_test_str, +}; pub use runner::{RunResult, run_test}; diff --git a/crates/mozart-test-harness/src/parser.rs b/crates/mozart-test-harness/src/parser.rs index 827272b..ecefb37 100644 --- a/crates/mozart-test-harness/src/parser.rs +++ b/crates/mozart-test-harness/src/parser.rs @@ -45,7 +45,7 @@ pub fn parse_test_file(path: &Path) -> Result<ParsedTest> { } pub fn parse_test_str(content: &str) -> Result<ParsedTest> { - let mut sections = split_sections(content)?; + let mut sections = split_sections(content, VALID_SECTIONS)?; for required in REQUIRED_SECTIONS { if !sections.contains_key(*required) { @@ -86,7 +86,15 @@ pub fn parse_test_str(content: &str) -> Result<ParsedTest> { }) } -fn split_sections(content: &str) -> Result<IndexMap<String, String>> { +/// Split a `.test` fixture into its `--SECTION--` blocks. +/// +/// Shared helper for both [`parse_test_str`] and the sibling pool-builder +/// parser; each caller passes its own allowed-section list so unknown +/// headers still surface as parse errors rather than silently ignored. +pub(crate) fn split_sections( + content: &str, + valid_sections: &[&str], +) -> Result<IndexMap<String, String>> { let header_re = regex::Regex::new(r"^--([A-Z][A-Z-]*)--$").unwrap(); let mut sections: IndexMap<String, String> = IndexMap::new(); @@ -97,7 +105,7 @@ fn split_sections(content: &str) -> Result<IndexMap<String, String>> { let trimmed = line.trim_end_matches('\n').trim_end_matches('\r'); if let Some(caps) = header_re.captures(trimmed) { let name = caps[1].to_string(); - if !VALID_SECTIONS.contains(&name.as_str()) { + if !valid_sections.contains(&name.as_str()) { bail!("unknown section: --{name}--"); } if let Some(prev) = current_section.take() { diff --git a/crates/mozart-test-harness/src/pool_builder_parser.rs b/crates/mozart-test-harness/src/pool_builder_parser.rs new file mode 100644 index 0000000..11d2179 --- /dev/null +++ b/crates/mozart-test-harness/src/pool_builder_parser.rs @@ -0,0 +1,178 @@ +//! Parser for Composer's `PoolBuilderTest` `.test` fixture format. +//! +//! Mirrors `composer/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php::readTestFile`. +//! Section bodies are stored as raw strings (typically JSON); the runner is +//! responsible for interpreting them. + +use anyhow::{Context, Result, bail}; +use std::fs; +use std::path::Path; + +use crate::parser::split_sections; + +const VALID_SECTIONS: &[&str] = &[ + "TEST", + "ROOT", + "REQUEST", + "FIXED", + "PACKAGE-REPOS", + "EXPECT", + "EXPECT-OPTIMIZED", +]; + +const REQUIRED_SECTIONS: &[&str] = &["TEST", "REQUEST", "PACKAGE-REPOS", "EXPECT"]; + +#[derive(Debug, Clone)] +pub struct ParsedPoolBuilderTest { + pub test: String, + pub root: Option<String>, + pub request: String, + pub fixed: Option<String>, + pub package_repos: String, + pub expect: String, + pub expect_optimized: Option<String>, +} + +pub fn parse_pool_builder_test_file(path: &Path) -> Result<ParsedPoolBuilderTest> { + let content = + fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + parse_pool_builder_test_str(&content) + .with_context(|| format!("failed to parse {}", path.display())) +} + +pub fn parse_pool_builder_test_str(content: &str) -> Result<ParsedPoolBuilderTest> { + let mut sections = split_sections(content, VALID_SECTIONS)?; + + for required in REQUIRED_SECTIONS { + if !sections.contains_key(*required) { + bail!("missing required section: --{required}--"); + } + } + + let mut take = |key: &str| sections.shift_remove(key); + + let test = take("TEST").unwrap(); + let request = take("REQUEST").unwrap(); + let package_repos = take("PACKAGE-REPOS").unwrap(); + let expect = take("EXPECT").unwrap(); + + Ok(ParsedPoolBuilderTest { + test, + root: take("ROOT"), + request, + fixed: take("FIXED"), + package_repos, + expect, + expect_optimized: take("EXPECT-OPTIMIZED"), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_minimal_required_sections() { + let input = "\ +--TEST-- +A pool builder test +--REQUEST-- +{\"require\": {\"a/a\": \"*\"}} +--PACKAGE-REPOS-- +[[{\"name\": \"a/a\", \"version\": \"1.0.0\"}]] +--EXPECT-- +[\"a/a-1.0.0.0\"] +"; + let t = parse_pool_builder_test_str(input).unwrap(); + assert_eq!(t.test, "A pool builder test"); + assert_eq!(t.request, "{\"require\": {\"a/a\": \"*\"}}"); + assert_eq!( + t.package_repos, + "[[{\"name\": \"a/a\", \"version\": \"1.0.0\"}]]" + ); + assert_eq!(t.expect, "[\"a/a-1.0.0.0\"]"); + assert!(t.root.is_none()); + assert!(t.fixed.is_none()); + assert!(t.expect_optimized.is_none()); + } + + #[test] + fn parses_all_optional_sections() { + let input = "\ +--TEST-- +desc +--ROOT-- +{\"minimum-stability\": \"dev\"} +--REQUEST-- +{\"require\": {}} +--FIXED-- +[{\"name\": \"x/x\", \"version\": \"1.0.0\", \"id\": 1}] +--PACKAGE-REPOS-- +[] +--EXPECT-- +[1] +--EXPECT-OPTIMIZED-- +[1] +"; + let t = parse_pool_builder_test_str(input).unwrap(); + assert_eq!(t.test, "desc"); + assert_eq!(t.root.as_deref(), Some("{\"minimum-stability\": \"dev\"}")); + assert_eq!( + t.fixed.as_deref(), + Some("[{\"name\": \"x/x\", \"version\": \"1.0.0\", \"id\": 1}]") + ); + assert_eq!(t.expect_optimized.as_deref(), Some("[1]")); + } + + #[test] + fn rejects_unknown_section() { + let input = "\ +--TEST-- +x +--MYSTERY-- +y +--REQUEST-- +{} +--PACKAGE-REPOS-- +[] +--EXPECT-- +[] +"; + let err = parse_pool_builder_test_str(input).unwrap_err(); + assert!(err.to_string().contains("unknown section"), "{err}"); + } + + #[test] + fn rejects_missing_required_section() { + let input = "\ +--TEST-- +x +--REQUEST-- +{} +--EXPECT-- +[] +"; + let err = parse_pool_builder_test_str(input).unwrap_err(); + assert!(err.to_string().contains("PACKAGE-REPOS"), "{err}"); + } + + #[test] + fn rejects_installer_only_section() { + // `--RUN--` is part of InstallerTest fixtures; PoolBuilder fixtures + // have no such section, so it must be flagged as unknown here. + let input = "\ +--TEST-- +x +--REQUEST-- +{} +--PACKAGE-REPOS-- +[] +--RUN-- +install +--EXPECT-- +[] +"; + let err = parse_pool_builder_test_str(input).unwrap_err(); + assert!(err.to_string().contains("RUN"), "{err}"); + } +} |
