diff --git a/testing/geckodriver/src/command.rs b/testing/geckodriver/src/command.rs index 3bc9302eaa76..dfeb49d321a7 100644 --- a/testing/geckodriver/src/command.rs +++ b/testing/geckodriver/src/command.rs @@ -1,7 +1,7 @@ +use core::u16; use std::collections::TreeMap; use serialize::json; -use serialize::{Encodable}; -use serialize::json::{ToJson}; +use serialize::json::{ToJson, Json}; use regex::Captures; use common::{WebDriverResult, WebDriverError, ErrorStatus}; @@ -22,7 +22,24 @@ pub enum WebDriverCommand { GetWindowHandle, GetWindowHandles, Close, - Timeouts(TimeoutsParameters) + Timeouts(TimeoutsParameters), + SetWindowSize(WindowSizeParameters), + GetWindowSize, + MaximizeWindow, +// FullscreenWindow // Not supported in marionette + SwitchToWindow(SwitchToWindowParameters), + SwitchToFrame(SwitchToFrameParameters), + SwitchToParentFrame, + IsDisplayed(WebElement), + IsSelected(WebElement), + GetElementAttribute(WebElement, String), + GetCSSValue(WebElement, String), + GetElementText(WebElement), + GetElementTagName(WebElement), + GetElementRect(WebElement), + IsEnabled(WebElement), + ExecuteScript(JavascriptCommandParameters), + ExecuteAsyncScript(JavascriptCommandParameters) } #[deriving(PartialEq)] @@ -41,22 +58,22 @@ impl WebDriverMessage { pub fn from_http(match_type: MatchType, params: &Captures, body: &str) -> WebDriverResult { let session_id = WebDriverMessage::get_session_id(params); - let body_data = match json::from_str(body) { - Ok(x) => x, - Err(_) => return Err(WebDriverError::new(ErrorStatus::UnknownError, - "Failed to decode request body")) + let body_data = if body != "" { + debug!("Got request body {}", body); + match json::from_str(body) { + Ok(x) => x, + Err(_) => return Err(WebDriverError::new(ErrorStatus::UnknownError, + format!("Failed to decode request body as json: {}", body).as_slice())) + } + } else { + json::Null }; let command = match match_type { MatchType::NewSession => WebDriverCommand::NewSession, MatchType::DeleteSession => WebDriverCommand::DeleteSession, MatchType::Get => { - match GetParameters::from_json(&body_data) { - Ok(parameters) => { - WebDriverCommand::Get(parameters) - }, - Err(_) => return Err(WebDriverError::new(ErrorStatus::UnknownError, - "Failed to decode request body")) - } + let parameters: GetParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::Get(parameters) }, MatchType::GetCurrentUrl => WebDriverCommand::GetCurrentUrl, MatchType::GoBack => WebDriverCommand::GoBack, @@ -67,52 +84,201 @@ impl WebDriverMessage { MatchType::GetWindowHandles => WebDriverCommand::GetWindowHandles, MatchType::Close => WebDriverCommand::Close, MatchType::Timeouts => { - let parameters_result = TimeoutsParameters::from_json(&body_data); - match parameters_result { - Ok(parameters) => WebDriverCommand::Timeouts(parameters), - Err(_) => return Err(WebDriverError::new(ErrorStatus::UnknownError, - "Failed to decode request body")) - } + let parameters: TimeoutsParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::Timeouts(parameters) + }, + MatchType::SetWindowSize => { + let parameters: WindowSizeParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::SetWindowSize(parameters) + }, + MatchType::GetWindowSize => WebDriverCommand::GetWindowSize, + MatchType::MaximizeWindow => WebDriverCommand::MaximizeWindow, + MatchType::SwitchToWindow => { + let parameters: SwitchToWindowParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::SwitchToWindow(parameters) + } + MatchType::SwitchToFrame => { + let parameters: SwitchToFrameParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::SwitchToFrame(parameters) + }, + MatchType::SwitchToParentFrame => WebDriverCommand::SwitchToParentFrame, + MatchType::IsDisplayed => { + let element = WebElement::new(params.name("elementId").to_string()); + WebDriverCommand::IsDisplayed(element) + }, + MatchType::IsSelected => { + let element = WebElement::new(params.name("elementId").to_string()); + WebDriverCommand::IsSelected(element) + }, + MatchType::GetElementAttribute => { + let element = WebElement::new(params.name("elementId").to_string()); + let attr = params.name("name").to_string(); + WebDriverCommand::GetElementAttribute(element, attr) + }, + MatchType::GetCSSValue => { + let element = WebElement::new(params.name("elementId").to_string()); + let property = params.name("propertyName").to_string(); + WebDriverCommand::GetCSSValue(element, property) + }, + MatchType::GetElementText => { + let element = WebElement::new(params.name("elementId").to_string()); + WebDriverCommand::GetElementText(element) + }, + MatchType::GetElementTagName => { + let element = WebElement::new(params.name("elementId").to_string()); + WebDriverCommand::GetElementTagName(element) + }, + MatchType::GetElementRect => { + let element = WebElement::new(params.name("elementId").to_string()); + WebDriverCommand::GetElementText(element) + }, + MatchType::IsEnabled => { + let element = WebElement::new(params.name("elementId").to_string()); + WebDriverCommand::IsEnabled(element) + }, + MatchType::ExecuteScript => { + let parameters: JavascriptCommandParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::ExecuteScript(parameters) + } + MatchType::ExecuteAsyncScript => { + let parameters: JavascriptCommandParameters = try!(Parameters::from_json(&body_data)); + WebDriverCommand::ExecuteAsyncScript(parameters) } }; Ok(WebDriverMessage::new(session_id, command)) } fn get_session_id(params: &Captures) -> Option { - let session_id_str = params.name("sessionId"); - if session_id_str == "" { - None - } else { - Some(session_id_str.to_string()) + match params.name("sessionId") { + "" => None, + x => Some(x.to_string()) } } } impl ToJson for WebDriverMessage { fn to_json(&self) -> json::Json { - match self.command { - WebDriverCommand::Get(ref x) => { - x.to_json() + let mut data = TreeMap::new(); + let parameters = match self.command { + WebDriverCommand::GetMarionetteId | WebDriverCommand::NewSession | + WebDriverCommand::DeleteSession | WebDriverCommand::GetCurrentUrl | + WebDriverCommand::GoBack | WebDriverCommand::GoForward | WebDriverCommand::Refresh | + WebDriverCommand::GetTitle | WebDriverCommand::GetWindowHandle | + WebDriverCommand::GetWindowHandles | WebDriverCommand::Close | + WebDriverCommand::GetWindowSize | WebDriverCommand::MaximizeWindow | + WebDriverCommand::SwitchToParentFrame | WebDriverCommand::IsDisplayed(_) | + WebDriverCommand::IsSelected(_) | WebDriverCommand::GetElementAttribute(_, _) | + WebDriverCommand::GetCSSValue(_, _) | WebDriverCommand::GetElementText(_) | + WebDriverCommand::GetElementTagName(_) | WebDriverCommand::GetElementRect(_) | + WebDriverCommand::IsEnabled(_) => { + None }, - WebDriverCommand::Timeouts(ref x) => { - x.to_json() - } - _ => { - json::Object(TreeMap::new()) + WebDriverCommand::Get(ref x) => Some(x.to_json()), + WebDriverCommand::Timeouts(ref x) => Some(x.to_json()), + WebDriverCommand::SetWindowSize(ref x) => Some(x.to_json()), + WebDriverCommand::SwitchToWindow(ref x) => Some(x.to_json()), + WebDriverCommand::SwitchToFrame(ref x) => Some(x.to_json()), + WebDriverCommand::ExecuteScript(ref x) | + WebDriverCommand::ExecuteAsyncScript(ref x) => Some(x.to_json()) + }; + if parameters.is_some() { + data.insert("parameters".to_string(), parameters.unwrap()); + } + json::Object(data) + } +} + +#[deriving(PartialEq)] +struct WebElement { + id: String +} + +impl WebElement { + fn new(id: String) -> WebElement { + WebElement { + id: id + } + } +} + +impl ToJson for WebElement { + fn to_json(&self) -> json::Json { + let mut data = TreeMap::new(); + data.insert("element-6066-11e4-a52e-4f735466cecf".to_string(), self.id.to_json()); + json::Object(data) + } +} + +#[deriving(PartialEq)] +enum FrameId { + Short(u16), + Element(WebElement), + Null +} + +impl ToJson for FrameId { + fn to_json(&self) -> json::Json { + match *self { + FrameId::Short(x) => { + json::Json::U64(x as u64) + }, + FrameId::Element(ref x) => { + json::Json::String(x.id.clone()) + }, + FrameId::Null => { + json::Json::Null } } } } +#[deriving(PartialEq, Clone)] +enum Nullable { + Value(T), + Null +} + +impl Nullable { + //This is not very pretty + fn from_json WebDriverResult>(value: &json::Json, f: F) -> WebDriverResult> { + if value.is_null() { + Ok(Nullable::Null) + } else { + Ok(Nullable::Value(try!(f(value)))) + } + } +} + +impl ToJson for Nullable { + fn to_json(&self) -> json::Json { + match *self { + Nullable::Value(ref x) => x.to_json(), + Nullable::Null => json::Json::Null + } + } +} + +trait Parameters { + fn from_json(body: &json::Json) -> WebDriverResult; +} + #[deriving(PartialEq)] struct GetParameters { url: String } -impl GetParameters { - pub fn from_json(body: &json::Json) -> Result { +impl Parameters for GetParameters { + fn from_json(body: &json::Json) -> WebDriverResult { + let data = try_opt!(body.as_object(), ErrorStatus::UnknownError, + "Message body was not an object"); + let url = try_opt!( + try_opt!(data.get("url"), + ErrorStatus::InvalidArgument, + "Missing 'url' parameter").as_string(), + ErrorStatus::InvalidArgument, + "'url' not a string"); return Ok(GetParameters { - url: body.find("url").unwrap().as_string().unwrap().to_string() + url: url.to_string() }) } } @@ -128,14 +294,29 @@ impl ToJson for GetParameters { #[deriving(PartialEq)] struct TimeoutsParameters { type_: String, - ms: u32 + ms: u64 } -impl TimeoutsParameters { - pub fn from_json(body: &json::Json) -> Result { +impl Parameters for TimeoutsParameters { + fn from_json(body: &json::Json) -> WebDriverResult { + let data = try_opt!(body.as_object(), ErrorStatus::UnknownError, + "Message body was not an object"); + let type_ = try_opt!( + try_opt!(data.get("type"), + ErrorStatus::InvalidArgument, + "Missing 'type' parameter").as_string(), + ErrorStatus::InvalidArgument, + "'type' not a string"); + + let ms = try_opt!( + try_opt!(data.get("ms"), + ErrorStatus::InvalidArgument, + "Missing 'ms' parameter").as_u64(), + ErrorStatus::InvalidArgument, + "'ms' not an integer"); return Ok(TimeoutsParameters { - type_: body.find("type").unwrap().as_string().unwrap().to_string(), - ms: body.find("ms").unwrap().as_i64().unwrap() as u32 + type_: type_.to_string(), + ms: ms }) } } @@ -148,3 +329,164 @@ impl ToJson for TimeoutsParameters { json::Object(data) } } + +#[deriving(PartialEq)] +struct WindowSizeParameters { + width: u64, + height: u64 +} + +impl Parameters for WindowSizeParameters { + fn from_json(body: &json::Json) -> WebDriverResult { + let data = try_opt!(body.as_object(), ErrorStatus::UnknownError, + "Message body was not an object"); + let height = try_opt!( + try_opt!(data.get("height"), + ErrorStatus::InvalidArgument, + "Missing 'height' parameter").as_u64(), + ErrorStatus::InvalidArgument, + "'height' is not a positive integer"); + let width = try_opt!( + try_opt!(data.get("width"), + ErrorStatus::InvalidArgument, + "Missing width parameter").as_u64(), + ErrorStatus::InvalidArgument, + "'width' is not a positive integer"); + return Ok(WindowSizeParameters { + height: height, + width: width + }) + } +} + +impl ToJson for WindowSizeParameters { + fn to_json(&self) -> json::Json { + let mut data = TreeMap::new(); + data.insert("width".to_string(), self.width.to_json()); + data.insert("height".to_string(), self.height.to_json()); + json::Object(data) + } +} + +#[deriving(PartialEq)] +struct SwitchToWindowParameters { + handle: String +} + +impl Parameters for SwitchToWindowParameters { + fn from_json(body: &json::Json) -> WebDriverResult { + let data = try_opt!(body.as_object(), ErrorStatus::UnknownError, + "Message body was not an object"); + let handle = try_opt!( + try_opt!(data.get("handle"), + ErrorStatus::InvalidArgument, + "Missing 'handle' parameter").as_string(), + ErrorStatus::InvalidArgument, + "'handle' not a string"); + return Ok(SwitchToWindowParameters { + handle: handle.to_string() + }) + } +} + +impl ToJson for SwitchToWindowParameters { + fn to_json(&self) -> json::Json { + let mut data = TreeMap::new(); + data.insert("handle".to_string(), self.handle.to_json()); + json::Object(data) + } +} + +#[deriving(PartialEq)] +struct SwitchToFrameParameters { + id: FrameId +} + +impl Parameters for SwitchToFrameParameters { + fn from_json(body: &json::Json) -> WebDriverResult { + let data = try_opt!(body.as_object(), ErrorStatus::UnknownError, + "Message body was not an object"); + let id_json = try_opt!(data.get("id"), + ErrorStatus::UnknownError, + "Missing 'id' parameter"); + let id = if id_json.is_u64() { + let value = id_json.as_u64().unwrap(); + if value <= u16::MAX as u64 { + FrameId::Short(value as u16) + } else { + return Err(WebDriverError::new(ErrorStatus::NoSuchFrame, + "frame id out of range")) + } + } else if id_json.is_null() { + FrameId::Null + } else if id_json.is_string() { + let value = id_json.as_string().unwrap(); + FrameId::Element(WebElement::new(value.to_string())) + } else { + return Err(WebDriverError::new(ErrorStatus::NoSuchFrame, + "frame id has unexpected type")) + }; + Ok(SwitchToFrameParameters { + id: id + }) + } +} + +impl ToJson for SwitchToFrameParameters { + fn to_json(&self) -> json::Json { + let mut data = TreeMap::new(); + data.insert("id".to_string(), self.id.to_json()); + json::Object(data) + } +} + +#[deriving(PartialEq)] +struct JavascriptCommandParameters { + script: String, + args: Nullable> +} + +impl Parameters for JavascriptCommandParameters { + fn from_json(body: &json::Json) -> WebDriverResult { + let data = try_opt!(body.as_object(), + ErrorStatus::UnknownError, + "Message body was not an object"); + + let args_json = try_opt!(data.get("args"), + ErrorStatus::UnknownError, + "Missing args parameter"); + + let args = try!(Nullable::from_json( + args_json, + |x| { + Ok((try_opt!(x.as_array(), + ErrorStatus::UnknownError, + "Failed to convert args to Array")).clone()) + })); + + //TODO: Look for WebElements in args? + let script = try_opt!( + try_opt!(data.get("script"), + ErrorStatus::UnknownError, + "Missing script parameter").as_string(), + ErrorStatus::UnknownError, + "Failed to convert script to String"); + Ok(JavascriptCommandParameters { + script: script.to_string(), + args: args.clone() + }) + } +} + +impl ToJson for JavascriptCommandParameters { + fn to_json(&self) -> json::Json { + let mut data = TreeMap::new(); + //TODO: Wrap script so that it becomes marionette-compatible + data.insert("script".to_string(), self.script.to_json()); + data.insert("args".to_string(), self.args.to_json()); + data.insert("newSandbox".to_string(), false.to_json()); + data.insert("specialPowers".to_string(), false.to_json()); + data.insert("scriptTimeout".to_string(), json::Json::Null); + json::Object(data) + } +} diff --git a/testing/geckodriver/src/common.rs b/testing/geckodriver/src/common.rs index ce8920aa6ff7..27643420b722 100644 --- a/testing/geckodriver/src/common.rs +++ b/testing/geckodriver/src/common.rs @@ -1,8 +1,9 @@ -use std::collections::TreeMap; use serialize::json; -use serialize::json::ToJson; +use serialize::json::{ToJson, ParserError}; +use std::collections::TreeMap; +use std::error::{Error, FromError}; -#[deriving(PartialEq)] +#[deriving(PartialEq, Show)] pub enum ErrorStatus { ElementNotSelectable, ElementNotVisible, @@ -13,7 +14,7 @@ pub enum ErrorStatus { InvalidSelector, InvalidSessionId, JavascriptError, - MoveTagetOutOfBounds, + MoveTargetOutOfBounds, NoSuchAlert, NoSuchElement, NoSuchFrame, @@ -32,6 +33,7 @@ pub enum ErrorStatus { pub type WebDriverResult = Result; +#[deriving(Show)] pub struct WebDriverError { pub status: ErrorStatus, pub message: String @@ -45,7 +47,7 @@ impl WebDriverError { } } - pub fn status_code(&self) -> String { + pub fn status_code(&self) -> &str { match self.status { ErrorStatus::ElementNotSelectable => "element not selectable", ErrorStatus::ElementNotVisible => "element not visible", @@ -56,7 +58,7 @@ impl WebDriverError { ErrorStatus::InvalidSelector => "invalid selector", ErrorStatus::InvalidSessionId => "invalid session id", ErrorStatus::JavascriptError => "javascript error", - ErrorStatus::MoveTagetOutOfBounds => "move target out of bounds", + ErrorStatus::MoveTargetOutOfBounds => "move target out of bounds", ErrorStatus::NoSuchAlert => "no such alert", ErrorStatus::NoSuchElement => "no such element", ErrorStatus::NoSuchFrame => "no such frame", @@ -71,7 +73,7 @@ impl WebDriverError { ErrorStatus::UnknownPath => "unknown command", ErrorStatus::UnknownMethod => "unknown command", ErrorStatus::UnsupportedOperation => "unsupported operation", - }.to_string() + } } pub fn http_status(&self) -> int { @@ -81,6 +83,10 @@ impl WebDriverError { _ => 500 } } + + pub fn to_json_string(&self) -> String { + self.to_json().to_string() + } } impl ToJson for WebDriverError { @@ -91,3 +97,24 @@ impl ToJson for WebDriverError { json::Object(data) } } + +impl Error for WebDriverError { + fn description(&self) -> &str { + self.status_code() + } + + fn detail(&self) -> Option { + Some(self.message.clone()) + } + + fn cause(&self) -> Option<&Error> { + None + } +} + +impl FromError for WebDriverError { + fn from_error(err: ParserError) -> WebDriverError { + let msg = format!("{}", err); + WebDriverError::new(ErrorStatus::UnknownError, msg.as_slice()) + } +} diff --git a/testing/geckodriver/src/httpserver.rs b/testing/geckodriver/src/httpserver.rs index 7fdfa929b838..6f32119ac8d5 100644 --- a/testing/geckodriver/src/httpserver.rs +++ b/testing/geckodriver/src/httpserver.rs @@ -1,7 +1,5 @@ use std::io::net::ip::IpAddr; use std::sync::Mutex; -use std::collections::HashMap; -use serialize::json::ToJson; use hyper::header::common::ContentLength; use hyper::method::Post; @@ -9,24 +7,24 @@ use hyper::server::{Server, Handler, Request, Response}; use hyper::uri::AbsolutePath; use response::WebDriverResponse; -use messagebuilder::{get_builder}; +use messagebuilder::{get_builder, MessageBuilder}; use marionette::MarionetteConnection; use command::WebDriverMessage; use common::WebDriverResult; enum DispatchMessage { - HandleWebDriver(WebDriverMessage, Sender>>), + HandleWebDriver(WebDriverMessage, Sender>>), Quit } struct Dispatcher { - connections: HashMap + connection: Option } impl Dispatcher { fn new() -> Dispatcher { Dispatcher { - connections: HashMap::new() + connection: None } } @@ -34,25 +32,53 @@ impl Dispatcher { loop { match msg_chan.recv() { DispatchMessage::HandleWebDriver(msg, resp_chan) => { - let opt_session_id = msg.session_id.clone(); - if opt_session_id.is_some() { - let session_id = opt_session_id.unwrap(); - let mut connection = match self.connections.get_mut(&session_id) { - Some(x) => x, - None => break - }; - let resp = connection.send_message(&msg); - resp_chan.send(resp); - return; + match msg.session_id { + Some(ref x) => { + match self.connection { + Some(ref conn) => { + if conn.session.session_id != *x { + error!("Got unexpected session id {} expected {}", + x, conn.session.session_id); + continue + } + }, + None => { + match self.create_connection(Some(x.clone())) { + Err(msg) => { + error!("{}", msg); + continue + }, + Ok(_) => {} + } + } + } + }, + None => { + if self.connection.is_some() { + error!("Missing session id for established connection"); + continue; + } + match self.create_connection(None) { + Err(msg) => { + error!("{}", msg); + continue + }, + Ok(_) => {} + } + } + }; + let resp = { + let mut connection = self.connection.as_mut().unwrap(); + connection.send_message(&msg) + }; + debug!("{}", resp); + match resp { + Ok(Some(WebDriverResponse::DeleteSession)) => { + debug!("Deleting session"); + self.connection = None; + }, + _ => {} } - let mut connection = MarionetteConnection::new(); - if connection.connect().is_err() { - error!("Failed to start marionette connection"); - return - } - let resp = connection.send_message(&msg); - self.connections.insert(connection.session.session_id.clone(), - connection); resp_chan.send(resp); }, DispatchMessage::Quit => { @@ -61,25 +87,33 @@ impl Dispatcher { } } } + + fn create_connection(&mut self, session_id: Option) -> Result<(), String> { + let mut connection = MarionetteConnection::new(session_id); + if connection.connect().is_err() { + return Err("Failed to start marionette connection".to_string()); + } + self.connection = Some(connection); + Ok(()) + } } struct MarionetteHandler { - chan: Mutex> + chan: Mutex>, + builder: Mutex } impl MarionetteHandler { - fn new(chan: Sender) -> MarionetteHandler { + fn new(builder: MessageBuilder, chan: Sender) -> MarionetteHandler { MarionetteHandler { - chan: Mutex::new(chan) + chan: Mutex::new(chan), + builder: Mutex::new(builder) } } } impl Handler for MarionetteHandler { fn handle(&self, req: Request, res: Response) { - let builder = get_builder(); - println!("{}", req.uri);; - let mut req = req; let mut res = res; @@ -87,10 +121,16 @@ impl Handler for MarionetteHandler { Post => req.read_to_string().unwrap(), _ => "".to_string() }; - println!("Got request {} {}", req.method, req.uri); + debug!("Got request {} {}", req.method, req.uri); match req.uri { AbsolutePath(path) => { - let (status, resp_data) = match builder.from_http(req.method, path[], body[]) { + let msg_result = { + // The fact that this locks for basically the whole request doesn't + // matter as long as we are only handling one request at a time. + let builder = self.builder.lock(); + builder.from_http(req.method, path[], body[]) + }; + let (status, resp_body) = match msg_result { Ok(message) => { let (send_res, recv_res) = channel(); { @@ -98,30 +138,32 @@ impl Handler for MarionetteHandler { c.send(DispatchMessage::HandleWebDriver(message, send_res)); } match recv_res.recv() { - Some(x) => { - match x { - Ok(response) => { - (200, response.to_json()) - } - Err(err) => (err.http_status(), err.to_json()) - } + Ok(None) => return, + Ok(Some(response)) => { + (200, response.to_json_string()) }, - None => return + Err(err) => (err.http_status(), err.to_json_string()), } }, Err(err) => { - (err.http_status(), err.to_json()) + (err.http_status(), err.to_json_string()) } }; - let body = format!("{}\n", resp_data.to_string()); + if status != 200 { + error!("Returning status code {}", status); + error!("Returning body {}", resp_body); + } else { + debug!("Returning status code {}", status); + debug!("Returning body {}", resp_body); + } { let status_code = res.status_mut(); *status_code = FromPrimitive::from_int(status).unwrap(); } - res.headers_mut().set(ContentLength(body.len())); + res.headers_mut().set(ContentLength(resp_body.len())); let mut stream = res.start(); - stream.write_str(body.as_slice()); - stream.unwrap().end(); + stream.write_str(resp_body.as_slice()).unwrap(); + stream.unwrap().end().unwrap(); }, _ => {} } @@ -137,6 +179,7 @@ pub fn start(ip_address: IpAddr, port: u16) { spawn(proc() { dispatcher.run(msg_recv); }); - let handler = MarionetteHandler::new(msg_send.clone()); + let builder = get_builder(); + let handler = MarionetteHandler::new(builder, msg_send.clone()); server.listen(handler).unwrap(); } diff --git a/testing/geckodriver/src/main.rs b/testing/geckodriver/src/main.rs index fcf9a08ff300..21272ef502e0 100644 --- a/testing/geckodriver/src/main.rs +++ b/testing/geckodriver/src/main.rs @@ -1,6 +1,9 @@ #![feature(slicing_syntax)] #![feature(phase)] +#![feature(macro_rules)] +#![feature(unboxed_closures)] +extern crate core; extern crate getopts; extern crate hyper; #[phase(plugin, link)] extern crate log; @@ -13,6 +16,15 @@ use std::io::net::ip::SocketAddr; use std::io; use std::os; +macro_rules! try_opt { + ($expr:expr, $err_type:expr, $err_msg:expr) => ({ + match $expr { + Some(x) => x, + None => return Err(WebDriverError::new($err_type, $err_msg)) + } + }) +} + mod command; mod common; mod httpserver; diff --git a/testing/geckodriver/src/marionette.rs b/testing/geckodriver/src/marionette.rs index b3fd0ff27eba..484f8e718c57 100644 --- a/testing/geckodriver/src/marionette.rs +++ b/testing/geckodriver/src/marionette.rs @@ -1,13 +1,18 @@ -use serialize::json::ToJson; +use serialize::json::{Json, ToJson}; use serialize::json; -use std::io::{IoResult, TcpStream, IoError}; use std::collections::TreeMap; +use std::io::{IoResult, TcpStream, IoError}; -use command::{WebDriverMessage, WebDriverCommand}; +use command::{WebDriverMessage}; use command::WebDriverCommand::{GetMarionetteId, NewSession, DeleteSession, Get, GetCurrentUrl, GoBack, GoForward, Refresh, GetTitle, GetWindowHandle, - GetWindowHandles, Close, Timeouts}; -use response::WebDriverResponse; + GetWindowHandles, Close, Timeouts, SetWindowSize, + GetWindowSize, MaximizeWindow, SwitchToWindow, SwitchToFrame, + SwitchToParentFrame, IsDisplayed, IsSelected, + GetElementAttribute, GetCSSValue, GetElementText, + GetElementTagName, GetElementRect, IsEnabled, ExecuteScript, + ExecuteAsyncScript}; +use response::{WebDriverResponse, NewSessionResponse, ValueResponse, WindowSizeResponse, ElementRectResponse}; use common::{WebDriverResult, WebDriverError, ErrorStatus}; pub struct MarionetteSession { @@ -15,10 +20,17 @@ pub struct MarionetteSession { pub to: String } +fn object_from_json(data: &str) -> WebDriverResult> { + Ok(try_opt!(try!(json::from_str(data)).as_object(), + ErrorStatus::UnknownError, + "Expected a json object").clone()) +} + impl MarionetteSession { - pub fn new() -> MarionetteSession { + pub fn new(session_id: Option) -> MarionetteSession { + let initital_id = session_id.unwrap_or("".to_string()); MarionetteSession { - session_id: "".to_string(), + session_id: initital_id, to: String::from_str("root") } } @@ -26,21 +38,24 @@ impl MarionetteSession { pub fn update(&mut self, msg: &WebDriverMessage, resp: &TreeMap) -> WebDriverResult<()> { match msg.command { GetMarionetteId => { - let to = match resp.get(&"to".to_string()) { - Some(x) => x, - None => return Err(WebDriverError::new(ErrorStatus::UnknownError, - "Unable to get to value")) - }; - self.to = to.to_string().clone(); + let to = try_opt!( + try_opt!(resp.get("to"), + ErrorStatus::UnknownError, + "Unable to get to value").as_string(), + ErrorStatus::UnknownError, + "Unable to convert 'to' to a string"); + + self.to = to.to_string(); }, NewSession => { - let session_id = match resp.get(&"value".to_string()) { - Some(x) => x, - None => return Err(WebDriverError::new(ErrorStatus::UnknownError, - "Unable to get session id")) - }; + let session_id = try_opt!( + try_opt!(resp.get("sessionId"), + ErrorStatus::SessionNotCreated, + "Unable to get session id").as_string(), + ErrorStatus::SessionNotCreated, + "Unable to convert session id to string"); self.session_id = session_id.to_string().clone(); - } + }, _ => {} } Ok(()) @@ -60,7 +75,23 @@ impl MarionetteSession { GetWindowHandle => "getWindowHandle", GetWindowHandles => "getWindowHandles", Close => "close", - Timeouts(_) => "timeouts" + Timeouts(_) => "timeouts", + SetWindowSize(_) => "setWindowSize", + GetWindowSize => "getWindowSize", + MaximizeWindow => "maximizeWindow", + SwitchToWindow(_) => "switchToWindow", + SwitchToFrame(_) => "switchToFrame", + SwitchToParentFrame => "switchToParentFrame", + IsDisplayed(_) => "isElementDisplayed", + IsSelected(_) => "isElementSelected", + GetElementAttribute(_, _) => "getElementAttribute", + GetCSSValue(_, _) => "getElementValueOfCssProperty", + GetElementText(_) => "getElementText", + GetElementTagName(_) => "getElementTagName", + GetElementRect(_) => "getElementRect", + IsEnabled(_) => "isElementEnabled", + ExecuteScript(_) => "executeScript", + ExecuteAsyncScript(_) => "executeAsyncScript" }.to_string() } @@ -71,70 +102,163 @@ impl MarionetteSession { None => None }; data.insert("to".to_string(), self.to.to_json()); - data.insert("command".to_string(), MarionetteSession::command_name(msg).to_json()); + data.insert("name".to_string(), MarionetteSession::command_name(msg).to_json()); json::Object(data) } pub fn response_from_json(&mut self, message: &WebDriverMessage, - data: &str) -> Option> { - let decoded = match json::from_str(data) { - Ok(data) => data, - Err(_) => { - return Some(Err(WebDriverError::new(ErrorStatus::UnknownError, - "Failed to decode marionette data as json"))); - } - }; - let json_data = match decoded { - json::Object(x) => x, - _ => { - return Some(Err(WebDriverError::new(ErrorStatus::UnknownError, - "Expected a json object"))); - } - }; + data: &str) -> WebDriverResult> { + let json_data = try!(object_from_json(data)); if json_data.contains_key(&"error".to_string()) { //TODO: convert the marionette error into the right webdriver error - let err_msg = match json_data.get(&"error".to_string()).unwrap().as_string() { - Some(x) => x, - None => "Unexpected error" - }; - return Some(Err(WebDriverError::new(ErrorStatus::UnknownError, - err_msg))); + let error = try_opt!(json_data.get("error").unwrap().as_object(), + ErrorStatus::UnknownError, + "Marionette error field was not an object"); + let status_code = try_opt!( + try_opt!(error.get("status"), + ErrorStatus::UnknownError, + "Error dict doesn't have a status field").as_u64(), + ErrorStatus::UnknownError, + "Error status isn't an integer"); + let status = self.error_from_code(status_code); + let default_msg = Json::String("Unknown error".into_string()); + let err_msg = try_opt!( + error.get("message").unwrap_or(&default_msg).as_string(), + ErrorStatus::UnknownError, + "Error message was not a string"); + return Err(WebDriverError::new(status, err_msg)); } self.update(message, &json_data); match message.command { //Everything that doesn't have a response value - GetMarionetteId => None, - Get(_) | GoBack | GoForward | Refresh | Close | Timeouts(_) => { - Some(Ok(WebDriverResponse::new(json::Null))) + GetMarionetteId => Ok(None), + Get(_) | GoBack | GoForward | Refresh | Close | Timeouts(_) | + SetWindowSize(_) | MaximizeWindow | SwitchToWindow(_) | SwitchToFrame(_) | + SwitchToParentFrame => { + Ok(Some(WebDriverResponse::Void)) }, //Things that simply return the contents of the marionette "value" property - GetCurrentUrl | GetTitle | GetWindowHandle | GetWindowHandles => { - let value = match json_data.get(&"value".to_string()) { - Some(data) => data, - None => { - return Some(Err(WebDriverError::new(ErrorStatus::UnknownError, - "Failed to find value field"))); - } - }; - Some(Ok(WebDriverResponse::new(value.clone()))) + GetCurrentUrl | GetTitle | GetWindowHandle | GetWindowHandles | IsDisplayed(_) | + IsSelected(_) | GetElementAttribute(_, _) | GetCSSValue(_, _) | GetElementText(_) | + GetElementTagName(_) | IsEnabled(_) | ExecuteScript(_) | ExecuteAsyncScript(_) => { + let value = try_opt!(json_data.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field"); + Ok(Some(WebDriverResponse::Generic(ValueResponse::new(value.clone())))) }, + GetWindowSize => { + let value = try_opt!( + try_opt!(json_data.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field").as_object(), + ErrorStatus::UnknownError, + "Failed to interpret value as object"); + + let width = try_opt!( + try_opt!(value.get("width"), + ErrorStatus::UnknownError, + "Failed to find width field").as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret width as integer"); + + let height = try_opt!( + try_opt!(value.get("height"), + ErrorStatus::UnknownError, + "Failed to find height field").as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret width as integer"); + + Ok(Some(WebDriverResponse::WindowSize(WindowSizeResponse::new(width, height)))) + }, + GetElementRect(_) => { + let value = try_opt!( + try_opt!(json_data.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field").as_object(), + ErrorStatus::UnknownError, + "Failed to interpret value as object"); + + let x = try_opt!( + try_opt!(value.get("x"), + ErrorStatus::UnknownError, + "Failed to find x field").as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret x as integer"); + + let y = try_opt!( + try_opt!(value.get("y"), + ErrorStatus::UnknownError, + "Failed to find y field").as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret y as integer"); + + let width = try_opt!( + try_opt!(value.get("width"), + ErrorStatus::UnknownError, + "Failed to find width field").as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret width as integer"); + + let height = try_opt!( + try_opt!(value.get("height"), + ErrorStatus::UnknownError, + "Failed to find height field").as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret width as integer"); + + Ok(Some(WebDriverResponse::ElementRect(ElementRectResponse::new(x, y, width, height)))) + } NewSession => { - let value = match json_data.get(&"value".to_string()) { - Some(data) => data, - None => { - return Some(Err(WebDriverError::new(ErrorStatus::UnknownError, - "Failed to find value field"))); - } - }; - Some(Ok(WebDriverResponse::new(value.clone()))) + let session_id = try_opt!( + try_opt!(json_data.get("sessionId"), + ErrorStatus::InvalidSessionId, + "Failed to find sessionId field").as_string(), + ErrorStatus::InvalidSessionId, + "sessionId was not a string"); + + let value = try_opt!( + try_opt!(json_data.get("value"), + ErrorStatus::SessionNotCreated, + "Failed to find value field").as_object(), + ErrorStatus::SessionNotCreated, + "value field was not an Object"); + + Ok(Some(WebDriverResponse::NewSession(NewSessionResponse::new( + session_id.to_string(), json::Object(value.clone()))))) } DeleteSession => { - Some(Ok(WebDriverResponse::new(json::Null))) + Ok(Some(WebDriverResponse::DeleteSession)) } } } + + pub fn error_from_code(&self, error_code: u64) -> ErrorStatus { + match error_code { + 7 => ErrorStatus::NoSuchElement, + 8 => ErrorStatus::NoSuchFrame, + 9 => ErrorStatus::UnsupportedOperation, + 10 => ErrorStatus::StaleElementReference, + 11 => ErrorStatus::ElementNotVisible, + 12 => ErrorStatus::InvalidElementState, + 15 => ErrorStatus::ElementNotSelectable, + 17 => ErrorStatus::JavascriptError, + 21 => ErrorStatus::Timeout, + 23 => ErrorStatus::NoSuchWindow, + 24 => ErrorStatus::InvalidCookieDomain, + 25 => ErrorStatus::UnableToSetCookie, + 26 => ErrorStatus::UnexpectedAlertOpen, + 27 => ErrorStatus::NoSuchAlert, + 28 => ErrorStatus::ScriptTimeout, + 29 => ErrorStatus::InvalidElementCoordinates, + 32 => ErrorStatus::InvalidSelector, + 34 => ErrorStatus::MoveTargetOutOfBounds, + 405 => ErrorStatus::UnsupportedOperation, + 13 | 19 | 51 | 52 | 53 | 54 | 55 | 56 | 500 | _ => ErrorStatus::UnknownError + + } + } } pub struct MarionetteConnection { @@ -143,11 +267,11 @@ pub struct MarionetteConnection { } impl MarionetteConnection { - pub fn new() -> MarionetteConnection { + pub fn new(session_id: Option) -> MarionetteConnection { let stream = TcpStream::connect("127.0.0.1:2828"); MarionetteConnection { stream: stream, - session: MarionetteSession::new() + session: MarionetteSession::new(session_id) } } @@ -155,10 +279,23 @@ impl MarionetteConnection { try!(self.read_resp()); //Would get traits and application type here let mut msg = TreeMap::new(); - msg.insert("name".to_string(), "getMarionetteId".to_json()); + msg.insert("name".to_string(), "getMarionetteID".to_json()); msg.insert("to".to_string(), "root".to_json()); match self.send(&msg.to_json()) { - Ok(_) => Ok(()), + Ok(resp) => { + let json_data = match object_from_json(resp.as_slice()) { + Ok(x) => x, + Err(_) => panic!("Failed to connect to marionette") + }; + match json_data.get(&"id".to_string()) { + Some(x) => match x.as_string() { + Some(id) => self.session.to = id.to_string(), + None => panic!("Failed to connect to marionette") + }, + None => panic!("Failed to connect to marionette") + }; + Ok(()) + } Err(_) => panic!("Failed to connect to marionette") } } @@ -172,20 +309,20 @@ impl MarionetteConnection { message } - pub fn send_message(&mut self, msg: &WebDriverMessage) -> Option> { + pub fn send_message(&mut self, msg: &WebDriverMessage) -> WebDriverResult> { let resp = { self.session.msg_to_marionette(msg) }; let resp = match self.send(&resp) { Ok(resp_data) => self.session.response_from_json(msg, resp_data[]), - Err(x) => Some(Err(x)) + Err(x) => Err(x) }; resp } fn send(&mut self, msg: &json::Json) -> WebDriverResult { let data = self.encode_msg(msg); - println!("{}", data); + debug!("Sending {}", data); match self.stream.write_str(data.as_slice()) { Ok(_) => {}, Err(_) => { @@ -195,6 +332,7 @@ impl MarionetteConnection { } match self.read_resp() { Ok(resp) => { + debug!("Marionette response {}", resp); Ok(resp) }, Err(_) => Err(WebDriverError::new(ErrorStatus::UnknownError, @@ -221,5 +359,4 @@ impl MarionetteConnection { //Need to handle the error here Ok(String::from_utf8(data).unwrap()) } - } diff --git a/testing/geckodriver/src/messagebuilder.rs b/testing/geckodriver/src/messagebuilder.rs index 29fb54ae2905..51d3a87fd6d3 100644 --- a/testing/geckodriver/src/messagebuilder.rs +++ b/testing/geckodriver/src/messagebuilder.rs @@ -18,7 +18,23 @@ pub enum MatchType { GetWindowHandle, GetWindowHandles, Close, - Timeouts + Timeouts, + SetWindowSize, + GetWindowSize, + MaximizeWindow, + SwitchToWindow, + SwitchToFrame, + SwitchToParentFrame, + IsDisplayed, + IsSelected, + GetElementAttribute, + GetCSSValue, + GetElementText, + GetElementTagName, + GetElementRect, + IsEnabled, + ExecuteScript, + ExecuteAsyncScript, } #[deriving(Clone)] @@ -39,7 +55,6 @@ impl RequestMatcher { } pub fn get_match<'t>(&'t self, method: Method, path: &'t str) -> (bool, Option) { - println!("{} {}", method, path); let captures = self.path_regexp.captures(path); (method == self.method, captures) } @@ -78,10 +93,8 @@ impl MessageBuilder { } pub fn from_http(&self, method: Method, path: &str, body: &str) -> WebDriverResult { - println!("{} {}", method, path); let mut error = ErrorStatus::UnknownPath; for &(ref match_method, ref matcher) in self.http_matchers.iter() { - println!("{} {}", match_method, matcher.path_regexp); if method == *match_method { let (method_match, captures) = matcher.get_match(method.clone(), path); if captures.is_some() { @@ -96,7 +109,7 @@ impl MessageBuilder { } } Err(WebDriverError::new(error, - format!("{} did not match a known command", path)[])) + format!("{} {} did not match a known command", method, path)[])) } pub fn add(&mut self, method: Method, path: &str, match_type: MatchType) { @@ -118,10 +131,26 @@ pub fn get_builder() -> MessageBuilder { (Get, "/session/{sessionId}/window_handle", MatchType::GetWindowHandle), (Get, "/session/{sessionId}/window_handles", MatchType::GetWindowHandles), (Delete, "/session/{sessionId}/window_handle", MatchType::Close), - (Post, "/session/{sessionId}/timeouts", MatchType::Timeouts) + (Post, "/session/{sessionId}/timeouts", MatchType::Timeouts), + (Post, "/session/{sessionId}/window/size", MatchType::SetWindowSize), + (Get, "/session/{sessionId}/window/size", MatchType::GetWindowSize), + (Post, "/session/{sessionId}/window/maximize", MatchType::MaximizeWindow), + (Post, "/session/{sessionId}/window", MatchType::SwitchToWindow), + (Post, "/session/{sessionId}/frame", MatchType::SwitchToFrame), + (Post, "/session/{sessionId}/frame/parent", MatchType::SwitchToParentFrame), + (Get, "/session/{sessionId}/element/{element}/isDisplayed", MatchType::IsDisplayed), + (Get, "/session/{sessionId}/element/{element}/isSelected", MatchType::IsSelected), + (Get, "/session/{sessionId}/element/{element}/attribute/{name}", MatchType::GetElementAttribute), + (Get, "/session/{sessionId}/element/{element}/css/{propertyName}", MatchType::GetCSSValue), + (Get, "/session/{sessionId}/element/{element}/text", MatchType::GetElementText), + (Get, "/session/{sessionId}/element/{element}/name", MatchType::GetElementTagName), + (Get, "/session/{sessionId}/element/{element}/rect", MatchType::GetElementRect), + (Get, "/session/{sessionId}/element/{element}/enabled", MatchType::IsEnabled), + (Post, "/session/{sessionId}/execute", MatchType::ExecuteScript), + (Post, "/session/{sessionId}/execute_async", MatchType::ExecuteAsyncScript), ]; + debug!("Creating routes"); for &(ref method, ref url, ref match_type) in matchers.iter() { - println!("{} {}", method, url); builder.add(method.clone(), *url, *match_type); } builder diff --git a/testing/geckodriver/src/response.rs b/testing/geckodriver/src/response.rs index 71f1130c0d72..7a29954f6e38 100644 --- a/testing/geckodriver/src/response.rs +++ b/testing/geckodriver/src/response.rs @@ -1,30 +1,86 @@ -use std::collections::TreeMap; use serialize::json; -use serialize::json::{ToJson}; -use command::WebDriverMessage; -use command::WebDriverCommand::{GetMarionetteId, NewSession, DeleteSession, Get, GetCurrentUrl, - GoBack, GoForward, Refresh, GetTitle, - GetWindowHandle, GetWindowHandles, Close, Timeouts}; -use marionette::{MarionetteSession}; - -use common::{ErrorStatus, WebDriverError, WebDriverResult}; - -pub struct WebDriverResponse { - value: json::Json +#[deriving(Show)] +pub enum WebDriverResponse { + NewSession(NewSessionResponse), + DeleteSession, + WindowSize(WindowSizeResponse), + ElementRect(ElementRectResponse), + Generic(ValueResponse), + Void } impl WebDriverResponse { - pub fn new(value: json::Json) -> WebDriverResponse { - WebDriverResponse { - value: value + pub fn to_json_string(self) -> String { + match self { + WebDriverResponse::NewSession(x) => json::encode(&x), + WebDriverResponse::DeleteSession => "".into_string(), + WebDriverResponse::WindowSize(x) => json::encode(&x), + WebDriverResponse::ElementRect(x) => json::encode(&x), + WebDriverResponse::Generic(x) => json::encode(&x), + WebDriverResponse::Void => "".into_string() } } - - pub fn to_json(&self) -> json::Json { - let mut data = TreeMap::new(); - data.insert("value".to_string(), self.value.to_json()); - json::Object(data) - } } +#[deriving(Encodable, Show)] +pub struct NewSessionResponse { + sessionId: String, + value: json::Json +} + +impl NewSessionResponse { + pub fn new(session_id: String, value: json::Json) -> NewSessionResponse { + NewSessionResponse { + value: value, + sessionId: session_id + } + } +} + +#[deriving(Encodable, Show)] +pub struct ValueResponse { + value: json::Json +} + +impl ValueResponse { + pub fn new(value: json::Json) -> ValueResponse { + ValueResponse { + value: value + } + } +} + +#[deriving(Encodable, Show)] +pub struct WindowSizeResponse { + width: u64, + height: u64 +} + +impl WindowSizeResponse { + pub fn new(width: u64, height: u64) -> WindowSizeResponse { + WindowSizeResponse { + width: width, + height: height + } + } +} + +#[deriving(Encodable, Show)] +pub struct ElementRectResponse { + x: u64, + y: u64, + width: u64, + height: u64 +} + +impl ElementRectResponse { + pub fn new(x: u64, y: u64, width: u64, height: u64) -> ElementRectResponse { + ElementRectResponse { + x: x, + y: y, + width: width, + height: height + } + } +}