summaryrefslogtreecommitdiffstats
path: root/testing/geckodriver/src
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /testing/geckodriver/src
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/geckodriver/src')
-rw-r--r--testing/geckodriver/src/android.rs470
-rw-r--r--testing/geckodriver/src/build.rs49
-rw-r--r--testing/geckodriver/src/capabilities.rs1088
-rw-r--r--testing/geckodriver/src/command.rs342
-rw-r--r--testing/geckodriver/src/logging.rs351
-rw-r--r--testing/geckodriver/src/main.rs351
-rw-r--r--testing/geckodriver/src/marionette.rs1749
-rw-r--r--testing/geckodriver/src/prefs.rs160
-rw-r--r--testing/geckodriver/src/test.rs12
-rw-r--r--testing/geckodriver/src/tests/profile.zipbin0 -> 444 bytes
10 files changed, 4572 insertions, 0 deletions
diff --git a/testing/geckodriver/src/android.rs b/testing/geckodriver/src/android.rs
new file mode 100644
index 0000000000..6aaf58ec6b
--- /dev/null
+++ b/testing/geckodriver/src/android.rs
@@ -0,0 +1,470 @@
+use crate::capabilities::AndroidOptions;
+use mozdevice::{AndroidStorage, Device, Host};
+use mozprofile::profile::Profile;
+use serde::Serialize;
+use serde_yaml::{Mapping, Value};
+use std::fmt;
+use std::io;
+use std::path::PathBuf;
+use std::time;
+use webdriver::error::{ErrorStatus, WebDriverError};
+
+// TODO: avoid port clashes across GeckoView-vehicles.
+// For now, we always use target port 2829, leading to issues like bug 1533704.
+const TARGET_PORT: u16 = 2829;
+
+const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML
+##
+## Auto-generated by geckodriver.
+## See https://mozilla.github.io/geckoview/consumer/docs/automation.
+"#;
+
+pub type Result<T> = std::result::Result<T, AndroidError>;
+
+#[derive(Debug)]
+pub enum AndroidError {
+ ActivityNotFound(String),
+ Device(mozdevice::DeviceError),
+ IO(io::Error),
+ PackageNotFound(String),
+ Serde(serde_yaml::Error),
+}
+
+impl fmt::Display for AndroidError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ AndroidError::ActivityNotFound(ref package) => {
+ write!(f, "Activity for package '{}' not found", package)
+ }
+ AndroidError::Device(ref message) => message.fmt(f),
+ AndroidError::IO(ref message) => message.fmt(f),
+ AndroidError::PackageNotFound(ref package) => {
+ write!(f, "Package '{}' not found", package)
+ }
+ AndroidError::Serde(ref message) => message.fmt(f),
+ }
+ }
+}
+
+impl From<io::Error> for AndroidError {
+ fn from(value: io::Error) -> AndroidError {
+ AndroidError::IO(value)
+ }
+}
+
+impl From<mozdevice::DeviceError> for AndroidError {
+ fn from(value: mozdevice::DeviceError) -> AndroidError {
+ AndroidError::Device(value)
+ }
+}
+
+impl From<serde_yaml::Error> for AndroidError {
+ fn from(value: serde_yaml::Error) -> AndroidError {
+ AndroidError::Serde(value)
+ }
+}
+
+impl From<AndroidError> for WebDriverError {
+ fn from(value: AndroidError) -> WebDriverError {
+ WebDriverError::new(ErrorStatus::UnknownError, value.to_string())
+ }
+}
+
+/// A remote Gecko instance.
+///
+/// Host refers to the device running `geckodriver`. Target refers to the
+/// Android device running Gecko in a GeckoView-based vehicle.
+#[derive(Debug)]
+pub struct AndroidProcess {
+ pub device: Device,
+ pub package: String,
+ pub activity: String,
+}
+
+impl AndroidProcess {
+ pub fn new(
+ device: Device,
+ package: String,
+ activity: String,
+ ) -> mozdevice::Result<AndroidProcess> {
+ Ok(AndroidProcess {
+ device,
+ package,
+ activity,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct AndroidHandler {
+ pub config: PathBuf,
+ pub options: AndroidOptions,
+ pub process: AndroidProcess,
+ pub profile: PathBuf,
+ pub test_root: PathBuf,
+
+ // For port forwarding host => target
+ pub host_port: u16,
+ pub target_port: u16,
+}
+
+impl Drop for AndroidHandler {
+ fn drop(&mut self) {
+ // Try to clean up various settings
+ let clear_command = format!("am clear-debug-app {}", self.process.package);
+ match self
+ .process
+ .device
+ .execute_host_shell_command(&clear_command)
+ {
+ Ok(_) => debug!("Disabled reading from configuration file"),
+ Err(e) => error!("Failed disabling from configuration file: {}", e),
+ }
+
+ match self.process.device.remove(&self.config) {
+ Ok(_) => debug!("Deleted GeckoView configuration file"),
+ Err(e) => error!("Failed deleting GeckoView configuration file: {}", e),
+ }
+
+ match self.process.device.kill_forward_port(self.host_port) {
+ Ok(_) => debug!(
+ "Android port forward ({} -> {}) stopped",
+ &self.host_port, &self.target_port
+ ),
+ Err(e) => error!(
+ "Android port forward ({} -> {}) failed to stop: {}",
+ &self.host_port, &self.target_port, e
+ ),
+ }
+ }
+}
+
+impl AndroidHandler {
+ pub fn new(options: &AndroidOptions, host_port: u16) -> Result<AndroidHandler> {
+ // We need to push profile.pathbuf to a safe space on the device.
+ // Make it per-Android package to avoid clashes and confusion.
+ // This naming scheme follows GeckoView's configuration file naming scheme,
+ // see bug 1533385.
+
+ let host = Host {
+ host: None,
+ port: None,
+ read_timeout: Some(time::Duration::from_millis(5000)),
+ write_timeout: Some(time::Duration::from_millis(5000)),
+ };
+
+ let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?;
+
+ // Set up port forward. Port forwarding will be torn down, if possible,
+ device.forward_port(host_port, TARGET_PORT)?;
+ debug!(
+ "Android port forward ({} -> {}) started",
+ host_port, TARGET_PORT
+ );
+
+ let test_root = match device.storage {
+ AndroidStorage::App => {
+ device.run_as_package = Some(options.package.to_owned());
+ let mut buf = PathBuf::from("/data/data");
+ buf.push(&options.package);
+ buf.push("test_root");
+ buf
+ }
+ AndroidStorage::Internal => PathBuf::from("/data/local/tmp/test_root"),
+ AndroidStorage::Sdcard => PathBuf::from("/mnt/sdcard/test_root"),
+ };
+
+ debug!(
+ "Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}",
+ options,
+ device.storage,
+ test_root.display(),
+ device.run_as_package
+ );
+
+ let mut profile = test_root.clone();
+ profile.push(format!("{}-geckodriver-profile", &options.package));
+
+ // Check if the specified package is installed
+ let response =
+ device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?;
+ let packages = response
+ .trim()
+ .split_terminator('\n')
+ .filter(|line| line.starts_with("package:"))
+ .map(|line| line.rsplit(':').next().expect("Package name found"))
+ .collect::<Vec<&str>>();
+ if !packages.contains(&options.package.as_str()) {
+ return Err(AndroidError::PackageNotFound(options.package.clone()));
+ }
+
+ let config = PathBuf::from(format!(
+ "/data/local/tmp/{}-geckoview-config.yaml",
+ &options.package
+ ));
+
+ // If activity hasn't been specified default to the main activity of the package
+ let activity = match options.activity {
+ Some(ref activity) => activity.clone(),
+ None => {
+ let response = device.execute_host_shell_command(&format!(
+ "cmd package resolve-activity --brief {}",
+ &options.package
+ ))?;
+ let activities = response
+ .split_terminator('\n')
+ .filter(|line| line.starts_with(&options.package))
+ .map(|line| line.rsplit('/').next().unwrap())
+ .collect::<Vec<&str>>();
+ if activities.is_empty() {
+ return Err(AndroidError::ActivityNotFound(options.package.clone()));
+ }
+
+ activities[0].to_owned()
+ }
+ };
+
+ let process = AndroidProcess::new(device, options.package.clone(), activity)?;
+
+ Ok(AndroidHandler {
+ options: options.clone(),
+ config,
+ process,
+ profile,
+ test_root,
+ host_port,
+ target_port: TARGET_PORT,
+ })
+ }
+
+ pub fn generate_config_file<I, K, V>(&self, envs: I) -> Result<String>
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: ToString,
+ V: ToString,
+ {
+ // To configure GeckoView, we use the automation techniques documented at
+ // https://mozilla.github.io/geckoview/consumer/docs/automation.
+ #[derive(Serialize, Deserialize, PartialEq, Debug)]
+ pub struct Config {
+ pub env: Mapping,
+ pub args: Value,
+ }
+
+ // TODO: Allow to write custom arguments and preferences from moz:firefoxOptions
+ let mut config = Config {
+ args: Value::Sequence(vec![
+ Value::String("--marionette".into()),
+ Value::String("--profile".into()),
+ Value::String(self.profile.display().to_string()),
+ ]),
+ env: Mapping::new(),
+ };
+
+ for (key, value) in envs {
+ config.env.insert(
+ Value::String(key.to_string()),
+ Value::String(value.to_string()),
+ );
+ }
+
+ config.env.insert(
+ Value::String("MOZ_CRASHREPORTER".to_owned()),
+ Value::String("1".to_owned()),
+ );
+ config.env.insert(
+ Value::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()),
+ Value::String("1".to_owned()),
+ );
+ config.env.insert(
+ Value::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()),
+ Value::String("1".to_owned()),
+ );
+
+ let mut contents: Vec<String> = vec![CONFIG_FILE_HEADING.to_owned()];
+ contents.push(serde_yaml::to_string(&config)?);
+
+ Ok(contents.concat())
+ }
+
+ pub fn prepare<I, K, V>(&self, profile: &Profile, env: I) -> Result<()>
+ where
+ I: IntoIterator<Item = (K, V)>,
+ K: ToString,
+ V: ToString,
+ {
+ self.process.device.clear_app_data(&self.process.package)?;
+
+ // These permissions, at least, are required to read profiles in /mnt/sdcard.
+ for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
+ self.process.device.execute_host_shell_command(&format!(
+ "pm grant {} android.permission.{}",
+ &self.process.package, perm
+ ))?;
+ }
+
+ // Make sure to create the test root.
+ self.process.device.create_dir(&self.test_root)?;
+ self.process.device.chmod(&self.test_root, "777", true)?;
+
+ // Replace the profile
+ self.process.device.remove(&self.profile)?;
+ self.process
+ .device
+ .push_dir(&profile.path, &self.profile, 0o777)?;
+
+ let contents = self.generate_config_file(env)?;
+ debug!("Content of generated GeckoView config file:\n{}", contents);
+ let reader = &mut io::BufReader::new(contents.as_bytes());
+
+ debug!(
+ "Pushing GeckoView configuration file to {}",
+ self.config.display()
+ );
+ self.process.device.push(reader, &self.config, 0o777)?;
+
+ // Tell GeckoView to read configuration even when `android:debuggable="false"`.
+ self.process.device.execute_host_shell_command(&format!(
+ "am set-debug-app --persistent {}",
+ self.process.package
+ ))?;
+
+ Ok(())
+ }
+
+ pub fn launch(&self) -> Result<()> {
+ // TODO: Remove the usage of intent arguments once Fennec is no longer
+ // supported. Packages which are using GeckoView always read the arguments
+ // via the YAML configuration file.
+ let mut intent_arguments = self
+ .options
+ .intent_arguments
+ .clone()
+ .unwrap_or_else(|| Vec::with_capacity(3));
+ intent_arguments.push("--es".to_owned());
+ intent_arguments.push("args".to_owned());
+ intent_arguments.push(format!("--marionette --profile {}", self.profile.display()));
+
+ debug!(
+ "Launching {}/{}",
+ self.process.package, self.process.activity
+ );
+ self.process
+ .device
+ .launch(
+ &self.process.package,
+ &self.process.activity,
+ &intent_arguments,
+ )
+ .map_err(|e| {
+ let message = format!(
+ "Could not launch Android {}/{}: {}",
+ self.process.package, self.process.activity, e
+ );
+ mozdevice::DeviceError::Adb(message)
+ })?;
+
+ Ok(())
+ }
+
+ pub fn force_stop(&self) -> Result<()> {
+ debug!(
+ "Force stopping the Android package: {}",
+ &self.process.package
+ );
+ self.process.device.force_stop(&self.process.package)?;
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ // To successfully run those tests the geckoview_example package needs to
+ // be installed on the device or emulator. After setting up the build
+ // environment (https://mzl.la/3muLv5M), the following mach commands have to
+ // be executed:
+ //
+ // $ ./mach build && ./mach install
+ //
+ // Currently the mozdevice API is not safe for multiple requests at the same
+ // time. It is recommended to run each of the unit tests on its own. Also adb
+ // specific tests cannot be run in CI yet. To check those locally, also run
+ // the ignored tests.
+ //
+ // Use the following command to accomplish that:
+ //
+ // $ cargo test -- --ignored --test-threads=1
+
+ use crate::android::AndroidHandler;
+ use crate::capabilities::AndroidOptions;
+ use mozdevice::{AndroidStorage, AndroidStorageInput};
+ use std::path::PathBuf;
+
+ fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) {
+ let options = AndroidOptions::new(package.to_owned(), storage);
+ let handler = AndroidHandler::new(&options, 4242).expect("has valid Android handler");
+
+ assert_eq!(handler.options, options);
+ assert_eq!(handler.process.package, package);
+
+ let expected_config_path = PathBuf::from(format!(
+ "/data/local/tmp/{}-geckoview-config.yaml",
+ &package
+ ));
+ assert_eq!(handler.config, expected_config_path);
+
+ if handler.process.device.storage == AndroidStorage::App {
+ assert_eq!(
+ handler.process.device.run_as_package,
+ Some(package.to_owned())
+ );
+ } else {
+ assert_eq!(handler.process.device.run_as_package, None);
+ }
+
+ let test_root = match handler.process.device.storage {
+ AndroidStorage::App => {
+ let mut buf = PathBuf::from("/data/data");
+ buf.push(&package);
+ buf.push("test_root");
+ buf
+ }
+ AndroidStorage::Internal => PathBuf::from("/data/local/tmp/test_root"),
+ AndroidStorage::Sdcard => PathBuf::from("/mnt/sdcard/test_root"),
+ };
+ assert_eq!(handler.test_root, test_root);
+
+ let mut profile = test_root.clone();
+ profile.push(format!("{}-geckodriver-profile", &package));
+ assert_eq!(handler.profile, profile);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_app() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(&package, AndroidStorageInput::App);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_auto() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::Auto);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_internal() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::Internal);
+ }
+
+ #[test]
+ #[ignore]
+ fn android_handler_storage_as_sdcard() {
+ let package = "org.mozilla.geckoview_example";
+ run_handler_storage_test(package, AndroidStorageInput::Sdcard);
+ }
+}
diff --git a/testing/geckodriver/src/build.rs b/testing/geckodriver/src/build.rs
new file mode 100644
index 0000000000..7ba3144755
--- /dev/null
+++ b/testing/geckodriver/src/build.rs
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use serde_json::Value;
+use std::fmt;
+
+include!(concat!(env!("OUT_DIR"), "/build-info.rs"));
+
+pub struct BuildInfo;
+
+impl BuildInfo {
+ pub fn version() -> &'static str {
+ crate_version!()
+ }
+
+ pub fn hash() -> Option<&'static str> {
+ COMMIT_HASH
+ }
+
+ pub fn date() -> Option<&'static str> {
+ COMMIT_DATE
+ }
+}
+
+impl fmt::Display for BuildInfo {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", BuildInfo::version())?;
+ match (BuildInfo::hash(), BuildInfo::date()) {
+ (Some(hash), Some(date)) => write!(f, " ({} {})", hash, date)?,
+ (Some(hash), None) => write!(f, " ({})", hash)?,
+ _ => {}
+ }
+ Ok(())
+ }
+}
+
+// TODO(Henrik): Change into From
+//std::convert::From<&str>` is not implemented for `rustc_serialize::json::Json
+impl Into<Value> for BuildInfo {
+ fn into(self) -> Value {
+ Value::String(BuildInfo::version().to_string())
+ }
+}
+
+/// Returns build-time information about geckodriver.
+pub fn build_info() -> BuildInfo {
+ BuildInfo {}
+}
diff --git a/testing/geckodriver/src/capabilities.rs b/testing/geckodriver/src/capabilities.rs
new file mode 100644
index 0000000000..e21651ea61
--- /dev/null
+++ b/testing/geckodriver/src/capabilities.rs
@@ -0,0 +1,1088 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::command::LogOptions;
+use crate::logging::Level;
+use base64;
+use mozdevice::AndroidStorageInput;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::Profile;
+use mozrunner::runner::platform::firefox_default_path;
+use mozversion::{self, firefox_binary_version, firefox_version, Version};
+use regex::bytes::Regex;
+use serde_json::{Map, Value};
+use std::collections::BTreeMap;
+use std::default::Default;
+use std::fmt::{self, Display};
+use std::fs;
+use std::io;
+use std::io::BufWriter;
+use std::io::Cursor;
+use std::path::{Path, PathBuf};
+use std::str::{self, FromStr};
+use webdriver::capabilities::{BrowserCapabilities, Capabilities};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+use zip;
+
+#[derive(Clone, Debug)]
+enum VersionError {
+ VersionError(mozversion::Error),
+ MissingBinary,
+}
+
+impl From<mozversion::Error> for VersionError {
+ fn from(err: mozversion::Error) -> VersionError {
+ VersionError::VersionError(err)
+ }
+}
+
+impl Display for VersionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ VersionError::VersionError(ref x) => x.fmt(f),
+ VersionError::MissingBinary => "No binary provided".fmt(f),
+ }
+ }
+}
+
+impl From<VersionError> for WebDriverError {
+ fn from(err: VersionError) -> WebDriverError {
+ WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string())
+ }
+}
+
+/// Provides matching of `moz:firefoxOptions` and resolutionnized of which Firefox
+/// binary to use.
+///
+/// `FirefoxCapabilities` is constructed with the fallback binary, should
+/// `moz:firefoxOptions` not contain a binary entry. This may either be the
+/// system Firefox installation or an override, for example given to the
+/// `--binary` flag of geckodriver.
+pub struct FirefoxCapabilities<'a> {
+ pub chosen_binary: Option<PathBuf>,
+ fallback_binary: Option<&'a PathBuf>,
+ version_cache: BTreeMap<PathBuf, Result<Version, VersionError>>,
+}
+
+impl<'a> FirefoxCapabilities<'a> {
+ pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> {
+ FirefoxCapabilities {
+ chosen_binary: None,
+ fallback_binary,
+ version_cache: BTreeMap::new(),
+ }
+ }
+
+ fn set_binary(&mut self, capabilities: &Map<String, Value>) {
+ self.chosen_binary = capabilities
+ .get("moz:firefoxOptions")
+ .and_then(|x| x.get("binary"))
+ .and_then(|x| x.as_str())
+ .map(PathBuf::from)
+ .or_else(|| self.fallback_binary.cloned())
+ .or_else(firefox_default_path);
+ }
+
+ fn version(&mut self, binary: Option<&Path>) -> Result<Version, VersionError> {
+ if let Some(binary) = binary {
+ if let Some(cache_value) = self.version_cache.get(binary) {
+ return cache_value.clone();
+ }
+ let rv = self
+ .version_from_ini(binary)
+ .or_else(|_| self.version_from_binary(binary));
+ if let Ok(ref version) = rv {
+ debug!("Found version {}", version);
+ } else {
+ debug!("Failed to get binary version");
+ }
+ self.version_cache.insert(binary.to_path_buf(), rv.clone());
+ rv
+ } else {
+ Err(VersionError::MissingBinary)
+ }
+ }
+
+ fn version_from_ini(&self, binary: &Path) -> Result<Version, VersionError> {
+ debug!("Trying to read firefox version from ini files");
+ let version = firefox_version(binary)?;
+ if let Some(version_string) = version.version_string {
+ Version::from_str(&version_string).map_err(|err| err.into())
+ } else {
+ Err(VersionError::VersionError(
+ mozversion::Error::MetadataError("Missing version string".into()),
+ ))
+ }
+ }
+
+ fn version_from_binary(&self, binary: &Path) -> Result<Version, VersionError> {
+ debug!("Trying to read firefox version from binary");
+ Ok(firefox_binary_version(binary)?)
+ }
+}
+
+impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
+ fn init(&mut self, capabilities: &Capabilities) {
+ self.set_binary(capabilities);
+ }
+
+ fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(Some("firefox".into()))
+ }
+
+ fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ let binary = self.chosen_binary.clone();
+ self.version(binary.as_ref().map(|x| x.as_ref()))
+ .map_err(|err| err.into())
+ .map(|x| Some(x.to_string()))
+ }
+
+ fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(if cfg!(target_os = "windows") {
+ Some("windows".into())
+ } else if cfg!(target_os = "macos") {
+ Some("mac".into())
+ } else if cfg!(target_os = "linux") {
+ Some("linux".into())
+ } else {
+ None
+ })
+ }
+
+ fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn compare_browser_version(
+ &mut self,
+ version: &str,
+ comparison: &str,
+ ) -> WebDriverResult<bool> {
+ Version::from_str(version)
+ .map_err(|err| VersionError::from(err))?
+ .matches(comparison)
+ .map_err(|err| VersionError::from(err).into())
+ }
+
+ fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> {
+ if !name.starts_with("moz:") {
+ return Ok(());
+ }
+ match name {
+ "moz:firefoxOptions" => {
+ let data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "moz:firefoxOptions is not an object"
+ );
+ for (key, value) in data.iter() {
+ match &**key {
+ "androidActivity"
+ | "androidDeviceSerial"
+ | "androidPackage"
+ | "profile" => {
+ if !value.is_string() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a string", &**key),
+ ));
+ }
+ }
+ "androidIntentArguments" | "args" => {
+ if !try_opt!(
+ value.as_array(),
+ ErrorStatus::InvalidArgument,
+ format!("{} is not an array", &**key)
+ )
+ .iter()
+ .all(|value| value.is_string())
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} entry is not a string", &**key),
+ ));
+ }
+ }
+ "binary" => {
+ if let Some(binary) = value.as_str() {
+ if !data.contains_key("androidPackage")
+ && self.version(Some(Path::new(binary))).is_err()
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a Firefox executable", &**key),
+ ));
+ }
+ } else {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a string", &**key),
+ ));
+ }
+ }
+ "env" => {
+ let env_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "env value is not an object"
+ );
+ if !env_data.values().all(Value::is_string) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Environment values were not all strings",
+ ));
+ }
+ }
+ "log" => {
+ let log_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "log value is not an object"
+ );
+ for (log_key, log_value) in log_data.iter() {
+ match &**log_key {
+ "level" => {
+ let level = try_opt!(
+ log_value.as_str(),
+ ErrorStatus::InvalidArgument,
+ "log level is not a string"
+ );
+ if Level::from_str(level).is_err() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Not a valid log level: {}", level),
+ ));
+ }
+ }
+ x => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid log field {}", x),
+ ))
+ }
+ }
+ }
+ }
+ "prefs" => {
+ let prefs_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "prefs value is not an object"
+ );
+ let is_pref_value_type = |x: &Value| {
+ x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean()
+ };
+ if !prefs_data.values().all(is_pref_value_type) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Preference values not all string or integer or boolean",
+ ));
+ }
+ }
+ x => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid moz:firefoxOptions field {}", x),
+ ))
+ }
+ }
+ }
+ }
+ "moz:useNonSpecCompliantPointerOrigin" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:useNonSpecCompliantPointerOrigin is not a boolean",
+ ));
+ }
+ }
+ "moz:webdriverClick" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:webdriverClick is not a boolean",
+ ));
+ }
+ }
+ "moz:debuggerAddress" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:debuggerAddress is not a boolean",
+ ));
+ }
+ }
+ _ => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Unrecognised option {}", name),
+ ))
+ }
+ }
+ Ok(())
+ }
+
+ fn accept_custom(&mut self, _: &str, _: &Value, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+}
+
+/// Android-specific options in the `moz:firefoxOptions` struct.
+/// These map to "androidCamelCase", following [chromedriver's Android-specific
+/// Capabilities](http://chromedriver.chromium.org/getting-started/getting-started---android).
+#[derive(Default, Clone, Debug, PartialEq)]
+pub struct AndroidOptions {
+ pub activity: Option<String>,
+ pub device_serial: Option<String>,
+ pub intent_arguments: Option<Vec<String>>,
+ pub package: String,
+ pub storage: AndroidStorageInput,
+}
+
+impl AndroidOptions {
+ pub fn new(package: String, storage: AndroidStorageInput) -> AndroidOptions {
+ AndroidOptions {
+ package,
+ storage,
+ ..Default::default()
+ }
+ }
+}
+
+/// Rust representation of `moz:firefoxOptions`.
+///
+/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
+/// the encoded profile, the binary arguments, log settings, and additional
+/// preferences to be checked and unmarshaled from the `moz:firefoxOptions`
+/// JSON Object into a Rust representation.
+#[derive(Default, Debug)]
+pub struct FirefoxOptions {
+ pub binary: Option<PathBuf>,
+ pub profile: Option<Profile>,
+ pub args: Option<Vec<String>>,
+ pub env: Option<Vec<(String, String)>>,
+ pub log: LogOptions,
+ pub prefs: Vec<(String, Pref)>,
+ pub android: Option<AndroidOptions>,
+}
+
+impl FirefoxOptions {
+ pub fn new() -> FirefoxOptions {
+ Default::default()
+ }
+
+ pub fn from_capabilities(
+ binary_path: Option<PathBuf>,
+ android_storage: AndroidStorageInput,
+ matched: &mut Capabilities,
+ ) -> WebDriverResult<FirefoxOptions> {
+ let mut rv = FirefoxOptions::new();
+ rv.binary = binary_path;
+
+ if let Some(json) = matched.remove("moz:firefoxOptions") {
+ let options = json.as_object().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "'moz:firefoxOptions' \
+ capability is not an object",
+ )
+ })?;
+
+ rv.android = FirefoxOptions::load_android(android_storage, &options)?;
+ rv.args = FirefoxOptions::load_args(&options)?;
+ rv.env = FirefoxOptions::load_env(&options)?;
+ rv.log = FirefoxOptions::load_log(&options)?;
+ rv.prefs = FirefoxOptions::load_prefs(&options)?;
+ rv.profile = FirefoxOptions::load_profile(&options)?;
+ }
+
+ if let Some(json) = matched.remove("moz:debuggerAddress") {
+ let use_web_socket = json.as_bool().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:debuggerAddress is not a boolean",
+ )
+ })?;
+
+ if use_web_socket {
+ let mut remote_args = Vec::new();
+ remote_args.push("--remote-debugging-port".to_owned());
+ remote_args.push("0".to_owned());
+
+ if let Some(ref mut args) = rv.args {
+ args.append(&mut remote_args);
+ } else {
+ rv.args = Some(remote_args);
+ }
+
+ // Force Fission disabled until Remote Agent is compatible,
+ // and preference hasn't been already set
+ let has_fission_pref = rv.prefs.iter().find(|&x| x.0 == "fission.autostart");
+ if has_fission_pref.is_none() {
+ rv.prefs
+ .push(("fission.autostart".to_owned(), Pref::new(false)));
+ }
+ }
+ }
+
+ Ok(rv)
+ }
+
+ fn load_profile(options: &Capabilities) -> WebDriverResult<Option<Profile>> {
+ if let Some(profile_json) = options.get("profile") {
+ let profile_base64 = profile_json.as_str().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string")
+ })?;
+ let profile_zip = &*base64::decode(profile_base64)?;
+
+ // Create an emtpy profile directory
+ let profile = Profile::new()?;
+ unzip_buffer(
+ profile_zip,
+ profile
+ .temp_dir
+ .as_ref()
+ .expect("Profile doesn't have a path")
+ .path(),
+ )?;
+
+ Ok(Some(profile))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> {
+ if let Some(args_json) = options.get("args") {
+ let args_array = args_json.as_array().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Arguments were not an \
+ array",
+ )
+ })?;
+ let args = args_array
+ .iter()
+ .map(|x| x.as_str().map(|x| x.to_owned()))
+ .collect::<Option<Vec<String>>>()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Arguments entries were not all strings",
+ )
+ })?;
+ Ok(Some(args))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub fn load_env(options: &Capabilities) -> WebDriverResult<Option<Vec<(String, String)>>> {
+ if let Some(env_data) = options.get("env") {
+ let env = env_data.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Env was not an object")
+ })?;
+ let mut rv = Vec::with_capacity(env.len());
+ for (key, value) in env.iter() {
+ rv.push((
+ key.clone(),
+ value
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Env value is not a string",
+ )
+ })?
+ .to_string(),
+ ));
+ }
+ Ok(Some(rv))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> {
+ if let Some(json) = options.get("log") {
+ let log = json.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Log section is not an object")
+ })?;
+
+ let level = match log.get("level") {
+ Some(json) => {
+ let s = json.as_str().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Log level is not a string",
+ )
+ })?;
+ Some(Level::from_str(s).ok().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Log level is unknown")
+ })?)
+ }
+ None => None,
+ };
+
+ Ok(LogOptions { level })
+ } else {
+ Ok(Default::default())
+ }
+ }
+
+ pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> {
+ if let Some(prefs_data) = options.get("prefs") {
+ let prefs = prefs_data.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Prefs were not an object")
+ })?;
+ let mut rv = Vec::with_capacity(prefs.len());
+ for (key, value) in prefs.iter() {
+ rv.push((key.clone(), pref_from_json(value)?));
+ }
+ Ok(rv)
+ } else {
+ Ok(vec![])
+ }
+ }
+
+ pub fn load_android(
+ storage: AndroidStorageInput,
+ options: &Capabilities,
+ ) -> WebDriverResult<Option<AndroidOptions>> {
+ if let Some(package_json) = options.get("androidPackage") {
+ let package = package_json
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidPackage is not a string",
+ )
+ })?
+ .to_owned();
+
+ // https://developer.android.com/studio/build/application-id
+ let package_regexp =
+ Regex::new(r#"^([a-zA-Z][a-zA-Z0-9_]*\.){1,}([a-zA-Z][a-zA-Z0-9_]*)$"#).unwrap();
+ if !package_regexp.is_match(package.as_bytes()) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Not a valid androidPackage name",
+ ));
+ }
+
+ let mut android = AndroidOptions::new(package, storage);
+
+ android.activity = match options.get("androidActivity") {
+ Some(json) => {
+ let activity = json
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidActivity is not a string",
+ )
+ })?
+ .to_owned();
+
+ if activity.contains("/") {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidActivity should not contain '/",
+ ));
+ }
+
+ Some(activity)
+ }
+ None => None,
+ };
+
+ android.device_serial = match options.get("androidDeviceSerial") {
+ Some(json) => Some(
+ json.as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidDeviceSerial is not a string",
+ )
+ })?
+ .to_owned(),
+ ),
+ None => None,
+ };
+
+ android.intent_arguments = match options.get("androidIntentArguments") {
+ Some(json) => {
+ let args_array = json.as_array().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidIntentArguments is not an array",
+ )
+ })?;
+ let args = args_array
+ .iter()
+ .map(|x| x.as_str().map(|x| x.to_owned()))
+ .collect::<Option<Vec<String>>>()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidIntentArguments entries are not all strings",
+ )
+ })?;
+
+ Some(args)
+ }
+ None => None,
+ };
+
+ Ok(Some(android))
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+fn pref_from_json(value: &Value) -> WebDriverResult<Pref> {
+ match *value {
+ Value::String(ref x) => Ok(Pref::new(x.clone())),
+ Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())),
+ Value::Bool(x) => Ok(Pref::new(x)),
+ _ => Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Could not convert pref value to string, boolean, or integer",
+ )),
+ }
+}
+
+fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
+ let reader = Cursor::new(buf);
+ let mut zip = zip::ZipArchive::new(reader)
+ .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile"))?;
+
+ for i in 0..zip.len() {
+ let mut file = zip.by_index(i).map_err(|_| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Processing profile zip file failed",
+ )
+ })?;
+ let unzip_path = {
+ let name = file.name();
+ let is_dir = name.ends_with('/');
+ let rel_path = Path::new(name);
+ let dest_path = dest_dir.join(rel_path);
+
+ {
+ let create_dir = if is_dir {
+ Some(dest_path.as_path())
+ } else {
+ dest_path.parent()
+ };
+ if let Some(dir) = create_dir {
+ if !dir.exists() {
+ debug!("Creating profile directory tree {}", dir.to_string_lossy());
+ fs::create_dir_all(dir)?;
+ }
+ }
+ }
+
+ if is_dir {
+ None
+ } else {
+ Some(dest_path)
+ }
+ };
+
+ if let Some(unzip_path) = unzip_path {
+ debug!("Extracting profile to {}", unzip_path.to_string_lossy());
+ let dest = fs::File::create(unzip_path)?;
+ if file.size() > 0 {
+ let mut writer = BufWriter::new(dest);
+ io::copy(&mut file, &mut writer)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate mozprofile;
+
+ use self::mozprofile::preferences::Pref;
+ use super::*;
+ use crate::marionette::MarionetteHandler;
+ use mozdevice::AndroidStorageInput;
+ use serde_json::json;
+ use std::default::Default;
+ use std::fs::File;
+ use std::io::Read;
+
+ use webdriver::capabilities::Capabilities;
+
+ fn example_profile() -> Value {
+ let mut profile_data = Vec::with_capacity(1024);
+ let mut profile = File::open("src/tests/profile.zip").unwrap();
+ profile.read_to_end(&mut profile_data).unwrap();
+ Value::String(base64::encode(&profile_data))
+ }
+
+ fn make_options(firefox_opts: Capabilities) -> WebDriverResult<FirefoxOptions> {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
+
+ FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps)
+ }
+
+ #[test]
+ fn fx_options_default() {
+ let opts = FirefoxOptions::new();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, None);
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ // Profile doesn't support PartialEq
+ // assert_eq!(opts.profile, None);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_no_binary_and_caps() {
+ let mut caps = Capabilities::new();
+
+ let opts =
+ FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps).unwrap();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, None);
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_binary_and_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert(
+ "moz:firefoxOptions".into(),
+ Value::Object(Capabilities::new()),
+ );
+
+ let binary = PathBuf::from("foo");
+
+ let opts = FirefoxOptions::from_capabilities(
+ Some(binary.clone()),
+ AndroidStorageInput::Auto,
+ &mut caps,
+ )
+ .unwrap();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, Some(binary));
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_not_set() {
+ let mut caps = Capabilities::new();
+
+ let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps)
+ .expect("Valid Firefox options");
+
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_false() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:debuggerAddress".into(), json!(false));
+
+ let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps)
+ .expect("Valid Firefox options");
+
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for remote protocol unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_true() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:debuggerAddress".into(), json!(true));
+
+ let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter
+ .find(|&arg| arg == &"--remote-debugging-port".to_owned())
+ .is_some());
+ assert_eq!(iter.next(), Some(&"0".to_owned()));
+ } else {
+ assert!(false, "CLI arguments for remote protocol not found");
+ }
+
+ assert!(opts
+ .prefs
+ .iter()
+ .any(|pref| pref == &("fission.autostart".to_owned(), Pref::new(false))));
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_invalid_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), json!(42));
+
+ FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps)
+ .expect_err("Firefox options need to be of type object");
+ }
+
+ #[test]
+ fn fx_options_android_no_package() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidAvtivity".into(), json!("foo"));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ assert_eq!(opts.android, None);
+ }
+
+ #[test]
+ fn fx_options_android_package_valid_value() {
+ for value in ["foo.bar", "foo.bar.cheese.is.good", "Foo.Bar_9"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(value));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ assert_eq!(
+ opts.android,
+ Some(AndroidOptions::new(
+ value.to_string(),
+ AndroidStorageInput::Auto
+ ))
+ );
+ }
+ }
+
+ #[test]
+ fn fx_options_android_package_invalid_type() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_package_invalid_value() {
+ for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(value));
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_valid_value() {
+ for value in ["cheese", "Cheese_9"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!(value));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let android_opts = AndroidOptions {
+ package: "foo.bar".to_owned(),
+ activity: Some(value.to_string()),
+ ..Default::default()
+ };
+ assert_eq!(opts.android, Some(android_opts));
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_invalid_type() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_activity_invalid_value() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese"));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_device_serial() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidDeviceSerial".into(), json!("cheese"));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let android_opts = AndroidOptions {
+ package: "foo.bar".to_owned(),
+ device_serial: Some("cheese".to_owned()),
+ ..Default::default()
+ };
+ assert_eq!(opts.android, Some(android_opts));
+ }
+
+ #[test]
+ fn fx_options_android_serial_invalid() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidDeviceSerial".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"]));
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let android_opts = AndroidOptions {
+ package: "foo.bar".to_owned(),
+ intent_arguments: Some(vec!["lorem".to_owned(), "ipsum".to_owned()]),
+ ..Default::default()
+ };
+ assert_eq!(opts.android, Some(android_opts));
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_no_array() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(42));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_invalid_value() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42]));
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_env() {
+ let mut env: Map<String, Value> = Map::new();
+ env.insert("TEST_KEY_A".into(), Value::String("test_value_a".into()));
+ env.insert("TEST_KEY_B".into(), Value::String("test_value_b".into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ let mut opts = make_options(firefox_opts).expect("valid firefox options");
+ for sorted in opts.env.iter_mut() {
+ sorted.sort()
+ }
+ assert_eq!(
+ opts.env,
+ Some(vec![
+ ("TEST_KEY_A".into(), "test_value_a".into()),
+ ("TEST_KEY_B".into(), "test_value_b".into()),
+ ])
+ );
+ }
+
+ #[test]
+ fn fx_options_env_invalid_container() {
+ let env = Value::Number(1.into());
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_env_invalid_value() {
+ let mut env: Map<String, Value> = Map::new();
+ env.insert("TEST_KEY".into(), Value::Number(1.into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ make_options(firefox_opts).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn test_profile() {
+ let encoded_profile = example_profile();
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+
+ let opts = make_options(firefox_opts).expect("valid firefox options");
+ let mut profile = opts.profile.expect("valid firefox profile");
+ let prefs = profile.user_prefs().expect("valid preferences");
+
+ println!("{:#?}", prefs.prefs);
+
+ assert_eq!(
+ prefs.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS"))
+ );
+ }
+
+ #[test]
+ fn test_prefs() {
+ let encoded_profile = example_profile();
+ let mut prefs: Map<String, Value> = Map::new();
+ prefs.insert(
+ "browser.display.background_color".into(),
+ Value::String("#00ff00".into()),
+ );
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+ firefox_opts.insert("prefs".into(), Value::Object(prefs));
+
+ let opts = make_options(firefox_opts).expect("valid profile and prefs");
+ let mut profile = opts.profile.expect("valid firefox profile");
+
+ let handler = MarionetteHandler::new(Default::default());
+ handler
+ .set_prefs(2828, &mut profile, true, opts.prefs)
+ .expect("set preferences");
+
+ let prefs_set = profile.user_prefs().expect("valid user preferences");
+ println!("{:#?}", prefs_set.prefs);
+
+ assert_eq!(
+ prefs_set.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS"))
+ );
+ assert_eq!(
+ prefs_set.get("browser.display.background_color"),
+ Some(&Pref::new("#00ff00"))
+ );
+ assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828)));
+ }
+}
diff --git a/testing/geckodriver/src/command.rs b/testing/geckodriver/src/command.rs
new file mode 100644
index 0000000000..f5bc27ad81
--- /dev/null
+++ b/testing/geckodriver/src/command.rs
@@ -0,0 +1,342 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::logging;
+use base64;
+use hyper::Method;
+use serde::de::{self, Deserialize, Deserializer};
+use serde_json::{self, Value};
+use std::env;
+use std::fs::File;
+use std::io::prelude::*;
+use uuid::Uuid;
+use webdriver::command::{WebDriverCommand, WebDriverExtensionCommand};
+use webdriver::error::WebDriverResult;
+use webdriver::httpapi::WebDriverExtensionRoute;
+use webdriver::Parameters;
+
+pub const CHROME_ELEMENT_KEY: &str = "chromeelement-9fc5-4b51-a3c8-01716eedeb04";
+
+pub fn extension_routes() -> Vec<(Method, &'static str, GeckoExtensionRoute)> {
+ return vec![
+ (
+ Method::GET,
+ "/session/{sessionId}/moz/context",
+ GeckoExtensionRoute::GetContext,
+ ),
+ (
+ Method::POST,
+ "/session/{sessionId}/moz/context",
+ GeckoExtensionRoute::SetContext,
+ ),
+ (
+ Method::POST,
+ "/session/{sessionId}/moz/addon/install",
+ GeckoExtensionRoute::InstallAddon,
+ ),
+ (
+ Method::POST,
+ "/session/{sessionId}/moz/addon/uninstall",
+ GeckoExtensionRoute::UninstallAddon,
+ ),
+ (
+ Method::GET,
+ "/session/{sessionId}/moz/screenshot/full",
+ GeckoExtensionRoute::TakeFullScreenshot,
+ ),
+ ];
+}
+
+#[derive(Clone, PartialEq)]
+pub enum GeckoExtensionRoute {
+ GetContext,
+ SetContext,
+ InstallAddon,
+ UninstallAddon,
+ TakeFullScreenshot,
+}
+
+impl WebDriverExtensionRoute for GeckoExtensionRoute {
+ type Command = GeckoExtensionCommand;
+
+ fn command(
+ &self,
+ _params: &Parameters,
+ body_data: &Value,
+ ) -> WebDriverResult<WebDriverCommand<GeckoExtensionCommand>> {
+ use self::GeckoExtensionRoute::*;
+
+ let command = match *self {
+ GetContext => GeckoExtensionCommand::GetContext,
+ SetContext => {
+ GeckoExtensionCommand::SetContext(serde_json::from_value(body_data.clone())?)
+ }
+ InstallAddon => {
+ GeckoExtensionCommand::InstallAddon(serde_json::from_value(body_data.clone())?)
+ }
+ UninstallAddon => {
+ GeckoExtensionCommand::UninstallAddon(serde_json::from_value(body_data.clone())?)
+ }
+ TakeFullScreenshot => GeckoExtensionCommand::TakeFullScreenshot,
+ };
+
+ Ok(WebDriverCommand::Extension(command))
+ }
+}
+
+#[derive(Clone)]
+pub enum GeckoExtensionCommand {
+ GetContext,
+ SetContext(GeckoContextParameters),
+ InstallAddon(AddonInstallParameters),
+ UninstallAddon(AddonUninstallParameters),
+ TakeFullScreenshot,
+}
+
+impl WebDriverExtensionCommand for GeckoExtensionCommand {
+ fn parameters_json(&self) -> Option<Value> {
+ use self::GeckoExtensionCommand::*;
+ match self {
+ GetContext => None,
+ InstallAddon(x) => Some(serde_json::to_value(x).unwrap()),
+ SetContext(x) => Some(serde_json::to_value(x).unwrap()),
+ UninstallAddon(x) => Some(serde_json::to_value(x).unwrap()),
+ TakeFullScreenshot => None,
+ }
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize)]
+pub struct AddonInstallParameters {
+ pub path: String,
+ pub temporary: Option<bool>,
+}
+
+impl<'de> Deserialize<'de> for AddonInstallParameters {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Debug, Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct Base64 {
+ addon: String,
+ temporary: Option<bool>,
+ };
+
+ #[derive(Debug, Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct Path {
+ path: String,
+ temporary: Option<bool>,
+ };
+
+ #[derive(Debug, Deserialize)]
+ #[serde(untagged)]
+ enum Helper {
+ Base64(Base64),
+ Path(Path),
+ };
+
+ let params = match Helper::deserialize(deserializer)? {
+ Helper::Path(ref mut data) => AddonInstallParameters {
+ path: data.path.clone(),
+ temporary: data.temporary,
+ },
+ Helper::Base64(ref mut data) => {
+ let content = base64::decode(&data.addon).map_err(de::Error::custom)?;
+
+ let path = env::temp_dir()
+ .as_path()
+ .join(format!("addon-{}.xpi", Uuid::new_v4()));
+ let mut xpi_file = File::create(&path).map_err(de::Error::custom)?;
+ xpi_file
+ .write(content.as_slice())
+ .map_err(de::Error::custom)?;
+
+ let path = match path.to_str() {
+ Some(path) => path.to_string(),
+ None => return Err(de::Error::custom("could not write addon to file")),
+ };
+
+ AddonInstallParameters {
+ path,
+ temporary: data.temporary,
+ }
+ }
+ };
+
+ Ok(params)
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct AddonUninstallParameters {
+ pub id: String,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum GeckoContext {
+ Content,
+ Chrome,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct GeckoContextParameters {
+ pub context: GeckoContext,
+}
+
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+pub struct XblLocatorParameters {
+ pub name: String,
+ pub value: String,
+}
+
+#[derive(Default, Debug, PartialEq)]
+pub struct LogOptions {
+ pub level: Option<logging::Level>,
+}
+
+#[cfg(test)]
+mod tests {
+ use serde_json::json;
+
+ use super::*;
+ use crate::test::assert_de;
+
+ #[test]
+ fn test_json_addon_install_parameters_invalid() {
+ assert!(serde_json::from_str::<AddonInstallParameters>("").is_err());
+ assert!(serde_json::from_value::<AddonInstallParameters>(json!(null)).is_err());
+ assert!(serde_json::from_value::<AddonInstallParameters>(json!({})).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_path_and_temporary() {
+ let params = AddonInstallParameters {
+ path: "/path/to.xpi".to_string(),
+ temporary: Some(true),
+ };
+ assert_de(&params, json!({"path": "/path/to.xpi", "temporary": true}));
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_path() {
+ let params = AddonInstallParameters {
+ path: "/path/to.xpi".to_string(),
+ temporary: None,
+ };
+ assert_de(&params, json!({"path": "/path/to.xpi"}));
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_path_invalid_type() {
+ let json = json!({"path": true, "temporary": true});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_path_and_temporary_invalid_type() {
+ let json = json!({"path": "/path/to.xpi", "temporary": "foo"});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_addon() {
+ let json = json!({"addon": "aGVsbG8=", "temporary": true});
+ let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap();
+
+ assert_eq!(data.temporary, Some(true));
+ let mut file = File::open(data.path).unwrap();
+ let mut contents = String::new();
+ file.read_to_string(&mut contents).unwrap();
+ assert_eq!(contents, "hello");
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_addon_only() {
+ let json = json!({"addon": "aGVsbG8="});
+ let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap();
+
+ assert_eq!(data.temporary, None);
+ let mut file = File::open(data.path).unwrap();
+ let mut contents = String::new();
+ file.read_to_string(&mut contents).unwrap();
+ assert_eq!(contents, "hello");
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_addon_invalid_type() {
+ let json = json!({"addon": true, "temporary": true});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_addon_and_temporary_invalid_type() {
+ let json = json!({"addon": "aGVsbG8=", "temporary": "foo"});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_install_parameters_with_temporary_only() {
+ let json = json!({"temporary": true});
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_install_parameters_with_both_path_and_addon() {
+ let json = json!({
+ "path": "/path/to.xpi",
+ "addon": "aGVsbG8=",
+ "temporary": true,
+ });
+ assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_uninstall_parameters_invalid() {
+ assert!(serde_json::from_str::<AddonUninstallParameters>("").is_err());
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json!(null)).is_err());
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json!({})).is_err());
+ }
+
+ #[test]
+ fn test_json_addon_uninstall_parameters() {
+ let params = AddonUninstallParameters {
+ id: "foo".to_string(),
+ };
+ assert_de(&params, json!({"id": "foo"}));
+ }
+
+ #[test]
+ fn test_json_addon_uninstall_parameters_id_invalid_type() {
+ let json = json!({"id": true});
+ assert!(serde_json::from_value::<AddonUninstallParameters>(json).is_err());
+ }
+
+ #[test]
+ fn test_json_gecko_context_parameters_content() {
+ let params = GeckoContextParameters {
+ context: GeckoContext::Content,
+ };
+ assert_de(&params, json!({"context": "content"}));
+ }
+
+ #[test]
+ fn test_json_gecko_context_parameters_chrome() {
+ let params = GeckoContextParameters {
+ context: GeckoContext::Chrome,
+ };
+ assert_de(&params, json!({"context": "chrome"}));
+ }
+
+ #[test]
+ fn test_json_gecko_context_parameters_context_invalid() {
+ type P = GeckoContextParameters;
+ assert!(serde_json::from_value::<P>(json!({})).is_err());
+ assert!(serde_json::from_value::<P>(json!({ "context": null })).is_err());
+ assert!(serde_json::from_value::<P>(json!({"context": "foo"})).is_err());
+ }
+}
diff --git a/testing/geckodriver/src/logging.rs b/testing/geckodriver/src/logging.rs
new file mode 100644
index 0000000000..7721bb7770
--- /dev/null
+++ b/testing/geckodriver/src/logging.rs
@@ -0,0 +1,351 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! Gecko-esque logger implementation for the [`log`] crate.
+//!
+//! The [`log`] crate provides a single logging API that abstracts over the
+//! actual logging implementation. This module uses the logging API
+//! to provide a log implementation that shares many aesthetical traits with
+//! [Log.jsm] from Gecko.
+//!
+//! Using the [`error!`], [`warn!`], [`info!`], [`debug!`], and
+//! [`trace!`] macros from `log` will output a timestamp field, followed by the
+//! log level, and then the message. The fields are separated by a tab
+//! character, making the output suitable for further text processing with
+//! `awk(1)`.
+//!
+//! This module shares the same API as `log`, except it provides additional
+//! entry functions [`init`] and [`init_with_level`] and additional log levels
+//! `Level::Fatal` and `Level::Config`. Converting these into the
+//! [`log::Level`] is lossy so that `Level::Fatal` becomes `log::Level::Error`
+//! and `Level::Config` becomes `log::Level::Debug`.
+//!
+//! [`log`]: https://docs.rs/log/newest/log/
+//! [Log.jsm]: https://developer.mozilla.org/en/docs/Mozilla/JavaScript_code_modules/Log.jsm
+//! [`error!`]: https://docs.rs/log/newest/log/macro.error.html
+//! [`warn!`]: https://docs.rs/log/newest/log/macro.warn.html
+//! [`info!`]: https://docs.rs/log/newest/log/macro.info.html
+//! [`debug!`]: https://docs.rs/log/newest/log/macro.debug.html
+//! [`trace!`]: https://docs.rs/log/newest/log/macro.trace.html
+//! [`init`]: fn.init.html
+//! [`init_with_level`]: fn.init_with_level.html
+
+use std::fmt;
+use std::io;
+use std::io::Write;
+use std::str;
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use chrono;
+use log;
+use mozprofile::preferences::Pref;
+
+static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0);
+const LOGGED_TARGETS: &[&str] = &[
+ "geckodriver",
+ "mozdevice",
+ "mozprofile",
+ "mozrunner",
+ "mozversion",
+ "webdriver",
+];
+
+/// Logger levels from [Log.jsm].
+///
+/// [Log.jsm]: https://developer.mozilla.org/en/docs/Mozilla/JavaScript_code_modules/Log.jsm
+#[repr(usize)]
+#[derive(Clone, Copy, Eq, Debug, Hash, PartialEq)]
+pub enum Level {
+ Fatal = 70,
+ Error = 60,
+ Warn = 50,
+ Info = 40,
+ Config = 30,
+ Debug = 20,
+ Trace = 10,
+}
+
+impl From<usize> for Level {
+ fn from(n: usize) -> Level {
+ use self::Level::*;
+ match n {
+ 70 => Fatal,
+ 60 => Error,
+ 50 => Warn,
+ 40 => Info,
+ 30 => Config,
+ 20 => Debug,
+ 10 => Trace,
+ _ => Info,
+ }
+ }
+}
+
+impl fmt::Display for Level {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use self::Level::*;
+ let s = match *self {
+ Fatal => "FATAL",
+ Error => "ERROR",
+ Warn => "WARN",
+ Info => "INFO",
+ Config => "CONFIG",
+ Debug => "DEBUG",
+ Trace => "TRACE",
+ };
+ write!(f, "{}", s)
+ }
+}
+
+impl str::FromStr for Level {
+ type Err = ();
+
+ fn from_str(s: &str) -> Result<Level, ()> {
+ use self::Level::*;
+ match s.to_lowercase().as_ref() {
+ "fatal" => Ok(Fatal),
+ "error" => Ok(Error),
+ "warn" => Ok(Warn),
+ "info" => Ok(Info),
+ "config" => Ok(Config),
+ "debug" => Ok(Debug),
+ "trace" => Ok(Trace),
+ _ => Err(()),
+ }
+ }
+}
+
+impl Into<log::Level> for Level {
+ fn into(self) -> log::Level {
+ use self::Level::*;
+ match self {
+ Fatal | Error => log::Level::Error,
+ Warn => log::Level::Warn,
+ Info => log::Level::Info,
+ Config | Debug => log::Level::Debug,
+ Trace => log::Level::Trace,
+ }
+ }
+}
+
+impl Into<Pref> for Level {
+ fn into(self) -> Pref {
+ use self::Level::*;
+ Pref::new(match self {
+ Fatal => "Fatal",
+ Error => "Error",
+ Warn => "Warn",
+ Info => "Info",
+ Config => "Config",
+ Debug => "Debug",
+ Trace => "Trace",
+ })
+ }
+}
+
+impl From<log::Level> for Level {
+ fn from(log_level: log::Level) -> Level {
+ use log::Level::*;
+ match log_level {
+ Error => Level::Error,
+ Warn => Level::Warn,
+ Info => Level::Info,
+ Debug => Level::Debug,
+ Trace => Level::Trace,
+ }
+ }
+}
+
+struct Logger;
+
+impl log::Log for Logger {
+ fn enabled(&self, meta: &log::Metadata) -> bool {
+ LOGGED_TARGETS.iter().any(|&x| meta.target().starts_with(x))
+ && meta.level() <= log::max_level()
+ }
+
+ fn log(&self, record: &log::Record) {
+ if self.enabled(record.metadata()) {
+ let ts = format_ts(chrono::Local::now());
+ println!(
+ "{}\t{}\t{}\t{}",
+ ts,
+ record.target(),
+ record.level(),
+ record.args()
+ );
+ }
+ }
+
+ fn flush(&self) {
+ io::stdout().flush().unwrap();
+ }
+}
+
+/// Initialises the logging subsystem with the default log level.
+pub fn init() -> Result<(), log::SetLoggerError> {
+ init_with_level(Level::Info)
+}
+
+/// Initialises the logging subsystem.
+pub fn init_with_level(level: Level) -> Result<(), log::SetLoggerError> {
+ let logger = Logger {};
+ set_max_level(level);
+ log::set_boxed_logger(Box::new(logger))?;
+ Ok(())
+}
+
+/// Returns the current maximum log level.
+pub fn max_level() -> Level {
+ MAX_LOG_LEVEL.load(Ordering::Relaxed).into()
+}
+
+/// Sets the global maximum log level.
+pub fn set_max_level(level: Level) {
+ MAX_LOG_LEVEL.store(level as usize, Ordering::SeqCst);
+
+ let slevel: log::Level = level.into();
+ log::set_max_level(slevel.to_level_filter())
+}
+
+/// Produces a 13-digit Unix Epoch timestamp similar to Gecko.
+fn format_ts(ts: chrono::DateTime<chrono::Local>) -> String {
+ format!("{}{:03}", ts.timestamp(), ts.timestamp_subsec_millis())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{format_ts, init_with_level, max_level, set_max_level, Level};
+
+ use std::str::FromStr;
+ use std::sync::Mutex;
+
+ use chrono;
+ use log;
+ use mozprofile::preferences::{Pref, PrefValue};
+
+ lazy_static! {
+ static ref LEVEL_MUTEX: Mutex<()> = Mutex::new(());
+ }
+
+ #[test]
+ fn test_level_repr() {
+ assert_eq!(Level::Fatal as usize, 70);
+ assert_eq!(Level::Error as usize, 60);
+ assert_eq!(Level::Warn as usize, 50);
+ assert_eq!(Level::Info as usize, 40);
+ assert_eq!(Level::Config as usize, 30);
+ assert_eq!(Level::Debug as usize, 20);
+ assert_eq!(Level::Trace as usize, 10);
+ }
+
+ #[test]
+ fn test_level_eq() {
+ assert_eq!(Level::Fatal, Level::Fatal);
+ assert_eq!(Level::Error, Level::Error);
+ assert_eq!(Level::Warn, Level::Warn);
+ assert_eq!(Level::Info, Level::Info);
+ assert_eq!(Level::Config, Level::Config);
+ assert_eq!(Level::Debug, Level::Debug);
+ assert_eq!(Level::Trace, Level::Trace);
+ }
+
+ #[test]
+ fn test_level_from_log() {
+ assert_eq!(Level::from(log::Level::Error), Level::Error);
+ assert_eq!(Level::from(log::Level::Warn), Level::Warn);
+ assert_eq!(Level::from(log::Level::Info), Level::Info);
+ assert_eq!(Level::from(log::Level::Debug), Level::Debug);
+ assert_eq!(Level::from(log::Level::Trace), Level::Trace);
+ }
+
+ #[test]
+ fn test_level_into_log() {
+ assert_eq!(Into::<log::Level>::into(Level::Fatal), log::Level::Error);
+ assert_eq!(Into::<log::Level>::into(Level::Error), log::Level::Error);
+ assert_eq!(Into::<log::Level>::into(Level::Warn), log::Level::Warn);
+ assert_eq!(Into::<log::Level>::into(Level::Info), log::Level::Info);
+ assert_eq!(Into::<log::Level>::into(Level::Config), log::Level::Debug);
+ assert_eq!(Into::<log::Level>::into(Level::Debug), log::Level::Debug);
+ assert_eq!(Into::<log::Level>::into(Level::Trace), log::Level::Trace);
+ }
+
+ #[test]
+ fn test_level_into_pref() {
+ let tests = [
+ (Level::Fatal, "Fatal"),
+ (Level::Error, "Error"),
+ (Level::Warn, "Warn"),
+ (Level::Info, "Info"),
+ (Level::Config, "Config"),
+ (Level::Debug, "Debug"),
+ (Level::Trace, "Trace"),
+ ];
+
+ for &(lvl, s) in tests.iter() {
+ let expected = Pref {
+ value: PrefValue::String(s.to_string()),
+ sticky: false,
+ };
+ assert_eq!(Into::<Pref>::into(lvl), expected);
+ }
+ }
+
+ #[test]
+ fn test_level_from_str() {
+ assert_eq!(Level::from_str("fatal"), Ok(Level::Fatal));
+ assert_eq!(Level::from_str("error"), Ok(Level::Error));
+ assert_eq!(Level::from_str("warn"), Ok(Level::Warn));
+ assert_eq!(Level::from_str("info"), Ok(Level::Info));
+ assert_eq!(Level::from_str("config"), Ok(Level::Config));
+ assert_eq!(Level::from_str("debug"), Ok(Level::Debug));
+ assert_eq!(Level::from_str("trace"), Ok(Level::Trace));
+
+ assert_eq!(Level::from_str("INFO"), Ok(Level::Info));
+
+ assert!(Level::from_str("foo").is_err());
+ }
+
+ #[test]
+ fn test_level_to_str() {
+ assert_eq!(Level::Fatal.to_string(), "FATAL");
+ assert_eq!(Level::Error.to_string(), "ERROR");
+ assert_eq!(Level::Warn.to_string(), "WARN");
+ assert_eq!(Level::Info.to_string(), "INFO");
+ assert_eq!(Level::Config.to_string(), "CONFIG");
+ assert_eq!(Level::Debug.to_string(), "DEBUG");
+ assert_eq!(Level::Trace.to_string(), "TRACE");
+ }
+
+ #[test]
+ fn test_max_level() {
+ let _guard = LEVEL_MUTEX.lock();
+ set_max_level(Level::Info);
+ assert_eq!(max_level(), Level::Info);
+ }
+
+ #[test]
+ fn test_set_max_level() {
+ let _guard = LEVEL_MUTEX.lock();
+ set_max_level(Level::Error);
+ assert_eq!(max_level(), Level::Error);
+ set_max_level(Level::Fatal);
+ assert_eq!(max_level(), Level::Fatal);
+ }
+
+ #[test]
+ fn test_init_with_level() {
+ let _guard = LEVEL_MUTEX.lock();
+ init_with_level(Level::Debug).unwrap();
+ assert_eq!(max_level(), Level::Debug);
+ assert!(init_with_level(Level::Warn).is_err());
+ }
+
+ #[test]
+ fn test_format_ts() {
+ let ts = chrono::Local::now();
+ let s = format_ts(ts);
+ assert_eq!(s.len(), 13);
+ }
+}
diff --git a/testing/geckodriver/src/main.rs b/testing/geckodriver/src/main.rs
new file mode 100644
index 0000000000..4dd6c668d9
--- /dev/null
+++ b/testing/geckodriver/src/main.rs
@@ -0,0 +1,351 @@
+#![forbid(unsafe_code)]
+
+extern crate chrono;
+#[macro_use]
+extern crate clap;
+#[macro_use]
+extern crate lazy_static;
+extern crate hyper;
+extern crate marionette as marionette_rs;
+extern crate mozdevice;
+extern crate mozprofile;
+extern crate mozrunner;
+extern crate mozversion;
+extern crate regex;
+extern crate serde;
+#[macro_use]
+extern crate serde_derive;
+extern crate serde_json;
+extern crate serde_yaml;
+extern crate uuid;
+extern crate webdriver;
+extern crate zip;
+
+#[macro_use]
+extern crate log;
+
+use std::env;
+use std::fmt;
+use std::io;
+use std::net::{IpAddr, SocketAddr};
+use std::path::PathBuf;
+use std::result;
+use std::str::FromStr;
+
+use clap::{App, Arg};
+
+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 android;
+mod build;
+mod capabilities;
+mod command;
+mod logging;
+mod marionette;
+mod prefs;
+
+#[cfg(test)]
+pub mod test;
+
+use crate::command::extension_routes;
+use crate::logging::Level;
+use crate::marionette::{MarionetteHandler, MarionetteSettings};
+use mozdevice::AndroidStorageInput;
+
+const EXIT_SUCCESS: i32 = 0;
+const EXIT_USAGE: i32 = 64;
+const EXIT_UNAVAILABLE: i32 = 69;
+
+enum FatalError {
+ Parsing(clap::Error),
+ Usage(String),
+ Server(io::Error),
+}
+
+impl FatalError {
+ fn exit_code(&self) -> i32 {
+ use FatalError::*;
+ match *self {
+ Parsing(_) | Usage(_) => EXIT_USAGE,
+ Server(_) => EXIT_UNAVAILABLE,
+ }
+ }
+
+ fn help_included(&self) -> bool {
+ match *self {
+ FatalError::Parsing(_) => true,
+ _ => false,
+ }
+ }
+}
+
+impl From<clap::Error> for FatalError {
+ fn from(err: clap::Error) -> FatalError {
+ FatalError::Parsing(err)
+ }
+}
+
+impl From<io::Error> for FatalError {
+ fn from(err: io::Error) -> FatalError {
+ FatalError::Server(err)
+ }
+}
+
+// harmonise error message from clap to avoid duplicate "error:" prefix
+impl fmt::Display for FatalError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use FatalError::*;
+ let s = match *self {
+ Parsing(ref err) => err.to_string(),
+ Usage(ref s) => format!("error: {}", s),
+ Server(ref err) => format!("error: {}", err.to_string()),
+ };
+ write!(f, "{}", s)
+ }
+}
+
+macro_rules! usage {
+ ($msg:expr) => {
+ return Err(FatalError::Usage($msg.to_string()));
+ };
+
+ ($fmt:expr, $($arg:tt)+) => {
+ return Err(FatalError::Usage(format!($fmt, $($arg)+)));
+ };
+}
+
+type ProgramResult<T> = result::Result<T, FatalError>;
+
+enum Operation {
+ Help,
+ Version,
+ Server {
+ log_level: Option<Level>,
+ address: SocketAddr,
+ settings: MarionetteSettings,
+ },
+}
+
+fn parse_args(app: &mut App) -> ProgramResult<Operation> {
+ let matches = app.get_matches_from_safe_borrow(env::args())?;
+
+ let log_level = if matches.is_present("log_level") {
+ Level::from_str(matches.value_of("log_level").unwrap()).ok()
+ } else {
+ Some(match matches.occurrences_of("verbosity") {
+ 0 => Level::Info,
+ 1 => Level::Debug,
+ _ => Level::Trace,
+ })
+ };
+
+ let host = matches.value_of("webdriver_host").unwrap();
+ let port = {
+ let s = matches.value_of("webdriver_port").unwrap();
+ match u16::from_str(s) {
+ Ok(n) => n,
+ Err(e) => usage!("invalid --port: {}: {}", e, s),
+ }
+ };
+ let address = match IpAddr::from_str(host) {
+ Ok(addr) => SocketAddr::new(addr, port),
+ Err(e) => usage!("{}: {}:{}", e, host, port),
+ };
+
+ let android_storage = value_t!(matches, "android_storage", AndroidStorageInput)?;
+
+ let binary = matches.value_of("binary").map(PathBuf::from);
+
+ let marionette_host = matches.value_of("marionette_host").unwrap();
+ let marionette_port = match matches.value_of("marionette_port") {
+ Some(s) => match u16::from_str(s) {
+ Ok(n) => Some(n),
+ Err(e) => usage!("invalid --marionette-port: {}", e),
+ },
+ None => None,
+ };
+
+ let op = if matches.is_present("help") {
+ Operation::Help
+ } else if matches.is_present("version") {
+ Operation::Version
+ } else {
+ let settings = MarionetteSettings {
+ host: marionette_host.to_string(),
+ port: marionette_port,
+ binary,
+ connect_existing: matches.is_present("connect_existing"),
+ jsdebugger: matches.is_present("jsdebugger"),
+ android_storage,
+ };
+ Operation::Server {
+ log_level,
+ address,
+ settings,
+ }
+ };
+
+ Ok(op)
+}
+
+fn inner_main(app: &mut App) -> ProgramResult<()> {
+ match parse_args(app)? {
+ Operation::Help => print_help(app),
+ Operation::Version => print_version(),
+
+ Operation::Server {
+ log_level,
+ address,
+ settings,
+ } => {
+ if let Some(ref level) = log_level {
+ logging::init_with_level(*level).unwrap();
+ } else {
+ logging::init().unwrap();
+ }
+
+ let handler = MarionetteHandler::new(settings);
+ let listening = webdriver::server::start(address, handler, extension_routes())?;
+ info!("Listening on {}", listening.socket);
+ }
+ }
+
+ Ok(())
+}
+
+fn main() {
+ use std::process::exit;
+
+ let mut app = make_app();
+
+ // use std::process:Termination when it graduates
+ exit(match inner_main(&mut app) {
+ Ok(_) => EXIT_SUCCESS,
+
+ Err(e) => {
+ eprintln!("{}: {}", get_program_name(), e);
+ if !e.help_included() {
+ print_help(&mut app);
+ }
+
+ e.exit_code()
+ }
+ });
+}
+
+fn make_app<'a, 'b>() -> App<'a, 'b> {
+ App::new(format!("geckodriver {}", build::build_info()))
+ .about("WebDriver implementation for Firefox")
+ .arg(
+ Arg::with_name("webdriver_host")
+ .long("host")
+ .takes_value(true)
+ .value_name("HOST")
+ .default_value("127.0.0.1")
+ .help("Host IP to use for WebDriver server"),
+ )
+ .arg(
+ Arg::with_name("webdriver_port")
+ .short("p")
+ .long("port")
+ .takes_value(true)
+ .value_name("PORT")
+ .default_value("4444")
+ .help("Port to use for WebDriver server"),
+ )
+ .arg(
+ Arg::with_name("binary")
+ .short("b")
+ .long("binary")
+ .takes_value(true)
+ .value_name("BINARY")
+ .help("Path to the Firefox binary"),
+ )
+ .arg(
+ Arg::with_name("marionette_host")
+ .long("marionette-host")
+ .takes_value(true)
+ .value_name("HOST")
+ .default_value("127.0.0.1")
+ .help("Host to use to connect to Gecko"),
+ )
+ .arg(
+ Arg::with_name("marionette_port")
+ .long("marionette-port")
+ .takes_value(true)
+ .value_name("PORT")
+ .help("Port to use to connect to Gecko [default: system-allocated port]"),
+ )
+ .arg(
+ Arg::with_name("connect_existing")
+ .long("connect-existing")
+ .requires("marionette_port")
+ .help("Connect to an existing Firefox instance"),
+ )
+ .arg(
+ Arg::with_name("jsdebugger")
+ .long("jsdebugger")
+ .help("Attach browser toolbox debugger for Firefox"),
+ )
+ .arg(
+ Arg::with_name("verbosity")
+ .multiple(true)
+ .conflicts_with("log_level")
+ .short("v")
+ .help("Log level verbosity (-v for debug and -vv for trace level)"),
+ )
+ .arg(
+ Arg::with_name("log_level")
+ .long("log")
+ .takes_value(true)
+ .value_name("LEVEL")
+ .possible_values(&["fatal", "error", "warn", "info", "config", "debug", "trace"])
+ .help("Set Gecko log level"),
+ )
+ .arg(
+ Arg::with_name("help")
+ .short("h")
+ .long("help")
+ .help("Prints this message"),
+ )
+ .arg(
+ Arg::with_name("version")
+ .short("V")
+ .long("version")
+ .help("Prints version and copying information"),
+ )
+ .arg(
+ Arg::with_name("android_storage")
+ .long("android-storage")
+ .possible_values(&["auto", "app", "internal", "sdcard"])
+ .default_value("auto")
+ .value_name("ANDROID_STORAGE")
+ .help("Selects storage location to be used for test data."),
+ )
+}
+
+fn get_program_name() -> String {
+ env::args().next().unwrap()
+}
+
+fn print_help(app: &mut App) {
+ app.print_help().ok();
+ println!();
+}
+
+fn print_version() {
+ println!("geckodriver {}", build::build_info());
+ println!();
+ println!("The source code of this program is available from");
+ println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central.");
+ println!();
+ println!("This program is subject to the terms of the Mozilla Public License 2.0.");
+ println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/.");
+}
diff --git a/testing/geckodriver/src/marionette.rs b/testing/geckodriver/src/marionette.rs
new file mode 100644
index 0000000000..99a82f3a89
--- /dev/null
+++ b/testing/geckodriver/src/marionette.rs
@@ -0,0 +1,1749 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use crate::android::AndroidHandler;
+use crate::command::{
+ AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters,
+ GeckoExtensionCommand, GeckoExtensionRoute, CHROME_ELEMENT_KEY,
+};
+use marionette_rs::common::{
+ Cookie as MarionetteCookie, Date as MarionetteDate, Frame as MarionetteFrame,
+ Timeouts as MarionetteTimeouts, WebElement as MarionetteWebElement, Window,
+};
+use marionette_rs::marionette::AppStatus;
+use marionette_rs::message::{Command, Message, MessageId, Request};
+use marionette_rs::webdriver::{
+ Command as MarionetteWebDriverCommand, Keys as MarionetteKeys, LegacyWebElement,
+ Locator as MarionetteLocator, NewWindow as MarionetteNewWindow,
+ PrintMargins as MarionettePrintMargins, PrintOrientation as MarionettePrintOrientation,
+ PrintPage as MarionettePrintPage, PrintParameters as MarionettePrintParameters,
+ ScreenshotOptions, Script as MarionetteScript, Selector as MarionetteSelector,
+ Url as MarionetteUrl, WindowRect as MarionetteWindowRect,
+};
+use mozdevice::AndroidStorageInput;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::Profile;
+use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess};
+use serde::de::{self, Deserialize, Deserializer};
+use serde::ser::{Serialize, Serializer};
+use serde_json::{self, Map, Value};
+use std::io::prelude::*;
+use std::io::Error as IoError;
+use std::io::ErrorKind;
+use std::io::Result as IoResult;
+use std::net::{TcpListener, TcpStream};
+use std::path::PathBuf;
+use std::sync::Mutex;
+use std::thread;
+use std::time;
+use webdriver::capabilities::CapabilitiesMatching;
+use webdriver::command::WebDriverCommand::{
+ AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert,
+ ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension,
+ FindElement, FindElementElement, FindElementElements, FindElements, FullscreenWindow, Get,
+ GetActiveElement, GetAlertText, GetCSSValue, GetCookies, GetCurrentUrl, GetElementAttribute,
+ GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie,
+ GetPageSource, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles, GetWindowRect, GoBack,
+ GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow, MinimizeWindow, NewSession,
+ NewWindow, PerformActions, Print, Refresh, ReleaseActions, SendAlertText, SetTimeouts,
+ SetWindowRect, Status, SwitchToFrame, SwitchToParentFrame, SwitchToWindow,
+ TakeElementScreenshot, TakeScreenshot,
+};
+use webdriver::command::{
+ ActionsParameters, AddCookieParameters, GetNamedCookieParameters, GetParameters,
+ JavascriptCommandParameters, LocatorParameters, NewSessionParameters, NewWindowParameters,
+ PrintMargins, PrintOrientation, PrintPage, PrintParameters, SendKeysParameters,
+ SwitchToFrameParameters, SwitchToWindowParameters, TimeoutsParameters, WindowRectParameters,
+};
+use webdriver::command::{WebDriverCommand, WebDriverMessage};
+use webdriver::common::{
+ Cookie, Date, FrameId, LocatorStrategy, WebElement, ELEMENT_KEY, FRAME_KEY, WINDOW_KEY,
+};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+use webdriver::response::{
+ CloseWindowResponse, CookieResponse, CookiesResponse, ElementRectResponse, NewSessionResponse,
+ NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse,
+};
+use webdriver::server::{Session, WebDriverHandler};
+
+use crate::build;
+use crate::capabilities::{FirefoxCapabilities, FirefoxOptions};
+use crate::logging;
+use crate::prefs;
+
+/// A running Gecko instance.
+#[derive(Debug)]
+pub enum Browser {
+ /// A local Firefox process, running on this (host) device.
+ Host(FirefoxProcess),
+
+ /// A remote instance, running on a (target) Android device.
+ Target(AndroidHandler),
+}
+
+#[derive(Debug, PartialEq, Deserialize)]
+pub struct MarionetteHandshake {
+ #[serde(rename = "marionetteProtocol")]
+ protocol: u16,
+ #[serde(rename = "applicationType")]
+ application_type: String,
+}
+
+#[derive(Default)]
+pub struct MarionetteSettings {
+ pub host: String,
+ pub port: Option<u16>,
+ pub binary: Option<PathBuf>,
+ pub connect_existing: bool,
+
+ /// Brings up the Browser Toolbox when starting Firefox,
+ /// letting you debug internals.
+ pub jsdebugger: bool,
+
+ pub android_storage: AndroidStorageInput,
+}
+
+#[derive(Default)]
+pub struct MarionetteHandler {
+ pub connection: Mutex<Option<MarionetteConnection>>,
+ pub settings: MarionetteSettings,
+ pub browser: Option<Browser>,
+}
+
+impl MarionetteHandler {
+ pub fn new(settings: MarionetteSettings) -> MarionetteHandler {
+ MarionetteHandler {
+ connection: Mutex::new(None),
+ settings,
+ browser: None,
+ }
+ }
+
+ pub fn create_connection(
+ &mut self,
+ session_id: &Option<String>,
+ new_session_parameters: &NewSessionParameters,
+ ) -> WebDriverResult<Map<String, Value>> {
+ let (options, capabilities) = {
+ let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref());
+ let mut capabilities = new_session_parameters
+ .match_browser(&mut fx_capabilities)?
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Unable to find a matching set of capabilities",
+ )
+ })?;
+
+ let options = FirefoxOptions::from_capabilities(
+ fx_capabilities.chosen_binary,
+ self.settings.android_storage,
+ &mut capabilities,
+ )?;
+ (options, capabilities)
+ };
+
+ if let Some(l) = options.log.level {
+ logging::set_max_level(l);
+ }
+
+ let host = self.settings.host.to_owned();
+ let port = self.settings.port.unwrap_or(get_free_port(&host)?);
+
+ match options.android {
+ Some(_) => {
+ // TODO: support connecting to running Apps. There's no real obstruction here,
+ // just some details about port forwarding to work through. We can't follow
+ // `chromedriver` here since it uses an abstract socket rather than a TCP socket:
+ // see bug 1240830 for thoughts on doing that for Marionette.
+ if self.settings.connect_existing {
+ return Err(WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Cannot connect to an existing Android App yet",
+ ));
+ }
+
+ self.start_android(port, options)?;
+ }
+ None => {
+ if !self.settings.connect_existing {
+ self.start_browser(port, options)?;
+ }
+ }
+ }
+
+ let mut connection = MarionetteConnection::new(host, port, session_id.clone());
+ connection.connect(&mut self.browser).or_else(|e| {
+ match self.browser {
+ Some(Browser::Host(ref mut runner)) => {
+ runner.kill()?;
+ }
+ Some(Browser::Target(ref mut handler)) => {
+ handler.force_stop().map_err(|e| {
+ WebDriverError::new(ErrorStatus::UnknownError, e.to_string())
+ })?;
+ }
+ _ => {}
+ }
+
+ Err(e)
+ })?;
+ self.connection = Mutex::new(Some(connection));
+ Ok(capabilities)
+ }
+
+ fn start_android(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> {
+ let android_options = options.android.unwrap();
+
+ let handler = AndroidHandler::new(&android_options, port)?;
+
+ // Profile management.
+ let is_custom_profile = options.profile.is_some();
+
+ let mut profile = options.profile.unwrap_or(Profile::new()?);
+
+ self.set_prefs(
+ handler.target_port,
+ &mut profile,
+ is_custom_profile,
+ options.prefs,
+ )
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e),
+ )
+ })?;
+
+ handler
+ .prepare(&profile, options.env.unwrap_or_default())
+ .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?;
+
+ handler
+ .launch()
+ .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?;
+
+ self.browser = Some(Browser::Target(handler));
+
+ Ok(())
+ }
+
+ fn start_browser(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> {
+ let binary = options.binary.ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ "Expected browser binary location, but unable to find \
+ binary in default location, no \
+ 'moz:firefoxOptions.binary' capability provided, and \
+ no binary flag set on the command line",
+ )
+ })?;
+
+ let is_custom_profile = options.profile.is_some();
+
+ let mut profile = match options.profile {
+ Some(x) => x,
+ None => Profile::new()?,
+ };
+
+ self.set_prefs(port, &mut profile, is_custom_profile, options.prefs)
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to set preferences: {}", e),
+ )
+ })?;
+
+ let mut runner = FirefoxRunner::new(&binary, profile);
+
+ runner.arg("--marionette");
+ if self.settings.jsdebugger {
+ runner.arg("--jsdebugger");
+ }
+ if let Some(args) = options.args.as_ref() {
+ runner.args(args);
+ }
+
+ // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting
+ runner
+ .env("MOZ_CRASHREPORTER", "1")
+ .env("MOZ_CRASHREPORTER_NO_REPORT", "1")
+ .env("MOZ_CRASHREPORTER_SHUTDOWN", "1");
+
+ let browser_proc = runner.start().map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::SessionNotCreated,
+ format!("Failed to start browser {}: {}", binary.display(), e),
+ )
+ })?;
+ self.browser = Some(Browser::Host(browser_proc));
+
+ Ok(())
+ }
+
+ pub fn set_prefs(
+ &self,
+ port: u16,
+ profile: &mut Profile,
+ custom_profile: bool,
+ extra_prefs: Vec<(String, Pref)>,
+ ) -> WebDriverResult<()> {
+ let prefs = profile.user_prefs().map_err(|_| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Unable to read profile preferences file",
+ )
+ })?;
+
+ for &(ref name, ref value) in prefs::DEFAULT.iter() {
+ if !custom_profile || !prefs.contains_key(name) {
+ prefs.insert((*name).to_string(), (*value).clone());
+ }
+ }
+
+ prefs.insert_slice(&extra_prefs[..]);
+
+ if self.settings.jsdebugger {
+ prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger"));
+ prefs.insert("devtools.debugger.remote-enabled", Pref::new(true));
+ prefs.insert("devtools.chrome.enabled", Pref::new(true));
+ prefs.insert("devtools.debugger.prompt-connection", Pref::new(false));
+ prefs.insert("marionette.debugging.clicktostart", Pref::new(true));
+ }
+
+ prefs.insert("marionette.log.level", logging::max_level().into());
+ prefs.insert("marionette.port", Pref::new(port));
+
+ prefs.write().map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Unable to write Firefox profile: {}", e),
+ )
+ })
+ }
+}
+
+impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
+ fn handle_command(
+ &mut self,
+ _: &Option<Session>,
+ msg: WebDriverMessage<GeckoExtensionRoute>,
+ ) -> WebDriverResult<WebDriverResponse> {
+ let mut resolved_capabilities = None;
+ {
+ let mut capabilities_options = None;
+ // First handle the status message which doesn't actually require a marionette
+ // connection or message
+ if let Status = msg.command {
+ let (ready, message) = self
+ .connection
+ .lock()
+ .map(|ref connection| {
+ connection
+ .as_ref()
+ .map(|_| (false, "Session already started"))
+ .unwrap_or((true, ""))
+ })
+ .unwrap_or((false, "geckodriver internal error"));
+ let mut value = Map::new();
+ value.insert("ready".to_string(), Value::Bool(ready));
+ value.insert("message".to_string(), Value::String(message.into()));
+ return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object(
+ value,
+ ))));
+ }
+
+ match self.connection.lock() {
+ Ok(ref connection) => {
+ if connection.is_none() {
+ match msg.command {
+ NewSession(ref capabilities) => {
+ capabilities_options = Some(capabilities);
+ }
+ _ => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidSessionId,
+ "Tried to run command without establishing a connection",
+ ));
+ }
+ }
+ }
+ }
+ Err(_) => {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to aquire Marionette connection",
+ ))
+ }
+ }
+ if let Some(capabilities) = capabilities_options {
+ resolved_capabilities =
+ Some(self.create_connection(&msg.session_id, &capabilities)?);
+ }
+ }
+
+ match self.connection.lock() {
+ Ok(ref mut connection) => {
+ match connection.as_mut() {
+ Some(conn) => {
+ conn.send_command(resolved_capabilities, &msg)
+ .map_err(|mut err| {
+ // Shutdown the browser if no session can
+ // be established due to errors.
+ if let NewSession(_) = msg.command {
+ err.delete_session = true;
+ }
+ err
+ })
+ }
+ None => panic!("Connection missing"),
+ }
+ }
+ Err(_) => Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to aquire Marionette connection",
+ )),
+ }
+ }
+
+ fn delete_session(&mut self, session: &Option<Session>) {
+ if let Some(ref s) = *session {
+ let delete_session = WebDriverMessage {
+ session_id: Some(s.id.clone()),
+ command: WebDriverCommand::DeleteSession,
+ };
+ let _ = self.handle_command(session, delete_session);
+ }
+
+ if let Ok(ref mut connection) = self.connection.lock() {
+ if let Some(conn) = connection.as_mut() {
+ conn.close();
+ }
+ }
+
+ match self.browser {
+ Some(Browser::Host(ref mut runner)) => {
+ // TODO(https://bugzil.la/1443922):
+ // Use toolkit.asyncshutdown.crash_timout pref
+ match runner.wait(time::Duration::from_secs(70)) {
+ Ok(x) => debug!("Browser process stopped: {}", x),
+ Err(e) => error!("Failed to stop browser process: {}", e),
+ }
+ }
+ Some(Browser::Target(ref mut handler)) => {
+ // Try to force-stop the process on the target device
+ match handler.force_stop() {
+ Ok(_) => debug!("Android package force-stopped"),
+ Err(e) => error!("Failed to force-stop Android package: {}", e),
+ }
+ }
+ None => {}
+ }
+
+ self.connection = Mutex::new(None);
+ self.browser = None;
+ }
+}
+
+pub struct MarionetteSession {
+ pub session_id: String,
+ protocol: Option<u16>,
+ application_type: Option<String>,
+ command_id: MessageId,
+}
+
+impl MarionetteSession {
+ pub fn new(session_id: Option<String>) -> MarionetteSession {
+ let initital_id = session_id.unwrap_or_else(|| "".to_string());
+ MarionetteSession {
+ session_id: initital_id,
+ protocol: None,
+ application_type: None,
+ command_id: 0,
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ resp: &MarionetteResponse,
+ ) -> WebDriverResult<()> {
+ if let NewSession(_) = msg.command {
+ let session_id = try_opt!(
+ try_opt!(
+ resp.result.get("sessionId"),
+ ErrorStatus::SessionNotCreated,
+ "Unable to get session id"
+ )
+ .as_str(),
+ ErrorStatus::SessionNotCreated,
+ "Unable to convert session id to string"
+ );
+ self.session_id = session_id.to_string().clone();
+ };
+ Ok(())
+ }
+
+ /// Converts a Marionette JSON response into a `WebElement`.
+ ///
+ /// Note that it currently coerces all chrome elements, web frames, and web
+ /// windows also into web elements. This will change at a later point.
+ fn to_web_element(&self, json_data: &Value) -> WebDriverResult<WebElement> {
+ let data = try_opt!(
+ json_data.as_object(),
+ ErrorStatus::UnknownError,
+ "Failed to convert data to an object"
+ );
+
+ let chrome_element = data.get(CHROME_ELEMENT_KEY);
+ let element = data.get(ELEMENT_KEY);
+ let frame = data.get(FRAME_KEY);
+ let window = data.get(WINDOW_KEY);
+
+ let value = try_opt!(
+ element.or(chrome_element).or(frame).or(window),
+ ErrorStatus::UnknownError,
+ "Failed to extract web element from Marionette response"
+ );
+ let id = try_opt!(
+ value.as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to convert web element reference value to string"
+ )
+ .to_string();
+ Ok(WebElement(id))
+ }
+
+ pub fn next_command_id(&mut self) -> MessageId {
+ self.command_id += 1;
+ self.command_id
+ }
+
+ pub fn response(
+ &mut self,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ resp: MarionetteResponse,
+ ) -> WebDriverResult<WebDriverResponse> {
+ use self::GeckoExtensionCommand::*;
+
+ if resp.id != self.command_id {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!(
+ "Marionette responses arrived out of sequence, expected {}, got {}",
+ self.command_id, resp.id
+ ),
+ ));
+ }
+
+ if let Some(error) = resp.error {
+ return Err(error.into());
+ }
+
+ self.update(msg, &resp)?;
+
+ Ok(match msg.command {
+ // Everything that doesn't have a response value
+ Get(_)
+ | GoBack
+ | GoForward
+ | Refresh
+ | SetTimeouts(_)
+ | SwitchToWindow(_)
+ | SwitchToFrame(_)
+ | SwitchToParentFrame
+ | AddCookie(_)
+ | DeleteCookies
+ | DeleteCookie(_)
+ | DismissAlert
+ | AcceptAlert
+ | SendAlertText(_)
+ | ElementClick(_)
+ | ElementClear(_)
+ | ElementSendKeys(_, _)
+ | PerformActions(_)
+ | ReleaseActions => WebDriverResponse::Void,
+ // Things that simply return the contents of the marionette "value" property
+ GetCurrentUrl
+ | GetTitle
+ | GetPageSource
+ | GetWindowHandle
+ | IsDisplayed(_)
+ | IsSelected(_)
+ | GetElementAttribute(_, _)
+ | GetElementProperty(_, _)
+ | GetCSSValue(_, _)
+ | GetElementText(_)
+ | GetElementTagName(_)
+ | IsEnabled(_)
+ | ExecuteScript(_)
+ | ExecuteAsyncScript(_)
+ | GetAlertText
+ | TakeScreenshot
+ | Print(_)
+ | TakeElementScreenshot(_) => {
+ WebDriverResponse::Generic(resp.into_value_response(true)?)
+ }
+ GetTimeouts => {
+ let script = match try_opt!(
+ resp.result.get("script"),
+ ErrorStatus::UnknownError,
+ "Missing field: script"
+ ) {
+ Value::Null => None,
+ n => try_opt!(
+ Some(n.as_u64()),
+ ErrorStatus::UnknownError,
+ "Failed to interpret script timeout duration as u64"
+ ),
+ };
+ // Check for the spec-compliant "pageLoad", but also for "page load",
+ // which was sent by Firefox 52 and earlier.
+ let page_load = try_opt!(
+ try_opt!(
+ resp.result
+ .get("pageLoad")
+ .or_else(|| resp.result.get("page load")),
+ ErrorStatus::UnknownError,
+ "Missing field: pageLoad"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret page load duration as u64"
+ );
+ let implicit = try_opt!(
+ try_opt!(
+ resp.result.get("implicit"),
+ ErrorStatus::UnknownError,
+ "Missing field: implicit"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret implicit search duration as u64"
+ );
+
+ WebDriverResponse::Timeouts(TimeoutsResponse {
+ script,
+ page_load,
+ implicit,
+ })
+ }
+ Status => panic!("Got status command that should already have been handled"),
+ GetWindowHandles => WebDriverResponse::Generic(resp.into_value_response(false)?),
+ NewWindow(_) => {
+ let handle: String = try_opt!(
+ try_opt!(
+ resp.result.get("handle"),
+ ErrorStatus::UnknownError,
+ "Failed to find handle field"
+ )
+ .as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret handle as string"
+ )
+ .into();
+ let typ: String = try_opt!(
+ try_opt!(
+ resp.result.get("type"),
+ ErrorStatus::UnknownError,
+ "Failed to find type field"
+ )
+ .as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret type as string"
+ )
+ .into();
+
+ WebDriverResponse::NewWindow(NewWindowResponse { handle, typ })
+ }
+ CloseWindow => {
+ let data = try_opt!(
+ resp.result.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array"
+ );
+ let handles = data
+ .iter()
+ .map(|x| {
+ Ok(try_opt!(
+ x.as_str(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret window handle as string"
+ )
+ .to_owned())
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ WebDriverResponse::CloseWindow(CloseWindowResponse(handles))
+ }
+ GetElementRect(_) => {
+ let x = try_opt!(
+ try_opt!(
+ resp.result.get("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as float"
+ );
+
+ let y = try_opt!(
+ try_opt!(
+ resp.result.get("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as float"
+ );
+
+ let width = try_opt!(
+ try_opt!(
+ resp.result.get("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float"
+ );
+
+ let height = try_opt!(
+ try_opt!(
+ resp.result.get("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find height field"
+ )
+ .as_f64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as float"
+ );
+
+ let rect = ElementRectResponse {
+ x,
+ y,
+ width,
+ height,
+ };
+ WebDriverResponse::ElementRect(rect)
+ }
+ FullscreenWindow | MinimizeWindow | MaximizeWindow | GetWindowRect
+ | SetWindowRect(_) => {
+ let width = try_opt!(
+ try_opt!(
+ resp.result.get("width"),
+ ErrorStatus::UnknownError,
+ "Failed to find width field"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret width as positive integer"
+ );
+
+ let height = try_opt!(
+ try_opt!(
+ resp.result.get("height"),
+ ErrorStatus::UnknownError,
+ "Failed to find heigenht field"
+ )
+ .as_u64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret height as positive integer"
+ );
+
+ let x = try_opt!(
+ try_opt!(
+ resp.result.get("x"),
+ ErrorStatus::UnknownError,
+ "Failed to find x field"
+ )
+ .as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret x as integer"
+ );
+
+ let y = try_opt!(
+ try_opt!(
+ resp.result.get("y"),
+ ErrorStatus::UnknownError,
+ "Failed to find y field"
+ )
+ .as_i64(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret y as integer"
+ );
+
+ let rect = WindowRectResponse {
+ x: x as i32,
+ y: y as i32,
+ width: width as i32,
+ height: height as i32,
+ };
+ WebDriverResponse::WindowRect(rect)
+ }
+ GetCookies => {
+ let cookies: Vec<Cookie> = serde_json::from_value(resp.result)?;
+ WebDriverResponse::Cookies(CookiesResponse(cookies))
+ }
+ GetNamedCookie(ref name) => {
+ let mut cookies: Vec<Cookie> = serde_json::from_value(resp.result)?;
+ cookies.retain(|x| x.name == *name);
+ let cookie = try_opt!(
+ cookies.pop(),
+ ErrorStatus::NoSuchCookie,
+ format!("No cookie with name {}", name)
+ );
+ WebDriverResponse::Cookie(CookieResponse(cookie))
+ }
+ FindElement(_) | FindElementElement(_, _) => {
+ let element = self.to_web_element(try_opt!(
+ resp.result.get("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field"
+ ))?;
+ WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?))
+ }
+ FindElements(_) | FindElementElements(_, _) => {
+ let element_vec = try_opt!(
+ resp.result.as_array(),
+ ErrorStatus::UnknownError,
+ "Failed to interpret value as array"
+ );
+ let elements = element_vec
+ .iter()
+ .map(|x| self.to_web_element(x))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ // TODO(Henrik): How to remove unwrap?
+ WebDriverResponse::Generic(ValueResponse(Value::Array(
+ elements
+ .iter()
+ .map(|x| serde_json::to_value(x).unwrap())
+ .collect(),
+ )))
+ }
+ GetActiveElement => {
+ let element = self.to_web_element(try_opt!(
+ resp.result.get("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field"
+ ))?;
+ WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?))
+ }
+ NewSession(_) => {
+ let session_id = try_opt!(
+ try_opt!(
+ resp.result.get("sessionId"),
+ ErrorStatus::InvalidSessionId,
+ "Failed to find sessionId field"
+ )
+ .as_str(),
+ ErrorStatus::InvalidSessionId,
+ "sessionId is not a string"
+ );
+
+ let mut capabilities = try_opt!(
+ try_opt!(
+ resp.result.get("capabilities"),
+ ErrorStatus::UnknownError,
+ "Failed to find capabilities field"
+ )
+ .as_object(),
+ ErrorStatus::UnknownError,
+ "capabilities field is not an object"
+ )
+ .clone();
+
+ capabilities.insert("moz:geckodriverVersion".into(), build::build_info().into());
+
+ WebDriverResponse::NewSession(NewSessionResponse::new(
+ session_id.to_string(),
+ Value::Object(capabilities.clone()),
+ ))
+ }
+ DeleteSession => WebDriverResponse::DeleteSession,
+ Extension(ref extension) => match extension {
+ GetContext => WebDriverResponse::Generic(resp.into_value_response(true)?),
+ SetContext(_) => WebDriverResponse::Void,
+ InstallAddon(_) => WebDriverResponse::Generic(resp.into_value_response(true)?),
+ UninstallAddon(_) => WebDriverResponse::Void,
+ TakeFullScreenshot => WebDriverResponse::Generic(resp.into_value_response(true)?),
+ },
+ })
+ }
+}
+
+fn try_convert_to_marionette_message(
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+) -> WebDriverResult<Option<Command>> {
+ use self::GeckoExtensionCommand::*;
+ use self::WebDriverCommand::*;
+
+ Ok(match msg.command {
+ AcceptAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::AcceptAlert)),
+ AddCookie(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::AddCookie(
+ x.to_marionette()?,
+ ))),
+ CloseWindow => Some(Command::WebDriver(MarionetteWebDriverCommand::CloseWindow)),
+ DeleteCookie(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::DeleteCookie(x.clone()),
+ )),
+ DeleteCookies => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::DeleteCookies,
+ )),
+ DeleteSession => Some(Command::Marionette(
+ marionette_rs::marionette::Command::DeleteSession {
+ flags: vec![AppStatus::eForceQuit],
+ },
+ )),
+ DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)),
+ ElementClear(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementClear(e.to_marionette()?),
+ )),
+ ElementClick(ref e) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementClick(e.to_marionette()?),
+ )),
+ ElementSendKeys(ref e, ref x) => {
+ let keys = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ElementSendKeys {
+ id: e.clone().to_string(),
+ text: keys.text.clone(),
+ value: keys.value.clone(),
+ },
+ ))
+ }
+ ExecuteAsyncScript(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ExecuteAsyncScript(x.to_marionette()?),
+ )),
+ ExecuteScript(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ExecuteScript(x.to_marionette()?),
+ )),
+ FindElement(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::FindElement(
+ x.to_marionette()?,
+ ))),
+ FindElements(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindElements(x.to_marionette()?),
+ )),
+ FindElementElement(ref e, ref x) => {
+ let locator = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindElementElement {
+ element: e.clone().to_string(),
+ using: locator.using.clone(),
+ value: locator.value.clone(),
+ },
+ ))
+ }
+ FindElementElements(ref e, ref x) => {
+ let locator = x.to_marionette()?;
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FindElementElements {
+ element: e.clone().to_string(),
+ using: locator.using.clone(),
+ value: locator.value.clone(),
+ },
+ ))
+ }
+ FullscreenWindow => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::FullscreenWindow,
+ )),
+ Get(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Get(
+ x.to_marionette()?,
+ ))),
+ GetActiveElement => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetActiveElement,
+ )),
+ GetAlertText => Some(Command::WebDriver(MarionetteWebDriverCommand::GetAlertText)),
+ GetCookies | GetNamedCookie(_) => {
+ Some(Command::WebDriver(MarionetteWebDriverCommand::GetCookies))
+ }
+ GetCSSValue(ref e, ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetCSSValue {
+ id: e.clone().to_string(),
+ property: x.clone(),
+ },
+ )),
+ GetCurrentUrl => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetCurrentUrl,
+ )),
+ GetElementAttribute(ref e, ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementAttribute {
+ id: e.clone().to_string(),
+ name: x.clone(),
+ },
+ )),
+ GetElementProperty(ref e, ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementProperty {
+ id: e.clone().to_string(),
+ name: x.clone(),
+ },
+ )),
+ GetElementRect(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementRect(x.to_marionette()?),
+ )),
+ GetElementTagName(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementTagName(x.to_marionette()?),
+ )),
+ GetElementText(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetElementText(x.to_marionette()?),
+ )),
+ GetPageSource => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetPageSource,
+ )),
+ GetTitle => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTitle)),
+ GetWindowHandle => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetWindowHandle,
+ )),
+ GetWindowHandles => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetWindowHandles,
+ )),
+ GetWindowRect => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::GetWindowRect,
+ )),
+ GetTimeouts => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTimeouts)),
+ GoBack => Some(Command::WebDriver(MarionetteWebDriverCommand::GoBack)),
+ GoForward => Some(Command::WebDriver(MarionetteWebDriverCommand::GoForward)),
+ IsDisplayed(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsDisplayed(
+ x.to_marionette()?,
+ ))),
+ IsEnabled(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsEnabled(
+ x.to_marionette()?,
+ ))),
+ IsSelected(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsSelected(
+ x.to_marionette()?,
+ ))),
+ MaximizeWindow => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::MaximizeWindow,
+ )),
+ MinimizeWindow => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::MinimizeWindow,
+ )),
+ NewWindow(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::NewWindow(
+ x.to_marionette()?,
+ ))),
+ Print(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Print(
+ x.to_marionette()?,
+ ))),
+ Refresh => Some(Command::WebDriver(MarionetteWebDriverCommand::Refresh)),
+ ReleaseActions => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::ReleaseActions,
+ )),
+ SendAlertText(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SendAlertText(x.to_marionette()?),
+ )),
+ SetTimeouts(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::SetTimeouts(
+ x.to_marionette()?,
+ ))),
+ SetWindowRect(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SetWindowRect(x.to_marionette()?),
+ )),
+ SwitchToFrame(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SwitchToFrame(x.to_marionette()?),
+ )),
+ SwitchToParentFrame => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SwitchToParentFrame,
+ )),
+ SwitchToWindow(ref x) => Some(Command::WebDriver(
+ MarionetteWebDriverCommand::SwitchToWindow(x.to_marionette()?),
+ )),
+ TakeElementScreenshot(ref e) => {
+ let screenshot = ScreenshotOptions {
+ id: Some(e.clone().to_string()),
+ highlights: vec![],
+ full: false,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeElementScreenshot(screenshot),
+ ))
+ }
+ TakeScreenshot => {
+ let screenshot = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: false,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeScreenshot(screenshot),
+ ))
+ }
+ Extension(ref extension) => match extension {
+ TakeFullScreenshot => {
+ let screenshot = ScreenshotOptions {
+ id: None,
+ highlights: vec![],
+ full: true,
+ };
+ Some(Command::WebDriver(
+ MarionetteWebDriverCommand::TakeFullScreenshot(screenshot),
+ ))
+ }
+ _ => None,
+ },
+ _ => None,
+ })
+}
+
+#[derive(Debug, PartialEq)]
+pub struct MarionetteCommand {
+ pub id: MessageId,
+ pub name: String,
+ pub params: Map<String, Value>,
+}
+
+impl Serialize for MarionetteCommand {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ let data = (&0, &self.id, &self.name, &self.params);
+ data.serialize(serializer)
+ }
+}
+
+impl MarionetteCommand {
+ fn new(id: MessageId, name: String, params: Map<String, Value>) -> MarionetteCommand {
+ MarionetteCommand { id, name, params }
+ }
+
+ fn encode_msg<T>(msg: T) -> WebDriverResult<String>
+ where
+ T: serde::Serialize,
+ {
+ let data = serde_json::to_string(&msg)?;
+
+ Ok(format!("{}:{}", data.len(), data))
+ }
+
+ fn from_webdriver_message(
+ id: MessageId,
+ capabilities: Option<Map<String, Value>>,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ ) -> WebDriverResult<String> {
+ use self::GeckoExtensionCommand::*;
+
+ if let Some(cmd) = try_convert_to_marionette_message(msg)? {
+ let req = Message::Incoming(Request(id, cmd));
+ MarionetteCommand::encode_msg(req)
+ } else {
+ let (opt_name, opt_parameters) = match msg.command {
+ Status => panic!("Got status command that should already have been handled"),
+ NewSession(_) => {
+ let caps = capabilities
+ .expect("Tried to create new session without processing capabilities");
+
+ let mut data = Map::new();
+ for (k, v) in caps.iter() {
+ data.insert(k.to_string(), serde_json::to_value(v)?);
+ }
+
+ (Some("WebDriver:NewSession"), Some(Ok(data)))
+ }
+ PerformActions(ref x) => {
+ (Some("WebDriver:PerformActions"), Some(x.to_marionette()))
+ }
+ Extension(ref extension) => match extension {
+ GetContext => (Some("Marionette:GetContext"), None),
+ InstallAddon(x) => (Some("Addon:Install"), Some(x.to_marionette())),
+ SetContext(x) => (Some("Marionette:SetContext"), Some(x.to_marionette())),
+ UninstallAddon(x) => (Some("Addon:Uninstall"), Some(x.to_marionette())),
+ _ => (None, None),
+ },
+ _ => (None, None),
+ };
+
+ let name = try_opt!(
+ opt_name,
+ ErrorStatus::UnsupportedOperation,
+ "Operation not supported"
+ );
+ let parameters = opt_parameters.unwrap_or_else(|| Ok(Map::new()))?;
+
+ let req = MarionetteCommand::new(id, name.into(), parameters);
+ MarionetteCommand::encode_msg(req)
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub struct MarionetteResponse {
+ pub id: MessageId,
+ pub error: Option<MarionetteError>,
+ pub result: Value,
+}
+
+impl<'de> Deserialize<'de> for MarionetteResponse {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ #[derive(Deserialize)]
+ struct ResponseWrapper {
+ msg_type: u64,
+ id: MessageId,
+ error: Option<MarionetteError>,
+ result: Value,
+ }
+
+ let wrapper: ResponseWrapper = Deserialize::deserialize(deserializer)?;
+
+ if wrapper.msg_type != 1 {
+ return Err(de::Error::custom(
+ "Expected '1' in first element of response",
+ ));
+ };
+
+ Ok(MarionetteResponse {
+ id: wrapper.id,
+ error: wrapper.error,
+ result: wrapper.result,
+ })
+ }
+}
+
+impl MarionetteResponse {
+ fn into_value_response(self, value_required: bool) -> WebDriverResult<ValueResponse> {
+ let value: &Value = if value_required {
+ try_opt!(
+ self.result.get("value"),
+ ErrorStatus::UnknownError,
+ "Failed to find value field"
+ )
+ } else {
+ &self.result
+ };
+
+ Ok(ValueResponse(value.clone()))
+ }
+}
+
+#[derive(Debug, PartialEq, Serialize, Deserialize)]
+pub struct MarionetteError {
+ #[serde(rename = "error")]
+ pub code: String,
+ pub message: String,
+ pub stacktrace: Option<String>,
+}
+
+impl Into<WebDriverError> for MarionetteError {
+ fn into(self) -> WebDriverError {
+ let status = ErrorStatus::from(self.code);
+ let message = self.message;
+
+ if let Some(stack) = self.stacktrace {
+ WebDriverError::new_with_stack(status, message, stack)
+ } else {
+ WebDriverError::new(status, message)
+ }
+ }
+}
+
+fn get_free_port(host: &str) -> IoResult<u16> {
+ TcpListener::bind((host, 0))
+ .and_then(|stream| stream.local_addr())
+ .map(|x| x.port())
+}
+
+pub struct MarionetteConnection {
+ host: String,
+ port: u16,
+ stream: Option<TcpStream>,
+ pub session: MarionetteSession,
+}
+
+impl MarionetteConnection {
+ pub fn new(host: String, port: u16, session_id: Option<String>) -> MarionetteConnection {
+ let session = MarionetteSession::new(session_id);
+ MarionetteConnection {
+ host,
+ port,
+ stream: None,
+ session,
+ }
+ }
+
+ pub fn connect(&mut self, browser: &mut Option<Browser>) -> WebDriverResult<()> {
+ let timeout = time::Duration::from_secs(60);
+ let poll_interval = time::Duration::from_millis(100);
+ let now = time::Instant::now();
+
+ debug!(
+ "Waiting {}s to connect to browser on {}:{}",
+ timeout.as_secs(),
+ self.host,
+ self.port
+ );
+
+ loop {
+ // immediately abort connection attempts if process disappears
+ if let Some(Browser::Host(ref mut runner)) = *browser {
+ let exit_status = match runner.try_wait() {
+ Ok(Some(status)) => Some(
+ status
+ .code()
+ .map(|c| c.to_string())
+ .unwrap_or_else(|| "signal".into()),
+ ),
+ Ok(None) => None,
+ Err(_) => Some("{unknown}".into()),
+ };
+ if let Some(s) = exit_status {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Process unexpectedly closed with status {}", s),
+ ));
+ }
+ }
+
+ let try_connect = || -> WebDriverResult<(TcpStream, MarionetteHandshake)> {
+ let mut stream = TcpStream::connect((&self.host[..], self.port))?;
+ let data = MarionetteConnection::handshake(&mut stream)?;
+
+ Ok((stream, data))
+ };
+
+ match try_connect() {
+ Ok((stream, data)) => {
+ debug!(
+ "Connection to Marionette established on {}:{}.",
+ self.host, self.port,
+ );
+
+ self.stream = Some(stream);
+ self.session.application_type = Some(data.application_type);
+ self.session.protocol = Some(data.protocol);
+ break;
+ }
+ Err(e) => {
+ if now.elapsed() < timeout {
+ thread::sleep(poll_interval);
+ } else {
+ return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string()));
+ }
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ fn handshake(stream: &mut TcpStream) -> WebDriverResult<MarionetteHandshake> {
+ let resp = (match stream.read_timeout() {
+ Ok(timeout) => {
+ // If platform supports changing the read timeout of the stream,
+ // use a short one only for the handshake with Marionette.
+ stream
+ .set_read_timeout(Some(time::Duration::from_millis(100)))
+ .ok();
+ let data = MarionetteConnection::read_resp(stream);
+ stream.set_read_timeout(timeout).ok();
+
+ data
+ }
+ _ => MarionetteConnection::read_resp(stream),
+ })
+ .map_err(|e| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Socket timeout reading Marionette handshake data: {}", e),
+ )
+ })?;
+
+ let data = serde_json::from_str::<MarionetteHandshake>(&resp)?;
+
+ if data.application_type != "gecko" {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!("Unrecognized application type {}", data.application_type),
+ ));
+ }
+
+ if data.protocol != 3 {
+ return Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ format!(
+ "Unsupported Marionette protocol version {}, required 3",
+ data.protocol
+ ),
+ ));
+ }
+
+ Ok(data)
+ }
+
+ pub fn close(&self) {}
+
+ pub fn send_command(
+ &mut self,
+ capabilities: Option<Map<String, Value>>,
+ msg: &WebDriverMessage<GeckoExtensionRoute>,
+ ) -> WebDriverResult<WebDriverResponse> {
+ let id = self.session.next_command_id();
+ let enc_cmd = MarionetteCommand::from_webdriver_message(id, capabilities, msg)?;
+ let resp_data = self.send(enc_cmd)?;
+ let data: MarionetteResponse = serde_json::from_str(&resp_data)?;
+
+ self.session.response(msg, data)
+ }
+
+ fn send(&mut self, data: String) -> WebDriverResult<String> {
+ let stream = match self.stream {
+ Some(ref mut stream) => {
+ if stream.write(&*data.as_bytes()).is_err() {
+ let mut err = WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to write request to stream",
+ );
+ err.delete_session = true;
+ return Err(err);
+ }
+
+ stream
+ }
+ None => {
+ let mut err = WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Tried to write before opening stream",
+ );
+ err.delete_session = true;
+ return Err(err);
+ }
+ };
+
+ match MarionetteConnection::read_resp(stream) {
+ Ok(resp) => Ok(resp),
+ Err(_) => {
+ let mut err = WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Failed to decode response from marionette",
+ );
+ err.delete_session = true;
+ Err(err)
+ }
+ }
+ }
+
+ fn read_resp(stream: &mut TcpStream) -> IoResult<String> {
+ let mut bytes = 0usize;
+
+ loop {
+ let buf = &mut [0 as u8];
+ let num_read = stream.read(buf)?;
+ let byte = match num_read {
+ 0 => {
+ return Err(IoError::new(
+ ErrorKind::Other,
+ "EOF reading marionette message",
+ ))
+ }
+ 1 => buf[0] as char,
+ _ => panic!("Expected one byte got more"),
+ };
+ match byte {
+ '0'..='9' => {
+ bytes *= 10;
+ bytes += byte as usize - '0' as usize;
+ }
+ ':' => break,
+ _ => {}
+ }
+ }
+
+ let buf = &mut [0 as u8; 8192];
+ let mut payload = Vec::with_capacity(bytes);
+ let mut total_read = 0;
+ while total_read < bytes {
+ let num_read = stream.read(buf)?;
+ if num_read == 0 {
+ return Err(IoError::new(
+ ErrorKind::Other,
+ "EOF reading marionette message",
+ ));
+ }
+ total_read += num_read;
+ for x in &buf[..num_read] {
+ payload.push(*x);
+ }
+ }
+
+ // TODO(jgraham): Need to handle the error here
+ Ok(String::from_utf8(payload).unwrap())
+ }
+}
+
+trait ToMarionette<T> {
+ fn to_marionette(&self) -> WebDriverResult<T>;
+}
+
+impl ToMarionette<Map<String, Value>> for AddonInstallParameters {
+ fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
+ let mut data = Map::new();
+ data.insert("path".to_string(), serde_json::to_value(&self.path)?);
+ if self.temporary.is_some() {
+ data.insert(
+ "temporary".to_string(),
+ serde_json::to_value(&self.temporary)?,
+ );
+ }
+ Ok(data)
+ }
+}
+
+impl ToMarionette<Map<String, Value>> for AddonUninstallParameters {
+ fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
+ let mut data = Map::new();
+ data.insert("id".to_string(), Value::String(self.id.clone()));
+ Ok(data)
+ }
+}
+
+impl ToMarionette<Map<String, Value>> for GeckoContextParameters {
+ fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
+ let mut data = Map::new();
+ data.insert(
+ "value".to_owned(),
+ serde_json::to_value(self.context.clone())?,
+ );
+ Ok(data)
+ }
+}
+
+impl ToMarionette<MarionettePrintParameters> for PrintParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintParameters> {
+ Ok(MarionettePrintParameters {
+ orientation: self.orientation.to_marionette()?,
+ scale: self.scale,
+ background: self.background,
+ page: self.page.to_marionette()?,
+ margin: self.margin.to_marionette()?,
+ page_ranges: self.page_ranges.clone(),
+ shrink_to_fit: self.shrink_to_fit,
+ })
+ }
+}
+
+impl ToMarionette<MarionettePrintOrientation> for PrintOrientation {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintOrientation> {
+ Ok(match self {
+ PrintOrientation::Landscape => MarionettePrintOrientation::Landscape,
+ PrintOrientation::Portrait => MarionettePrintOrientation::Portrait,
+ })
+ }
+}
+
+impl ToMarionette<MarionettePrintPage> for PrintPage {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintPage> {
+ Ok(MarionettePrintPage {
+ width: self.width,
+ height: self.height,
+ })
+ }
+}
+
+impl ToMarionette<MarionettePrintMargins> for PrintMargins {
+ fn to_marionette(&self) -> WebDriverResult<MarionettePrintMargins> {
+ Ok(MarionettePrintMargins {
+ top: self.top,
+ bottom: self.bottom,
+ left: self.left,
+ right: self.right,
+ })
+ }
+}
+
+impl ToMarionette<Map<String, Value>> for ActionsParameters {
+ fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
+ Ok(try_opt!(
+ serde_json::to_value(self)?.as_object(),
+ ErrorStatus::UnknownError,
+ "Expected an object"
+ )
+ .clone())
+ }
+}
+
+impl ToMarionette<MarionetteCookie> for AddCookieParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteCookie> {
+ Ok(MarionetteCookie {
+ name: self.name.clone(),
+ value: self.value.clone(),
+ path: self.path.clone(),
+ domain: self.domain.clone(),
+ secure: self.secure,
+ http_only: self.httpOnly,
+ expiry: match &self.expiry {
+ Some(date) => Some(date.to_marionette()?),
+ None => None,
+ },
+ same_site: self.sameSite.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteDate> for Date {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteDate> {
+ Ok(MarionetteDate(self.0))
+ }
+}
+
+impl ToMarionette<Map<String, Value>> for GetNamedCookieParameters {
+ fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> {
+ Ok(try_opt!(
+ serde_json::to_value(self)?.as_object(),
+ ErrorStatus::UnknownError,
+ "Expected an object"
+ )
+ .clone())
+ }
+}
+
+impl ToMarionette<MarionetteUrl> for GetParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteUrl> {
+ Ok(MarionetteUrl {
+ url: self.url.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteScript> for JavascriptCommandParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteScript> {
+ Ok(MarionetteScript {
+ script: self.script.clone(),
+ args: self.args.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteLocator> for LocatorParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteLocator> {
+ Ok(MarionetteLocator {
+ using: self.using.to_marionette()?,
+ value: self.value.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteSelector> for LocatorStrategy {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteSelector> {
+ use self::LocatorStrategy::*;
+ match self {
+ CSSSelector => Ok(MarionetteSelector::CSS),
+ LinkText => Ok(MarionetteSelector::LinkText),
+ PartialLinkText => Ok(MarionetteSelector::PartialLinkText),
+ TagName => Ok(MarionetteSelector::TagName),
+ XPath => Ok(MarionetteSelector::XPath),
+ }
+ }
+}
+
+impl ToMarionette<MarionetteNewWindow> for NewWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteNewWindow> {
+ Ok(MarionetteNewWindow {
+ type_hint: self.type_hint.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteKeys> for SendKeysParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteKeys> {
+ Ok(MarionetteKeys {
+ text: self.text.clone(),
+ value: self
+ .text
+ .chars()
+ .map(|x| x.to_string())
+ .collect::<Vec<String>>(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteFrame> for SwitchToFrameParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteFrame> {
+ Ok(match &self.id {
+ Some(x) => match x {
+ FrameId::Short(n) => MarionetteFrame::Index(n.clone()),
+ FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()),
+ },
+ None => MarionetteFrame::Parent,
+ })
+ }
+}
+
+impl ToMarionette<Window> for SwitchToWindowParameters {
+ fn to_marionette(&self) -> WebDriverResult<Window> {
+ Ok(Window {
+ name: self.handle.clone(),
+ handle: self.handle.clone(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteTimeouts> for TimeoutsParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteTimeouts> {
+ Ok(MarionetteTimeouts {
+ implicit: self.implicit,
+ page_load: self.page_load,
+ script: self.script,
+ })
+ }
+}
+
+impl ToMarionette<LegacyWebElement> for WebElement {
+ fn to_marionette(&self) -> WebDriverResult<LegacyWebElement> {
+ Ok(LegacyWebElement {
+ id: self.to_string(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteWebElement> for WebElement {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteWebElement> {
+ Ok(MarionetteWebElement {
+ element: self.to_string(),
+ })
+ }
+}
+
+impl ToMarionette<MarionetteWindowRect> for WindowRectParameters {
+ fn to_marionette(&self) -> WebDriverResult<MarionetteWindowRect> {
+ Ok(MarionetteWindowRect {
+ x: self.x,
+ y: self.y,
+ width: self.width,
+ height: self.height,
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{MarionetteHandler, MarionetteSettings};
+ use mozprofile::preferences::PrefValue;
+ use mozprofile::profile::Profile;
+
+ // This is not a pretty test, mostly due to the nature of
+ // mozprofile's and MarionetteHandler's APIs, but we have had
+ // several regressions related to marionette.log.level.
+ #[test]
+ fn test_marionette_log_level() {
+ let mut profile = Profile::new().unwrap();
+ let handler = MarionetteHandler::new(MarionetteSettings::default());
+ handler.set_prefs(2828, &mut profile, false, vec![]).ok();
+ let user_prefs = profile.user_prefs().unwrap();
+
+ let pref = user_prefs.get("marionette.log.level").unwrap();
+ let value = match pref.value {
+ PrefValue::String(ref s) => s,
+ _ => panic!(),
+ };
+ for (i, ch) in value.chars().enumerate() {
+ if i == 0 {
+ assert!(ch.is_uppercase());
+ } else {
+ assert!(ch.is_lowercase());
+ }
+ }
+ }
+}
diff --git a/testing/geckodriver/src/prefs.rs b/testing/geckodriver/src/prefs.rs
new file mode 100644
index 0000000000..075d0c6809
--- /dev/null
+++ b/testing/geckodriver/src/prefs.rs
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use mozprofile::preferences::Pref;
+
+// ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A GECKODRIVER PEER!
+//
+// All preferences in this file are not immediately effective, and
+// require a restart of Firefox, or have to be set in the profile before
+// Firefox gets started the first time. If a preference has to be added,
+// which is immediately effective, it needs to be done in Marionette
+// (marionette.js).
+//
+// Note: geckodriver is used out-of-tree with various builds of Firefox.
+// Removing a preference from this file will cause regressions,
+// so please be careful and get review from a Testing :: geckodriver peer
+// before you make any changes to this file.
+lazy_static! {
+ pub static ref DEFAULT: Vec<(&'static str, Pref)> = vec![
+ // Make sure Shield doesn't hit the network.
+ ("app.normandy.api_url", Pref::new("")),
+
+ // Disable Firefox old build background check
+ ("app.update.checkInstallTime", Pref::new(false)),
+
+ // Disable automatically upgrading Firefox
+ //
+ // Note: Possible update tests could reset or flip the value to allow
+ // updates to be downloaded and applied.
+ ("app.update.disabledForTesting", Pref::new(true)),
+ // !!! For backward compatibility up to Firefox 64. Only remove
+ // when this Firefox version is no longer supported by geckodriver !!!
+ ("app.update.auto", Pref::new(false)),
+
+ // Enable the dump function, which sends messages to the system
+ // console
+ ("browser.dom.window.dump.enabled", Pref::new(true)),
+ ("devtools.console.stdout.chrome", Pref::new(true)),
+
+ // Disable safebrowsing components
+ ("browser.safebrowsing.blockedURIs.enabled", Pref::new(false)),
+ ("browser.safebrowsing.downloads.enabled", Pref::new(false)),
+ ("browser.safebrowsing.passwords.enabled", Pref::new(false)),
+ ("browser.safebrowsing.malware.enabled", Pref::new(false)),
+ ("browser.safebrowsing.phishing.enabled", Pref::new(false)),
+
+ // Do not restore the last open set of tabs if the browser crashed
+ ("browser.sessionstore.resume_from_crash", Pref::new(false)),
+
+ // Skip check for default browser on startup
+ ("browser.shell.checkDefaultBrowser", Pref::new(false)),
+
+ // Do not redirect user when a milestone upgrade of Firefox
+ // is detected
+ ("browser.startup.homepage_override.mstone", Pref::new("ignore")),
+
+ // Start with a blank page (about:blank)
+ ("browser.startup.page", Pref::new(0)),
+
+ // Do not close the window when the last tab gets closed
+ // TODO: Remove once minimum supported Firefox release is 61.
+ ("browser.tabs.closeWindowWithLastTab", Pref::new(false)),
+
+ // Do not warn when closing all open tabs
+ // TODO: Remove once minimum supported Firefox release is 61.
+ ("browser.tabs.warnOnClose", Pref::new(false)),
+
+ // Disable the UI tour
+ ("browser.uitour.enabled", Pref::new(false)),
+
+ // Do not warn on quitting Firefox
+ ("browser.warnOnQuit", Pref::new(false)),
+
+ // Defensively disable data reporting systems
+ ("datareporting.healthreport.documentServerURI", Pref::new("http://%(server)s/dummy/healthreport/")),
+ ("datareporting.healthreport.logging.consoleEnabled", Pref::new(false)),
+ ("datareporting.healthreport.service.enabled", Pref::new(false)),
+ ("datareporting.healthreport.service.firstRun", Pref::new(false)),
+ ("datareporting.healthreport.uploadEnabled", Pref::new(false)),
+
+ // Do not show datareporting policy notifications which can
+ // interfere with tests
+ ("datareporting.policy.dataSubmissionEnabled", Pref::new(false)),
+ ("datareporting.policy.dataSubmissionPolicyBypassNotification", Pref::new(true)),
+
+ // Disable the ProcessHangMonitor
+ ("dom.ipc.reportProcessHangs", Pref::new(false)),
+
+ // Only load extensions from the application and user profile
+ // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION
+ ("extensions.autoDisableScopes", Pref::new(0)),
+ ("extensions.enabledScopes", Pref::new(5)),
+
+ // Disable intalling any distribution extensions or add-ons
+ ("extensions.installDistroAddons", Pref::new(false)),
+
+ // Turn off extension updates so they do not bother tests
+ ("extensions.update.enabled", Pref::new(false)),
+ ("extensions.update.notifyUser", Pref::new(false)),
+
+ // Allow the application to have focus even it runs in the
+ // background
+ ("focusmanager.testmode", Pref::new(true)),
+
+ // Disable useragent updates
+ ("general.useragent.updates.enabled", Pref::new(false)),
+
+ // Always use network provider for geolocation tests so we bypass
+ // the macOS dialog raised by the corelocation provider
+ ("geo.provider.testing", Pref::new(true)),
+
+ // Do not scan wi-fi
+ ("geo.wifi.scan", Pref::new(false)),
+
+ // No hang monitor
+ ("hangmonitor.timeout", Pref::new(0)),
+
+ // Disable idle-daily notifications to avoid expensive operations
+ // that may cause unexpected test timeouts.
+ ("idle.lastDailyNotification", Pref::new(-1)),
+
+ // Show chrome errors and warnings in the error console
+ ("javascript.options.showInConsole", Pref::new(true)),
+
+ // Disable download and usage of OpenH264, and Widevine plugins
+ ("media.gmp-manager.updateEnabled", Pref::new(false)),
+
+ // Disable the GFX sanity window
+ ("media.sanity-test.disabled", Pref::new(true)),
+
+ // Do not prompt with long usernames or passwords in URLs
+ // TODO: Remove once minimum supported Firefox release is 61.
+ ("network.http.phishy-userpass-length", Pref::new(255)),
+
+ // Do not automatically switch between offline and online
+ ("network.manage-offline-status", Pref::new(false)),
+
+ // Make sure SNTP requests do not hit the network
+ ("network.sntp.pools", Pref::new("%(server)s")),
+
+ // Disable Flash. The plugin container it is run in is
+ // causing problems when quitting Firefox from geckodriver,
+ // c.f. https://github.com/mozilla/geckodriver/issues/225.
+ ("plugin.state.flash", Pref::new(0)),
+
+ // Don't do network connections for mitm priming
+ ("security.certerrors.mitm.priming.enabled", Pref::new(false)),
+
+ // Ensure blocklist updates don't hit the network
+ ("services.settings.server", Pref::new("http://%(server)s/dummy/blocklist/")),
+
+ // Disable first run pages
+ ("startup.homepage_welcome_url", Pref::new("about:blank")),
+ ("startup.homepage_welcome_url.additional", Pref::new("")),
+
+ // Prevent starting into safe mode after application crashes
+ ("toolkit.startup.max_resumed_crashes", Pref::new(-1)),
+ ];
+}
diff --git a/testing/geckodriver/src/test.rs b/testing/geckodriver/src/test.rs
new file mode 100644
index 0000000000..e664aadf08
--- /dev/null
+++ b/testing/geckodriver/src/test.rs
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+pub fn assert_de<T>(data: &T, json: serde_json::Value)
+where
+ T: std::fmt::Debug,
+ T: std::cmp::PartialEq,
+ T: serde::de::DeserializeOwned,
+{
+ assert_eq!(data, &serde_json::from_value::<T>(json).unwrap());
+}
diff --git a/testing/geckodriver/src/tests/profile.zip b/testing/geckodriver/src/tests/profile.zip
new file mode 100644
index 0000000000..286b118183
--- /dev/null
+++ b/testing/geckodriver/src/tests/profile.zip
Binary files differ