зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
b4b1f6820c
Коммит
dd5317eeba
|
@ -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 of 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 of 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> 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> 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> 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> 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
|
||||
|
|
Загрузка…
Ссылка в новой задаче