Bug 1560836 - Create marionette subcrate in geckodriver. r=ato

Differential Revision: https://phabricator.services.mozilla.com/D37375

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nupur Baghel 2019-07-09 12:10:10 +00:00
Родитель e95055a0fe
Коммит e8f6e00b4c
9 изменённых файлов: 711 добавлений и 0 удалений

Просмотреть файл

@ -37,6 +37,10 @@ exclude = [
"media/mp4parse-rust/mp4parse_capi",
"media/mp4parse-rust/mp4parse_fallible",
"xpcom/rust/gkrust_utils",
# Exclude temporarily till it is developed independently
# TODO(nupur): Remove after this is completed
"testing/geckodriver/marionette",
]
# Explicitly specify what our profiles use. The opt-level setting here is

Просмотреть файл

@ -0,0 +1,11 @@
[workspace]
[package]
name = "marionette"
version = "0.1.0"
authors = ["Mozilla"]
edition = "2018"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_repr = "0.1"

Просмотреть файл

@ -0,0 +1,12 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BoolValue {
value: bool,
}
impl BoolValue {
pub fn new(val: bool) -> Self {
BoolValue { value: val }
}
}

Просмотреть файл

@ -0,0 +1,177 @@
use std::error;
use std::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum Error {
Marionette(MarionetteError),
}
impl Error {
pub fn kind(&self) -> ErrorKind {
match *self {
Error::Marionette(ref err) => err.kind,
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Marionette(ref err) => fmt
.debug_struct("Marionette")
.field("kind", &err.kind)
.field("message", &err.message)
.field("stacktrace", &err.stack.clone())
.finish(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::Marionette(ref err) => write!(fmt, "{}: {}", err.kind, err.message),
}
}
}
impl error::Error for Error {
fn description(&self) -> &str {
match self {
Error::Marionette(_) => self.kind().as_str(),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub struct MarionetteError {
#[serde(rename = "error", skip_serializing)]
pub kind: ErrorKind,
#[serde(default = "empty_string")]
pub message: String,
#[serde(rename = "stacktrace", default = "empty_string")]
pub stack: String,
}
fn empty_string() -> String {
"".to_owned()
}
impl Into<Error> for MarionetteError {
fn into(self) -> Error {
Error::Marionette(self)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum ErrorKind {
#[serde(rename = "element click intercepted")]
ElementClickIntercepted,
#[serde(rename = "element not accessible")]
ElementNotAccessible,
#[serde(rename = "element not interactable")]
ElementNotInteractable,
#[serde(rename = "insecure certificate")]
InsecureCertificate,
#[serde(rename = "invalid argument")]
InvalidArgument,
#[serde(rename = "invalid cookie")]
InvalidCookieDomain,
#[serde(rename = "invalid element state")]
InvalidElementState,
#[serde(rename = "invalid selector")]
InvalidSelector,
#[serde(rename = "invalid session id")]
InvalidSessionId,
#[serde(rename = "javascript error")]
JavaScript,
#[serde(rename = "move target out of bounds")]
MoveTargetOutOfBounds,
#[serde(rename = "no such alert")]
NoSuchAlert,
#[serde(rename = "no such element")]
NoSuchElement,
#[serde(rename = "no such frame")]
NoSuchFrame,
#[serde(rename = "no such window")]
NoSuchWindow,
#[serde(rename = "script timeout")]
ScriptTimeout,
#[serde(rename = "session not created")]
SessionNotCreated,
#[serde(rename = "stale element reference")]
StaleElementReference,
#[serde(rename = "timeout")]
Timeout,
#[serde(rename = "unable to set cookie")]
UnableToSetCookie,
#[serde(rename = "unexpected alert open")]
UnexpectedAlertOpen,
#[serde(rename = "unknown command")]
UnknownCommand,
#[serde(rename = "unknown error")]
Unknown,
#[serde(rename = "unsupported operation")]
UnsupportedOperation,
#[serde(rename = "webdriver error")]
WebDriver,
}
impl ErrorKind {
pub(crate) fn as_str(self) -> &'static str {
use ErrorKind::*;
match self {
ElementClickIntercepted => "element click intercepted",
ElementNotAccessible => "element not accessible",
ElementNotInteractable => "element not interactable",
InsecureCertificate => "insecure certificate",
InvalidArgument => "invalid argument",
InvalidCookieDomain => "invalid cookie",
InvalidElementState => "invalid element state",
InvalidSelector => "invalid selector",
InvalidSessionId => "invalid session id",
JavaScript => "javascript error",
MoveTargetOutOfBounds => "move target out of bounds",
NoSuchAlert => "no such alert",
NoSuchElement => "no such element",
NoSuchFrame => "no such frame",
NoSuchWindow => "no such window",
ScriptTimeout => "script timeout",
SessionNotCreated => "session not created",
StaleElementReference => "stale eelement referencee",
Timeout => "timeout",
UnableToSetCookie => "unable to set cookie",
UnexpectedAlertOpen => "unexpected alert open",
UnknownCommand => "unknown command",
Unknown => "unknown error",
UnsupportedOperation => "unsupported operation",
WebDriver => "webdriver error",
}
}
}
impl fmt::Display for ErrorKind {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
write!(fmt, "{}", self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::assert_ser;
use serde_json::json;
#[test]
fn test_error_serialize() {
let err = MarionetteError {
kind: ErrorKind::Timeout,
message: "".into(),
stack: "".into(),
};
assert_ser(&err, json!({"message": "", "stacktrace": ""}));
}
}

Просмотреть файл

@ -0,0 +1,9 @@
pub mod error;
pub mod common;
pub mod marionette;
pub mod message;
pub mod webdriver;
#[cfg(test)]
mod test;

Просмотреть файл

@ -0,0 +1,46 @@
use serde::{Deserialize, Serialize};
use crate::common::BoolValue;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Command {
#[serde(rename = "Marionette:AcceptConnections")]
AcceptConnections(BoolValue),
#[serde(rename = "Marionette:GetContext")]
GetContext,
#[serde(rename = "Marionette:GetScreenOrientation")]
GetScreenOrientation,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::assert_ser_de;
use serde_json::json;
#[test]
fn test_json_command_accept_connections() {
assert_ser_de(
&Command::AcceptConnections(BoolValue::new(false)),
json!({"Marionette:AcceptConnections": {"value": false }}),
);
}
#[test]
fn test_json_command_get_context() {
assert_ser_de(&Command::GetContext, json!("Marionette:GetContext"));
}
#[test]
fn test_json_command_get_screen_orientation() {
assert_ser_de(
&Command::GetScreenOrientation,
json!("Marionette:GetScreenOrientation"),
);
}
#[test]
fn test_json_command_invalid() {
assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
}
}

Просмотреть файл

@ -0,0 +1,333 @@
use serde::de::{self, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::{Map, Value};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::error::Error as StdError;
use std::fmt;
use crate::error::{Error, ErrorKind};
use crate::marionette;
use crate::webdriver;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
enum Command {
WebDriver(webdriver::Command),
Marionette(marionette::Command),
}
impl Command {
pub fn name(&self) -> String {
let (command_name, _) = self.first_entry();
command_name
}
fn params(&self) -> Value {
let (_, params) = self.first_entry();
params
}
fn first_entry(&self) -> (String, serde_json::Value) {
match serde_json::to_value(&self).unwrap() {
Value::String(cmd) => (cmd, Value::Object(Map::new())),
Value::Object(items) => {
let mut iter = items.iter();
let (cmd, params) = iter.next().unwrap();
(cmd.to_string(), params.clone())
}
_ => unreachable!(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize_repr, Deserialize_repr)]
#[repr(u8)]
enum MessageDirection {
Incoming = 0,
Outgoing = 1,
}
type MessageId = u32;
type Payload = Map<String, Value>;
#[derive(Debug, Clone, PartialEq)]
struct Request(MessageId, Command);
impl Request {
pub fn id(&self) -> MessageId {
self.0
}
pub fn command(&self) -> &Command {
&self.1
}
pub fn params(&self) -> Value {
self.command().params()
}
}
impl Serialize for Request {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
(
MessageDirection::Incoming,
self.id(),
self.command().name(),
self.params(),
)
.serialize(serializer)
}
}
#[derive(Debug, PartialEq)]
enum Response {
Result { id: MessageId, result: Payload },
Error { id: MessageId, error: Error },
}
impl Serialize for Response {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Response::Result { id, result } => {
(MessageDirection::Outgoing, id, Value::Null, &result).serialize(serializer)
}
Response::Error { id, error } => {
(MessageDirection::Outgoing, id, &error.description(), &error).serialize(serializer)
}
}
}
}
#[derive(Debug, PartialEq, Serialize)]
#[serde(untagged)]
enum Message {
Incoming(Request),
Outgoing(Response),
}
struct MessageVisitor;
impl<'de> Visitor<'de> for MessageVisitor {
type Value = Message;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("four-element array")
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let direction = seq
.next_element::<MessageDirection>()?
.ok_or_else(|| de::Error::invalid_length(0, &self))?;
let id: MessageId = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(1, &self))?;
let msg = match direction {
MessageDirection::Incoming => {
let name: String = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let params: Value = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
let command = match params {
Value::Object(ref items) if !items.is_empty() => {
let command_to_params = {
let mut m = Map::new();
m.insert(name, params);
Value::Object(m)
};
serde_json::from_value(command_to_params).map_err(de::Error::custom)
}
Value::Object(_) | Value::Null => {
serde_json::from_value(Value::String(name)).map_err(de::Error::custom)
}
x => Err(de::Error::custom(format!("unknown params type: {}", x))),
}?;
Message::Incoming(Request(id, command))
}
MessageDirection::Outgoing => {
let maybe_error: Option<ErrorKind> = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(2, &self))?;
let response = if let Some(kind) = maybe_error {
// intermediary transformation to Error by inserting {"error": <kind>}
// so it can be treated as a MarionetteError
let mut details: Map<String, Value> = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
details.insert("error".to_string(), serde_json::to_value(kind).unwrap());
let error: Error = serde_json::from_value(Value::Object(details)).unwrap();
Response::Error { id, error }
} else {
let result: Payload = seq
.next_element()?
.ok_or_else(|| de::Error::invalid_length(3, &self))?;
Response::Result { id, result }
};
Message::Outgoing(response)
}
};
Ok(msg)
}
}
impl<'de> Deserialize<'de> for Message {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(MessageVisitor)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::common::*;
use crate::error::MarionetteError;
use crate::test::assert_ser_de;
#[test]
fn test_incoming() {
let json =
json!([0, 42, "WebDriver:FindElement", {"using": "css selector", "value": "value"}]);
let find_element = webdriver::Command::FindElement(webdriver::Locator {
using: webdriver::Selector::CSS,
value: "value".into(),
});
let req = Request(42, Command::WebDriver(find_element));
let msg = Message::Incoming(req);
assert_ser_de(&msg, json);
}
#[test]
fn test_incoming_empty_params() {
let json = json!([0, 42, "WebDriver:GetTimeouts", {}]);
let req = Request(42, Command::WebDriver(webdriver::Command::GetTimeouts));
let msg = Message::Incoming(req);
assert_ser_de(&msg, json);
}
#[test]
fn test_incoming_common_params() {
let json = json!([0, 42, "Marionette:AcceptConnections", {"value": false}]);
let params = BoolValue::new(false);
let req = Request(
42,
Command::Marionette(marionette::Command::AcceptConnections(params)),
);
let msg = Message::Incoming(req);
assert_ser_de(&msg, json);
}
#[test]
fn test_incoming_params_derived() {
assert!(serde_json::from_value::<Message>(
json!([0,42,"WebDriver:FindElement",{"using":"foo","value":"foo"}])
)
.is_err());
assert!(serde_json::from_value::<Message>(
json!([0,42,"Marionette:AcceptConnections",{"value":"foo"}])
)
.is_err());
}
#[test]
fn test_incoming_no_params() {
assert!(serde_json::from_value::<Message>(
json!([0,42,"WebDriver:GetTimeouts",{"value":true}])
)
.is_err());
assert!(serde_json::from_value::<Message>(
json!([0,42,"Marionette:Context",{"value":"foo"}])
)
.is_err());
assert!(serde_json::from_value::<Message>(
json!([0,42,"Marionette:GetScreenOrientation",{"value":true}])
)
.is_err());
}
#[test]
fn test_outgoing_result() {
let json = json!([1, 42, null, { "value": null }]);
// TODO: payload is currently untyped
let mut result = Map::new();
result.insert("value".into(), Value::Null);
let msg = Message::Outgoing(Response::Result { id: 42, result });
assert_ser_de(&msg, json);
}
#[test]
fn test_outgoing_error() {
let json = json!([1, 42, "no such element", {"message": "", "stacktrace": ""}]);
let error: Error = MarionetteError {
kind: ErrorKind::NoSuchElement,
message: "".into(),
stack: "".into(),
}
.into();
let msg = Message::Outgoing(Response::Error { id: 42, error });
assert_ser_de(&msg, json);
}
#[test]
fn test_invalid_type() {
assert!(
serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts", {}])).is_err()
);
assert!(serde_json::from_value::<Message>(json!([3, 42, "no such element", {}])).is_err());
}
#[test]
fn test_missing_fields() {
// all fields are required
assert!(
serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts"])).is_err()
);
assert!(serde_json::from_value::<Message>(json!([2, 42])).is_err());
assert!(serde_json::from_value::<Message>(json!([2])).is_err());
assert!(serde_json::from_value::<Message>(json!([])).is_err());
}
#[test]
fn test_unknown_command() {
assert!(serde_json::from_value::<Message>(json!([0, 42, "hooba", {}])).is_err());
}
#[test]
fn test_unknown_error() {
assert!(serde_json::from_value::<Message>(json!([1, 42, "flooba", {}])).is_err());
}
#[test]
fn test_message_id_bounds() {
let overflow = i64::from(std::u32::MAX) + 1;
let underflow = -1;
fn get_timeouts(message_id: i64) -> Value {
json!([0, message_id, "WebDriver:GetTimeouts", {}])
}
assert!(serde_json::from_value::<Message>(get_timeouts(overflow)).is_err());
assert!(serde_json::from_value::<Message>(get_timeouts(underflow)).is_err());
}
}

Просмотреть файл

@ -0,0 +1,19 @@
pub fn assert_ser_de<T>(data: &T, json: serde_json::Value)
where
T: std::fmt::Debug,
T: std::cmp::PartialEq,
T: serde::de::DeserializeOwned,
T: serde::Serialize,
{
assert_eq!(serde_json::to_value(data).unwrap(), json);
assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
}
pub fn assert_ser<T>(data: &T, json: serde_json::Value)
where
T: std::fmt::Debug,
T: std::cmp::PartialEq,
T: serde::Serialize,
{
assert_eq!(serde_json::to_value(data).unwrap(), json);
}

Просмотреть файл

@ -0,0 +1,100 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Locator {
pub using: Selector,
pub value: String,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Selector {
#[serde(rename = "css selector")]
CSS,
#[serde(rename = "link text")]
LinkText,
#[serde(rename = "partial link text")]
PartialLinkText,
#[serde(rename = "tag name")]
TagName,
#[serde(rename = "xpath")]
XPath,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Command {
#[serde(rename = "WebDriver:FindElement")]
FindElement(Locator),
#[serde(rename = "WebDriver:GetTimeouts")]
GetTimeouts,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::assert_ser_de;
use serde_json::json;
#[test]
fn test_json_selector_css() {
assert_ser_de(&Selector::CSS, json!("css selector"));
}
#[test]
fn test_json_selector_link_text() {
assert_ser_de(&Selector::LinkText, json!("link text"));
}
#[test]
fn test_json_selector_partial_link_text() {
assert_ser_de(&Selector::PartialLinkText, json!("partial link text"));
}
#[test]
fn test_json_selector_tag_name() {
assert_ser_de(&Selector::TagName, json!("tag name"));
}
#[test]
fn test_json_selector_xpath() {
assert_ser_de(&Selector::XPath, json!("xpath"));
}
#[test]
fn test_json_selector_invalid() {
assert!(serde_json::from_value::<Selector>(json!("foo")).is_err());
}
#[test]
fn test_json_locator() {
let json = json!({
"using": "partial link text",
"value": "link text",
});
let data = Locator {
using: Selector::PartialLinkText,
value: "link text".into(),
};
assert_ser_de(&data, json);
}
#[test]
fn test_command_with_params() {
let locator = Locator {
using: Selector::CSS,
value: "value".into(),
};
let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}});
assert_ser_de(&Command::FindElement(locator), json);
}
#[test]
fn test_empty_commands() {
assert_ser_de(&Command::GetTimeouts, json!("WebDriver:GetTimeouts"));
}
#[test]
fn test_json_command_invalid() {
assert!(serde_json::from_value::<Command>(json!("foo")).is_err());
}
}