geckodriver: Proxy for converting webdriver HTTP protocol to marionette protocol.
authorJames Graham <james@hoppipolla.co.uk>
Fri, 17 Oct 2014 12:13:22 +0100
changeset 359392 c6b8df631a64cf20992cd221ae99ceb5ca4a84df
parent 359391 1c743eebadc7735278911075c12d213d69b1de7a
child 359393 1772f06e6ad8f50f6724954472ccb72dd6f2ce10
push id31854
push userarchaeopteryx@coole-files.de
push dateSat, 20 May 2017 16:46:00 +0000
treeherdermozilla-central@51736db67723 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
geckodriver: Proxy for converting webdriver HTTP protocol to marionette protocol. Initial commit with terrible, terrible code. Source-Repo: https://github.com/mozilla/geckodriver Source-Revision: 8d43506b8a9afc0fcbba56685a31328cf2866b68
testing/geckodriver/Cargo.lock
testing/geckodriver/Cargo.toml
testing/geckodriver/src/command.rs
testing/geckodriver/src/httpserver.rs
testing/geckodriver/src/marionette.rs
testing/geckodriver/src/messagebuilder.rs
testing/geckodriver/src/response.rs
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/Cargo.lock
@@ -0,0 +1,70 @@
+[root]
+name = "wires"
+version = "0.0.1"
+dependencies = [
+ "hyper 0.0.1 (git+https://github.com/hyperium/hyper.git#46e1f4443f766c98696150b6056d387fcac0f1a0)",
+ "uuid 0.0.1 (git+https://github.com/rust-lang/uuid.git#c6159089a491ff4398c6d423263a085c95092a04)",
+]
+
+[[package]]
+name = "encoding"
+version = "0.1.0"
+source = "git+https://github.com/lifthrasiir/rust-encoding#97e5a56658d11e6386d0d17386560094490b1daf"
+
+[[package]]
+name = "hyper"
+version = "0.0.1"
+source = "git+https://github.com/hyperium/hyper.git#46e1f4443f766c98696150b6056d387fcac0f1a0"
+dependencies = [
+ "intertwine 0.0.1 (git+https://github.com/reem/rust-intertwine#3b23760cf7da3c7f38db16bf488c88f6e506de2d)",
+ "mime 0.0.1 (git+https://github.com/seanmonstar/mime.rs#62a454863b189fa03a3f8c3db0207cb8fe366b7a)",
+ "move-acceptor 0.0.1 (git+https://github.com/reem/rust-move-acceptor#4eef9c13745db3da74c22a26b659dc054aec5500)",
+ "openssl 0.0.0 (git+https://github.com/sfackler/rust-openssl#d6578469a85bcc06a5761c1e25a73036427affef)",
+ "typeable 0.0.1 (git+https://github.com/reem/rust-typeable#55154e1809db8ceec8f8519bdbb638c2fbd712f5)",
+ "unsafe-any 0.1.0 (git+https://github.com/reem/rust-unsafe-any#75cff194795447a901c1658e31629899c9c8d52f)",
+ "url 0.1.0 (git+https://github.com/servo/rust-url#55b18b796ef2cef5490069dd450c60c2cc2c0c1b)",
+]
+
+[[package]]
+name = "intertwine"
+version = "0.0.1"
+source = "git+https://github.com/reem/rust-intertwine#3b23760cf7da3c7f38db16bf488c88f6e506de2d"
+
+[[package]]
+name = "mime"
+version = "0.0.1"
+source = "git+https://github.com/seanmonstar/mime.rs#62a454863b189fa03a3f8c3db0207cb8fe366b7a"
+
+[[package]]
+name = "move-acceptor"
+version = "0.0.1"
+source = "git+https://github.com/reem/rust-move-acceptor#4eef9c13745db3da74c22a26b659dc054aec5500"
+
+[[package]]
+name = "openssl"
+version = "0.0.0"
+source = "git+https://github.com/sfackler/rust-openssl#d6578469a85bcc06a5761c1e25a73036427affef"
+
+[[package]]
+name = "typeable"
+version = "0.0.1"
+source = "git+https://github.com/reem/rust-typeable#55154e1809db8ceec8f8519bdbb638c2fbd712f5"
+
+[[package]]
+name = "unsafe-any"
+version = "0.1.0"
+source = "git+https://github.com/reem/rust-unsafe-any#75cff194795447a901c1658e31629899c9c8d52f"
+
+[[package]]
+name = "url"
+version = "0.1.0"
+source = "git+https://github.com/servo/rust-url#55b18b796ef2cef5490069dd450c60c2cc2c0c1b"
+dependencies = [
+ "encoding 0.1.0 (git+https://github.com/lifthrasiir/rust-encoding#97e5a56658d11e6386d0d17386560094490b1daf)",
+]
+
+[[package]]
+name = "uuid"
+version = "0.0.1"
+source = "git+https://github.com/rust-lang/uuid.git#c6159089a491ff4398c6d423263a085c95092a04"
+
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+
+name = "wires"
+version = "0.0.1"
+authors = ["James Graham <james@hoppipolla.co.uk>"]
+
+[dependencies.hyper]
+git = "https://github.com/hyperium/hyper.git"
+
+[dependencies.uuid]
+git = "https://github.com/rust-lang/uuid.git"
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/src/command.rs
@@ -0,0 +1,104 @@
+use std::collections::TreeMap;
+use serialize::json;
+use serialize::{Decodable, Encodable};
+use serialize::json::{ToJson};
+use regex::Captures;
+
+use hyper::method;
+use hyper::method::Method;
+
+use messagebuilder::{MessageBuilder, MatchType, MatchNewSession, MatchGet, MatchGetCurrentUrl};
+
+#[deriving(PartialEq)]
+pub enum WebDriverCommand {
+    GetMarionetteId,
+    NewSession,
+    Get(GetParameters),
+    GetCurrentUrl
+}
+
+pub struct WebDriverMessage {
+    pub command: WebDriverCommand,
+    pub session_id: Option<String>
+}
+
+impl WebDriverMessage {
+    pub fn new(command: WebDriverCommand, session_id: Option<String>) -> WebDriverMessage {
+        WebDriverMessage {
+            command: command,
+            session_id: session_id
+        }
+    }
+
+    pub fn name(&self) -> String {
+        match self.command {
+            GetMarionetteId => "getMarionetteID",
+            NewSession => "newSession",
+            Get(_) => "get",
+            GetCurrentUrl => "getCurrentUrl"
+        }.to_string()
+    }
+
+    fn parameters_json(&self) -> json::Json {
+        match self.command {
+            Get(ref x) => {
+                x.to_json()
+            },
+            _ => {
+                json::Object(TreeMap::new())
+            }
+        }
+    }
+
+    pub fn from_http(match_type: MatchType, params: &Captures, body: &str) -> WebDriverMessage {
+        let session_id = WebDriverMessage::get_session_id(params);
+        let command = match match_type {
+            MatchNewSession => {
+                NewSession
+            },
+            MatchGet => {
+                let parameters: GetParameters = json::decode(body).unwrap();
+                Get(parameters)
+            },
+            MatchGetCurrentUrl => {
+                GetCurrentUrl
+            }
+        };
+        WebDriverMessage {
+            session_id: session_id,
+            command: command
+        }
+    }
+
+    fn get_session_id(params: &Captures) -> Option<String> {
+        let session_id_str = params.name("sessionId");
+        if session_id_str == "" {
+            None
+        } else {
+            Some(session_id_str.to_string())
+        }
+    }
+}
+
+impl ToJson for WebDriverMessage {
+    fn to_json(&self) -> json::Json {
+        let mut data = TreeMap::new();
+        data.insert("name".to_string(), self.name().to_json());
+        data.insert("parameters".to_string(), self.parameters_json());
+        data.insert("sessionId".to_string(), self.session_id.to_json());
+        json::Object(data)
+    }
+}
+
+#[deriving(Decodable, Encodable, PartialEq)]
+struct GetParameters {
+    url: String
+}
+
+impl ToJson for GetParameters {
+    fn to_json(&self) -> json::Json {
+        let mut data = TreeMap::new();
+        data.insert("url".to_string(), self.url.to_json());
+        json::Object(data)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/src/httpserver.rs
@@ -0,0 +1,45 @@
+use std::io::net::ip::IpAddr;
+use regex::Regex;
+
+use serialize::json;
+
+use hyper;
+use hyper::header::common::ContentLength;
+use hyper::server::{Server, Incoming};
+use hyper::uri::AbsolutePath;
+
+use messagebuilder::{get_builder};
+use marionette::{MarionetteSession, MarionetteConnection};
+
+fn handle(mut incoming: Incoming) {
+    let mut marionette = MarionetteConnection::new();
+    marionette.connect();
+
+    let builder = get_builder();
+    for (mut req, mut resp) in incoming {
+        println!("{}", req.uri);
+        let body = req.read_to_string().unwrap();
+        match req.uri {
+            AbsolutePath(path) => {
+                let message = builder.from_http(req.method, path[], body[]);
+                //Should return a Result instead
+                if message.is_some() {
+                    let response = marionette.send_message(&message.unwrap());
+                    if response.is_some() {
+                        let body = response.unwrap().to_json().to_string();
+                        resp.headers_mut().set(ContentLength(body.len()));
+                        let mut stream = resp.start();
+                        stream.write_str(body.as_slice());
+                        stream.unwrap().end();
+                    }
+                }
+            },
+            _ => {}
+        };
+    }
+}
+
+pub fn start(ip_address: IpAddr, port: u16) {
+    let server = Server::http(ip_address, port);
+    server.listen(handle).unwrap();
+}
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/src/marionette.rs
@@ -0,0 +1,138 @@
+use std::io::{IoResult, TcpStream};
+use serialize::json;
+use serialize::json::ToJson;
+use command::{WebDriverMessage, GetMarionetteId, NewSession};
+use response::WebDriverResponse;
+use std::collections::TreeMap;
+
+use uuid::Uuid;
+
+pub struct MarionetteSession {
+    pub session_id: String,
+    pub to: String,
+    pub marionette_session_id: Option<json::Json>
+}
+
+impl MarionetteSession {
+    pub fn new() -> MarionetteSession {
+        MarionetteSession {
+            session_id: Uuid::new_v4().to_string(),
+            to: String::from_str("root"),
+            marionette_session_id: None
+        }
+    }
+
+    pub fn update(&mut self, msg: &WebDriverMessage, from: &json::Json, session_id: &json::Json) {
+        match msg.command {
+            GetMarionetteId => {
+                self.to = from.to_string().clone();
+            },
+            NewSession =>  {
+                self.marionette_session_id = Some(session_id.clone());
+            }
+            _ => {}
+        }
+    }
+
+    fn id_to_marionette(&self, msg: &WebDriverMessage) -> Option<json::Json> {
+        match msg.command {
+            // Clean up these fails! to return the right error instead
+            GetMarionetteId | NewSession => {
+                match msg.session_id {
+                    Some(_) => fail!("Tried to start session but session was already started"),
+                    None => {}
+                }
+            },
+            _ => {
+                match msg.session_id {
+                    Some(ref x) if *x != self.session_id => {
+                        fail!("Invalid session id");
+                    },
+                    None => {
+                        fail!("Session id not supplied");
+                    }
+                    _ => {}
+                }
+            }
+        }
+        match msg.command {
+            GetMarionetteId => None,
+            _ => Some(match self.marionette_session_id {
+                Some(ref x) => x.clone(),
+                None => json::Null
+            })
+        }
+
+    }
+
+    pub fn msg_to_json(&self, msg: &WebDriverMessage) -> json::Json {
+        //needing a clone here seems unfortunate
+        let mut data = msg.to_json().as_object().expect("Message was not a map").clone();
+        let session_id = self.id_to_marionette(msg);
+        if session_id.is_some() {
+            data.insert("sessionId".to_string(), session_id.unwrap());
+        }
+        data.insert("to".to_string(), self.to.to_json());
+        json::Object(data)
+    }
+}
+
+pub struct MarionetteConnection {
+    stream: IoResult<TcpStream>,
+    session: MarionetteSession
+}
+
+impl MarionetteConnection {
+    pub fn new() -> MarionetteConnection {
+        let stream = TcpStream::connect("127.0.0.1", 2828);
+        MarionetteConnection {
+            stream: stream,
+            session: MarionetteSession::new()
+        }
+    }
+
+    pub fn connect(&mut self) {
+        self.read_resp();
+        //Would get traits and application type here
+        self.send_message(&WebDriverMessage::new(GetMarionetteId, None));
+    }
+
+    fn encode_msg(&self, msg: &WebDriverMessage) ->  String {
+        let data = format!("{}", self.session.msg_to_json(msg));
+        let len = data.len().to_string();
+        let mut message = len;
+        message.push_str(":");
+        message.push_str(data.as_slice());
+        message
+    }
+
+    pub fn send_message(&mut self, msg: &WebDriverMessage) -> Option<WebDriverResponse> {
+        let data = self.encode_msg(msg);
+        println!("{}", data);
+        //TODO: Error handling
+        self.stream.write_str(data.as_slice()).unwrap();
+        let resp = self.read_resp();
+        println!("{}", resp);
+        WebDriverResponse::from_json(&mut self.session, msg, resp.as_slice())
+    }
+
+    fn read_resp(&mut self) -> String {
+        let mut bytes = 0 as uint;
+        loop {
+            //TODO: better error handling here
+            let byte = self.stream.read_byte().unwrap() as char;
+            match byte {
+                '0'...'9' => {
+                    bytes = bytes * 10;
+                    bytes += byte as uint - '0' as uint;
+                },
+                ':' => {
+                    break
+                }
+                _ => {}
+            }
+        }
+        let data = self.stream.read_exact(bytes).unwrap();
+        String::from_utf8(data).unwrap()
+    }
+}
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/src/messagebuilder.rs
@@ -0,0 +1,104 @@
+use std::collections::{TreeMap, HashMap};
+use regex::{Regex, Captures};
+use serialize::json;
+
+use hyper::method::{Method, Get, Post};
+
+use command::{WebDriverMessage, WebDriverCommand};
+
+#[deriving(Clone)]
+pub enum MatchType {
+    MatchNewSession,
+    MatchGet,
+    MatchGetCurrentUrl
+}
+
+#[deriving(Clone)]
+pub struct RequestMatcher {
+    method: Method,
+    path_regexp: Regex,
+    match_type: MatchType
+}
+
+impl RequestMatcher {
+    pub fn new(method: Method, path: &str, match_type: MatchType) -> RequestMatcher {
+        let path_regexp = RequestMatcher::compile_path(path);
+        RequestMatcher {
+            method: method,
+            path_regexp: path_regexp,
+            match_type: match_type
+        }
+    }
+
+    pub fn get_match<'t>(&'t self, method: Method, path: &'t str) -> Option<Captures> {
+        println!("{}", path);
+        if method == self.method {
+            self.path_regexp.captures(path)
+        } else {
+            None
+        }
+    }
+
+    fn compile_path(path: &str) -> Regex {
+        let mut rv = String::new();
+        rv.push_str("^");
+        let mut components = path.split('/');
+        for component in components {
+            if component.starts_with("{") {
+                if !component.ends_with("}") {
+                    fail!("Invalid url pattern")
+                }
+                rv.push_str(format!("(?P<{}>[^/]+)/", component[1..component.len()-1])[]);
+            } else {
+                rv.push_str(format!("{}/", component)[]);
+            }
+        }
+        //Remove the trailing /
+        rv.pop();
+        rv.push_str("$");
+        println!("{}", rv);
+        Regex::new(rv[]).unwrap()
+    }
+}
+
+pub struct MessageBuilder {
+    http_matchers: Vec<(Method, RequestMatcher)>
+}
+
+impl MessageBuilder {
+    pub fn new() -> MessageBuilder {
+        MessageBuilder {
+            http_matchers: vec![]
+        }
+    }
+
+    pub fn from_http(&self, method: Method, path: &str, body: &str) -> Option<WebDriverMessage> {
+        for &(ref match_method, ref matcher) in self.http_matchers.iter() {
+            if method == *match_method {
+                let captures = matcher.get_match(method.clone(), path);
+                if captures.is_some() {
+                    return Some(WebDriverMessage::from_http(matcher.match_type,
+                                                            &captures.unwrap(),
+                                                            body))
+                }
+            }
+        }
+        None
+    }
+
+    pub fn add(&mut self, method: Method, path: &str, match_type: MatchType) {
+        let http_matcher = RequestMatcher::new(method.clone(), path, match_type);
+        self.http_matchers.push((method, http_matcher));
+    }
+}
+
+pub fn get_builder() -> MessageBuilder {
+    let mut builder = MessageBuilder::new();
+    let matchers = vec![(Post, "/session", MatchNewSession),
+                        (Post, "/session/{sessionId}/url", MatchGet),
+                        (Get, "/session/{sessionId}/url", MatchGetCurrentUrl)];
+    for &(ref method, ref url, ref match_type) in matchers.iter() {
+        builder.add(method.clone(), *url, *match_type);
+    }
+    builder
+}
new file mode 100644
--- /dev/null
+++ b/testing/geckodriver/src/response.rs
@@ -0,0 +1,77 @@
+use std::collections::TreeMap;
+use serialize::json;
+use serialize::json::{decode, ToJson, Builder};
+
+use command::{WebDriverMessage, GetMarionetteId, NewSession, Get, GetCurrentUrl};
+use marionette::{MarionetteSession};
+
+pub struct WebDriverResponse {
+    session_id: String,
+    status: Status,
+    value: TreeMap<String,String>
+}
+
+#[deriving(PartialEq)]
+enum Status {
+    Success,
+    Timeout,
+    UnknownError
+}
+
+impl WebDriverResponse {
+    pub fn from_json(session: &mut MarionetteSession,
+                     message: &WebDriverMessage,
+                     data: &str) -> Option<WebDriverResponse> {
+        println!("Decoding json data");
+        let decoded = json::from_str(data).unwrap();
+        println!("Decoded json data");
+        let json_data = match decoded {
+            json::Object(x) => x,
+            _ => fail!("Expected an object")
+        };
+        let status = if json_data.contains_key(&"error".to_string()) {
+            UnknownError
+        } else {
+            Success
+        };
+        match message.command {
+            GetMarionetteId => None,
+            NewSession => {
+                if status == Success {
+                    session.update(message,
+                                   json_data.find(&"from".to_string()).unwrap(),
+                                   json_data.find(&"value".to_string()).unwrap());
+                };
+                Some(WebDriverResponse {status: status,
+                                        session_id: session.session_id.clone(),
+                                        value: TreeMap::new()})
+            },
+            Get(_) => {
+                Some(WebDriverResponse {status: status,
+                                        session_id: session.session_id.clone(),
+                                        value: TreeMap::new()})
+            },
+            GetCurrentUrl => {
+                Some(WebDriverResponse {status: status,
+                                        session_id: session.session_id.clone(),
+                                        value: TreeMap::new()})
+            }
+        }
+    }
+
+    fn status_string(&self) -> String {
+        match self.status {
+            Success => "success".to_string(),
+            Timeout => "timeout".to_string(),
+            UnknownError => "unknown error".to_string()
+        }
+    }
+
+    pub fn to_json(&self) -> json::Json {
+        let mut data = TreeMap::new();
+        data.insert("sessionId".to_string(), self.session_id.to_json());
+        data.insert("status".to_string(), self.status_string().to_json());
+        data.insert("capabilties".to_string(), self.value.to_json());
+        json::Object(data)
+    }
+}