aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-test-harness
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-04 10:35:58 +0900
committernsfisis <nsfisis@gmail.com>2026-05-04 10:35:58 +0900
commit1427c101f98c72e551fcc72671ab3cde0991bb6d (patch)
treeb83fe05cc22a38813243d86890fde34721b7985a /crates/mozart-test-harness
parentbc72b70daea7db03456508540f96ab6f019ef5e3 (diff)
downloadphp-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.rs4
-rw-r--r--crates/mozart-test-harness/src/parser.rs14
-rw-r--r--crates/mozart-test-harness/src/pool_builder_parser.rs178
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}");
+ }
+}