Files
rust_browser/tests/wpt_harness/manifest.rs
Zachary D. Rowitsch 16abbd78e7 Bulk-import 2899 WPT CSS reftests and add import tooling
Add scripts/import_wpt_reftests.py to sparse-clone the upstream WPT repo
and bulk-import qualifying CSS reftests (no JS, no external resources) as
known_fail entries. 23 tests already pass and are promoted. The import
script is idempotent and exposed via `just import-wpt`. CI now prints the
WPT summary (pass=36 known_fail=2877 skip=1) on every run.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 00:15:27 -05:00

258 lines
8.0 KiB
Rust

use crate::types::{WptCase, WptMode, WptStatus};
use std::fs;
use std::path::{Path, PathBuf};
pub fn load_manifest(root: &Path) -> Result<Vec<WptCase>, String> {
let manifest_path = root.join("wpt_manifest.toml");
let content = fs::read_to_string(&manifest_path)
.map_err(|e| format!("failed to read {}: {e}", manifest_path.display()))?;
parse_manifest(&content)
.and_then(validate_cases)
.map(|cases| {
cases
.into_iter()
.map(|case| WptCase {
input: root.join(case.input),
reference: case.reference.map(|p| root.join(p)),
expected_layout: case.expected_layout.map(|p| root.join(p)),
expected_dl: case.expected_dl.map(|p| root.join(p)),
..case
})
.collect()
})
}
pub fn parse_manifest(input: &str) -> Result<Vec<WptCase>, String> {
let mut cases = Vec::new();
let mut current: Option<CaseBuilder> = None;
for (idx, raw_line) in input.lines().enumerate() {
let line_no = idx + 1;
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line == "[[case]]" {
if let Some(case) = current.take() {
cases.push(case.build()?);
}
current = Some(CaseBuilder::default());
continue;
}
let builder = current
.as_mut()
.ok_or_else(|| format!("line {line_no}: key/value outside [[case]] block"))?;
let (key, value) = parse_key_value(line)
.ok_or_else(|| format!("line {line_no}: invalid key/value syntax"))?;
match key {
"id" => builder.id = Some(parse_string(value, line_no)?),
"input" => builder.input = Some(PathBuf::from(parse_string(value, line_no)?)),
"mode" => {
let mode = parse_string(value, line_no)?;
builder.mode = Some(match mode.as_str() {
"single" => WptMode::Single,
"reftest" => WptMode::Reftest,
_ => return Err(format!("line {line_no}: unknown mode '{mode}'")),
});
}
"reference" => {
builder.reference = Some(PathBuf::from(parse_string(value, line_no)?));
}
"expected_layout" => {
builder.expected_layout = Some(PathBuf::from(parse_string(value, line_no)?));
}
"expected_dl" => {
builder.expected_dl = Some(PathBuf::from(parse_string(value, line_no)?));
}
"status" => {
let status = parse_string(value, line_no)?;
builder.status = Some(status);
}
"reason" => builder.reason = Some(parse_string(value, line_no)?),
"flags" => builder.flags = parse_string_array(value, line_no)?,
_ => return Err(format!("line {line_no}: unknown key '{key}'")),
}
}
if let Some(case) = current.take() {
cases.push(case.build()?);
}
if cases.is_empty() {
return Err("manifest is empty".to_string());
}
Ok(cases)
}
fn validate_cases(cases: Vec<WptCase>) -> Result<Vec<WptCase>, String> {
let mut ids = std::collections::BTreeSet::new();
for case in &cases {
if !ids.insert(case.id.clone()) {
return Err(format!(
"duplicate case id '{}'; ids must be unique",
case.id
));
}
if let WptStatus::KnownFail { reason } | WptStatus::Skip { reason } = &case.status {
if reason.trim().is_empty() {
return Err(format!("case '{}' has empty reason", case.id));
}
}
match case.mode {
WptMode::Single => {
if case.expected_layout.is_none() || case.expected_dl.is_none() {
return Err(format!(
"case '{}' in single mode must define expected_layout and expected_dl",
case.id
));
}
if case.reference.is_some() {
return Err(format!(
"case '{}' in single mode must not define reference",
case.id
));
}
}
WptMode::Reftest => {
if case.reference.is_none() {
return Err(format!(
"case '{}' in reftest mode must define reference",
case.id
));
}
}
}
}
Ok(cases)
}
fn parse_key_value(line: &str) -> Option<(&str, &str)> {
let mut parts = line.splitn(2, '=');
let key = parts.next()?.trim();
let value = parts.next()?.trim();
if key.is_empty() || value.is_empty() {
return None;
}
Some((key, value))
}
fn parse_string(value: &str, line_no: usize) -> Result<String, String> {
if value.starts_with('"') && value.ends_with('"') && value.len() >= 2 {
return Ok(value[1..value.len() - 1].to_string());
}
Err(format!("line {line_no}: expected quoted string"))
}
fn parse_string_array(value: &str, line_no: usize) -> Result<Vec<String>, String> {
if !value.starts_with('[') || !value.ends_with(']') {
return Err(format!("line {line_no}: expected array syntax [..]"));
}
let inner = value[1..value.len() - 1].trim();
if inner.is_empty() {
return Ok(Vec::new());
}
inner
.split(',')
.map(|entry| parse_string(entry.trim(), line_no))
.collect()
}
#[derive(Default)]
struct CaseBuilder {
id: Option<String>,
input: Option<PathBuf>,
mode: Option<WptMode>,
reference: Option<PathBuf>,
expected_layout: Option<PathBuf>,
expected_dl: Option<PathBuf>,
status: Option<String>,
reason: Option<String>,
flags: Vec<String>,
}
impl CaseBuilder {
fn build(self) -> Result<WptCase, String> {
let id = self
.id
.ok_or_else(|| "missing required key: id".to_string())?;
let input = self
.input
.ok_or_else(|| format!("case '{id}' missing required key: input"))?;
let mode = self
.mode
.ok_or_else(|| format!("case '{id}' missing required key: mode"))?;
let status = match self.status.as_deref().unwrap_or("pass") {
"pass" => WptStatus::Pass,
"known_fail" => WptStatus::KnownFail {
reason: self.reason.unwrap_or_default(),
},
"skip" => WptStatus::Skip {
reason: self.reason.unwrap_or_default(),
},
other => {
return Err(format!(
"case '{id}' has invalid status '{other}' (expected pass|known_fail|skip)"
));
}
};
Ok(WptCase {
id,
input,
mode,
reference: self.reference,
expected_layout: self.expected_layout,
expected_dl: self.expected_dl,
status,
flags: self.flags,
})
}
}
#[cfg(test)]
mod tests {
use super::parse_manifest;
use crate::types::WptStatus;
#[test]
fn parses_minimal_case() {
let input = r#"
[[case]]
id = "basic"
input = "fixtures/basic.html"
mode = "single"
expected_layout = "expected/basic.layout.txt"
expected_dl = "expected/basic.dl.txt"
"#;
let cases = parse_manifest(input).expect("manifest parse should succeed");
assert_eq!(cases.len(), 1);
assert_eq!(cases[0].id, "basic");
assert!(matches!(cases[0].status, WptStatus::Pass));
}
#[test]
fn rejects_key_outside_case_block() {
let input = r#"
id = "bad"
"#;
let err = parse_manifest(input).expect_err("manifest should fail");
assert!(err.contains("outside [[case]]"));
}
}