Repository: servo/rust-url
Pull request #857 (2023-07-13 11:40:57)

Rewrite WPT runner
This PR rewrites the WPT test data runner for `url` to use a custom harness.
This has the following benefits:

- Tests can now be filtered, for example
  `cargo test --test url_wpt "set username"` runs only tests with "set username"
  in the name.

- There is now an "expected_failures.txt" file that lists tests that are
  expected to fail, by name. The test harness will ensure that these tests fail,
  and will report a failure if they pass.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/servo/rust-url/pull/857?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo) Report
Patch coverage: **`19.07`**% and project coverage change: **`-12.00`** :warning:
> Comparison is base [(`b228574`)](https://app.codecov.io/gh/servo/rust-url/commit/b22857487af7c48525d0297b9db4b61ccab96b43?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo) 82.05% compared to head [(`24b5ce7`)](https://app.codecov.io/gh/servo/rust-url/pull/857?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo) 70.05%.

> :exclamation: Current head 24b5ce7 differs from pull request most recent head 3ecf8d2. Consider uploading reports for the commit 3ecf8d2 to get more accurate results

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           master     #857       +/-   ##
===========================================
- Coverage   82.05%   70.05%   -12.00%     
===========================================
  Files          20       20               
  Lines        3365     3490      +125     
===========================================
- Hits         2761     2445      -316     
- Misses        604     1045      +441     
```


| [Impacted Files](https://app.codecov.io/gh/servo/rust-url/pull/857?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo) | Coverage Δ | |
|---|---|---|
| [url/tests/wpt.rs](https://app.codecov.io/gh/servo/rust-url/pull/857?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo#diff-dXJsL3Rlc3RzL3dwdC5ycw==) | `19.07% <19.07%> (ø)` | |

... and [8 files with indirect coverage changes](https://app.codecov.io/gh/servo/rust-url/pull/857/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/servo/rust-url/pull/857?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=servo).


Patch:
diff --git a/url/Cargo.toml b/url/Cargo.toml
index f327cbb..105f5c8 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"


Test patch:
diff --git a/url/tests/data.rs b/url/tests/data.rs
deleted file mode 100644
index dc4660b..0000000
--- 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 <LICENSE-APACHE or
-// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
-// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<Value>;
-    fn string(self) -> String;
-    fn maybe_string(self) -> Option<String>;
-    fn take_string(&mut self, key: &str) -> String;
-}
-
-impl JsonExt for Value {
-    fn take_key(&mut self, key: &str) -> Option<Value> {
-        self.as_object_mut().unwrap().remove(key)
-    }
-
-    fn string(self) -> String {
-        self.maybe_string().expect("")
-    }
-
-    fn maybe_string(self) -> Option<String> {
-        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 0000000..229636d
--- /dev/null
+++ b/url/tests/expected_failures.txt
@@ -0,0 +1,37 @@
+</> against <file://h/C:/a/b>
+<file:\\\\//> against <about:blank>
+<file:\\\\\\\\> against <about:blank>
+<file:\\\\\\\\?fox> against <about:blank>
+<file:\\\\\\\\#guppy> against <about:blank>
+<file://spider///> against <about:blank>
+<file:\\\\localhost//> against <about:blank>
+<file://\\/localhost//cat> against <about:blank>
+<file://localhost//a//../..//> against <about:blank>
+</////mouse> against <file:///elephant>
+<\\/localhost//pig> against <file://lion/>
+<//localhost//pig> against <file://lion/>
+</..//localhost//pig> against <file://lion/>
+<C|> against <file://host/dir/file>
+<C|> against <file://host/D:/dir1/dir2/file>
+<C|#> against <file://host/dir/file>
+<C|?> against <file://host/dir/file>
+<C|/> against <file://host/dir/file>
+<C|\n/> against <file://host/dir/file>
+<C|\\> against <file://host/dir/file>
+</c:/foo/bar> against <file://host/path>
+<file://example.net/C:/> against <about:blank>
+<file://1.2.3.4/C:/> against <about:blank>
+<file://[1::8]/C:/> against <about:blank>
+<C|/> against <file://host/>
+</C:/> against <file://host/>
+<file:C:/> against <file://host/>
+<file:/C:/> against <file://host/>
+<file://localhost//a//../..//foo> against <about:blank>
+<file://localhost////foo> against <about:blank>
+<file:////foo> against <about:blank>
+<file:////one/two> against <file:///>
+<////one/two> against <file:///>
+<file:///.//> against <file:////>
+<file:.//p> against <about:blank>
+<file:/.//p> against <about:blank>
+<https://x/?#\u{0}y> against <about:blank>
diff --git a/url/tests/urltestdata.json b/url/tests/urltestdata.json
index 53d0368..75a4bc2 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 0000000..701044d
--- /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 <LICENSE-APACHE or
+// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
+// <LICENSE-MIT or http://opensource.org/licenses/MIT>, 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<String>,
+    #[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<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>,
+}
+
+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::<Vec<_>>();
+
+    let mut errors = vec![];
+
+    // Copied from https://github.com/web-platform-tests/wpt/blob/master/url/
+    let url_json: Vec<Value> = 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::<UrlTest>(val).expect("parsing failed"))
+        .collect::<Vec<_>>();
+
+    let setter_json: HashMap<String, Value> =
+        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::<Vec<SetterTest>>(v).expect("parsing failed");
+            (k, test)
+        })
+        .collect::<HashMap<_, _>>();
+
+    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<Url, String>, 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(())
+}