Bug 1396819 - Improve error messages from capabilities matching. r?whimboo draft
authorAndreas Tolfsen <ato@sny.no>
Wed, 14 Feb 2018 15:43:56 +0000
changeset 756953 158081f5bf49e1f7ec6ec493cad79bd77b653538
parent 756941 d0d3693d9beff5477175a441fdb06e281e8b7f17
push id99596
push userbmo:ato@sny.no
push dateMon, 19 Feb 2018 12:43:19 +0000
reviewerswhimboo
bugs1396819
milestone60.0a1
Bug 1396819 - Improve error messages from capabilities matching. r?whimboo This patch somewhat marginally improves error messages returned during capabilities negotiation. MozReview-Commit-ID: IHTRk7Rl4ZU
testing/geckodriver/CHANGES.md
testing/webdriver/src/capabilities.rs
testing/webdriver/src/command.rs
--- a/testing/geckodriver/CHANGES.md
+++ b/testing/geckodriver/CHANGES.md
@@ -4,33 +4,37 @@ Change log
 All notable changes to this program is documented in this file.
 
 Unreleased
 ----------
 
 ### Added
 
 - New `--jsdebugger` flag to open the Browser Toolbox when Firefox
-  launches.  This is useful for debugging Marionette internals.
+  launches.  This is useful for debugging Marionette internals
 
 - Introduced the temporary, boolean capability
   `moz:useNonSpecCompliantPointerOrigin` to disable the WebDriver
-  conforming behavior of calculating the Pointer Origin.
+  conforming behavior of calculating the Pointer Origin
 
 ### Changed
 
 - HTTP status code for the [`StaleElementReference`] error changed
   from 400 (Bad Request) to 404 (Not Found)
 
 - Backtraces from geckodriver no longer substitute for missing
   Marionette stacktraces
 
 - `Delete Session` now allows Firefox to safely shutdown within 70s before
   force-killing the process
 
+### Fixed
+
+- Improved error messages for malformed capabilities
+
 
 0.19.1 (2017-10-30)
 -------------------
 
 ### Changed
 
 - Search suggestions in the location bar turned off as not to
   trigger network connections
--- a/testing/webdriver/src/capabilities.rs
+++ b/testing/webdriver/src/capabilities.rs
@@ -82,40 +82,40 @@ impl SpecNewSessionParameters {
         for key in null_entries {
             capabilities.remove(&key);
         }
 
         for (key, value) in capabilities.iter() {
             match &**key {
                 "acceptInsecureCerts" => if !value.is_boolean() {
                         return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                       "acceptInsecureCerts is not a boolean"))
+                                                       format!("acceptInsecureCerts is not boolean: {}", value)))
                     },
                 x @ "browserName" |
                 x @ "browserVersion" |
                 x @ "platformName" => if !value.is_string() {
                         return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                       format!("{} is not a string", x)))
+                                                       format!("{} is not a string: {}", x, value)))
                     },
                 "pageLoadStrategy" => {
                     try!(SpecNewSessionParameters::validate_page_load_strategy(value))
                 }
                 "proxy" => {
                     try!(SpecNewSessionParameters::validate_proxy(value))
                 },
                 "timeouts" => {
                     try!(SpecNewSessionParameters::validate_timeouts(value))
                 },
                 "unhandledPromptBehavior" => {
                     try!(SpecNewSessionParameters::validate_unhandled_prompt_behaviour(value))
                 }
                 x => {
                     if !x.contains(":") {
                         return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                       format!("{} is not the name of a known capability or a valid extension capability", x)))
+                                                       format!("{} is not the name of a known capability or extension capability", x)))
                     } else {
                         try!(browser_capabilities.validate_custom(x, value));
                     }
                 }
             }
         }
         Ok(capabilities)
     }
