diff --git a/CHANGES.md b/CHANGES.md
index b1f76ce1..eb7902c4 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,12 +1,59 @@
Change log
==========
-All notable changes to this program is documented in this file.
+All notable changes to this program are documented in this file.
+0.28.0 (2020-11-03, `c00d2b6acd3f`)
+--------------------
+
+### Known problems
+
+- _macOS 10.15 (Catalina):_
+
+ Due to the requirement from Apple that all programs must be
+ notarized, geckodriver will not work on Catalina if you manually
+ download it through another notarized program, such as Firefox.
+
+ Whilst we are working on a repackaging fix for this problem, you can
+ find more details on how to work around this issue in the [macOS
+ notarization] section of the documentation.
+
+### Added
+
+- The command line flag `--android-storage` has been added, to allow geckodriver
+to also control Firefox on root-less Android devices. See the [documentation][Flags]
+for available values.
+
+### Fixed
-0.27.0 (2020-07-27, `90ec81285ff6`)
+- Firefox can be started again via a shell script that is located outside of the
+ Firefox directory on Linux.
+
+- If Firefox cannot be started by geckodriver the real underlying error message is
+ now being reported.
+
+- Version numbers for minor and extended support releases of Firefox are now parsed correctly.
+
+### Removed
+
+- Since Firefox 72 extension commands for finding an element’s anonymous children
+ and querying its attributes are no longer needed, and have been removed.
+
+0.27.0 (2020-07-27, `7b8c4f32cdde`)
--------------------
+### Security Fixes
+
+- CVE-2020-15660
+
+ - Added additional checks on the `Content-Type` header for `POST`
+ requests to disallow `application/x-www-form-urlencoded`,
+ `multipart/form-data` and `text/plain`.
+
+ - Added checking of the `Origin` header for `POST` requests.
+
+ - The version number of Firefox is now checked when establishing a session.
+
### Known problems
- _macOS 10.15 (Catalina):_
@@ -15,9 +62,9 @@ All notable changes to this program is documented in this file.
notarized, geckodriver will not work on Catalina if you manually
download it through another notarized program, such as Firefox.
- Whilst we are working on a repackaging fix for this problem, you
- can find more details on how to work around this issue in the
- [macOS notarization] section of the documentation.
+ Whilst we are working on a repackaging fix for this problem, you can
+ find more details on how to work around this issue in the [macOS
+ notarization] section of the documentation.
### Added
@@ -42,8 +89,7 @@ All notable changes to this program is documented in this file.
- _Android:_
- * Firefox running on Android devices can now be controlled from a
- Windows host.
+ * Firefox running on Android devices can now be controlled from a Windows host.
* Setups with multiple connected Android devices are now supported.
@@ -1327,6 +1373,7 @@ and greater.
[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
[Capabilities]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html
+[Flags]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Flags.html
[enable remote debugging on the Android device]: https://developers.google.com/web/tools/chrome-devtools/remote-debugging
[macOS notarization]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Notarization.html
diff --git a/Cargo.toml b/Cargo.toml
index 7a1ebdb5..a5d4a274 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "geckodriver"
-version = "0.27.0"
+version = "0.28.0"
description = "Proxy for using WebDriver clients to interact with Gecko-based browsers."
keywords = ["webdriver", "w3c", "httpd", "mozilla", "firefox"]
repository = "https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver"
@@ -17,17 +17,17 @@ hyper = "0.13"
lazy_static = "1.0"
log = { version = "0.4", features = ["std"] }
marionette = { path = "./marionette" }
-mozdevice = "0.2.0"
+mozdevice = "0.3.0"
mozprofile = "0.7.0"
-mozrunner = "0.11"
-mozversion = "0.3"
+mozrunner = "0.12.0"
+mozversion = "0.4.0"
regex = { version="1.0", default-features = false, features = ["perf", "std"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
serde_yaml = "0.8"
uuid = { version = "0.8", features = ["v4"] }
-webdriver = "0.41"
+webdriver = "0.42.0"
zip = { version = "0.4", default-features = false, features = ["deflate"] }
[[bin]]
diff --git a/doc/Flags.md b/doc/Flags.md
index 209acf19..8889e8b5 100644
--- a/doc/Flags.md
+++ b/doc/Flags.md
@@ -1,7 +1,53 @@
Flags
=====
-#### -b BINARY/--binary BINARY
+#### --android-storage ANDROID_STORAGE
+
+Selects the test data location on the Android device, eg. the Firefox profile.
+By default `auto` is used.
+
+
+
+
+
+
+
Value
+
Description
+
+
+
+
+
auto
+
Best suitable location based on whether the device is rooted.
+ If the device is rooted internal is used, otherwise app.
+
+
app
+
Location: /data/data/%androidPackage%/test_root
+ Based on the androidPackage capability that is passed as part of
+ moz:firefoxOptions when creating a new session. Commands that
+ change data in the app's directory are executed using run-as. This requires
+ that the installed app is debuggable.
+
+
internal
+
Location: /data/local/tmp/test_root
+ The device must be rooted since when the app runs, files that are created
+ in the profile, which is owned by the app user, cannot be changed by the
+ shell user. Commands will be executed via su.
+
+
+
+#### -b BINARY / --binary BINARY
Path to the Firefox binary to use. By default geckodriver tries to
find and use the system installation of Firefox, but that behaviour
@@ -28,7 +74,7 @@ scanning the Windows registry.
[whereis(1)]: http://www.manpagez.com/man/1/whereis/
-#### `--connect-existing`
+#### --connect-existing
Connect geckodriver to an existing Firefox instance. This means
geckodriver will abstain from the default of starting a new Firefox
@@ -55,6 +101,12 @@ Set the Gecko and geckodriver log level. Possible values are `fatal`,
`error`, `warn`, `info`, `config`, `debug`, and `trace`.
+#### --marionette-host HOST
+
+Selects the host for geckodriver’s connection to the [Marionette]
+remote protocol. Defaults to 127.0.0.1.
+
+
#### --marionette-port PORT
Selects the port for geckodriver’s connection to the [Marionette]
@@ -70,7 +122,7 @@ under geckodriver’s control, it will simply connect to PORT.
[`--connect-existing`]: #connect-existing
-#### -p PORT/--port PORT
+#### -p PORT / --port PORT
Port to use for the WebDriver server. Defaults to 4444.
diff --git a/doc/Releasing.md b/doc/Releasing.md
index 9618045d..39185809 100644
--- a/doc/Releasing.md
+++ b/doc/Releasing.md
@@ -124,10 +124,10 @@ repository, the changeset id for the release has to be added to the
change log. Therefore add a final place-holder commit to the patch
series, to already get review for.
-Once all previous revisions of the patch series have been reviewed and
-landed, it's known which commit id the version bump commit has, finalize the
-change log, and land that remaining revision.
-
+Once all previous revisions of the patch series have been landed, and got merged
+to `mozilla-central`, the changeset id from the merge commit has to picked for
+finalizing the change log. This specific id is needed because Taskcluster creates
+the final signed builds based on that merge.
Release new in-tree dependency crates
-------------------------------------
diff --git a/doc/Support.md b/doc/Support.md
index c3dcfe7f..a7478d4c 100644
--- a/doc/Support.md
+++ b/doc/Support.md
@@ -23,6 +23,16 @@ and required versions of Selenium and Firefox:
+
+
0.28.0
+
≥ 3.11 (3.14 Python)
+
60
+
n/a
+
+
0.27.0
+
≥ 3.11 (3.14 Python)
+
60
+
n/a
0.26.0
≥ 3.11 (3.14 Python)
diff --git a/doc/Usage.md b/doc/Usage.md
index a5d915e9..d3ce68a4 100644
--- a/doc/Usage.md
+++ b/doc/Usage.md
@@ -68,9 +68,9 @@ Using [curl(1)]:
% geckodriver &
[1] 16010
% 1491834109194 geckodriver INFO Listening on 127.0.0.1:4444
- % curl -d '{"capabilities": {"alwaysMatch": {"acceptInsecureCerts": true}}}' http://localhost:4444/session
- {"sessionId":"d4605710-5a4e-4d64-a52a-778bb0c31e00","value":{"XULappId":"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}","acceptSslCerts":false,"appBuildId":"20160913030425","browserName":"firefox","browserVersion":"51.0a1","command_id":1,"platform":"LINUX","platformName":"linux","platformVersion":"4.9.0-1-amd64","processId":17474,"proxy":{},"raisesAccessibilityExceptions":false,"rotatable":false,"specificationLevel":0,"takesElementScreenshot":true,"takesScreenshot":true,"version":"51.0a1"}}
- % curl -d '{"url": "https://mozilla.org"}' http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
+ % curl -H 'Content-Type: application/json' -d '{"capabilities": {"alwaysMatch": {"acceptInsecureCerts": true}}}' http://localhost:4444/session
+ {"value":{"sessionId":"d4605710-5a4e-4d64-a52a-778bb0c31e00","capabilities":{"acceptInsecureCerts":true,[...]}}}
+ % curl -H 'Content-Type: application/json' -d '{"url": "https://mozilla.org"}' http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
{}
% curl http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00/url
{"value":"https://www.mozilla.org/en-US/"
diff --git a/mach_commands.py b/mach_commands.py
index efcf7591..beb4b93b 100644
--- a/mach_commands.py
+++ b/mach_commands.py
@@ -19,37 +19,56 @@
@CommandProvider
class GeckoDriver(MachCommandBase):
-
- @Command("geckodriver",
- category="post-build",
- description="Run the WebDriver implementation for Gecko.")
- @CommandArgument("--binary", type=str,
- help="Firefox binary (defaults to the local build).")
- @CommandArgument("params", nargs="...",
- help="Flags to be passed through to geckodriver.")
+ @Command(
+ "geckodriver",
+ category="post-build",
+ description="Run the WebDriver implementation for Gecko.",
+ )
+ @CommandArgument(
+ "--binary", type=str, help="Firefox binary (defaults to the local build)."
+ )
+ @CommandArgument(
+ "params", nargs="...", help="Flags to be passed through to geckodriver."
+ )
@CommandArgumentGroup("debugging")
- @CommandArgument("--debug", action="store_true", group="debugging",
- help="Enable the debugger. Not specifying a --debugger "
- "option will result in the default debugger "
- "being used.")
- @CommandArgument("--debugger", default=None, type=str, group="debugging",
- help="Name of debugger to use.")
- @CommandArgument("--debugger-args", default=None, metavar="params",
- type=str, group="debugging",
- help="Flags to pass to the debugger itself; "
- "split as the Bourne shell would.")
+ @CommandArgument(
+ "--debug",
+ action="store_true",
+ group="debugging",
+ help="Enable the debugger. Not specifying a --debugger "
+ "option will result in the default debugger "
+ "being used.",
+ )
+ @CommandArgument(
+ "--debugger",
+ default=None,
+ type=str,
+ group="debugging",
+ help="Name of debugger to use.",
+ )
+ @CommandArgument(
+ "--debugger-args",
+ default=None,
+ metavar="params",
+ type=str,
+ group="debugging",
+ help="Flags to pass to the debugger itself; "
+ "split as the Bourne shell would.",
+ )
def run(self, binary, params, debug, debugger, debugger_args):
try:
binpath = self.get_binary_path("geckodriver")
except BinaryNotFoundException as e:
- self.log(logging.ERROR, 'geckodriver',
- {'error': str(e)},
- 'ERROR: {error}')
- self.log(logging.INFO, 'geckodriver', {},
- "It looks like geckodriver isn't built. "
- "Add ac_add_options --enable-geckodriver to your "
- "mozconfig "
- "and run |./mach build| to build it.")
+ self.log(logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}")
+ self.log(
+ logging.INFO,
+ "geckodriver",
+ {},
+ "It looks like geckodriver isn't built. "
+ "Add ac_add_options --enable-geckodriver to your "
+ "mozconfig "
+ "and run |./mach build| to build it.",
+ )
return 1
args = [binpath]
@@ -61,12 +80,10 @@ def run(self, binary, params, debug, debugger, debugger_args):
try:
binary = self.get_binary_path("app")
except BinaryNotFoundException as e:
- self.log(logging.ERROR, 'geckodriver',
- {'error': str(e)},
- 'ERROR: {error}')
- self.log(logging.INFO, 'geckodriver',
- {'help': e.help()},
- '{help}')
+ self.log(
+ logging.ERROR, "geckodriver", {"error": str(e)}, "ERROR: {error}"
+ )
+ self.log(logging.INFO, "geckodriver", {"help": e.help()}, "{help}")
return 1
args.extend(["--binary", binary])
@@ -76,10 +93,13 @@ def run(self, binary, params, debug, debugger, debugger_args):
self.log_manager.terminal_handler.setLevel(logging.WARNING)
import mozdebug
+
if not debugger:
# No debugger name was provided. Look for the default ones on
# current OS.
- debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
+ debugger = mozdebug.get_default_debugger_name(
+ mozdebug.DebuggerSearch.KeepLooking
+ )
if debugger:
self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
@@ -91,29 +111,35 @@ def run(self, binary, params, debug, debugger, debugger_args):
# their use.
if debugger_args:
from mozbuild import shellutil
+
try:
debugger_args = shellutil.split(debugger_args)
except shellutil.MetaCharacterException as e:
- print("The --debugger-args you passed require a real shell to parse them.")
+ print(
+ "The --debugger-args you passed require a real shell to parse them."
+ )
print("(We can't handle the %r character.)" % e.char)
return 1
# Prepend the debugger args.
args = [self.debuggerInfo.path] + self.debuggerInfo.args + args
- return self.run_process(args=args, ensure_exit_code=False,
- pass_thru=True)
+ return self.run_process(args=args, ensure_exit_code=False, pass_thru=True)
@CommandProvider
class GeckoDriverTest(MachCommandBase):
-
- @Command("geckodriver-test",
- category="post-build",
- description="Run geckodriver unit tests.")
- @CommandArgument("-v", "--verbose", action="store_true",
- help="Verbose output for what"
- " commands the build is running.")
+ @Command(
+ "geckodriver-test",
+ category="post-build",
+ description="Run geckodriver unit tests.",
+ )
+ @CommandArgument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Verbose output for what" " commands the build is running.",
+ )
def test(self, verbose=False, **kwargs):
from mozbuild.controller.building import BuildDriver
@@ -123,4 +149,5 @@ def test(self, verbose=False, **kwargs):
return driver.build(
what=["testing/geckodriver/check"],
verbose=verbose,
- mach_context=self._mach_context)
+ mach_context=self._mach_context,
+ )
diff --git a/moz.build b/moz.build
index 9f191598..9cda32b4 100644
--- a/moz.build
+++ b/moz.build
@@ -10,10 +10,9 @@ 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
+ "mozdevice",
"mozprofile",
"mozrunner",
"mozversion",
diff --git a/src/android.rs b/src/android.rs
index 87cce8bd..6aaf58ec 100644
--- a/src/android.rs
+++ b/src/android.rs
@@ -1,5 +1,5 @@
use crate::capabilities::AndroidOptions;
-use mozdevice::{Device, Host};
+use mozdevice::{AndroidStorage, Device, Host};
use mozprofile::profile::Profile;
use serde::Serialize;
use serde_yaml::{Mapping, Value};
@@ -7,6 +7,7 @@ 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.
@@ -25,7 +26,6 @@ pub enum AndroidError {
ActivityNotFound(String),
Device(mozdevice::DeviceError),
IO(io::Error),
- NotConnected,
PackageNotFound(String),
Serde(serde_yaml::Error),
}
@@ -38,7 +38,6 @@ impl fmt::Display for AndroidError {
}
AndroidError::Device(ref message) => message.fmt(f),
AndroidError::IO(ref message) => message.fmt(f),
- AndroidError::NotConnected => write!(f, "Not connected to any Android device"),
AndroidError::PackageNotFound(ref package) => {
write!(f, "Package '{}' not found", package)
}
@@ -65,6 +64,12 @@ impl From for AndroidError {
}
}
+impl From 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
@@ -90,12 +95,13 @@ impl AndroidProcess {
}
}
-#[derive(Debug, Default)]
+#[derive(Debug)]
pub struct AndroidHandler {
pub config: PathBuf,
pub options: AndroidOptions,
- pub process: Option,
+ pub process: AndroidProcess,
pub profile: PathBuf,
+ pub test_root: PathBuf,
// For port forwarding host => target
pub host_port: u16,
@@ -105,59 +111,41 @@ pub struct AndroidHandler {
impl Drop for AndroidHandler {
fn drop(&mut self) {
// Try to clean up various settings
- if let Some(ref process) = self.process {
- let clear_command = format!("am clear-debug-app {}", process.package);
- match process.device.execute_host_shell_command(&clear_command) {
- Ok(_) => debug!("Disabled reading from configuration file"),
- Err(e) => error!("Failed disabling from configuration file: {}", e),
- }
+ 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),
+ }
- let remove_command = format!("rm -rf {}", self.config.display());
- match process.device.execute_host_shell_command(&remove_command) {
- Ok(_) => debug!("Deleted GeckoView configuration file"),
- Err(e) => error!("Failed deleting GeckoView 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 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
- ),
- }
+ 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) -> AndroidHandler {
+ pub fn new(options: &AndroidOptions, host_port: u16) -> Result {
// 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
- ));
- let config = PathBuf::from(format!(
- "/data/local/tmp/{}-geckoview-config.yaml",
- &options.package
- ));
-
- AndroidHandler {
- options: options.clone(),
- profile,
- config,
- process: None,
- ..Default::default()
- }
- }
-
- pub fn connect(&mut self, host_port: u16) -> Result<()> {
let host = Host {
host: None,
port: None,
@@ -165,58 +153,88 @@ impl AndroidHandler {
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;
+ 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(self.host_port, self.target_port)?;
+ device.forward_port(host_port, TARGET_PORT)?;
debug!(
"Android port forward ({} -> {}) started",
- &self.host_port, &self.target_port
+ 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 {}", &self.options.package))?;
+ 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::>();
- if !packages.contains(&self.options.package.as_str()) {
- return Err(AndroidError::PackageNotFound(self.options.package.clone()));
+ 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 self.options.activity {
+ let activity = match options.activity {
Some(ref activity) => activity.clone(),
None => {
let response = device.execute_host_shell_command(&format!(
"cmd package resolve-activity --brief {}",
- &self.options.package
+ &options.package
))?;
let activities = response
.split_terminator('\n')
- .filter(|line| line.starts_with(&self.options.package))
+ .filter(|line| line.starts_with(&options.package))
.map(|line| line.rsplit('/').next().unwrap())
.collect::>();
if activities.is_empty() {
- return Err(AndroidError::ActivityNotFound(self.options.package.clone()));
+ return Err(AndroidError::ActivityNotFound(options.package.clone()));
}
activities[0].to_owned()
}
};
- self.process = Some(AndroidProcess::new(
- device,
- self.options.package.clone(),
- activity,
- )?);
+ let process = AndroidProcess::new(device, options.package.clone(), activity)?;
- Ok(())
+ Ok(AndroidHandler {
+ options: options.clone(),
+ config,
+ process,
+ profile,
+ test_root,
+ host_port,
+ target_port: TARGET_PORT,
+ })
}
pub fn generate_config_file(&self, envs: I) -> Result
@@ -275,102 +293,178 @@ impl AndroidHandler {
K: ToString,
V: ToString,
{
- match self.process {
- Some(ref process) => {
- process.device.clear_app_data(&process.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.{}",
- &process.package, perm
- ))?;
- }
-
- debug!("Deleting {}", self.profile.display());
- process
- .device
- .execute_host_shell_command(&format!("rm -rf {}", self.profile.display()))?;
+ 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
+ ))?;
+ }
- debug!(
- "Pushing {} to {}",
- profile.path.display(),
- self.profile.display()
- );
- process
- .device
- .push_dir(&profile.path, &self.profile, 0o777)?;
+ // Make sure to create the test root.
+ self.process.device.create_dir(&self.test_root)?;
+ self.process.device.chmod(&self.test_root, "777", true)?;
- 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());
+ // Replace the profile
+ self.process.device.remove(&self.profile)?;
+ self.process
+ .device
+ .push_dir(&profile.path, &self.profile, 0o777)?;
- debug!(
- "Pushing GeckoView configuration file to {}",
- self.config.display()
- );
- process.device.push(reader, &self.config, 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());
- // Bug 1584966: File permissions are not correctly set by push()
- process
- .device
- .execute_host_shell_command(&format!("chmod a+rw {}", self.config.display()))?;
+ 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"`.
- process.device.execute_host_shell_command(&format!(
- "am set-debug-app --persistent {}",
- process.package
- ))?;
- }
- None => return Err(AndroidError::NotConnected),
- }
+ // 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<()> {
- match self.process {
- Some(ref process) => {
- // 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()).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),
- }
+ // 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<()> {
- match &self.process {
- Some(process) => {
- debug!("Force stopping the Android package: {}", &process.package);
- process.device.force_stop(&process.package)?;
- }
- None => return Err(AndroidError::NotConnected),
- }
+ 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/src/capabilities.rs b/src/capabilities.rs
index ef1b0d74..ab4ed687 100644
--- a/src/capabilities.rs
+++ b/src/capabilities.rs
@@ -1,26 +1,54 @@
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_version, Version};
+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::process::{Command, Stdio};
use std::str::{self, FromStr};
use webdriver::capabilities::{BrowserCapabilities, Capabilities};
use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
use zip;
-/// Provides matching of `moz:firefoxOptions` and resolution of which Firefox
+#[derive(Clone, Debug)]
+enum VersionError {
+ VersionError(mozversion::Error),
+ MissingBinary,
+}
+
+impl From 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 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
@@ -30,7 +58,7 @@ use zip;
pub struct FirefoxCapabilities<'a> {
pub chosen_binary: Option,
fallback_binary: Option<&'a PathBuf>,
- version_cache: BTreeMap,
+ version_cache: BTreeMap>,
}
impl<'a> FirefoxCapabilities<'a> {
@@ -52,57 +80,42 @@ impl<'a> FirefoxCapabilities<'a> {
.or_else(firefox_default_path);
}
- fn version(&mut self, binary: Option<&Path>) -> Option {
+ fn version(&mut self, binary: Option<&Path>) -> Result {
if let Some(binary) = binary {
- if let Some(value) = self.version_cache.get(binary) {
- return Some((*value).clone());
+ if let Some(cache_value) = self.version_cache.get(binary) {
+ return cache_value.clone();
}
- debug!("Trying to read firefox version from ini files");
- let rv = firefox_version(&*binary)
- .ok()
- .and_then(|x| x.version_string)
- .or_else(|| {
- debug!("Trying to read firefox version from binary");
- self.version_from_binary(binary)
- });
- if let Some(ref version) = rv {
+ let rv = self
+ .version_from_ini(binary)
+ .or_else(|_| self.version_from_binary(binary));
+ if let Ok(ref version) = rv {
debug!("Found version {}", version);
- self.version_cache
- .insert(binary.to_path_buf(), version.clone());
} else {
debug!("Failed to get binary version");
}
+ self.version_cache.insert(binary.to_path_buf(), rv.clone());
rv
} else {
- None
+ Err(VersionError::MissingBinary)
}
}
- fn version_from_binary(&self, binary: &Path) -> Option {
- let version_regexp = Regex::new(r#"Mozilla Firefox [0-9]+\.[0-9]+(?:[a-z][0-9]+)?"#)
- .expect("Error parsing version regexp");
- let output = Command::new(binary)
- .args(&["--version"])
- .stdout(Stdio::piped())
- .spawn()
- .and_then(|child| child.wait_with_output())
- .ok();
-
- if let Some(x) = output {
- version_regexp
- .captures(&*x.stdout)
- .and_then(|captures| captures.get(0))
- .and_then(|m| str::from_utf8(m.as_bytes()).ok())
- .map(|x| x.into())
+ fn version_from_ini(&self, binary: &Path) -> Result {
+ 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 {
- None
+ Err(VersionError::VersionError(
+ mozversion::Error::MetadataError("Missing version string".into()),
+ ))
}
}
-}
-// TODO: put this in webdriver-rust
-fn convert_version_error(err: mozversion::Error) -> WebDriverError {
- WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string())
+ fn version_from_binary(&self, binary: &Path) -> Result {
+ debug!("Trying to read firefox version from binary");
+ Ok(firefox_binary_version(binary)?)
+ }
}
impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
@@ -116,7 +129,9 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult