Bug 1525126 - [geckodriver] Add Android support. r=jgraham,nalexander

This patch allows geckodriver to interact with processes of
GeckoView packages on Android devices while running itself
on a host machine. The connection to and from the application
under test is done via adb forward ports.

Depends on D45363

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Henrik Skupin 2019-09-25 23:00:40 +00:00
Родитель b4b1f6820c
Коммит dd5317eeba
12 изменённых файлов: 707 добавлений и 84 удалений

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

@ -1143,6 +1143,7 @@ dependencies = [
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"marionette 0.1.0",
"mozdevice 0.1.0",
"mozprofile 0.6.0",
"mozrunner 0.10.0",
"mozversion 0.2.1",
@ -1840,6 +1841,16 @@ dependencies = [
"xpcom 0.1.0",
]
[[package]]
name = "mozdevice"
version = "0.1.0"
dependencies = [
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 1.1.9 (registry+https://github.com/rust-lang/crates.io-index)",
"tempfile 3.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
"walkdir 2.1.4 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "mozilla-central-workspace-hack"
version = "0.1.0"

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

@ -16,6 +16,7 @@ hyper = "0.12"
lazy_static = "1.0"
log = { version = "0.4", features = ["std"] }
marionette = { path = "./marionette" }
mozdevice = { path = "../mozbase/rust/mozdevice" }
mozprofile = { path = "../mozbase/rust/mozprofile" }
mozrunner = { path = "../mozbase/rust/mozrunner" }
mozversion = { path = "../mozbase/rust/mozversion" }

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

@ -29,7 +29,7 @@ You can run your freshly built geckodriver this way:
% ./mach geckodriver -- --other --flags
See <Testing.md> for how to run tests.
See [Testing](Testing.html) for how to run tests.
[Rust]: https://www.rust-lang.org/
[webdriver crate]: https://crates.io/crates/webdriver

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

@ -1,11 +1,9 @@
Firefox capabilities
====================
# Firefox capabilities
geckodriver has a few capabilities that are specific to Firefox.
`moz:firefoxOptions`
--------------------
## `moz:firefoxOptions`
A dictionary used to define options which control how Firefox gets
started and run. It may contain any of the following fields:
@ -16,7 +14,7 @@ started and run. It may contain any of the following fields:
td, th { padding: 5px 10px; }
</style>
<table>
<table id="capabilities-common">
<thead>
<tr>
<th>Name
@ -25,6 +23,17 @@ started and run. It may contain any of the following fields:
</tr>
</thead>
<tr id=capability-args>
<td><code>args</code>
<td align="center">array&nbsp;of&nbsp;strings
<td><p>Command line arguments to pass to the Firefox binary.
These must include the leading dash (<code>-</code>) where required,
e.g. <code>["-devtools"]</code>.
<p>To have geckodriver pick up an existing profile on the filesystem,
you may pass <code>["-profile", "/path/to/profile"]</code>.
</tr>
<tr id=capability-binary>
<td><code>binary</code>
<td align="center">string
@ -44,15 +53,20 @@ started and run. It may contain any of the following fields:
or <code>/Applications/Firefox Nightly.app/Contents/MacOS/firefox</code>.
</tr>
<tr id=capability-args>
<td><code>args</code>
<td align="center">array&nbsp;of&nbsp;strings
<td><p>Command line arguments to pass to the Firefox binary.
These must include the leading dash (<code>-</code>) where required,
e.g. <code>["-devtools"]</code>.
<tr id=capability-log>
<td><code>log</code>
<td align="center"><a href=#log-object><code>log</code></a>&nbsp;object
<td>To increase the logging verbosity of geckodriver and Firefox,
you may pass a <a href=#log-object><code>log</code> object</a>
that may look like <code>{"log": {"level": "trace"}}</code>
to include all trace-level logs and above.
</tr>
<p>To have geckodriver pick up an existing profile on the filesystem,
you may pass <code>["-profile", "/path/to/profile"]</code>.
<tr id=capability-prefs>
<td><code>prefs</code>
<td align="center"><a href=#prefs-object><code>prefs</code></a>&nbsp;object
<td>Map of preference name to preference value, which can be a
string, a boolean or an integer.
</tr>
<tr id=capability-profile>
@ -77,27 +91,55 @@ started and run. It may contain any of the following fields:
please set the <a href=#capability-args><code>args</code></a> field
to <code>{"args": ["-profile", "/path/to/your/profile"]}</code>.
</tr>
</table>
<tr id=capability-log>
<td><code>log</code>
<td align="center"><a href=#log-object><code>log</code></a>&nbsp;object
<td>To increase the logging verbosity of geckodriver and Firefox,
you may pass a <a href=#log-object><code>log</code> object</a>
that may look like <code>{"log": {"level": "trace"}}</code>
to include all trace-level logs and above.
### Android
Starting with geckodriver 0.26.0 additional capabilities exist if Firefox
or an application embedding [GeckoView] has to be controlled on Android:
<table id="capabilities-android">
<thead>
<tr>
<th>Name
<th>Type
<th>Optional
<th>Description
</tr>
</thead>
<tr id=capability-androidPackage>
<td><code>androidPackage</code>
<td align="center">string
<td align="center">no
<td><p>
The package name of the application embedding GeckoView, eg.
<code>org.mozilla.geckoview_example</code>.
</tr>
<tr id=capability-prefs>
<td><code>prefs</code>
<td align="center"><a href=#prefs-object><code>prefs</code></a>&nbsp;object
<td>Map of preference name to preference value, which can be a
string, a boolean or an integer.
<tr id=capability-androidActivity>
<td><code>androidActivity</code>
<td align="center">string
<td align="center">yes
<td><p>
The fully qualified class name of the activity to be launched, eg.
<code>.GeckoViewActivity</code>.
If not specified the package's default activity will be used.
</tr>
<tr id=capability-androidDeviceSerial>
<td><code>androidDeviceSerial</code>
<td align="center">string
<td align="center">yes
<td><p>
The serial number of the device on which to launch the application.
If not specified and multiple devices are attached, an error will be raised.
</tr>
</table>
[GeckoView]: https://wiki.mozilla.org/Mobile/GeckoView
`moz:useNonSpecCompliantPointerOrigin`
--------------------------------------
## `moz:useNonSpecCompliantPointerOrigin`
A boolean value to indicate how the pointer origin for an action
command will be calculated.
@ -114,8 +156,7 @@ Please note that this capability exists only temporarily, and that
it will be removed once all Selenium bindings can handle the new behavior.
`moz:webdriverClick`
--------------------
## `moz:webdriverClick`
A boolean value to indicate which kind of interactability checks
to run when performing a click or sending keys to an elements. For
@ -140,8 +181,7 @@ Please note that this capability exists only temporarily, and that
it will be removed once the interactability checks have been stabilized.
`log` object
------------
## `log` object
<table>
<thead>
@ -166,8 +206,7 @@ it will be removed once the interactability checks have been stabilized.
</table>
`prefs` object
--------------
## `prefs` object
<table>
<thead>
@ -186,13 +225,13 @@ it will be removed once the interactability checks have been stabilized.
</table>
Capabilities example
====================
## Capabilities examples
The following example selects a specific Firefox binary to run with
a prepared profile from the filesystem in headless mode (available on
certain systems and recent Firefoxen). It also increases the number
of IPC processes through a preference and enables more verbose logging.
### Custom profile, and headless mode
This runs a specific Firefox binary with a prepared profile from the filesystem
in headless mode. It also increases the number of IPC processes through a
preference and enables more verbose logging.
{
"capabilities": {
@ -209,4 +248,25 @@ of IPC processes through a preference and enables more verbose logging.
}
}
}
}
}
### Android
This runs the GeckoView example application as installed on the first Android
emulator running on the host machine.
{
"capabilities": {
"alwaysMatch": {
"moz:firefoxOptions": {
"androidPackage": "org.mozilla.geckoview_example",
"androidActivity": "org.mozilla.geckoview_example.GeckoViewActivity",
"androidDeviceSerial": "emulator-5554",
"androidIntentArguments": [
"-d", "http://example.org"
]
}
}
}
}

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

@ -87,7 +87,6 @@ Clients
Other clients that follow the [W3C WebDriver specification][WebDriver]
are also supported.
Firefoxen
---------
@ -105,6 +104,18 @@ in the most recent Firefox versions, and we strongly advise using the
latest [Firefox Nightly] with geckodriver. Since Windows XP support
in Firefox was dropped with Firefox 53, we do not support this platform.
Android
-------
Starting with the 0.26.0 release geckodriver is able to connect
to Android devices, and to control packages which are based on [GeckoView]
(eg. [Firefox Preview] aka Fenix, or [Firefox Reality]). But it also still
supports versions of Fennec up to 68 ESR, which is the last officially
supported release from Mozilla.
To run tests on Android specific capabilities under `moz:firefoxOptions`
have to be set when requesting a new session. See the Android section under
[Firefox Capabilties](Capabilities.html#android) for more details.
[geckodriver releases]: https://github.com/mozilla/geckodriver/releases
[Selenium]: https://github.com/seleniumhq/selenium
@ -115,3 +126,6 @@ in Firefox was dropped with Firefox 53, we do not support this platform.
[specification]: https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Aspec
[issue tracker]: https://github.com/mozilla/geckodriver/issues
[Firefox Nightly]: https://nightly.mozilla.org/
[GeckoView]: https://wiki.mozilla.org/Mobile/GeckoView
[Firefox Preview]: https://play.google.com/store/apps/details?id=org.mozilla.fenix
[Firefox Reality]: https://play.google.com/store/apps/details?id=org.mozilla.vrbrowser

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

@ -10,9 +10,10 @@ RUST_TESTS = [
"geckodriver",
"webdriver",
"marionette",
# TODO: Move to mozbase/rust/moz.build once those crates can be
# tested separately.
# "mozdevice", // Tests require adb, and cannot be run in CI
"mozprofile",
"mozrunner",
"mozversion",

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

@ -0,0 +1,213 @@
use crate::capabilities::{AndroidOptions};
use mozdevice::{Device, Host};
use mozprofile::profile::Profile;
use std::fmt;
use std::path::PathBuf;
use std::time;
// 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;
pub type Result<T> = std::result::Result<T, AndroidError>;
#[derive(Debug)]
pub enum AndroidError {
ActivityNotFound(String),
Device(mozdevice::DeviceError),
NotConnected,
}
impl fmt::Display for AndroidError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
AndroidError::ActivityNotFound(ref package) => {
write!(f, "Activity not found for package '{}'", package)
},
AndroidError::Device(ref message) => message.fmt(f),
AndroidError::NotConnected =>
write!(f, "Not connected to any Android device"),
}
}
}
impl From<mozdevice::DeviceError> for AndroidError {
fn from(value: mozdevice::DeviceError) -> AndroidError {
AndroidError::Device(value)
}
}
/// 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, Default)]
pub struct AndroidHandler {
pub options: AndroidOptions,
pub process: Option<AndroidProcess>,
pub profile: 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 port forwarding.
if let Some(ref process) = self.process {
match 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) -> 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 profile = PathBuf::from(format!(
"/mnt/sdcard/{}-geckodriver-profile", &options.package));
AndroidHandler {
options: options.clone(),
profile,
process: None,
..Default::default()
}
}
pub fn connect(&mut self, host_port: u16) -> Result<()> {
let host = Host {
host: None,
port: None,
read_timeout: Some(time::Duration::from_millis(5000)),
write_timeout: Some(time::Duration::from_millis(5000)),
};
let device = host.device_or_default(self.options.device_serial.as_ref())?;
self.host_port = host_port;
self.target_port = TARGET_PORT;
// Set up port forward. Port forwarding will be torn down, if possible,
device.forward_port(self.host_port, self.target_port)?;
debug!("Android port forward ({} -> {}) started", &self.host_port, &self.target_port);
// If activity hasn't been specified default to the main activity of the package
let activity = match self.options.activity {
Some(ref activity) => activity.clone(),
None => {
let response = device.execute_host_shell_command(&format!(
"cmd package resolve-activity --brief {} | tail -n 1",
&self.options.package))?;
let parts = response
.trim_end()
.split("/")
.collect::<Vec<&str>>();
if parts.len() == 1 {
return Err(AndroidError::ActivityNotFound(self.options.package.clone()));
}
parts[1].to_owned()
}
};
self.process = Some(AndroidProcess::new(
device,
self.options.package.clone(),
activity,
)?);
Ok(())
}
pub fn prepare(&self, profile: &Profile) -> Result<()> {
match self.process {
Some(ref process) => {
process.device.clear_app_data(&self.options.package)?;
// These permissions, at least, are required to read profiles in /mnt/sdcard.
for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
process.device.execute_host_shell_command(&format!(
"pm grant {} android.permission.{}", &self.options.package, perm))?;
}
debug!("Deleting {}", self.profile.display());
process.device.execute_host_shell_command(&format!(
"rm -rf {}", self.profile.display()))?;
debug!("Pushing {} to {}", profile.path.display(), self.profile.display());
process.device.push_dir(&profile.path, &self.profile, 0o777)?;
},
None => return Err(AndroidError::NotConnected)
}
Ok(())
}
pub fn launch(&self) -> Result<()> {
match self.process {
Some(ref process) => {
// TODO: Use GeckoView's local configuration file (bug 1577850) unless a special
// "I'm Fennec" flag is set, indicating we should use command line arguments.
// Fenix does not handle command line arguments at this time.
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()).to_owned());
debug!("Launching {}/{}", process.package, process.activity);
process.device
.launch(&process.package, &process.activity, &intent_arguments)
.map_err(|e| {
let message = format!(
"Could not launch Android {}/{}: {}", process.package, process.activity, e);
mozdevice::DeviceError::Adb(message)
})?;
},
None => return Err(AndroidError::NotConnected)
}
Ok(())
}
pub fn force_stop(&self) -> Result<()> {
match &self.process {
Some(process) => {
debug!("Force stopping the Android package: {}", &process.package);
process.device.force_stop(&process.package)?;
},
None => return Err(AndroidError::NotConnected)
}
Ok(())
}
}

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

@ -181,34 +181,31 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
);
for (key, value) in data.iter() {
match &**key {
"binary" => {
"androidActivity" |
"androidDeviceSerial" |
"androidPackage" |
"binary" |
"profile" => {
if !value.is_string() {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
"binary path is not a string",
format!("{} is not a string", &**key),
));
}
}
"androidIntentArguments" |
"args" => {
if !try_opt!(
value.as_array(),
ErrorStatus::InvalidArgument,
"args is not an array"
format!("{} is not an array", &**key)
)
.iter()
.all(|value| value.is_string())
{
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
"args entry is not a string",
));
}
}
"profile" => {
if !value.is_string() {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
"profile is not a string",
format!("{} entry is not a string", &**key),
));
}
}
@ -298,6 +295,26 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
}
}
/// 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,
}
impl AndroidOptions {
fn new(package: String) -> AndroidOptions {
AndroidOptions {
package,
..Default::default()
}
}
}
/// Rust representation of `moz:firefoxOptions`.
///
/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
@ -311,6 +328,7 @@ pub struct FirefoxOptions {
pub args: Option<Vec<String>>,
pub log: LogOptions,
pub prefs: Vec<(String, Pref)>,
pub android: Option<AndroidOptions>,
}
impl FirefoxOptions {
@ -332,10 +350,11 @@ impl FirefoxOptions {
capability is not an object",
))?;
rv.profile = FirefoxOptions::load_profile(&options)?;
rv.android = FirefoxOptions::load_android(&options)?;
rv.args = FirefoxOptions::load_args(&options)?;
rv.log = FirefoxOptions::load_log(&options)?;
rv.prefs = FirefoxOptions::load_prefs(&options)?;
rv.profile = FirefoxOptions::load_profile(&options)?;
}
Ok(rv)
@ -379,8 +398,7 @@ impl FirefoxOptions {
.collect::<Option<Vec<String>>>()
.ok_or_else(|| WebDriverError::new(
ErrorStatus::UnknownError,
"Arguments entries were not all \
strings",
"Arguments entries were not all strings",
))?;
Ok(Some(args))
} else {
@ -430,6 +448,61 @@ impl FirefoxOptions {
Ok(vec![])
}
}
pub fn load_android(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 was not a string"
))?.to_owned();
let mut android = AndroidOptions::new(package);
android.activity = match options.get("androidActivity") {
Some(json) => {
Some(json.as_str().ok_or_else(|| WebDriverError::new(
ErrorStatus::InvalidArgument,
"androidActivity was not a string"
))?.to_owned())
},
None => None
};
android.device_serial = match options.get("androidDeviceSerial") {
Some(json) => {
Some(json.as_str().ok_or_else(|| WebDriverError::new(
ErrorStatus::InvalidArgument,
"androidDeviceSerial was 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 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,
"androidIntentArguments entries were not all strings"
))?;
Some(args)
}
None => None
};
Ok(Some(android))
} else {
Ok(None)
}
}
}
fn pref_from_json(value: &Value) -> WebDriverResult<Pref> {
@ -500,9 +573,11 @@ fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
mod tests {
extern crate mozprofile;
use self::mozprofile::preferences::Pref;
use super::*;
use crate::marionette::MarionetteHandler;
use self::mozprofile::preferences::Pref;
use serde_json::json;
use std::default::Default;
use std::fs::File;
use std::io::Read;
@ -516,11 +591,166 @@ mod tests {
Value::String(base64::encode(&profile_data))
}
fn make_options(firefox_opts: Capabilities) -> FirefoxOptions {
fn make_options(firefox_opts: Capabilities) -> WebDriverResult<FirefoxOptions> {
let mut caps = Capabilities::new();
caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
let binary = None;
FirefoxOptions::from_capabilities(binary, &mut caps).unwrap()
FirefoxOptions::from_capabilities(None, &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, &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()), &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_invalid_caps() {
let mut caps = Capabilities::new();
caps.insert("moz:firefoxOptions".into(), json!(42));
FirefoxOptions::from_capabilities(None, &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_name() {
let mut firefox_opts = Capabilities::new();
firefox_opts.insert("androidPackage".into(), json!("foo"));
let opts = make_options(firefox_opts).expect("valid firefox options");
assert_eq!(opts.android, Some(AndroidOptions::new("foo".to_owned())));
}
#[test]
fn fx_options_android_package_name_invalid() {
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_activity() {
let mut firefox_opts = Capabilities::new();
firefox_opts.insert("androidPackage".into(), json!("foo"));
firefox_opts.insert("androidActivity".into(), json!("bar"));
let opts = make_options(firefox_opts).expect("valid firefox options");
let android_opts = AndroidOptions {
package: "foo".to_owned(),
activity: Some("bar".to_owned()),
..Default::default()
};
assert_eq!(opts.android, Some(android_opts));
}
#[test]
fn fx_options_android_activity_invalid() {
let mut firefox_opts = Capabilities::new();
firefox_opts.insert("androidPackage".into(), json!("foo"));
firefox_opts.insert("androidActivity".into(), json!(42));
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"));
firefox_opts.insert("androidDeviceSerial".into(), json!("bar"));
let opts = make_options(firefox_opts).expect("valid firefox options");
let android_opts = AndroidOptions {
package: "foo".to_owned(),
device_serial: Some("bar".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"));
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"));
firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"]));
let opts = make_options(firefox_opts).expect("valid firefox options");
let android_opts = AndroidOptions {
package: "foo".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"));
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"));
firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42]));
make_options(firefox_opts).expect_err("invalid firefox options");
}
#[test]
@ -529,9 +759,9 @@ mod tests {
let mut firefox_opts = Capabilities::new();
firefox_opts.insert("profile".into(), encoded_profile);
let opts = make_options(firefox_opts);
let mut profile = opts.profile.unwrap();
let prefs = profile.user_prefs().unwrap();
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);
@ -554,15 +784,15 @@ mod tests {
firefox_opts.insert("profile".into(), encoded_profile);
firefox_opts.insert("prefs".into(), Value::Object(prefs));
let opts = make_options(firefox_opts);
let mut profile = opts.profile.unwrap();
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)
.unwrap();
.expect("set preferences");
let prefs_set = profile.user_prefs().unwrap();
let prefs_set = profile.user_prefs().expect("valid user preferences");
println!("{:#?}", prefs_set.prefs);
assert_eq!(

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

@ -227,7 +227,7 @@ pub struct XblLocatorParameters {
pub value: String,
}
#[derive(Default, Debug)]
#[derive(Default, Debug, PartialEq)]
pub struct LogOptions {
pub level: Option<logging::Level>,
}

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

@ -40,6 +40,7 @@ use mozprofile::preferences::Pref;
static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0);
const LOGGED_TARGETS: &[&str] = &[
"geckodriver",
"mozdevice",
"mozprofile",
"mozrunner",
"mozversion",

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

@ -8,6 +8,7 @@ extern crate clap;
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;
@ -42,6 +43,7 @@ macro_rules! try_opt {
}};
}
mod android;
mod build;
mod capabilities;
mod command;

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

@ -1,3 +1,4 @@
use crate::android::{AndroidHandler};
use crate::command::{
AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters,
GeckoExtensionCommand, GeckoExtensionRoute, XblLocatorParameters, CHROME_ELEMENT_KEY,
@ -62,6 +63,16 @@ 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")]
@ -86,7 +97,7 @@ pub struct MarionetteSettings {
pub struct MarionetteHandler {
pub connection: Mutex<Option<MarionetteConnection>>,
pub settings: MarionetteSettings,
pub browser: Option<FirefoxProcess>,
pub browser: Option<Browser>,
}
impl MarionetteHandler {
@ -125,21 +136,90 @@ impl MarionetteHandler {
let host = self.settings.host.to_owned();
let port = self.settings.port.unwrap_or(get_free_port(&host)?);
if !self.settings.connect_existing {
self.start_browser(port, options)?;
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| {
if let Some(ref mut runner) = self.browser {
runner.kill()?;
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 mut handler = AndroidHandler::new(&android_options);
handler.connect(port).map_err(|e| WebDriverError::new(
ErrorStatus::UnknownError,
e.to_string()
))?;
// 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).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,
@ -187,7 +267,7 @@ impl MarionetteHandler {
format!("Failed to start browser {}: {}", binary.display(), e),
)
})?;
self.browser = Some(browser_proc);
self.browser = Some(Browser::Host(browser_proc));
Ok(())
}
@ -332,13 +412,23 @@ impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler {
}
}
if let Some(ref mut runner) = self.browser {
// 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),
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);
@ -1171,7 +1261,7 @@ impl MarionetteConnection {
}
}
pub fn connect(&mut self, browser: &mut Option<FirefoxProcess>) -> WebDriverResult<()> {
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();
@ -1185,7 +1275,7 @@ impl MarionetteConnection {
loop {
// immediately abort connection attempts if process disappears
if let Some(ref mut runner) = *browser {
if let Some(Browser::Host(ref mut runner)) = *browser {
let exit_status = match runner.try_wait() {
Ok(Some(status)) => Some(
status