diff --git a/url/Cargo.toml b/url/Cargo.toml index f327cbb58..105f5c822 100644 --- a/url/Cargo.toml +++ b/url/Cargo.toml @@ -17,6 +17,7 @@ edition = "2018" rust-version = "1.56" [dev-dependencies] +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" bencher = "0.1" @@ -34,6 +35,11 @@ debugger_visualizer = [] # Expose internal offsets of the URL. expose_internals = [] +[[test]] +name = "url_wpt" +path = "tests/wpt.rs" +harness = false + [[bench]] name = "parse_url" path = "benches/parse_url.rs" diff --git a/url/tests/data.rs b/url/tests/data.rs deleted file mode 100644 index dc4660b9b..000000000 --- a/url/tests/data.rs +++ /dev/null @@ -1,260 +0,0 @@ -// Copyright 2013-2014 The rust-url developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! Data-driven tests - -use std::str::FromStr; - -use serde_json::Value; -use url::{quirks, Url}; - -#[test] -fn urltestdata() { - // Copied from https://github.com/web-platform-tests/wpt/blob/master/url/ - let mut json = Value::from_str(include_str!("urltestdata.json")) - .expect("JSON parse error in urltestdata.json"); - - let mut passed = true; - let mut skip_next = false; - for entry in json.as_array_mut().unwrap() { - if entry.is_string() { - if entry.as_str().unwrap() == "skip next" { - skip_next = true; - } - continue; // ignore comments - } - - if skip_next { - skip_next = false; - continue; - } - - let maybe_base = entry - .take_key("base") - .expect("missing base key") - .maybe_string(); - let input = entry.take_string("input"); - let failure = entry.take_key("failure").is_some(); - - let res = if let Some(base) = maybe_base { - let base = match Url::parse(&base) { - Ok(base) => base, - Err(_) if failure => continue, - Err(message) => { - eprint_failure( - format!(" failed: error parsing base {:?}: {}", base, message), - &format!("parse base for {:?}", input), - None, - ); - passed = false; - continue; - } - }; - base.join(&input) - } else { - Url::parse(&input) - }; - - let url = match (res, failure) { - (Ok(url), false) => url, - (Err(_), true) => continue, - (Err(message), false) => { - eprint_failure( - format!(" failed: {}", message), - &format!("parse URL for {:?}", input), - None, - ); - passed = false; - continue; - } - (Ok(_), true) => { - eprint_failure( - format!(" failed: expected parse error for URL {:?}", input), - &format!("parse URL for {:?}", input), - None, - ); - passed = false; - continue; - } - }; - - passed &= check_invariants(&url, &format!("invariants for {:?}", input), None); - - for &attr in ATTRIBS { - passed &= test_eq_eprint( - entry.take_string(attr), - get(&url, attr), - &format!("{:?} - {}", input, attr), - None, - ); - } - - if let Some(expected_origin) = entry.take_key("origin").map(|s| s.string()) { - passed &= test_eq_eprint( - expected_origin, - &quirks::origin(&url), - &format!("origin for {:?}", input), - None, - ); - } - } - - assert!(passed) -} - -#[test] -fn setters_tests() { - let mut json = Value::from_str(include_str!("setters_tests.json")) - .expect("JSON parse error in setters_tests.json"); - - let mut passed = true; - for &attr in ATTRIBS { - if attr == "href" { - continue; - } - - let mut tests = json.take_key(attr).unwrap(); - for mut test in tests.as_array_mut().unwrap().drain(..) { - let comment = test.take_key("comment").map(|s| s.string()); - { - if let Some(comment) = comment.as_ref() { - if comment.starts_with("IDNA Nontransitional_Processing") { - continue; - } - } - } - let href = test.take_string("href"); - let new_value = test.take_string("new_value"); - let name = format!("{:?}.{} = {:?}", href, attr, new_value); - let mut expected = test.take_key("expected").unwrap(); - - let mut url = Url::parse(&href).unwrap(); - let comment_ref = comment.as_deref(); - passed &= check_invariants(&url, &name, comment_ref); - set(&mut url, attr, &new_value); - - for attr in ATTRIBS { - if let Some(value) = expected.take_key(attr) { - passed &= test_eq_eprint(value.string(), get(&url, attr), &name, comment_ref); - }; - } - - passed &= check_invariants(&url, &name, comment_ref); - } - } - - assert!(passed); -} - -fn check_invariants(url: &Url, name: &str, comment: Option<&str>) -> bool { - let mut passed = true; - if let Err(e) = url.check_invariants() { - passed = false; - eprint_failure( - format!(" failed: invariants checked -> {:?}", e), - name, - comment, - ); - } - - #[cfg(feature = "serde")] - { - let bytes = serde_json::to_vec(url).unwrap(); - let new_url: Url = serde_json::from_slice(&bytes).unwrap(); - passed &= test_eq_eprint(url.to_string(), &new_url.to_string(), name, comment); - } - - passed -} - -trait JsonExt { - fn take_key(&mut self, key: &str) -> Option; - fn string(self) -> String; - fn maybe_string(self) -> Option; - fn take_string(&mut self, key: &str) -> String; -} - -impl JsonExt for Value { - fn take_key(&mut self, key: &str) -> Option { - self.as_object_mut().unwrap().remove(key) - } - - fn string(self) -> String { - self.maybe_string().expect("") - } - - fn maybe_string(self) -> Option { - match self { - Value::String(s) => Some(s), - Value::Null => None, - _ => panic!("Not a Value::String or Value::Null"), - } - } - - fn take_string(&mut self, key: &str) -> String { - self.take_key(key).unwrap().string() - } -} - -fn get<'a>(url: &'a Url, attr: &str) -> &'a str { - match attr { - "href" => quirks::href(url), - "protocol" => quirks::protocol(url), - "username" => quirks::username(url), - "password" => quirks::password(url), - "hostname" => quirks::hostname(url), - "host" => quirks::host(url), - "port" => quirks::port(url), - "pathname" => quirks::pathname(url), - "search" => quirks::search(url), - "hash" => quirks::hash(url), - _ => unreachable!(), - } -} - -#[allow(clippy::unit_arg)] -fn set(url: &mut Url, attr: &str, new: &str) { - let _ = match attr { - "protocol" => quirks::set_protocol(url, new), - "username" => quirks::set_username(url, new), - "password" => quirks::set_password(url, new), - "hostname" => quirks::set_hostname(url, new), - "host" => quirks::set_host(url, new), - "port" => quirks::set_port(url, new), - "pathname" => Ok(quirks::set_pathname(url, new)), - "search" => Ok(quirks::set_search(url, new)), - "hash" => Ok(quirks::set_hash(url, new)), - _ => unreachable!(), - }; -} - -fn test_eq_eprint(expected: String, actual: &str, name: &str, comment: Option<&str>) -> bool { - if expected == actual { - return true; - } - eprint_failure( - format!("expected: {}\n actual: {}", expected, actual), - name, - comment, - ); - false -} - -fn eprint_failure(err: String, name: &str, comment: Option<&str>) { - eprintln!(" test: {}\n{}", name, err); - if let Some(comment) = comment { - eprintln!("{}\n", comment); - } else { - eprintln!(); - } -} - -const ATTRIBS: &[&str] = &[ - "href", "protocol", "username", "password", "host", "hostname", "port", "pathname", "search", - "hash", -]; diff --git a/url/tests/expected_failures.txt b/url/tests/expected_failures.txt new file mode 100644 index 000000000..229636de7 --- /dev/null +++ b/url/tests/expected_failures.txt @@ -0,0 +1,37 @@ + against + against + against + against + against + against + against + against + against + against +<\\/localhost//pig> against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against + against diff --git a/url/tests/urltestdata.json b/url/tests/urltestdata.json index 53d036886..75a4bc2fd 100644 --- a/url/tests/urltestdata.json +++ b/url/tests/urltestdata.json @@ -4759,10 +4759,10 @@ "hash": "", "host": "host", "hostname": "host", - "href":"foo://host/", + "href": "foo://host/", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "" @@ -4773,10 +4773,10 @@ "hash": "", "host": "host", "hostname": "host", - "href":"foo://host/", + "href": "foo://host/", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "" @@ -4787,10 +4787,10 @@ "hash": "", "host": "host", "hostname": "host", - "href":"foo://host/", + "href": "foo://host/", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "" @@ -4993,10 +4993,10 @@ "hash": "", "host": "host", "hostname": "host", - "href":"http://host/", + "href": "http://host/", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "http:", "search": "", "username": "" @@ -5007,10 +5007,10 @@ "hash": "", "host": "host", "hostname": "host", - "href":"http://host/", + "href": "http://host/", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "http:", "search": "", "username": "" @@ -5021,10 +5021,10 @@ "hash": "", "host": "host", "hostname": "host", - "href":"http://host/", + "href": "http://host/", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "http:", "search": "", "username": "" @@ -5935,7 +5935,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "/", "base": "file://h/C:/a/b", @@ -6105,7 +6104,6 @@ "hash": "#x" }, "# File URLs and many (back)slashes", - "skip next", { "input": "file:\\\\//", "base": "about:blank", @@ -6120,7 +6118,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:\\\\\\\\", "base": "about:blank", @@ -6135,7 +6132,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:\\\\\\\\?fox", "base": "about:blank", @@ -6150,7 +6146,6 @@ "search": "?fox", "hash": "" }, - "skip next", { "input": "file:\\\\\\\\#guppy", "base": "about:blank", @@ -6165,7 +6160,6 @@ "search": "", "hash": "#guppy" }, - "skip next", { "input": "file://spider///", "base": "about:blank", @@ -6180,7 +6174,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:\\\\localhost//", "base": "about:blank", @@ -6209,7 +6202,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file://\\/localhost//cat", "base": "about:blank", @@ -6224,7 +6216,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file://localhost//a//../..//", "base": "about:blank", @@ -6239,7 +6230,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "/////mouse", "base": "file:///elephant", @@ -6268,7 +6258,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "\\/localhost//pig", "base": "file://lion/", @@ -6283,7 +6272,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "//localhost//pig", "base": "file://lion/", @@ -6298,7 +6286,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "/..//localhost//pig", "base": "file://lion/", @@ -6357,7 +6344,6 @@ "hash": "" }, "# Windows drive letter handling with the 'file:' base URL", - "skip next", { "input": "C|", "base": "file://host/dir/file", @@ -6372,7 +6358,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "C|", "base": "file://host/D:/dir1/dir2/file", @@ -6387,7 +6372,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "C|#", "base": "file://host/dir/file", @@ -6402,7 +6386,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "C|?", "base": "file://host/dir/file", @@ -6417,7 +6400,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "C|/", "base": "file://host/dir/file", @@ -6432,7 +6414,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "C|\n/", "base": "file://host/dir/file", @@ -6447,7 +6428,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "C|\\", "base": "file://host/dir/file", @@ -6533,7 +6513,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "/c:/foo/bar", "base": "file://host/path", @@ -6549,7 +6528,6 @@ "hash": "" }, "# Do not drop the host in the presence of a drive letter", - "skip next", { "input": "file://example.net/C:/", "base": "about:blank", @@ -6564,7 +6542,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file://1.2.3.4/C:/", "base": "about:blank", @@ -6579,7 +6556,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file://[1::8]/C:/", "base": "about:blank", @@ -6595,7 +6571,6 @@ "hash": "" }, "# Copy the host from the base URL in the following cases", - "skip next", { "input": "C|/", "base": "file://host/", @@ -6610,7 +6585,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "/C:/", "base": "file://host/", @@ -6625,7 +6599,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:C:/", "base": "file://host/", @@ -6640,7 +6613,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:/C:/", "base": "file://host/", @@ -6656,7 +6628,6 @@ "hash": "" }, "# Copy the empty host from the input in the following cases", - "skip next", { "input": "//C:/", "base": "file://host/", @@ -6912,7 +6883,6 @@ "inputCanBeRelative": true }, "# Additional file URL tests for (https://github.com/whatwg/url/issues/405)", - "skip next", { "input": "file://localhost//a//../..//foo", "base": "about:blank", @@ -6927,7 +6897,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file://localhost////foo", "base": "about:blank", @@ -6942,7 +6911,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:////foo", "base": "about:blank", @@ -6971,7 +6939,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:////one/two", "base": "file:///", @@ -7014,7 +6981,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "////one/two", "base": "file:///", @@ -7029,7 +6995,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:///.//", "base": "file:////", @@ -7045,7 +7010,6 @@ "hash": "" }, "File URL tests for https://github.com/whatwg/url/issues/549", - "skip next", { "input": "file:.//p", "base": "about:blank", @@ -7060,7 +7024,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "file:/.//p", "base": "about:blank", @@ -7571,7 +7534,6 @@ "search": "", "hash": "" }, - "skip next", { "input": "/.//path", "base": "non-spec:/p", @@ -8291,7 +8253,7 @@ "origin": "null", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "%20!%22$%&'()*+,-.%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~" @@ -8306,7 +8268,7 @@ "origin": "wss://host", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "wss:", "search": "", "username": "%20!%22$%&'()*+,-.%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~" @@ -8321,7 +8283,7 @@ "origin": "null", "password": "%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5C%5D%5E_%60%7B%7C%7D~", "pathname": "/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "joe" @@ -8336,7 +8298,7 @@ "origin": "wss://host", "password": "%20!%22$%&'()*+,-.%3A%3B%3C%3D%3E%40%5B%5D%5E_%60%7B%7C%7D~", "pathname": "/", - "port":"", + "port": "", "protocol": "wss:", "search": "", "username": "joe" @@ -8347,11 +8309,11 @@ "hash": "", "host": "!\"$%&'()*+,-.;=_`{}~", "hostname": "!\"$%&'()*+,-.;=_`{}~", - "href":"foo://!\"$%&'()*+,-.;=_`{}~/", + "href": "foo://!\"$%&'()*+,-.;=_`{}~/", "origin": "null", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "" @@ -8362,11 +8324,11 @@ "hash": "", "host": "!\"$&'()*+,-.;=_`{}~", "hostname": "!\"$&'()*+,-.;=_`{}~", - "href":"wss://!\"$&'()*+,-.;=_`{}~/", + "href": "wss://!\"$&'()*+,-.;=_`{}~/", "origin": "wss://!\"$&'()*+,-.;=_`{}~", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "wss:", "search": "", "username": "" @@ -8381,7 +8343,7 @@ "origin": "null", "password": "", "pathname": "/%20!%22$%&'()*+,-./:;%3C=%3E@[\\]^_%60%7B|%7D~", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "" @@ -8396,7 +8358,7 @@ "origin": "wss://host", "password": "", "pathname": "/%20!%22$%&'()*+,-./:;%3C=%3E@[/]^_%60%7B|%7D~", - "port":"", + "port": "", "protocol": "wss:", "search": "", "username": "" @@ -8411,7 +8373,7 @@ "origin": "null", "password": "", "pathname": "/dir/", - "port":"", + "port": "", "protocol": "foo:", "search": "?%20!%22$%&'()*+,-./:;%3C=%3E?@[\\]^_`{|}~", "username": "" @@ -8426,7 +8388,7 @@ "origin": "wss://host", "password": "", "pathname": "/dir/", - "port":"", + "port": "", "protocol": "wss:", "search": "?%20!%22$%&%27()*+,-./:;%3C=%3E?@[\\]^_`{|}~", "username": "" @@ -8441,7 +8403,7 @@ "origin": "null", "password": "", "pathname": "/dir/", - "port":"", + "port": "", "protocol": "foo:", "search": "", "username": "" @@ -8456,7 +8418,7 @@ "origin": "wss://host", "password": "", "pathname": "/dir/", - "port":"", + "port": "", "protocol": "wss:", "search": "", "username": "" @@ -8468,10 +8430,10 @@ "hash": "", "host": "", "hostname": "", - "href":"abc:rootless", + "href": "abc:rootless", "password": "", "pathname": "rootless", - "port":"", + "port": "", "protocol": "abc:", "search": "", "username": "" @@ -8482,10 +8444,10 @@ "hash": "", "host": "", "hostname": "", - "href":"abc:rootless", + "href": "abc:rootless", "password": "", "pathname": "rootless", - "port":"", + "port": "", "protocol": "abc:", "search": "", "username": "" @@ -8496,10 +8458,10 @@ "hash": "", "host": "", "hostname": "", - "href":"abc:rootless", + "href": "abc:rootless", "password": "", "pathname": "rootless", - "port":"", + "port": "", "protocol": "abc:", "search": "", "username": "" @@ -8510,10 +8472,10 @@ "hash": "", "host": "", "hostname": "", - "href":"abc:/rooted", + "href": "abc:/rooted", "password": "", "pathname": "/rooted", - "port":"", + "port": "", "protocol": "abc:", "search": "", "username": "" @@ -8661,10 +8623,10 @@ "hash": "", "host": "foo.09..", "hostname": "foo.09..", - "href":"http://foo.09../", + "href": "http://foo.09../", "password": "", "pathname": "/", - "port":"", + "port": "", "protocol": "http:", "search": "", "username": "" @@ -8733,7 +8695,7 @@ "password": "", "pathname": "/", "port": "", - "protocol": "https:", + "protocol": "http:", "search": "", "username": "" }, diff --git a/url/tests/wpt.rs b/url/tests/wpt.rs new file mode 100644 index 000000000..701044d67 --- /dev/null +++ b/url/tests/wpt.rs @@ -0,0 +1,477 @@ +// Copyright 2013-2014 The rust-url developers. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Data-driven tests imported from web-platform-tests + +use std::collections::HashMap; +use std::fmt::Write; +use std::panic; + +use serde_json::Value; +use url::Url; + +#[derive(Debug, serde::Deserialize)] +struct UrlTest { + input: String, + base: Option, + #[serde(flatten)] + result: UrlTestResult, +} + +#[derive(Debug, serde::Deserialize)] +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +enum UrlTestResult { + Ok(UrlTestOk), + Fail(UrlTestFail), +} + +#[derive(Debug, serde::Deserialize)] +struct UrlTestOk { + href: String, + protocol: String, + username: String, + password: String, + host: String, + hostname: String, + port: String, + pathname: String, + search: String, + hash: String, +} + +#[derive(Debug, serde::Deserialize)] +struct UrlTestFail { + failure: bool, +} + +#[derive(Debug, serde::Deserialize)] +struct SetterTest { + href: String, + new_value: String, + expected: SetterTestExpected, +} + +#[derive(Debug, serde::Deserialize)] +struct SetterTestExpected { + href: Option, + protocol: Option, + username: Option, + password: Option, + host: Option, + hostname: Option, + port: Option, + pathname: Option, + search: Option, + hash: Option, +} + +fn main() { + let mut filter = None; + let mut args = std::env::args().skip(1); + while filter.is_none() { + if let Some(arg) = args.next() { + if arg == "--test-threads" { + args.next(); + continue; + } + filter = Some(arg); + } else { + break; + } + } + + let mut expected_failures = include_str!("expected_failures.txt") + .lines() + .collect::>(); + + let mut errors = vec![]; + + // Copied from https://github.com/web-platform-tests/wpt/blob/master/url/ + let url_json: Vec = serde_json::from_str(include_str!("urltestdata.json")) + .expect("JSON parse error in urltestdata.json"); + let url_tests = url_json + .into_iter() + .filter(|val| val.is_object()) + .map(|val| serde_json::from_value::(val).expect("parsing failed")) + .collect::>(); + + let setter_json: HashMap = + serde_json::from_str(include_str!("setters_tests.json")) + .expect("JSON parse error in setters_tests.json"); + let setter_tests = setter_json + .into_iter() + .filter(|(k, _)| k != "comment") + .map(|(k, v)| { + let test = serde_json::from_value::>(v).expect("parsing failed"); + (k, test) + }) + .collect::>(); + + for url_test in url_tests { + let mut name = format!("<{}>", url_test.input.escape_default()); + if let Some(base) = &url_test.base { + write!(&mut name, " against <{}>", base.escape_default()).unwrap(); + } + if should_skip(&name, filter.as_deref()) { + continue; + } + print!("{} ... ", name); + + let res = run_url_test(url_test); + report(name, res, &mut errors, &mut expected_failures); + } + + for (kind, tests) in setter_tests { + for test in tests { + let name = format!( + "<{}> set {} to <{}>", + test.href.escape_default(), + kind, + test.new_value.escape_default() + ); + if should_skip(&name, filter.as_deref()) { + continue; + } + + print!("{} ... ", name); + + let res = run_setter_test(&kind, test); + report(name, res, &mut errors, &mut expected_failures); + } + } + + println!(); + println!("===================="); + println!(); + + if !errors.is_empty() { + println!("errors:"); + println!(); + + for (name, err) in errors { + println!(" name: {}", name); + println!(" err: {}", err); + println!(); + } + + std::process::exit(1); + } else { + println!("all tests passed"); + } + + if !expected_failures.is_empty() && filter.is_none() { + println!(); + println!("===================="); + println!(); + println!("tests were expected to fail but did not run:"); + println!(); + + for name in expected_failures { + println!(" {}", name); + } + + println!(); + println!("if these tests were removed, update expected_failures.txt"); + println!(); + + std::process::exit(1); + } +} + +fn should_skip(name: &str, filter: Option<&str>) -> bool { + match filter { + Some(filter) => !name.contains(filter), + None => false, + } +} + +fn report( + name: String, + res: Result<(), String>, + errors: &mut Vec<(String, String)>, + expected_failures: &mut Vec<&str>, +) { + let expected_failure = expected_failures.contains(&&*name); + expected_failures.retain(|&s| s != &*name); + match res { + Ok(()) => { + if expected_failure { + println!("🟠 (unexpected success)"); + errors.push((name, "unexpected success".to_string())); + } else { + println!("✅"); + } + } + Err(err) => { + if expected_failure { + println!("✅ (expected fail)"); + } else { + println!("❌"); + errors.push((name, err)); + } + } + } +} + +fn run_url_test( + UrlTest { + base, + input, + result, + }: UrlTest, +) -> Result<(), String> { + let base = match base { + Some(base) => { + let base = panic::catch_unwind(|| Url::parse(&base)) + .map_err(|_| "panicked while parsing base".to_string())? + .map_err(|e| format!("errored while parsing base: {}", e))?; + Some(base) + } + None => None, + }; + + let res = panic::catch_unwind(move || Url::options().base_url(base.as_ref()).parse(&input)) + .map_err(|_| "panicked while parsing input".to_string())? + .map_err(|e| format!("errored while parsing input: {}", e)); + + match result { + UrlTestResult::Ok(ok) => check_url_ok(res, ok), + UrlTestResult::Fail(fail) => { + assert!(fail.failure); + if res.is_ok() { + return Err("expected failure, but parsed successfully".to_string()); + } + + Ok(()) + } + } +} + +fn check_url_ok(res: Result, ok: UrlTestOk) -> Result<(), String> { + let url = match res { + Ok(url) => url, + Err(err) => { + return Err(format!("expected success, but errored: {:?}", err)); + } + }; + + let href = url::quirks::href(&url); + if href != ok.href { + return Err(format!("expected href {:?}, but got {:?}", ok.href, href)); + } + + let protocol = url::quirks::protocol(&url); + if protocol != ok.protocol { + return Err(format!( + "expected protocol {:?}, but got {:?}", + ok.protocol, protocol + )); + } + + let username = url::quirks::username(&url); + if username != ok.username { + return Err(format!( + "expected username {:?}, but got {:?}", + ok.username, username + )); + } + + let password = url::quirks::password(&url); + if password != ok.password { + return Err(format!( + "expected password {:?}, but got {:?}", + ok.password, password + )); + } + + let host = url::quirks::host(&url); + if host != ok.host { + return Err(format!("expected host {:?}, but got {:?}", ok.host, host)); + } + + let hostname = url::quirks::hostname(&url); + if hostname != ok.hostname { + return Err(format!( + "expected hostname {:?}, but got {:?}", + ok.hostname, hostname + )); + } + + let port = url::quirks::port(&url); + if port != ok.port { + return Err(format!("expected port {:?}, but got {:?}", ok.port, port)); + } + + let pathname = url::quirks::pathname(&url); + if pathname != ok.pathname { + return Err(format!( + "expected pathname {:?}, but got {:?}", + ok.pathname, pathname + )); + } + + let search = url::quirks::search(&url); + if search != ok.search { + return Err(format!( + "expected search {:?}, but got {:?}", + ok.search, search + )); + } + + let hash = url::quirks::hash(&url); + if hash != ok.hash { + return Err(format!("expected hash {:?}, but got {:?}", ok.hash, hash)); + } + + Ok(()) +} + +fn run_setter_test( + kind: &str, + SetterTest { + href, + new_value, + expected, + }: SetterTest, +) -> Result<(), String> { + let mut url = panic::catch_unwind(|| Url::parse(&href)) + .map_err(|_| "panicked while parsing href".to_string())? + .map_err(|e| format!("errored while parsing href: {}", e))?; + + let url = panic::catch_unwind(move || { + match kind { + "protocol" => { + url::quirks::set_protocol(&mut url, &new_value).ok(); + } + "username" => { + url::quirks::set_username(&mut url, &new_value).ok(); + } + "password" => { + url::quirks::set_password(&mut url, &new_value).ok(); + } + "host" => { + url::quirks::set_host(&mut url, &new_value).ok(); + } + "hostname" => { + url::quirks::set_hostname(&mut url, &new_value).ok(); + } + "port" => { + url::quirks::set_port(&mut url, &new_value).ok(); + } + "pathname" => url::quirks::set_pathname(&mut url, &new_value), + "search" => url::quirks::set_search(&mut url, &new_value), + "hash" => url::quirks::set_hash(&mut url, &new_value), + _ => panic!("unknown setter kind: {:?}", kind), + }; + url + }) + .map_err(|_| "panicked while setting value".to_string())?; + + if let Some(expected_href) = expected.href { + let href = url::quirks::href(&url); + if href != expected_href { + return Err(format!( + "expected href {:?}, but got {:?}", + expected_href, href + )); + } + } + + if let Some(expected_protocol) = expected.protocol { + let protocol = url::quirks::protocol(&url); + if protocol != expected_protocol { + return Err(format!( + "expected protocol {:?}, but got {:?}", + expected_protocol, protocol + )); + } + } + + if let Some(expected_username) = expected.username { + let username = url::quirks::username(&url); + if username != expected_username { + return Err(format!( + "expected username {:?}, but got {:?}", + expected_username, username + )); + } + } + + if let Some(expected_password) = expected.password { + let password = url::quirks::password(&url); + if password != expected_password { + return Err(format!( + "expected password {:?}, but got {:?}", + expected_password, password + )); + } + } + + if let Some(expected_host) = expected.host { + let host = url::quirks::host(&url); + if host != expected_host { + return Err(format!( + "expected host {:?}, but got {:?}", + expected_host, host + )); + } + } + + if let Some(expected_hostname) = expected.hostname { + let hostname = url::quirks::hostname(&url); + if hostname != expected_hostname { + return Err(format!( + "expected hostname {:?}, but got {:?}", + expected_hostname, hostname + )); + } + } + + if let Some(expected_port) = expected.port { + let port = url::quirks::port(&url); + if port != expected_port { + return Err(format!( + "expected port {:?}, but got {:?}", + expected_port, port + )); + } + } + + if let Some(expected_pathname) = expected.pathname { + let pathname = url::quirks::pathname(&url); + if pathname != expected_pathname { + return Err(format!( + "expected pathname {:?}, but got {:?}", + expected_pathname, pathname + )); + } + } + + if let Some(expected_search) = expected.search { + let search = url::quirks::search(&url); + if search != expected_search { + return Err(format!( + "expected search {:?}, but got {:?}", + expected_search, search + )); + } + } + + if let Some(expected_hash) = expected.hash { + let hash = url::quirks::hash(&url); + if hash != expected_hash { + return Err(format!( + "expected hash {:?}, but got {:?}", + expected_hash, hash + )); + } + } + + Ok(()) +}