@@ -125,203 +125,242 @@ impl SpecNewSessionParameters {
             &Json::String(ref x) => {
                 match &**x {
                     "normal" |
                     "eager" |
                     "none" => {},
                     x => {
                         return Err(WebDriverError::new(
                             ErrorStatus::InvalidArgument,
-                            format!("\"{}\" is not a valid page load strategy", x)))
+                            format!("Invalid page load strategy: {}", x)))
                     }
                 }
             }
             _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
                                                 "pageLoadStrategy is not a string"))
         }
         Ok(())
     }
 
     fn validate_proxy(proxy_value: &Json) -> WebDriverResult<()> {
         let obj = try_opt!(proxy_value.as_object(),
                            ErrorStatus::InvalidArgument,
                            "proxy is not an object");
+
         for (key, value) in obj.iter() {
             match &**key {
                 "proxyType" => match value.as_string() {
                     Some("pac") |
                     Some("direct") |
                     Some("autodetect") |
                     Some("system") |
                     Some("manual") => {},
                     Some(x) => return Err(WebDriverError::new(
                         ErrorStatus::InvalidArgument,
-                        format!("{} is not a valid proxyType value", x))),
+                        format!("Invalid proxyType value: {}", x))),
                     None => return Err(WebDriverError::new(
                         ErrorStatus::InvalidArgument,
-                        "proxyType value is not a string")),
+                        format!("proxyType is not a string: {}", value))),
                 },
+
                 "proxyAutoconfigUrl" => match value.as_string() {
                     Some(x) => {
-                        try!(Url::parse(x).or(Err(WebDriverError::new(
+                        Url::parse(x).or(Err(WebDriverError::new(
                             ErrorStatus::InvalidArgument,
-                            "proxyAutoconfigUrl is not a valid url"))));
+                            format!("proxyAutoconfigUrl is not a valid URL: {}", x))))?;
                     },
                     None => return Err(WebDriverError::new(
                         ErrorStatus::InvalidArgument,
                         "proxyAutoconfigUrl is not a string"
                     ))
                 },
-                "ftpProxy" => try!(SpecNewSessionParameters::validate_host(value)),
-                "httpProxy" => try!(SpecNewSessionParameters::validate_host(value)),
-                "noProxy" => try!(SpecNewSessionParameters::validate_no_proxy(value)),
-                "sslProxy" => try!(SpecNewSessionParameters::validate_host(value)),
-                "socksProxy" => try!(SpecNewSessionParameters::validate_host(value)),
+
+                "ftpProxy" => SpecNewSessionParameters::validate_host(value, "ftpProxy")?,
+                "httpProxy" => SpecNewSessionParameters::validate_host(value, "httpProxy")?,
+                "noProxy" => SpecNewSessionParameters::validate_no_proxy(value)?,
+                "sslProxy" => SpecNewSessionParameters::validate_host(value, "sslProxy")?,
+                "socksProxy" => SpecNewSessionParameters::validate_host(value, "socksProxy")?,
                 "socksVersion" => if !value.is_number() {
-                    return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                   "socksVersion is not a number"))
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("socksVersion is not a number: {}", value)
+                    ))
                 },
+
                 x => return Err(WebDriverError::new(
                     ErrorStatus::InvalidArgument,
-                    format!("{} is not a valid proxy configuration capability", x)))
+                    format!("Invalid proxy configuration entry: {}", x)))
             }
         }
+
         Ok(())
     }
 
     fn validate_no_proxy(value: &Json) -> WebDriverResult<()> {
         match value.as_array() {
             Some(hosts) => {
                 for host in hosts {
                     match host.as_string() {
                         Some(_) => {},
                         None => return Err(WebDriverError::new(
                             ErrorStatus::InvalidArgument,
-                            format!("{} is not a string", host)
+                            format!("noProxy item is not a string: {}", host)
                         ))
                     }
                 }
             },
             None => return Err(WebDriverError::new(
                 ErrorStatus::InvalidArgument,
-                format!("{} is not an array", value)
+                format!("noProxy is not an array: {}", value)
             ))
         }
 
         Ok(())
     }
 
     /// Validate whether a named capability is JSON value is a string containing a host
     /// and possible port
