Repository: servo/rust-url
Pull request #858 (2023-07-14 15:49:36)

Add `--generate-link-to-definition` option when building on docs.rs
This option generates links in source code pages, allowing to jump to definition and to jump back to doc. It makes browsing the source code pages much nicer. You can see it in action [here](https://doc.rust-lang.org/stable/nightly-rustc/src/rustc_middle/lib.rs.html#90).


Patch:
diff --git a/data-url/Cargo.toml b/data-url/Cargo.toml
index 70aa5ea..0edfd28 100644
--- a/data-url/Cargo.toml
+++ b/data-url/Cargo.toml
@@ -26,3 +26,6 @@ test = false
 [[test]]
 name = "wpt"
 harness = false
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--generate-link-to-definition"]
diff --git a/data-url/src/forgiving_base64.rs b/data-url/src/forgiving_base64.rs
index 5cb1bc4..ed713d8 100644
--- a/data-url/src/forgiving_base64.rs
+++ b/data-url/src/forgiving_base64.rs
@@ -21,6 +21,9 @@ impl fmt::Display for InvalidBase64 {
     }
 }
 
+#[cfg(feature = "std")]
+impl std::error::Error for InvalidBase64 {}
+
 #[derive(Debug)]
 enum InvalidBase64Details {
     UnexpectedSymbol(u8),
diff --git a/form_urlencoded/Cargo.toml b/form_urlencoded/Cargo.toml
index 11e8325..8e385d7 100644
--- a/form_urlencoded/Cargo.toml
+++ b/form_urlencoded/Cargo.toml
@@ -19,3 +19,6 @@ alloc = ["percent-encoding/alloc"]
 
 [dependencies]
 percent-encoding = { version = "2.3.0", default-features = false, path = "../percent_encoding" }
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--generate-link-to-definition"]
diff --git a/idna/Cargo.toml b/idna/Cargo.toml
index 0d0c750..e5c9886 100644
--- a/idna/Cargo.toml
+++ b/idna/Cargo.toml
@@ -38,3 +38,6 @@ unicode-normalization = { version = "0.1.22", default-features = false }
 [[bench]]
 name = "all"
 harness = false
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--generate-link-to-definition"]
diff --git a/percent_encoding/Cargo.toml b/percent_encoding/Cargo.toml
index a61774c..4a3c087 100644
--- a/percent_encoding/Cargo.toml
+++ b/percent_encoding/Cargo.toml
@@ -13,3 +13,6 @@ rust-version = "1.51"
 default = ["std"]
 std = ["alloc"]
 alloc = []
+
+[package.metadata.docs.rs]
+rustdoc-args = ["--generate-link-to-definition"]
diff --git a/url/Cargo.toml b/url/Cargo.toml
index f327cbb..0afd903 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"
@@ -41,6 +47,7 @@ harness = false
 
 [package.metadata.docs.rs]
 features = ["serde"]
+rustdoc-args = ["--generate-link-to-definition"]
 
 [package.metadata.playground]
 features = ["serde"]


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(())
+}