geckodriver: Merge pull request #161 from mozilla/firefox_options

Change the format used for capabilities

Source-Repo: https://github.com/mozilla/geckodriver
Source-Revision: 38902f618fe435f134e3d73a98c9f074453bf9e3

--HG--
extra : rebase_source : 37db4bb63c1b0d90e9d2aacc3b4d27724e4d83e3
This commit is contained in:
James Graham 2016-08-22 17:18:05 +01:00
Родитель 69fe7f8e36
Коммит 2cd07eccde
5 изменённых файлов: 241 добавлений и 207 удалений

8
testing/geckodriver/Cargo.lock сгенерированный
Просмотреть файл

@ -12,8 +12,8 @@ dependencies = [
"regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)",
"rustc-serialize 0.3.19 (registry+https://github.com/rust-lang/crates.io-index)",
"uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
"webdriver 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zip 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)",
"webdriver 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"zip 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@ -430,7 +430,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "webdriver"
version = "0.12.0"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"cookie 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
@ -465,7 +465,7 @@ dependencies = [
[[package]]
name = "zip"
version = "0.1.18"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"bzip2 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",

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

@ -16,7 +16,7 @@ mozrunner = "^0.3.1"
regex = "0.1.47"
rustc-serialize = "0.3.16"
uuid = "0.1.18"
webdriver = "0.12"
webdriver = "0.13"
zip = "0.1.16"
[dependencies.clap]

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

@ -36,29 +36,46 @@ the more bug fixes and features.
## Firefox capabilities
**Note**: The names of these capabilities will change in a future release to better match
the precedent set by ChromeDriver.
geckodriver supports a capability named `firefoxOptions` which takes
Firefox-specific preference values. This must be a dictionary and may
contain any of the following fields:
geckodriver supports a couple of non-standard capabilities
to customise and configure a Firefox session:
<dl>
<dt><code>firefox_binary</code>
<dd>Set to full path of the Firefox binary,
e.g. <i>/usr/bin/firefox</i> or <i>/Applications/Firefox.app/Contents/MacOS/firefox</i>,
to select which custom browser binary to use.
If left undefined geckodriver will attempt
to deduce the default location of Firefox
on the current system.
<dt><code>firefox_profile</code>
<dd>For each session, geckodriver creates a new temporary profile by default.
To set custom preferences or use an add-on/extension,
you may want to provide a custom profile.
A profile can be sent across the wire protocol
by setting this capability to its path
on the local filesystem.
</dl>
<table>
<thead>
<tr>
<th>Name
<th>Type
<th>Default
<th>Description
</tr>
</thead>
<tr>
<td><code>binary</code>
<td>String
<td>Taken from geckodriver command line or system defaults.
<td>Absolute path of the Firefox binary,
e.g. <code>/usr/bin/firefox</code> or <code>/Applications/Firefox.app/Contents/MacOS/firefox</code>,
to select which custom browser binary to use.
If left undefined geckodriver will attempt
to deduce the default location of Firefox
on the current system.
<tr>
<td><code>args</code>
<td>Array of strings
<td>
<td>Command line arguments to pass to the
Firefox binary. These must include the leading `--` where required
e.g. `["--devtools"]`.
</tr>
<tr>
<td><code>profile</code>
<td>String
<td>New empty profile
<td>Base64-encoded zip of a profile directory
to use as the profile for the Firefox instance. This may be used to
e.g. install extensions or custom certificates.
</tr>
</table>
## Building
@ -73,7 +90,7 @@ ensure you do a compilation with optimisations:
Or if you want a non-optimised binary for debugging:
% cargo build
## Usage
Usage steps are [documented on MDN](https://developer.mozilla.org/en-US/docs/Mozilla/QA/Marionette/WebDriver),

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

@ -164,7 +164,7 @@ fn main() {
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use marionette::{MarionetteSettings, MarionetteHandler};
use marionette::{FirefoxOptions};
use webdriver::command::NewSessionParameters;
use rustc_serialize::json::Json;
use std::fs::File;
@ -172,8 +172,6 @@ mod tests {
use mozprofile::preferences::Pref;
use std::io::Read;
const MARIONETTE_PORT: u16 = 2828;
#[test]
fn test_profile() {
let mut profile_data = Vec::with_capacity(1024);
@ -189,31 +187,22 @@ mod tests {
let desired: BTreeMap<String, Json> = BTreeMap::new();
let mut required: BTreeMap<String, Json> = BTreeMap::new();
required.insert("firefox_profile".into(), encoded_profile);
let capabilities = NewSessionParameters {
let mut firefox_options: BTreeMap<String, Json> = BTreeMap::new();
firefox_options.insert("profile".into(), encoded_profile);
required.insert("firefoxOptions".into(), Json::Object(firefox_options));
let mut capabilities = NewSessionParameters {
desired: desired,
required: required
};
let settings = MarionetteSettings {
port: None,
binary: None,
connect_existing: false,
log_level: None,
};
let handler = MarionetteHandler::new(settings);
let mut gecko_profile = handler.load_profile(&capabilities).unwrap().unwrap();
handler.set_prefs(MARIONETTE_PORT, &mut gecko_profile, true).unwrap();
let prefs = gecko_profile.user_prefs().unwrap();
let options = FirefoxOptions::from_capabilities(&mut capabilities).unwrap();
let mut profile = options.profile.unwrap();
let prefs = profile.user_prefs().unwrap();
println!("{:?}",prefs.prefs);
assert_eq!(prefs.get("startup.homepage_welcome_url"),
Some(&Pref::new("data:text/html,PASS")));
assert_eq!(prefs.get("marionette.defaultPrefs.enabled"),
Some(&Pref::new(true)));
}
}

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

@ -221,11 +221,6 @@ impl ToMarionette for GeckoContextParameters {
}
}
pub enum BrowserLauncher {
None,
BinaryLauncher(PathBuf),
}
/// Logger levels from [Log.jsm]
/// (https://developer.mozilla.org/en/docs/Mozilla/JavaScript_code_modules/Log.jsm).
#[derive(Debug)]
@ -271,156 +266,75 @@ impl FromStr for LogLevel {
}
}
pub struct MarionetteSettings {
pub port: Option<u16>,
#[derive(Default)]
pub struct FirefoxOptions {
pub binary: Option<PathBuf>,
pub connect_existing: bool,
/// Optionally increase Marionette's verbosity by providing a log
/// level. The Gecko default is LogLevel::Info for optimised
/// builds and LogLevel::Debug for debug builds.
pub log_level: Option<LogLevel>,
pub profile: Option<Profile>,
pub args: Option<Vec<String>>,
}
pub struct MarionetteHandler {
connection: Mutex<Option<MarionetteConnection>>,
binary: Option<PathBuf>,
connect_existing: bool,
browser: Option<FirefoxRunner>,
port: Option<u16>,
log_level: Option<LogLevel>,
}
impl MarionetteHandler {
pub fn new(settings: MarionetteSettings) -> MarionetteHandler {
MarionetteHandler {
connection: Mutex::new(None),
binary: settings.binary,
connect_existing: settings.connect_existing,
browser: None,
port: settings.port,
log_level: settings.log_level,
}
}
fn create_connection(&mut self, session_id: &Option<String>,
capabilities: &NewSessionParameters) -> WebDriverResult<()> {
let profile = try!(self.load_profile(capabilities));
let args = try!(self.load_browser_args(capabilities));
let port = match self.port {
Some(x) => x,
None => try!(get_free_port())
};
let launcher = if self.connect_existing {
BrowserLauncher::None
impl FirefoxOptions {
pub fn from_capabilities(capabilities: &mut NewSessionParameters) -> WebDriverResult<FirefoxOptions> {
if let Some(options) = capabilities.consume("firefoxOptions") {
let firefox_options = try!(options
.as_object()
.ok_or(WebDriverError::new(
ErrorStatus::InvalidArgument,
"'firefoxOptions' capability was not an object")));
let binary = try!(FirefoxOptions::load_binary(&firefox_options));
let profile = try!(FirefoxOptions::load_profile(&firefox_options));
let args = try!(FirefoxOptions::load_args(&firefox_options));
Ok(FirefoxOptions {
binary: binary,
profile: profile,
args: args,
})
} else {
let binary = try!(self.binary_path(capabilities));
if let Some(path) = binary {
BrowserLauncher::BinaryLauncher(path)
} else {
return Err(WebDriverError::new(ErrorStatus::UnknownError,
"Expected browser binary location, \
but unable to find binary in default location, \
no 'firefox_binary' capability provided, \
and no binary flag set on the command line"));
}
};
match self.start_browser(launcher, port, profile, args) {
Err(e) => {
return Err(WebDriverError::new(ErrorStatus::UnknownError,
e.description().to_owned()));
},
Ok(_) => {}
Ok(Default::default())
}
let mut connection = MarionetteConnection::new(port, session_id.clone());
try!(connection.connect());
self.connection = Mutex::new(Some(connection));
Ok(())
}
fn start_browser(&mut self, launcher: BrowserLauncher, port: u16, profile: Option<Profile>, args: Option<Vec<String>>) -> Result<(), RunnerError> {
let custom_profile = profile.is_some();
match launcher {
BrowserLauncher::BinaryLauncher(ref binary) => {
let mut runner = try!(FirefoxRunner::new(&binary, profile));
if let Some(cmd_args) = args {
runner.args().extend(cmd_args);
};
try!(self.set_prefs(port, &mut runner.profile, custom_profile));
info!("Starting browser {}", binary.to_string_lossy());
try!(runner.start());
self.browser = Some(runner);
},
BrowserLauncher::None => {}
}
Ok(())
}
pub fn set_prefs(&self, port: u16, profile: &mut Profile, custom_profile: bool)
-> Result<(), RunnerError> {
let prefs = try!(profile.user_prefs());
prefs.insert("marionette.defaultPrefs.port", Pref::new(port as i64));
prefs.insert_slice(&FIREFOX_REQUIRED_PREFERENCES[..]);
if !custom_profile {
prefs.insert_slice(&FIREFOX_DEFAULT_PREFERENCES[..]);
};
if let Some(ref l) = self.log_level {
prefs.insert("marionette.logging", Pref::new(l.to_string()));
};
try!(prefs.write());
Ok(())
}
fn binary_path(&self, capabilities: &NewSessionParameters) -> WebDriverResult<Option<PathBuf>> {
if let Some(binary_capability) = capabilities.get("firefox_binary") {
Ok(Some(PathBuf::from(try!(binary_capability
fn load_binary(options: &BTreeMap<String, Json>) -> WebDriverResult<Option<PathBuf>> {
if let Some(path) = options.get("binary") {
Ok(Some(PathBuf::from(try!(path
.as_string()
.ok_or(WebDriverError::new(
ErrorStatus::InvalidArgument,
"'firefox_binary' capability was not a string"))))))
} else if self.binary.is_some() {
Ok(self.binary.as_ref().map(|x| x.clone()))
"'binary' capability was not a string"))))))
} else {
Ok(firefox_default_path())
Ok(None)
}
}
pub fn load_profile(&self, capabilities: &NewSessionParameters) -> WebDriverResult<Option<Profile>> {
let profile_opt = capabilities.get("firefox_profile");
if profile_opt.is_none() {
return Ok(None);
fn load_profile(options: &BTreeMap<String, Json>) -> WebDriverResult<Option<Profile>> {
if let Some(profile_json) = options.get("profile") {
let profile_base64 = try!(profile_json
.as_string()
.ok_or(
WebDriverError::new(ErrorStatus::UnknownError,
"Profile was not a string")));
let profile_zip = &*try!(profile_base64.from_base64());
// Create an emtpy profile directory
let profile = try!(Profile::new(None));
try!(unzip_buffer(profile_zip,
profile.temp_dir
.as_ref()
.expect("Profile doesn't have a path")
.path()));
Ok(Some(profile))
} else {
Ok(None)
}
debug!("Using custom profile");
let profile_json = profile_opt.unwrap();
let profile_base64 = try!(profile_json.as_string().ok_or(
WebDriverError::new(ErrorStatus::UnknownError,"Profile was not a string")));
let profile_zip = &*try!(profile_base64.from_base64());
// Create an emtpy profile directory
let profile = try!(Profile::new(None));
try!(unzip_buffer(profile_zip,
profile.temp_dir.as_ref().expect("Profile doesn't have a path").path()));
Ok(Some(profile))
}
pub fn load_browser_args(&self, capabilities: &NewSessionParameters) -> WebDriverResult<Option<Vec<String>>> {
if let Some(args_json) = capabilities.get("firefox_args") {
fn load_args(options: &BTreeMap<String, Json>) -> WebDriverResult<Option<Vec<String>>> {
if let Some(args_json) = options.get("args") {
let args_array = try!(args_json.as_array()
.ok_or(WebDriverError::new(ErrorStatus::UnknownError,
"Arguments was not an array")));
"Arguments were not an array")));
let args = try!(args_array
.iter()
.map(|x| x.as_string().map(|x| x.to_owned()))
@ -435,6 +349,118 @@ impl MarionetteHandler {
}
}
pub struct MarionetteSettings {
pub port: Option<u16>,
pub binary: Option<PathBuf>,
pub connect_existing: bool,
/// Optionally increase Marionette's verbosity by providing a log
/// level. The Gecko default is LogLevel::Info for optimised
/// builds and LogLevel::Debug for debug builds.
pub log_level: Option<LogLevel>,
}
impl Default for MarionetteSettings {
fn default() -> MarionetteSettings {
MarionetteSettings {
port: None,
binary: None,
connect_existing: false,
log_level: Some(LogLevel::Info),
}
}
}
pub struct MarionetteHandler {
connection: Mutex<Option<MarionetteConnection>>,
settings: MarionetteSettings,
browser: Option<FirefoxRunner>,
}
impl MarionetteHandler {
pub fn new(settings: MarionetteSettings) -> MarionetteHandler {
MarionetteHandler {
connection: Mutex::new(None),
settings: settings,
browser: None,
}
}
fn create_connection(&mut self, session_id: &Option<String>,
capabilities: &mut NewSessionParameters) -> WebDriverResult<()> {
let options = try!(FirefoxOptions::from_capabilities(capabilities));
let port = self.settings.port.unwrap_or(try!(get_free_port()));
if !self.settings.connect_existing {
try!(self.start_browser(port, options));
};
let mut connection = MarionetteConnection::new(port, session_id.clone());
try!(connection.connect());
self.connection = Mutex::new(Some(connection));
Ok(())
}
fn start_browser(&mut self, port: u16, mut options: FirefoxOptions) -> WebDriverResult<()> {
let binary = try!(self.binary_path(&mut options)
.ok_or(WebDriverError::new(ErrorStatus::UnknownError,
"Expected browser binary location, \
but unable to find binary in default location, \
no 'firefoxOptions.binary' capability provided, \
and no binary flag set on the command line")));
let custom_profile = options.profile.is_some();
let mut runner = try!(FirefoxRunner::new(&binary, options.profile.take())
.map_err(|e| WebDriverError::new(ErrorStatus::UnknownError,
e.description().to_owned())));
if let Some(args) = options.args.take() {
runner.args().extend(args);
};
try!(self.set_prefs(port, &mut runner.profile, custom_profile)
.map_err(|e| WebDriverError::new(ErrorStatus::UnknownError,
format!("Failed to set preferences:\n{}",
e.description()))));
info!("Starting browser {}", binary.to_string_lossy());
try!(runner.start()
.map_err(|e| WebDriverError::new(ErrorStatus::UnknownError,
format!("Failed to start browser:\n{}",
e.description()))));
self.browser = Some(runner);
Ok(())
}
fn binary_path(&self, options: &mut FirefoxOptions) -> Option<PathBuf> {
options.binary.take()
.or_else(|| self.settings.binary.as_ref().map(|x| x.clone()))
.or_else(|| firefox_default_path())
}
pub fn set_prefs(&self, port: u16, profile: &mut Profile, custom_profile: bool)
-> Result<(), RunnerError> {
let prefs = try!(profile.user_prefs());
prefs.insert("marionette.defaultPrefs.port", Pref::new(port as i64));
prefs.insert_slice(&FIREFOX_REQUIRED_PREFERENCES[..]);
if !custom_profile {
prefs.insert_slice(&FIREFOX_DEFAULT_PREFERENCES[..]);
};
if let Some(ref l) = self.settings.log_level {
prefs.insert("marionette.logging", Pref::new(l.to_string()));
};
try!(prefs.write());
Ok(())
}
}
fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
let reader = Cursor::new(buf);
let mut zip = try!(zip::ZipArchive::new(reader).map_err(|_| {
@ -484,37 +510,39 @@ fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
}
impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
fn handle_command(&mut self, _: &Option<Session>, msg: &WebDriverMessage<GeckoExtensionRoute>) -> WebDriverResult<WebDriverResponse> {
let mut new_capabilities = None;
match self.connection.lock() {
Ok(ref mut connection) => {
if connection.is_none() {
match msg.command {
NewSession(ref capabilities) => {
new_capabilities = Some(capabilities)
},
_ => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Tried to run command without establishing a connection"));
fn handle_command(&mut self, _: &Option<Session>, mut msg: WebDriverMessage<GeckoExtensionRoute>) -> WebDriverResult<WebDriverResponse> {
{
let mut new_capabilities = None;
match self.connection.lock() {
Ok(ref connection) => {
if connection.is_none() {
match msg.command {
NewSession(ref mut capabilities) => {
new_capabilities = Some(capabilities);
},
_ => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Tried to run command without establishing a connection"));
}
}
}
},
Err(_) => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Failed to aquire Marionette connection"))
}
},
Err(_) => {
return Err(WebDriverError::new(
ErrorStatus::UnknownError,
"Failed to aquire Marionette connection"))
}
if let Some(capabilities) = new_capabilities {
try!(self.create_connection(&msg.session_id, capabilities));
}
}
if let Some(capabilities) = new_capabilities {
try!(self.create_connection(&msg.session_id, &capabilities));
}
match self.connection.lock() {
Ok(ref mut connection) => {
match connection.as_mut() {
Some(conn) => conn.send_command(msg),
Some(conn) => conn.send_command(&msg),
None => panic!()
}
},
@ -606,7 +634,7 @@ impl MarionetteSession {
self.command_id
}
pub fn response(&mut self, message: &WebDriverMessage<GeckoExtensionRoute>,
pub fn response(&mut self, msg: &WebDriverMessage<GeckoExtensionRoute>,
resp: MarionetteResponse) -> WebDriverResult<WebDriverResponse> {
if resp.id != self.command_id {
@ -621,9 +649,9 @@ impl MarionetteSession {
return Err(WebDriverError::new(status, error.message));
}
try!(self.update(message, &resp));
try!(self.update(msg, &resp));
Ok(match message.command {
Ok(match msg.command {
//Everything that doesn't have a response value
Get(_) | GoBack | GoForward | Refresh | Close | SetTimeouts(_) |
SetWindowSize(_) | MaximizeWindow | SwitchToWindow(_) | SwitchToFrame(_) |