-    fn validate_host(value: &Json) -> WebDriverResult<()> {
+    fn validate_host(value: &Json, entry: &str) -> WebDriverResult<()> {
         match value.as_string() {
             Some(host) => {
                 if host.contains("://") {
                     return Err(WebDriverError::new(
                         ErrorStatus::InvalidArgument,
-                        format!("{} contains a scheme", host)));
+                        format!("{} must not contain a scheme: {}", entry, host)));
                 }
 
                 // Temporarily add a scheme so the host can be parsed as URL
                 let s = String::from(format!("http://{}", host));
-                let url = try!(Url::parse(s.as_str()).or(Err(WebDriverError::new(
+                let url = Url::parse(s.as_str()).or(Err(WebDriverError::new(
                     ErrorStatus::InvalidArgument,
-                    format!("{} is not a valid host", host)))));
+                    format!("{} is not a valid URL: {}", entry, host))))?;
 
                 if url.username() != "" ||
                     url.password() != None ||
                     url.path() != "/" ||
                     url.query() != None ||
                     url.fragment() != None {
                         return Err(WebDriverError::new(
                             ErrorStatus::InvalidArgument,
-                            format!("{} is not of the form host[:port]", host)));
+                            format!("{} is not of the form host[:port]: {}", entry, host)));
                     }
             },
+
             None => return Err(WebDriverError::new(
                 ErrorStatus::InvalidArgument,
-                format!("{} is not a string", value)
+                format!("{} is not a string: {}", entry, value)
             ))
         }
+
         Ok(())
     }
 
     fn validate_timeouts(value: &Json) -> WebDriverResult<()> {
-        let obj = try_opt!(value.as_object(),
-                           ErrorStatus::InvalidArgument,
-                           "timeouts capability is not an object");
+        let obj = try_opt!(
+            value.as_object(),
+            ErrorStatus::InvalidArgument,
+            "timeouts capability is not an object"
+        );
+
         for (key, value) in obj.iter() {
             match &**key {
-                x @ "script" |
-                x @ "pageLoad" |
-                x @ "implicit" => {
-                    let timeout = try_opt!(value.as_i64(),
-                                           ErrorStatus::InvalidArgument,
-                                           format!("{} timeouts value is not an integer", x));
+                x @ "script" | x @ "pageLoad" | x @ "implicit" => {
+                    let timeout = try_opt!(
+                        value.as_i64(),
+                        ErrorStatus::InvalidArgument,
+                        format!("{} timeouts value is not an integer: {}", x, value)
+                    );
                     if timeout < 0 {
-                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                       format!("{} timeouts value is negative", x)))
+                        return Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            format!("{} timeouts value is negative: {}", x, timeout),
+                        ));
                     }
-                },
-                x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                    format!("{} is not a valid timeouts capability", x)))
+                }
+
+                x => {
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("Invalid timeouts capability entry: {}", x),
+                    ))
+                }
             }
         }
