diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-04 10:35:58 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-04 10:35:58 +0900 |
| commit | 1427c101f98c72e551fcc72671ab3cde0991bb6d (patch) | |
| tree | b83fe05cc22a38813243d86890fde34721b7985a /crates/mozart-test-harness | |
| parent | bc72b70daea7db03456508540f96ab6f019ef5e3 (diff) | |
| download | php-mozart-1427c101f98c72e551fcc72671ab3cde0991bb6d.tar.gz php-mozart-1427c101f98c72e551fcc72671ab3cde0991bb6d.tar.zst php-mozart-1427c101f98c72e551fcc72671ab3cde0991bb6d.zip | |
test(resolver): scaffold PoolBuilder fixture suite from Composer
Port the 31 .test fixtures under
composer/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder/
as #[ignore]'d cases in mozart-registry/tests/poolbuilder.rs. Each
fixture is parsed eagerly so format-level regressions surface
immediately, while the runner itself is unimplemented\!() — removing
#[ignore] from a case will force the missing pool-build entry point
into existence rather than silently mis-run. Generalize
mozart-test-harness's split_sections to take a per-format valid-section
list and add a poolbuilder parser alongside the installer one.
Diffstat (limited to 'crates/mozart-test-harness')
| -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 |
3 files changed, 193 insertions, 3 deletions
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}"); + } +} |
