use std::fmt::Write as _;
use serde::Deserialize;
use sorug::Url;
const WPT_JSON: &str = include_str!("urltestdata.json");
#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum Entry {
#[allow(dead_code)] Comment(String),
Case(TestCase),
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)] struct TestCase {
input: String,
base: Option<String>,
#[serde(default)]
failure: Option<bool>,
href: Option<String>,
protocol: Option<String>,
username: Option<String>,
password: Option<String>,
host: Option<String>,
hostname: Option<String>,
port: Option<String>,
pathname: Option<String>,
search: Option<String>,
hash: Option<String>,
origin: Option<String>,
comment: Option<String>,
#[serde(default, rename = "relativeTo")]
relative_to: Option<String>,
#[serde(default, rename = "searchParams")]
search_params: Option<serde_json::Value>,
}
impl TestCase {
fn expects_failure(&self) -> bool {
self.failure == Some(true)
}
fn name(&self) -> String {
match &self.base {
Some(base) => format!(
"<{}> against <{}>",
self.input.escape_default(),
base.escape_default()
),
None => format!("<{}>", self.input.escape_default()),
}
}
}
#[test]
fn wpt_urltestdata() {
let entries: Vec<Entry> =
serde_json::from_str(WPT_JSON).expect("urltestdata.json must be valid JSON");
let mut passed = 0usize;
let mut failed = 0usize;
let mut ignored_comments = 0usize;
let mut failures: Vec<String> = Vec::new();
for entry in &entries {
let case = match entry {
Entry::Comment(_) => {
ignored_comments += 1;
continue;
}
Entry::Case(case) => case,
};
match run_case(case) {
Ok(()) => passed += 1,
Err(err) => {
failed += 1;
if failures.len() < 64 {
failures.push(format!("{} — {err}", case.name()));
}
}
}
}
eprintln!(
"WPT urltestdata: {passed} passed, {failed} failed, {ignored_comments} comments skipped ({} entries)",
entries.len()
);
if failed > 0 {
let extra = failed.saturating_sub(failures.len());
let mut msg = format!("{failed} WPT case(s) failed:\n");
for line in &failures {
msg.push_str(" • ");
msg.push_str(line);
msg.push('\n');
}
if extra > 0 {
let _ = writeln!(msg, " … and {extra} more");
}
panic!("{msg}");
}
}
fn run_case(case: &TestCase) -> Result<(), String> {
let base = match &case.base {
Some(base_str) => {
let parsed = Url::parse(base_str)
.map_err(|e| format!("base URL failed to parse ({e}): {base_str:?}"))?;
Some(parsed)
}
None => None,
};
let result = Url::parse_with_base(&case.input, base.as_ref());
if case.expects_failure() {
match result {
Err(_) => {}
Ok(url) => {
return Err(format!("expected parse failure, got href {:?}", url.href()));
}
}
if let Some(rel) = case.relative_to.as_deref() {
return assert_relative_to_succeeds(&case.input, rel);
}
if case.base.is_none() {
let hierarchical = Url::parse("https://example.org/foo/bar")
.map_err(|e| format!("hierarchical base parse: {e}"))?;
if Url::parse_with_base(&case.input, Some(&hierarchical)).is_ok() {
return Err(format!(
"expected absolute failure, but <{}> parsed against hierarchical base",
case.input.escape_default()
));
}
}
return Ok(());
}
let url = result.map_err(|e| format!("expected success, got {e}"))?;
assert_success_components(&url, case)
}
fn assert_relative_to_succeeds(input: &str, kind: &str) -> Result<(), String> {
const HIERARCHICAL: &[&str] = &[
"http://example.org/foo/bar",
"https://example.org/foo/bar",
"file:///tmp/x",
"ws://example.org/x",
"ftp://example.org/x",
];
const OPAQUE: &[&str] = &["mailto:user@example.org", "data:text/plain,hi"];
let parse_ok = |base: &str| -> Result<bool, String> {
let base_url =
Url::parse(base).map_err(|e| format!("base {base:?} failed to parse: {e}"))?;
Ok(Url::parse_with_base(input, Some(&base_url)).is_ok())
};
match kind {
"non-opaque-path-base" => {
let mut any_hier = false;
for base in HIERARCHICAL {
if parse_ok(base)? {
any_hier = true;
break;
}
}
if !any_hier {
return Err(
"relativeTo=non-opaque-path-base: expected success against some hierarchical base"
.into(),
);
}
for base in OPAQUE {
if parse_ok(base)? {
return Err(format!(
"relativeTo=non-opaque-path-base: unexpectedly parsed against opaque-path base {base}"
));
}
}
Ok(())
}
"any-base" => {
let mut any_hier = false;
for base in HIERARCHICAL {
if parse_ok(base)? {
any_hier = true;
break;
}
}
if !any_hier {
return Err(
"relativeTo=any-base: expected success against some hierarchical base".into(),
);
}
let mut any_opaque = false;
for base in OPAQUE {
if parse_ok(base)? {
any_opaque = true;
break;
}
}
if !any_opaque {
return Err(
"relativeTo=any-base: expected success against some opaque-path base".into(),
);
}
Ok(())
}
other => Err(format!("unknown relativeTo value: {other:?}")),
}
}
fn assert_success_components(url: &Url, case: &TestCase) -> Result<(), String> {
let href = require(case.href.as_deref(), "href")?;
let protocol = require(case.protocol.as_deref(), "protocol")?;
let username = require(case.username.as_deref(), "username")?;
let password = require(case.password.as_deref(), "password")?;
let host = require(case.host.as_deref(), "host")?;
let hostname = require(case.hostname.as_deref(), "hostname")?;
let port = require(case.port.as_deref(), "port")?;
let pathname = require(case.pathname.as_deref(), "pathname")?;
let search = require(case.search.as_deref(), "search")?;
let hash = require(case.hash.as_deref(), "hash")?;
eq("href", url.href(), href)?;
eq("protocol", url.protocol(), protocol)?;
eq("username", url.username(), username)?;
eq("password", url.password(), password)?;
eq("host", url.host_with_port(), host)?;
eq("hostname", url.hostname(), hostname)?;
eq("port", &url.port_str(), port)?;
eq("pathname", url.pathname(), pathname)?;
eq("path", url.path(), pathname)?;
eq("search", url.search(), search)?;
eq("hash", url.hash(), hash)?;
let scheme = protocol
.strip_suffix(':')
.ok_or_else(|| format!("WPT protocol missing trailing ':': {protocol:?}"))?;
eq("scheme", url.scheme(), scheme)?;
if let Some(expected_origin) = case.origin.as_deref() {
eq("origin", &url.origin().serialized(), expected_origin)?;
}
if let Some(serde_json::Value::String(expected_sp)) = case.search_params.as_ref() {
eq(
"searchParams",
&url.search_params().serialize(),
expected_sp,
)?;
}
Ok(())
}
fn require<'a>(value: Option<&'a str>, field: &str) -> Result<&'a str, String> {
value.ok_or_else(|| format!("success case missing expected field `{field}`"))
}
fn eq(field: &str, actual: &str, expected: &str) -> Result<(), String> {
if actual == expected {
Ok(())
} else {
Err(format!("{field}: expected {expected:?}, got {actual:?}"))
}
}