+
         Ok(())
     }
 
     fn validate_unhandled_prompt_behaviour(value: &Json) -> WebDriverResult<()> {
-        let behaviour = try_opt!(value.as_string(),
-                                 ErrorStatus::InvalidArgument,
-                                 "unhandledPromptBehavior capability is not a string");
+        let behaviour = try_opt!(
+            value.as_string(),
+            ErrorStatus::InvalidArgument,
+            format!("unhandledPromptBehavior is not a string: {}", value)
+        );
+
         match behaviour {
-            "dismiss" |
-            "accept" => {},
-            x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                format!("{} is not a valid unhandledPromptBehavior value", x)))        }
+            "dismiss" | "accept" => {}
+            x => {
+                return Err(WebDriverError::new(
+                    ErrorStatus::InvalidArgument,
+                    format!("Invalid unhandledPromptBehavior value: {}", x),
+                ))
+            }
+        }
+
         Ok(())
     }
 }
 
 impl Parameters for SpecNewSessionParameters {
     fn from_json(body: &Json) -> WebDriverResult<SpecNewSessionParameters> {
-        let data = try_opt!(body.as_object(),
-                            ErrorStatus::UnknownError,
-                            "Message body is not an object");
+        let data = try_opt!(
+            body.as_object(),
+            ErrorStatus::UnknownError,
+            format!("Malformed capabilities, message body is not an object: {}", body)
+        );
 
         let capabilities = try_opt!(
-            try_opt!(data.get("capabilities"),
-                     ErrorStatus::InvalidArgument,
-                     "Missing 'capabilities' parameter").as_object(),
+            try_opt!(
+                data.get("capabilities"),
+                ErrorStatus::InvalidArgument,
+                "Malformed capabilities, missing \"capabilities\" field"
+            ).as_object(),
             ErrorStatus::InvalidArgument,
-                     "'capabilities' parameter is not an object");
+            "Malformed capabilities, \"capabilities\" field is not an object}"
+        );
 
         let default_always_match = Json::Object(Capabilities::new());
-        let always_match = try_opt!(capabilities.get("alwaysMatch")
-                                   .unwrap_or(&default_always_match)
-                                   .as_object(),
-                                   ErrorStatus::InvalidArgument,
-                                   "'alwaysMatch' parameter is not an object");
+        let always_match = try_opt!(
+            capabilities
+                .get("alwaysMatch")
+                .unwrap_or(&default_always_match)
+                .as_object(),
+            ErrorStatus::InvalidArgument,
+            "Malformed capabilities, alwaysMatch field is not an object"
+        );
         let default_first_matches = Json::Array(vec![]);
-        let first_matches = try!(
-            try_opt!(capabilities.get("firstMatch")
-                     .unwrap_or(&default_first_matches)
-                     .as_array(),
-                     ErrorStatus::InvalidArgument,
-                     "'firstMatch' parameter is not an array")
-                .iter()
-                .map(|x| x.as_object()
-                     .map(|x| x.clone())
-                     .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
-                                                "'firstMatch' entry is not an object")))
-                .collect::<WebDriverResult<Vec<Capabilities>>>());
+        let first_matches = try_opt!(
+            capabilities
+                .get("firstMatch")
+                .unwrap_or(&default_first_matches)
+                .as_array(),
+            ErrorStatus::InvalidArgument,
+            "Malformed capabilities, firstMatch field is not an array"
+        ).iter()
+            .map(|x| {
+                x.as_object().map(|x| x.clone()).ok_or(WebDriverError::new(
+                    ErrorStatus::InvalidArgument,
+                    "Malformed capabilities, firstMatch entry is not an object",
+                ))
+            })
+            .collect::<WebDriverResult<Vec<Capabilities>>>()?;
 
         return Ok(SpecNewSessionParameters {
             alwaysMatch: always_match.clone(),
-            firstMatch: first_matches
+            firstMatch: first_matches,
         });
     }
 }
 
 impl ToJson for SpecNewSessionParameters {
     fn to_json(&self) -> Json {
         let mut body = BTreeMap::new();
         let mut capabilities = BTreeMap::new();
@@ -342,23 +381,20 @@ impl CapabilitiesMatching for SpecNewSes
             &self.firstMatch
         } else {
             &default
         };
 
         let merged_capabilities = capabilities_list
             .iter()
             .map(|first_match_entry| {
-                if first_match_entry.keys().any(
-                    |k| self.alwaysMatch.contains_key(k),
-                )
-                {
+                if first_match_entry.keys().any(|k| self.alwaysMatch.contains_key(k)) {
                     return Err(WebDriverError::new(
                         ErrorStatus::InvalidArgument,
-                        "'firstMatch' key shadowed a value in 'alwaysMatch'",
+                        "firstMatch key shadowed a value in alwaysMatch",
                     ));
                 }
                 let mut merged = self.alwaysMatch.clone();
                 merged.append(&mut first_match_entry.clone());
                 Ok(merged)
             })
             .map(|merged| {
                 merged.and_then(|x| self.validate(x, browser_capabilities))
@@ -477,58 +513,59 @@ impl CapabilitiesMatching for LegacyNewS
         );
         browser_capabilities.init(&capabilities);
         Ok(Some(capabilities))
     }
 }
 
 impl Parameters for LegacyNewSessionParameters {
     fn from_json(body: &Json) -> WebDriverResult<LegacyNewSessionParameters> {
-        let data = try_opt!(body.as_object(),
-                            ErrorStatus::UnknownError,
-                            "Message body is not an object");
-
-        let desired_capabilities =
-            if let Some(capabilities) = data.get("desiredCapabilities") {
-                try_opt!(capabilities.as_object(),
-                         ErrorStatus::InvalidArgument,
-                         "'desiredCapabilities' parameter is not an object").clone()
-            } else {
-                BTreeMap::new()
-            };
+        let data = try_opt!(
+            body.as_object(),
+            ErrorStatus::UnknownError,
+            format!("Malformed legacy capabilities, message body is not an object: {}", body)
+        );
 
-        let required_capabilities =
-            if let Some(capabilities) = data.get("requiredCapabilities") {
-                try_opt!(capabilities.as_object(),
-                         ErrorStatus::InvalidArgument,
-                         "'requiredCapabilities' parameter is not an object").clone()
-            } else {
-                BTreeMap::new()
-            };
+        let desired = if let Some(capabilities) = data.get("desiredCapabilities") {
+            try_opt!(
+                capabilities.as_object(),
+                ErrorStatus::InvalidArgument,
+                "Malformed legacy capabilities, desiredCapabilities field is not an object"
+            ).clone()
+        } else {
+            BTreeMap::new()
+        };
 
-        Ok(LegacyNewSessionParameters {
-            desired: desired_capabilities,
-            required: required_capabilities
-        })
+        let required = if let Some(capabilities) = data.get("requiredCapabilities") {
+            try_opt!(
+                capabilities.as_object(),
+                ErrorStatus::InvalidArgument,
+                "Malformed legacy capabilities, requiredCapabilities field is not an object"
+            ).clone()
+        } else {
+            BTreeMap::new()
+        };
+
+        Ok(LegacyNewSessionParameters { desired, required })
     }
 }
 
 impl ToJson for LegacyNewSessionParameters {
     fn to_json(&self) -> Json {
         let mut data = BTreeMap::new();
         data.insert("desiredCapabilities".to_owned(), self.desired.to_json());
         data.insert("requiredCapabilities".to_owned(), self.required.to_json());
         Json::Object(data)
     }
 }
 
 #[cfg(test)]
 mod tests {
     use rustc_serialize::json::Json;
-    use super::{WebDriverResult, SpecNewSessionParameters};
+    use super::{SpecNewSessionParameters, WebDriverResult};
 
     fn validate_proxy(value: &str) -> WebDriverResult<()> {
         let data = Json::from_str(value).unwrap();
         SpecNewSessionParameters::validate_proxy(&data)
     }
 
     #[test]
     fn test_validate_proxy() {
--- a/testing/webdriver/src/command.rs
+++ b/testing/webdriver/src/command.rs
@@ -348,17 +348,17 @@ impl<U: WebDriverExtensionRoute> WebDriv
         if requires_body {
             match Json::from_str(body) {
                 Ok(x @ Json::Object(_)) => Ok(x),
                 Ok(_) => {
                     Err(WebDriverError::new(ErrorStatus::InvalidArgument,
                                             "Body was not a JSON Object"))
                 }
                 Err(json::ParserError::SyntaxError(_, line, col)) => {
-                    let msg = format!("Failed to decode request as JSON: {}", body);
+                    let msg = format!("Failed to decode request as JSON: \"{}\"", body);
                     let stack = format!("Syntax error at :{}:{}", line, col);
                     Err(WebDriverError::new_with_stack(ErrorStatus::InvalidArgument, msg, stack))
                 }
                 Err(json::ParserError::IoError(e)) => {
                     Err(WebDriverError::new(ErrorStatus::InvalidArgument,
                                             format!("I/O error whilst decoding body: {}", e)))
                 }
             }