diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /testing/geckodriver | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/geckodriver')
42 files changed, 9457 insertions, 0 deletions
diff --git a/testing/geckodriver/.cargo/config b/testing/geckodriver/.cargo/config new file mode 100644 index 0000000000..18086c84c5 --- /dev/null +++ b/testing/geckodriver/.cargo/config @@ -0,0 +1,3 @@ +[target.i686-pc-windows-gnu] +linker = "i686-w64-mingw32-gcc" +rustflags = "-C panic=abort" diff --git a/testing/geckodriver/CHANGES.md b/testing/geckodriver/CHANGES.md new file mode 100644 index 0000000000..ed684cf5a1 --- /dev/null +++ b/testing/geckodriver/CHANGES.md @@ -0,0 +1,1471 @@ +Change log +========== + +All notable changes to this program are documented in this file. + +0.29.0 (2021-01-14, `cf6956a5ec8e`) +-------------------- + +### 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 + +- Introduced the new boolean capability `moz:debuggerAddress` that can be used + to opt-in to the experimental Chrome DevTools Protocol (CDP) implementation. + A string capability with the same name will be returned by [`NewSession`], + which contains the `host:port` combination of the HTTP server that can be + used to query for websockets of available targets. + + Note: For this experimental feature the site-isolation support of + Firefox aka [Fission] will be not available. + +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 + +- 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):_ + + 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 + +- To set environment variables for the launched Firefox for Android, + it is now possible to add an `env` object on `moz:firefoxOptions` + (note: this is not supported for Firefox Desktop) + +- Support for print-to-PDF + + The newly standardised WebDriver [Print] endpoint provides a way to + render pages to a paginated PDF representation. This endpoint is + supported by geckodriver when using Firefox version ≥78. + +- Support for same-site cookies + + Cookies can now be set with a `same-site` parameter, and the value + of that parameter will be returned when cookies are + retrieved. Requires Firefox version ≥79. Thanks to [Peter Major] for + the patch. + +### Fixed + +- _Android:_ + + * Firefox running on Android devices can now be controlled from a Windows host. + + * Setups with multiple connected Android devices are now supported. + + * Improved cleanup of configuration files. This prevents crashes if + the application is started manually after launching it through + geckodriver. + +- Windows and Linux binaries are again statically linked. + +0.26.0 (2019-10-12, `e9783a644016'`) +------------------------------------ + +Note that with this release the minimum recommended Firefox version +has changed to Firefox ≥60. + +### Known problems + +- _macOS 10.15 (Catalina):_ + + Due to the recent 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. + +- _Windows:_ + + You must still have the [Microsoft Visual Studio redistributable + runtime] installed on your system for the binary to run. This + is a known bug which we weren't able fix for this release. + +### Added + +- Support for Firefox on Android + + Starting with this release geckodriver is able to connect to + Firefox on Android systems, and to control packages based on + [GeckoView]. + + Support for Android works by the geckodriver process running on + a host system and Firefox running within either an emulator or + on a physical device connected to the host system. This requires + you to first [enable remote debugging on the Android device]. + + The WebDriver client must set the [`platformName` capability] to + "`android`" and the `androidPackage` capability within + [`moz:firefoxOptions`] to the Android package name of the Firefox + application. + + The full list of new capabilities specific to Android, instructions + how to use them, and examples can be found in the [`moz:firefoxOptions`] + documentation on MDN. + + When the session is created, the `platformName` capability will + return "`android`" instead of reporting the platform of the host + system. + +### Changed + +- Continued Marionette refactoring changes + + 0.25.0 came with a series of internal changes for how geckodriver + communicates with Firefox over the Marionette protocol. This + release contains the second half of the refactoring work. + +### Fixed + +- Connection attempts to Firefox made more reliable + + geckodriver now waits for the Marionette handshake before assuming + the session has been established. This should improve reliability + in creating new WebDriver sessions. + +- Corrected error codes used during session creation + + When a new session was being configured with invalid input data, + the error codes returned was not always consistent. Attempting + to start a session with a malformed capabilities configuration + will now return the [`invalid argument`] error consistently. + + +0.25.0 (2019-09-09, `bdb64cf16b68`) +----------------------------------- + +__Note to Windows users!__ +With this release you must have the [Microsoft Visual Studio redistributable runtime] +installed on your system for the binary to run. +This is a [known bug](https://github.com/mozilla/geckodriver/issues/1617) +with this particular release that we intend to release a fix for soon. + +### Added + +- Added support for HTTP `HEAD` requests to the HTTPD + + geckodriver now responds correctly to HTTP `HEAD` requests, + which can be used for probing whether it supports a particular API. + + Thanks to [Bastien Orivel] for this patch. + +- Added support for searching for Nightly’s default path on macOS + + If the location of the Firefox binary is not given, geckodriver + will from now also look for the location of Firefox Nightly in + the default locations. The ordered list of search paths on macOS + is as follows: + + 1. `/Applications/Firefox.app/Contents/MacOS/firefox-bin` + 2. `$HOME/Applications/Firefox.app/Contents/MacOS/firefox-bin` + 3. `/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin` + 4. `$HOME/Applications/Firefox Nightly.app/Contents/MacOS/firefox-bin` + + Thanks to [Kriti Singh] for this patch. + +- Support for application bundle paths on macOS + + It is now possible to pass an application bundle path, such as + `/Applications/Firefox.app` as argument to the `binary` field in + [`moz:firefoxOptions`]. This will be automatically resolved to + the absolute path of the binary when Firefox is started. + + Thanks to [Nupur Baghel] for this patch. + +- macOS and Windows builds are signed + + With this release of geckodriver, executables for macOS and Windows + are signed using the same certificate key as Firefox. This should + help in cases where geckodriver previously got misidentified as + a virus by antivirus software. + +### Removed + +- Dropped support for legacy Selenium web element references + + The legacy way of serialising web elements, using `{"ELEMENT": <UUID>}`, + has been removed in this release. This may break older Selenium + clients and clients which are otherwise not compatible with the + WebDriver standard. + + Thanks to [Shivam Singhal] for this patch. + +- Removed `--webdriver-port` command-line option + + `--webdriver-port <PORT>` was an undocumented alias for `--port`, + initially used for backwards compatibility with clients + prior to Selenium 3.0.0. + +### Changed + +- Refactored Marionette serialisation + + Much of geckodriver’s internal plumbing for serialising WebDriver + requests to Marionette messages has been refactored to decrease + the amount of manual lifting. + + This work should have no visible side-effects for users. + + Thanks to [Nupur Baghel] for working on this throughout her + Outreachy internship at Mozilla. + +- Improved error messages for incorrect command-line usage + +### Fixed + +- Errors related to incorrect command-line usage no longer hidden + + By mistake, earlier versions of geckodriver failed to print incorrect + flag use. With this release problems are again written to stderr. + +- Search system path for Firefox binary on BSDs + + geckodriver would previously only search the system path for the + `firefox` binary on Linux. Now it supports different BSD flavours + as well. + + +0.24.0 (2019-01-28, `917474f3473e`) +----------------------------------- + +### Added + +- Introduces `strictFileInteractability` capability + + The new capability indicates if strict interactability checks + should be applied to `<input type=file>` elements. As strict + interactability checks are off by default, there is a change + in behaviour when using [Element Send Keys] with hidden file + upload controls. + +- Added new endpoint `GET /session/{session id}/moz/screenshot/full` + for taking full document screenshots, thanks to Greg Fraley. + +- Added new `--marionette-host <hostname>` flag for binding to a + particular interface/IP layer on the system. + +- Added new endpoint `POST /session/{session_id}/window/new` + for the [New Window] command to create a new top-level browsing + context, which can be either a window or a tab. The first version + of Firefox supporting this command is Firefox 66.0. + +- When using the preference `devtools.console.stdout.content` set to + `true` logging of console API calls like `info()`, `warn()`, and + `error()` can be routed to stdout. + +- geckodriver now sets the `app.update.disabledForTesting` preference + to prevent Firefox >= 65 from automatically updating whilst under + automation. + +### Removed + +- ARMv7 HF builds have been discontinued + + We [announced](https://lists.mozilla.org/pipermail/tools-marionette/2018-September/000035.html) + back in September 2018 that we would stop building for ARM, + but builds can be self-serviced by building from source. + + To cross-compile from another host system, you can use this command: + + % cargo build --target armv7-unknown-linux-gnueabihf + +### Changed + +- Allow file uploads to hidden `<input type=file>` elements + + Through a series of changes to the WebDriver specification, + geckodriver is now aligned with chromedriver’s behaviour that + allows interaction with hidden `<input type=file>` elements. + + This allows WebDriver to be used with various popular web + frameworks that—through indirection—hides the file upload control + and invokes it through other means. + +- Allow use of an indefinite script timeout for the [Set Timeouts] + command, thanks to reimu. + +### Fixed + +- Corrected `Content-Type` of response header to `utf-8` to fix + an HTTP/1.1 compatibility bug. + +- Relaxed the deserialization of timeouts parameters to allow unknown + fields for the [Set Timeouts] command. + +- Fixed a regression in the [Take Element Screenshot] to not screenshot + the viewport, but the requested element. + + +0.23.0 (2018-10-03) +------------------- + +This release contains a number of fixes for regressions introduced +in 0.22.0, where we shipped a significant refactoring to the way +geckodriver internally dealt with JSON serialisation. + +### Removed + +- The POST `/session/{session id}/element/{element id}/tap` endpoint + was removed, thanks to Kerem Kat. + +### Changed + +- [webdriver crate] upgraded to 0.38.0. + +### Fixed + +- `desiredCapabilities` and `requiredCapabilities` are again + recognised on session creation + + A regression in 0.22.0 caused geckodriver to recognise `desired` + and `required` instead of the correct `desiredCapabilities` + and `requiredCapabilities`. This will have caused significant + problems for users who relied on this legacy Selenium-style + session creation pattern. + + Do however note that support for Selenium-styled new session + requests is temporary and that this will be removed sometime + before the 1.0 release. + +- `duration` field made optional on pause actions + + A regression in 0.22.0 caused the pause action primitive to + require a `duration` field. This has now been fixed so that + pauses in action chains can be achieved with the default duration. + +- Log level formatted to expected Marionette input + + A regression in 0.22.0 caused the log level to be improperly + formatted when using Firefox pre-releases. This is now fixed so + that the requested log level is correctly interpreted by Marionette. + +- `temporary` field on addon installation made optional + + A regression in 0.22.0 caused the `temporary` field for POST + `/session/{session id}/moz/addon/install` to be mandatory. This has + now been fixed so that an addon is installed permanently by default. + +- SHA1s in version information uses limited number of characters + + The SHA1 used in `--version` when building geckodriver from a + git repository is now limited to 12 characters, as it is when + building from an hg checkout. This ensures reproducible builds. + + +0.22.0 (2018-09-15) +------------------- + +This release marks an important milestone on the path towards +a stable release of geckodriver. Large portions of geckodriver +and the [webdriver] library it is based on has been refactored to +accommodate using [serde] for JSON serialization. + +We have also made great strides to improving [WebDriver conformance], +to the extent that geckodriver is now _almost_ entirely conforming +to the standard. + +### Added + +- Support for WebDriver web element-, web frame-, and web window + identifiers from Firefox. + +- Added support for the non-configurable `setWindowRect` capability + from WebDriver. + + This capability informs whether the attached browser supports + manipulating the window dimensions and position. + +- A new extension capability `moz:geckodriverVersion` is returned + upon session creation. + +### Changed + +- All JSON serialization and deserialisation has moved from + rustc_serialize to [serde]. + +- The HTTP status codes used for [script timeout] and [timeout] + errors has changed from Request Timeout (408) to Internal Server + Error (500) in order to not break HTTP/1.1 `Keep-Alive` support, + as HTTP clients interpret the old status code to mean they should + duplicate the request. + +- The HTTP/1.1 `Keep-Alive` timeout for persistent connections has + been increased to 90 seconds. + +- An [invalid session ID] error is now returned when there is no + active session. + +- An [invalid argument] error is now returned when [Add Cookie] + is given invalid parameters. + +- The handshake when geckodriver connects to Marionette has been + hardened by killing the Firefox process if it fails. + +- The handshake read timeout has been reduced to 10 seconds instead + of waiting forever. + +- The HTTP server geckodriver uses, [hyper], has been upgraded to + version 0.12, thanks to [Bastien Orivel]. + +- geckodriver version number is no longer logged on startup, as + the log level is not configured until a session is created. + + The version number is available through `--version`, and now + also through a new `moz:geckodriverVersion` field in the matched + capabilities. + +- [webdriver crate] upgraded to 0.37.0. + +### Fixed + +- Parsing [timeout object] values has been made WebDriver conforming, + by allowing floats as input. + +- Implicit downloads of OpenH264 and Widevine plugins has been disabled. + +- The commit hash and date displayed when invoking `--version` + is now well-formatted when built from an hg repository, thanks to + [Jeremy Lempereur]. + +- Many documentation improvements, now published on + https://firefox-source-docs.mozilla.org/testing/geckodriver/. + + +0.21.0 (2018-06-15) +------------------- + +Note that with this release of geckodriver the minimum recommended +Firefox and Selenium versions have changed: + + - Firefox 57 (and greater) + - Selenium 3.11 (and greater) + +### Added + +- Support for the chrome element identifier from Firefox. + +- The `unhandledPromptBehavior` capability now accepts `accept and + notify`, `dismiss and notify`, and `ignore` options. + + Note that the unhandled prompt handler is not fully supported in + Firefox at the time of writing. + +### Changed + +- Firefox will now be started with the `-foreground` and `-no-remote` + flags if they have not already been specified by the user in + `moz:firefoxOptions`. + + `-foreground` will ensure the application window gets focus when + Firefox is started, and `-no-remote` will prevent remote commands + to this instance of Firefox and also ensure we always start a new + instance. + +- WebDriver commands that do not have a return value now correctly + return `{value: null}` instead of an empty dictionary. + +- The HTTP server now accepts `Keep-Alive` connections. + +- Firefox remote protocol command mappings updated. + + All Marionette commands changed to make use of the `WebDriver:` + prefixes introduced with Firefox 56. + +- Overhaul of Firefox preferences. + + Already deprecated preferences in Firefox versions earlier than + 57 got removed. + +- [webdriver crate] upgraded to 0.36.0. + +### Fixed + +- Force use of IPv4 network stack. + + On certain system configurations, where `localhost` resolves to + an IPv6 address, geckodriver would attempt to connect to Firefox + on the wrong IP stack, causing the connection attempt to time out + after 60 seconds. We now ensure that geckodriver uses IPv4 + consistently to both connect to Firefox and for allocating a free + port. + +- geckodriver failed to locate the correct Firefox binary if it was + found under a _firefox_ or _firefox-bin_ directory, depending on + the system, because it thought the parent directory was the + executable. + +- On Unix systems (macOS, Linux), geckodriver falsely reported + non-executable files as valid binaries. + +- When stdout and stderr is redirected by geckodriver, a bug prevented + the redirections from taking effect. + + +0.20.1 (2018-04-06) +------------------- + +### Fixed + +- Avoid attempting to kill Firefox process that has stopped. + + With the change to allow Firefox enough time to shut down in + 0.20.0, geckodriver started unconditionally killing the process + to reap its exit status. This caused geckodriver to inaccurately + report a successful Firefox shutdown as a failure. + + The regression should not have caused any functional problems, but + the termination cause and the exit status are now reported correctly. + + +0.20.0 (2018-03-08) +------------------- + +### Added + +- New `--jsdebugger` flag to open the [Browser Toolbox] when Firefox + launches. This is useful for debugging Marionette internals. + +- Introduced the temporary, boolean capability + `moz:useNonSpecCompliantPointerOrigin` to disable the WebDriver + conforming behavior of calculating the Pointer Origin. + +### Changed + +- HTTP status code for the [`StaleElementReference`] error changed + from 400 (Bad Request) to 404 (Not Found). + +- Backtraces from geckodriver no longer substitute for missing + Marionette stacktraces. + +- [webdriver crate] upgraded to 0.35.0. + +### Fixed + +- The Firefox process is now given ample time to shut down, allowing + enough time for the Firefox shutdown hang monitor to kick in. + + Firefox has an integrated background monitor that observes + long-running threads during shutdown. These threads will be + killed after 63 seconds in the event of a hang. To allow Firefox + to shut down these threads on its own, geckodriver has to wait + that time and some additional seconds. + +- Grapheme clusters are now accepted as input for keyboard input + to actions. + + Input to the `value` field of the `keyDown` and `keyUp` action + primitives used to only accept single characters, which means + geckodriver would error when a valid grapheme cluster was sent in, + for example with the tamil nadu character U+0BA8 U+0BBF. + + Thanks to Greg Fraley for fixing this bug. + +- Improved error messages for malformed capability values. + + +0.19.1 (2017-10-30) +------------------- + +### Changed + +- Search suggestions in the location bar turned off as not to + trigger network connections + +- Block addons incompatible with E10s + +### Fixed + +- Marionette stacktraces are now correctly propagated + +- Some error messages have been clarified + +### Removed + +- Removed obsolete `socksUsername` and `socksPassword` proxy + configuration keys because neither were picked up or recognised + + +0.19.0 (2017-09-16) +------------------- + +Note that with geckodriver 0.19.0 the following versions are recommended: +- Firefox 55.0 (and greater) +- Selenium 3.5 (and greater) + +### Added + +- Added endpoint: + - POST `/session/{session id}/window/minimize` for the [Minimize Window] + command + +- Added preference `extensions.shield-recipe-client.api_url` to disable + shield studies which could unexpectedly change the behavior of Firefox + +- Introduced the temporary, boolean capability `moz:webdriverClick` to + enable the WebDriver conforming behavior of the [Element Click] command + +- Added crashreporter environment variables to better control the browser + in case of crashes + +- Added preference `dom.file.createInChild` set to true to allow file + object creation in content processes + +### Changed + +- Log all used application arguments and not only `-marionette` + +- Early abort connection attempts to Marionette if the Firefox process + closed unexpectetly + +- Removed deprecated `socksProxyVersion` in favor of `socksVersion` + +- Removed `ftpProxyPort`, `httpProxyPort`, `sslProxyPort`, and + `socksProxyPort` because _ports_ have to be set for `ftpProxy`, + `httpProxy`, `sslProxy`, and `socksProxy` using ":<PORT>" + +- The `proxyType` `noproxy` has been replaced with `direct` in accordance + with recent WebDriver specification changes + +- The [`WindowRectParameters`] have been updated to return signed 32-bit + integers in accordance with the CSS and WebDriver specifications, and + to be more liberal with the input types + +- Mapped the [`FullscreenWindow`] to the correct Marionette command + +- To make sure no browser process is left behind when the [`NewSession`] + fails, the process is closed immediately now + +- `/moz/addon/install` command accepts an `addon` parameter, in lieu of + `path`, containing an addon as a Base64 string (fixed by [Jason Juang]) + +- [webdriver crate] upgraded to version 0.31.0 + +- [mozrunner crate] upgraded to version 0.5.0 + +### Removed + +- Removed the following obsolete preferences for Firefox: + - `browser.safebrowsing.enabled` + - `browser.safebrowsing.forbiddenURIs.enabled` + - `marionette.defaultPrefs.port` + - `marionette.logging` + + +0.18.0 (2017-07-10) +------------------- + +### Changed + +- [`RectResponse`] permits returning floats for `width` and `height` + fields + +- New type [`CookieResponse`] for the [`GetNamedCookie`] command returns + a single cookie, as opposed to an array of a single cookie + +- To pick up a prepared profile from the filesystem, it is now possible + to pass `["-profile", "/path/to/profile"]` in the `args` array on + `moz:firefoxOptions` + +- geckodriver now recommends Firefox 53 and greater + +- Version information (`--version`) contains the hash from from the + commit used to build geckodriver + +- geckodriver version logged on startup + +- [webdriver crate] upgraded to version 0.27.0 + +- [mozrunner crate] upgraded to version 0.4.1 + +### Fixed + +- The [`SetTimeouts`] command maps to the Marionette `setTimeouts` + command, which makes geckodriver compatible with Firefox 56 and greater + +- Linux x86 (i686-unknown-linux-musl) builds are fixed + + +0.17.0 (2017-06-09) +------------------- + +### Added + +- Added endpoints: + - POST `/session/{session id}/window/fullscreen` to invoke the window + manager-specific `full screen` operation + - POST `/session/{session id}/moz/addon/install` to install an extension + (Gecko only) + - POST `/session/{session id}/moz/addon/uninstall` to uninstall an + extension (Gecko only) + +### Changed + +- Increasing the length of the `network.http.phishy-userpass-length` + preference will cause Firefox to not prompt when navigating to a + website with a username or password in the URL + +- Library dependencies upgraded to mozrunner 0.4 and mozprofile 0.3 + to allow overriding of preferences via capabilities if those have been + already set in the profile + +- Library dependencies upgraded to mozversion 0.1.2 to only use the + normalized path of the Firefox binary for version checks but not to + actually start the browser, which broke several components in Firefox + on Windows + +### Fixed + +- The [SetWindowRect] command now returns the [WindowRectResponse] + when it is done + +- Use ASCII versions of array symbols to properly display them in the + Windows command prompt + +- Use [`SessionNotCreated`] error instead of [`UnknownError`] if there + is no current session + + +0.16.1 (2017-04-26) +------------------- + +### Fixed + +- Read Firefox version number from stdout when failing + to look for the application .ini file (fixes [Selenium + #3884](https://github.com/SeleniumHQ/selenium/issues/3884)) + +- Session is now ended when closing the last Firefox window (fixes + [#613](https://github.com/mozilla/geckodriver/issues/613)) + + +0.16.0 (2017-04-21) +------------------- + +Note that geckodriver v0.16.0 is only compatible with Selenium 3.4 +and greater. + +### Added + +- Support for WebDriver-conforming [New Session] negotiation, with + `desiredCapabilities`/`requiredCapabilities` negotiation as fallback + +- Added two new endpoints: + - GET `/session/{session id}/window/rect` for [Get Window Rect] + - POST `/session/{session id}/window/rect` for [Set Window Rect] + +- Align errors with the [WebDriver errors]: + - Introduces new errors [`ElementClickIntercepted`], + [`ElementNotInteractable`], [`InvalidCoordinates`], [`NoSuchCookie`], + [`UnableToCaptureScreen`], and [`UnknownCommand`] + - Removes `ElementNotVisible` and `InvalidElementCoordinates` errors + +### Removed + +- Removed following list of unused endpoints: + - GET `/session/{session id}/alert_text` + - POST `/session/{session id}/alert_text` + - POST `/session/{session id}/accept_alert` + - POST `/session/{session id}/dismiss_alert` + - GET `/session/{session id}/window_handle` + - DELETE `/session/{session id}/window_handle` + - POST `/session/{session id}/execute_async` + - POST `/session/{session id}/execute` + +### Changed + +- [`SendKeysParameters`], which is used for the [Element Send Keys] and + [Send Alert Text] commands, has been updated to take a string `text` + field + +- [`CookieResponse`] and [`CloseWindowResponse`] fixed to be properly + wrapped in a `value` field, like other responses + +- Allow negative numbers for `x` and `y` fields in `pointerMove` action + +- Disable Flash and the plugin container in Firefox by + default, which should help mitigate the “Plugin Container + for Firefox has stopped working” problems [many users were + reporting](https://github.com/mozilla/geckodriver/issues/225) when + deleting a session + +- Preferences passed in a profile now take precedence over + set of default preferences defined by geckodriver (fixed by + [Marc Fisher](https://github.com/DrMarcII)) + - The exceptions are the `marionette.port` and `marionette.log.level` + preferences and their fallbacks, which are set unconditionally and + cannot be overridden + +- Remove default preference that disables unsafe CPOW checks + +- WebDriver library updated to 0.25.2 + +### Fixed + +- Fix for the “corrupt deflate stream” exception that + sometimes occurred when trying to write an empty profile by + [@kirhgoph](https://github.com/kirhgoph) + +- Recognise `sslProxy` and `sslProxyPort` entries in the proxy + configuration object (fixed by [Jason Juang]) + +- Fix “`httpProxyPort` was not an integer” error (fixed by [Jason + Juang]) + +- Fix broken unmarshaling of _Get Timeouts_ response format from Firefox + 52 and earlier (fixed by [Jason Juang]) + +- Allow preferences in `moz:firefoxOptions` to be both positive- and + negative integers (fixed by [Jason Juang]) + +- Allow IPv6 hostnames in the proxy configuration object + +- i686-unknown-linux-musl (Linux 32-bit) build fixed + +- Log messages from other Rust modules are now ignored + +- Improved log messages to the HTTPD + + +0.15.0 (2017-03-08) +------------------- + +### Added + +- Added routing and parsing for the [Get Timeouts] command + +### Changed + +- All HTTP responses are now wrapped in `{value: …}` objects per the + WebDriver specification; this may likely require you to update your + client library + +- Pointer move action’s `element` key changed to `origin`, which + lets pointer actions originate within the context of the viewport, + the pointer’s current position, or from an element + +- Now uses about:blank as the new tab document; this was previously + disabled due to [bug 1333736](https://bugzil.la/1333736) in Marionette + +- WebDriver library updated to 0.23.0 + +### Fixed + +- Aligned the data structure accepted by the [Set Timeouts] command with + the WebDriver specification + + +0.14.0 (2017-01-31) +------------------- + +### Changed + +- Firefox process is now terminated and session ended when the last + window is closed + +- WebDriver library updated to version 0.20.0 + +### Fixed + +- Stacktraces are now included when the error originates from within + the Rust stack + +- HTTPD now returns correct response headers for `Content-Type` and + `Cache-Control` thanks to [Mike Pennisi] + + +0.13.0 (2017-01-06) +------------------- + +### Changed + +- When navigating to a document with an insecure- or otherwise invalid + TLS certificate, an [insecure certificate] error will be returned + +- On macOS, deducing Firefox’ location on the system will look for + _firefox-bin_ on the system path (`PATH` environmental variable) before + looking in the applications folder + +- Window position coordinates are allowed to be negative numbers, to + cater for maximised window positioning on Windows + +- WebDriver library updated to version 0.18.0 + +### Fixed + +- Check for single-character key codes in action sequences now counts + characters instead of bytes + + +0.12.0 (2017-01-03) +------------------- + +### Added + +- Added [Take Element Screenshot] command + +- Added new [Status] command + +- Added routing for the [Get Timeouts] command, but it is not yet + implemented in Marionette, and will return an _unsupported operation_ + error until it is + +- Implemented routing for [new actions API](Actions), but it too is not + yet fully implemented in Marionette + +### Changed + +- [Synced Firefox + preferences](https://github.com/mozilla/geckodriver/commit/2bfdc3ec8151c427a6a75a6ba3ad203459540495) + with those used in Mozilla automation + +- Default log level for debug builds of Firefox, which used to be `DEBUG`, + changed to `INFO`-level + +- WebDriver library dependency upgraded to 0.17.1 + +- Using _session not created_ error when failing to start session + +- geckodriver will exit with exit code 69 to indicate that the port + is unavailable + +### Fixed + +- Improved logging when starting Firefox + +- Reverted to synchronous logging, which should address cases of + inconsistent output when failing to bind to port + +- Clarified in README that geckodriver is not supported on Windows XP + +- Added documentation of supported capabilities to [README] + +- Included capabilities example in the [README] + + +0.11.1 (2016-10-10) +------------------- + +### Fixed + +- Version number in binary now reflects the release version + + +0.11.0 (2016-10-10) +------------------- + +### Added + +- Introduced continuous integration builds for Linux- and Windows 32-bit + binaries + +- Added commands for setting- and getting the window position + +- Added new extension commands for finding an element’s anonymous + children and querying its attributes; accessible through the + `/session/{sessionId}/moz/xbl/{elementId}/anonymous_children` + to return all anonymous children and + `/session/{sessionId}/moz/xbl/{elementId}/anonymous_by_attribute` to + return an anonymous element by a name and attribute query + +- Introduced a `moz:firefoxOptions` capability to customise a Firefox + session: + + - The `binary`, `args`, and `profile` entries on this dictionary + is equivalent to the old `firefox_binary`, `firefox_args`, and + `firefox_profile` capabilities, which have now all been removed + + - The `log` capability takes a dictionary such as `{log: "trace"}` + to enable trace level verbosity in Gecko + + - The `prefs` capability lets you define Firefox preferences through + capabilities + +- Re-introduced the `--webdriver-port` argument as a hidden alias to + `--port` + +### Changed + +- `firefox_binary`, `firefox_args`, and `firefox_profile` capabilities + removed in favour of the `moz:firefoxOptions` dictionary detailed above + and in the [README] + +- Removed `--no-e10s` flag, and geckodriver will from now rely on the + Firefox default multiprocessing settings (override using preferences) + +- Disable pop-up blocker in the default profile by @juangj + +- Changed Rust compiler version to 1.12 (beta) + temporarily because of [trouble linking Musl + binaries](https://github.com/rust-lang/rust/issues/34978) + +- Replaced _env_logger_ logging facility with the _slog_ package, + causing the `RUST_LOG` environment variable to no longer have any affect + +- Updated the WebDriver Rust library to version 0.15 + +### Fixed + +- Corrected link to repository in Cargo metadata + +- Verbosity shorthand flag `-v[v]` now works again, following the + replacement of the argument parsing library in the previous release + +- When the HTTPD fails to start, errors are propagated to the user + +- Disabled the additional welcome URL + (`startup.homepage_welcome_url.additional`) so that officially branded + Firefox builds do not start with two open tabs in fresh profiles + +- Disabled homepage override URL redirection on milestone upgrades, + which means a tab with an upgrade notice is not displayed when launching + a new Firefox version + + +0.10.0 (2016-08-02) +------------------- + +### Changed + +- Use multi-process Firefox (e10s) by default, added flag `--no-e10s` + to disable it and removed `--e10s` flag + +- Disable autofilling of forms by default by [Sven Jost] + +- Replace _argparse_ with _clap_ for arguments parsing + +### Fixed + +- Attempt to deploy a single file from Travis when making a release + +- Grammar fix in [README] + + +0.9.0 (2016-06-30) +------------------ + +### Added + +- Add ability to use `firefox_binary` capability to define location of + Firefox to use + +- Automatically detect the default Firefox path if one is not given + +- Cross-compile to Windows and ARMv7 (HF) in CI + +- Add Musl C library-backed static binaries in CI + +- Add `-v`, `-vv`, and `--log LEVEL` flags to increase Gecko verbosity + +- Add Get Element Property endpoint + +- Add new `--version` flag showing copying information and a link to + the repository + +### Changed + +- Now connects to a Marionette on a random port by default + +- Update webdriver-rust library dependency + +- Migrated to use Travis to deploy new releases + +- Reduced amount of logging + +- Introduced a changelog (this) + + +0.8.0 (2016-06-07) +------------------ + +### Added + +- Allow specifying array of arguments to the Firefox binary through the + `firefox_args` capability + +- Pass parameters with [New Session] command + +### Changed + +- Change product name to _geckodriver_ + +- Make README more exhaustive + +- Quit Firefox when deleting a session + +- Update webdriver-rust library + +- Update dependencies + +### Fixed + +- Fix tests + +- FIx typo in error message for parsing errors + + +0.7.1 (2016-04-27) +------------------ + +### Added + +- Add command line flag for using e10s enabled Firefox by [Kalpesh + Krishna] + +- Allow providing custom profiles + +### Changed + +- Allow binding to an IPv6 address by [Jason Juang] + +- By default, connect to host-agnostic localhost by [Jason Juang] + +- Make `GeckoContextParameters` public + +- Update dependencies + +### Fixed + +- Squash rustc 1.6 warnings by using `std::thread::sleep(dur: Duration)` + + +0.6.2 (2016-01-20) +------------------ + +### Added + +- Add LICENSE file from [Joshua Burning] + +- Schedule builds in CI on pushes and pull requests + +### Changed + +- Enable CPOWs in Marionette + + +0.6.0 (2016-01-12) +------------------ + +### Added + +- Add Get Page Source endpoint + +### Changed + +- Handle arrays being sent from Marionette + +- Correct build steps in [README] + +- Update what properties are read from errors sent by Marionette + +- Update dependencies + + +0.5.0 (2015-12-10) +------------------ + +### Changed + +- Update argparse dependency to use Cargo + +- Update to the latest version of the Marionette wire protocol + +- Update to latest webdriver-rust library + +- Update dependencies + + +0.4.2 (2015-10-02) +------------------ + +### Changed + +- Skip compiling optional items in hyper + + +0.4.1 (2015-10-02) +------------------ + +### Changed + +- Update webdriver-rust library + +- Update dependencies + + +0.4.0 (2015-09-28) +------------------ + +### Added + +- Add command extensions for switching between content- and chrome + contexts + +- Add more documentation from [Vlad Filippov] + +### Changed + +- Update Cargo.lock with new dependencies for building + +- Update for protocol updates that flatten commands + +- Update to new protocol error handling + +- Update for Marionette protocol version 3 changes + +- Strip any leading and trailing `{}` from the `sessionId` Marionette + returns + +- Update dependencies + +### Fixed + +- Fix `GetCSSValue` message to send correct key `propertyName` + +- Fix example in documentation from @vladikoff + + +0.3.0 (2015-08-17) +------------------ + +### Added + +- Add support for finding elements in subtrees + + +0.2.0 (2015-05-20) +------------------ + +### Added + +- Extra debug messages + +- Add ability to set WebDriver port + +- Add support for getting the active element + +- Add support for `GetCookies` and `DeleteCookie`/`DeleteCookies` + +- Add preferences that switch off certain features not required for + WebDriver tests + +### Changed + +- Make failing to communicate with Firefox a fatal error that closes + the session + +- Shut down session only when losing connection + +- Better handling of missing command line flags + +- Poll for connection every 100ms rather than every 100s + +- Switch to string-based error codes + +- Switch webdriver-rust library dependency to be pulled from git + +- Update dependencies + +### Fixed + +- Handle null id for switching to frame more correctly + + +0.1.0 (2015-04-09) +------------------ + +### Added + +- Add proxy for converting WebDriver HTTP protocol to Marionette protocol + +- Add endpoints for modal dialogue support + +- Allow connecting to a running Firefox instance + +- Add explicit Cargo.lock file + +- Start Firefox when we get a [NewSession] command + +- Add flag parsing and address parsing + +- Add basic error handling + +### Changed + +- Update for Rust beta + +- Switch to new IO libraries + +- Pin webdriver-rust commit so we can upgrade rustc versions independently + +- Set preferences when starting Firefox + +- Improve some error messages + +- Re-enable environment variable based logging + +### Fixed + +- Fix Get Element Rect command to return floats instead of integers + +- Fix passing of web elements to Switch To Frame command + +- Fix serialisation of script commands + +- Fix assorted bugs found by the Selenium test suite + +- Fix conversion of Find Element/Find Elements responses from Marionette + to WebDriver + +- Fixed build by updating Cargo.lock with new dependencies for building + +- Squash compile warnings + + + +[README]: https://github.com/mozilla/geckodriver/blob/master/README.md +[Browser Toolbox]: https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox +[WebDriver conformance]: https://wpt.fyi/results/webdriver/tests?label=experimental +[`moz:firefoxOptions`]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions +[`moz:debuggerAddress`]: https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#moz-debuggeraddress +[Microsoft Visual Studio redistributable runtime]: https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads +[GeckoView]: https://wiki.mozilla.org/Mobile/GeckoView +[Fission]: https://wiki.mozilla.org/Project_Fission +[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 + +[`CloseWindowResponse`]: https://docs.rs/webdriver/newest/webdriver/response/struct.CloseWindowResponse.html +[`CookieResponse`]: https://docs.rs/webdriver/newest/webdriver/response/struct.CookieResponse.html +[`DeleteSession`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.DeleteSession +[`ElementClickIntercepted`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.ElementClickIntercepted +[`ElementNotInteractable`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.ElementNotInteractable +[`FullscreenWindow`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.FullscreenWindow +[`GetNamedCookie`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.GetNamedCookie +[`GetWindowRect`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.GetWindowRect +[`InvalidCoordinates`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.InvalidCoordinates +[`MaximizeWindow`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.MaximizeWindow +[`MinimizeWindow`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.MinimizeWindow +[`NewSession`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.NewSession +[`NoSuchCookie`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.NoSuchCookie +[`RectResponse`]: https://docs.rs/webdriver/0.27.0/webdriver/response/struct.RectResponse.html +[`SendKeysParameters`]: https://docs.rs/webdriver/newest/webdriver/command/struct.SendKeysParameters.html +[`SessionNotCreated`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.SessionNotCreated +[`SetTimeouts`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.SetTimeouts +[`SetWindowRect`]: https://docs.rs/webdriver/newest/webdriver/command/enum.WebDriverCommand.html#variant.SetWindowRect +[`StaleElementReference`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.StaleElementReference +[`UnableToCaptureScreen`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.UnableToCaptureScreen +[`UnknownCommand`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.UnknownCommand +[`UnknownError`]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html#variant.UnknownError +[`WindowRectParameters`]: https://docs.rs/webdriver/newest/webdriver/command/struct.WindowRectParameters.html + +[Add Cookie]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands/AddCookie +[invalid argument]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/InvalidArgument +[invalid session id]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/InvalidSessionID +[script timeout]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/ScriptTimeout +[timeout]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors/Timeout +[timeout object]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Timeouts +[`platformName` capability]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities#platformName + +[hyper]: https://hyper.rs/ +[mozrunner crate]: https://crates.io/crates/mozrunner +[serde]: https://serde.rs/ +[webdriver crate]: https://crates.io/crates/webdriver + +[Actions]: https://w3c.github.io/webdriver/webdriver-spec.html#actions +[Delete Session]: https://w3c.github.io/webdriver/webdriver-spec.html#delete-session +[Element Click]: https://w3c.github.io/webdriver/webdriver-spec.html#element-click +[Get Timeouts]: https://w3c.github.io/webdriver/webdriver-spec.html#get-timeouts +[Get Window Rect]: https://w3c.github.io/webdriver/webdriver-spec.html#get-window-rect +[insecure certificate]: https://w3c.github.io/webdriver/webdriver-spec.html#dfn-insecure-certificate +[Minimize Window]: https://w3c.github.io/webdriver/webdriver-spec.html#minimize-window +[New Session]: https://w3c.github.io/webdriver/webdriver-spec.html#new-session +[New Window]: https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands/New_Window +[Print]: https://w3c.github.io/webdriver/webdriver-spec.html#print +[Send Alert Text]: https://w3c.github.io/webdriver/webdriver-spec.html#send-alert-text +[Set Timeouts]: https://w3c.github.io/webdriver/webdriver-spec.html#set-timeouts +[Set Window Rect]: https://w3c.github.io/webdriver/webdriver-spec.html#set-window-rect +[Status]: https://w3c.github.io/webdriver/webdriver-spec.html#status +[Take Element Screenshot]: https://w3c.github.io/webdriver/webdriver-spec.html#take-element-screenshot +[WebDriver errors]: https://w3c.github.io/webdriver/webdriver-spec.html#handling-errors + +[Bastien Orivel]: https://github.com/Eijebong +[Jason Juang]: https://github.com/juangj +[Jeremy Lempereur]: https://github.com/o0Ignition0o +[Joshua Bruning]: https://github.com/joshbruning +[Kalpesh Krishna]: https://github.com/martiansideofthemoon +[Kriti Singh]: https://github.com/kritisingh1 +[Mike Pennisi]: https://github.com/jugglinmike +[Nupur Baghel]: https://github.com/nupurbaghel +[Peter Major]: https://github.com/aldaris +[Shivam Singhal]: https://github.com/championshuttler +[Sven Jost]: https://github/mythsunwind +[Vlad Filippov]: https://github.com/vladikoff diff --git a/testing/geckodriver/CONTRIBUTING.md b/testing/geckodriver/CONTRIBUTING.md new file mode 100644 index 0000000000..9f3e5df604 --- /dev/null +++ b/testing/geckodriver/CONTRIBUTING.md @@ -0,0 +1,2 @@ +Please see our contributor documentation at +https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers. diff --git a/testing/geckodriver/Cargo.toml b/testing/geckodriver/Cargo.toml new file mode 100644 index 0000000000..ee65fe4912 --- /dev/null +++ b/testing/geckodriver/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "geckodriver" +version = "0.29.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" +readme = "README.md" +license = "MPL-2.0" +publish = false +edition = "2018" + +[dependencies] +base64 = "0.12" +chrono = "0.4.6" +clap = { version = "^2.19", default-features = false, features = ["suggestions", "wrap_help"] } +hyper = "0.13" +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" } +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 = { path = "../webdriver" } +zip = { version = "0.4", default-features = false, features = ["deflate"] } + +[[bin]] +name = "geckodriver" diff --git a/testing/geckodriver/ISSUE_TEMPLATE.md b/testing/geckodriver/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..2b547946a5 --- /dev/null +++ b/testing/geckodriver/ISSUE_TEMPLATE.md @@ -0,0 +1,31 @@ +## System + +* Version: <!-- geckodriver version --> +* Platform: <!-- e.g. Linux/macOS/Windows + version --> +* Firefox: <!-- from the about dialogue --> +* Selenium: <!-- client + version --> + + +## Testcase + +<!-- +Please provide a minimal HTML document which permits the problem +to be reproduced. +--> + + +## Stacktrace + +<!-- +Error and stacktrace produced by client. +--> + + +## Trace-level log + +<!-- +See https://searchfox.org/mozilla-central/source/testing/geckodriver/doc/TraceLogs.md +for how to produce a trace-level log. + +For trace logs with more than 20 lines please add its contents as attachment. +--> diff --git a/testing/geckodriver/README.md b/testing/geckodriver/README.md new file mode 100644 index 0000000000..78e441423d --- /dev/null +++ b/testing/geckodriver/README.md @@ -0,0 +1,79 @@ +geckodriver +=========== + +Proxy for using W3C [WebDriver] compatible clients to interact with +Gecko-based browsers. + +This program provides the HTTP API described by the [WebDriver +protocol] to communicate with Gecko browsers, such as Firefox. It +translates calls into the [Marionette remote protocol] by acting +as a proxy between the local- and remote ends. + +[WebDriver protocol]: https://w3c.github.io/webdriver/#protocol +[Marionette remote protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/ +[WebDriver]: https://developer.mozilla.org/en-US/docs/Web/WebDriver + + +Downloads +--------- + +* [Releases](https://github.com/mozilla/geckodriver/releases/latest) +* [Change log](https://searchfox.org/mozilla-central/source/testing/geckodriver/CHANGES.md) + + +Documentation +------------- + +* [WebDriver] (work in progress) + * [Commands](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Commands) + * [Errors](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Errors) + * [Types](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Types) + +* [Cross browser testing](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing) + +* [Selenium](https://seleniumhq.github.io/docs/) (work in progress) + * [C# API](https://seleniumhq.github.io/selenium/docs/api/dotnet/) + * [JavaScript API](https://seleniumhq.github.io/selenium/docs/api/javascript/) + * [Java API](https://seleniumhq.github.io/selenium/docs/api/java/) + * [Perl API](https://metacpan.org/pod/Selenium::Remote::Driver) + * [Python API](https://seleniumhq.github.io/selenium/docs/api/py/) + * [Ruby API](https://seleniumhq.github.io/selenium/docs/api/rb/) + +* [geckodriver usage](https://firefox-source-docs.mozilla.org/testing/geckodriver/Usage.html) + * [Supported platforms](https://firefox-source-docs.mozilla.org/testing/geckodriver/Support.html) + * [Firefox capabilities](https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html) + * [Capabilities example](https://firefox-source-docs.mozilla.org/testing/geckodriver/Capabilities.html#capabilities-example) + * [Enabling trace logs](https://firefox-source-docs.mozilla.org/testing/geckodriver/TraceLogs.html) + * [Analyzing crash data from Firefox](https://firefox-source-docs.mozilla.org/testing/geckodriver/CrashReports.html) + +* [Contributing](https://firefox-source-docs.mozilla.org/testing/geckodriver/#for-developers) + * [Building](https://firefox-source-docs.mozilla.org/testing/geckodriver/Building.html) + * [Testing](https://firefox-source-docs.mozilla.org/testing/geckodriver/Testing.html) + * [Releasing](https://firefox-source-docs.mozilla.org/testing/geckodriver/Releasing.html) + * [Self-serving an ARM build](https://firefox-source-docs.mozilla.org/testing/geckodriver/ARM.html) + + +Source code +----------- + +geckodriver is made available under the [Mozilla Public License]. + +Its source code can be found in [mozilla-central] under testing/geckodriver. +This GitHub repository is only used for issue tracking and making releases. + +[source code]: https://hg.mozilla.org/mozilla-unified/file/tip/testing/geckodriver +[Mozilla Public License]: https://www.mozilla.org/en-US/MPL/2.0/ +[mozilla-central]: https://hg.mozilla.org/mozilla-central/file/tip/testing/geckodriver + + +Contact +------- + +The mailing list for geckodriver discussion is +tools-marionette@lists.mozilla.org ([subscribe], [archive]). + +There is also a Matrix channel to talk about using and developing +geckodriver on `#interop:mozilla.org <https://chat.mozilla.org/#/room/#interop:mozilla.org>`__ + +[subscribe]: https://lists.mozilla.org/listinfo/tools-marionette +[archive]: https://lists.mozilla.org/pipermail/tools-marionette/ diff --git a/testing/geckodriver/build.rs b/testing/geckodriver/build.rs new file mode 100644 index 0000000000..2fca20dd23 --- /dev/null +++ b/testing/geckodriver/build.rs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Writes build information to ${OUT_DIR}/build-info.rs which is included in +// the program during compilation: +// +// ```no_run +// const COMMIT_HASH: Option<&'static str> = Some("c31a366"); +// const COMMIT_DATE: Option<&'static str> = Some("1988-05-10"); +// ``` +// +// The values are `None` if running hg failed, e.g. if it is not installed or +// if we are not in an hg repo. + +use std::env; +use std::ffi::OsStr; +use std::fs::File; +use std::io; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn main() -> io::Result<()> { + let cur_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let build_info = get_build_info(&cur_dir); + + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()); + let mut fh = File::create(out_dir.join("build-info.rs"))?; + writeln!( + fh, + "const COMMIT_HASH: Option<&'static str> = {:?};", + build_info.hash() + )?; + writeln!( + fh, + "const COMMIT_DATE: Option<&'static str> = {:?};", + build_info.date() + )?; + + Ok(()) +} + +fn get_build_info(dir: &Path) -> Box<dyn BuildInfo> { + if Path::exists(&dir.join(".hg")) { + Box::new(Hg {}) + } else if Path::exists(&dir.join(".git")) { + Box::new(Git {}) + } else if let Some(parent) = dir.parent() { + get_build_info(parent) + } else { + eprintln!("unable to detect vcs"); + Box::new(Noop {}) + } +} + +trait BuildInfo { + fn hash(&self) -> Option<String>; + fn date(&self) -> Option<String>; +} + +struct Hg; + +impl Hg { + fn exec<I, S>(&self, args: I) -> Option<String> + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + Command::new("hg") + .env("HGPLAIN", "1") + .args(args) + .output() + .ok() + .and_then(|r| String::from_utf8(r.stdout).ok()) + .map(|s| s.trim_end().into()) + } +} + +impl BuildInfo for Hg { + fn hash(&self) -> Option<String> { + self.exec(&["log", "-r.", "-T{node|short}"]) + } + + fn date(&self) -> Option<String> { + self.exec(&["log", "-r.", "-T{date|isodate}"]) + } +} + +struct Git; + +impl Git { + fn exec<I, S>(&self, args: I) -> Option<String> + where + I: IntoIterator<Item = S>, + S: AsRef<OsStr>, + { + Command::new("git") + .env("GIT_CONFIG_NOSYSTEM", "1") + .args(args) + .output() + .ok() + .and_then(|r| String::from_utf8(r.stdout).ok()) + .map(|s| s.trim_end().into()) + } + + fn to_hg_sha(&self, git_sha: String) -> Option<String> { + self.exec(&["cinnabar", "git2hg", &git_sha]) + } +} + +impl BuildInfo for Git { + fn hash(&self) -> Option<String> { + self.exec(&["rev-parse", "HEAD"]) + .and_then(|sha| self.to_hg_sha(sha)) + .map(|mut s| { + s.truncate(12); + s + }) + } + + fn date(&self) -> Option<String> { + self.exec(&["log", "-1", "--date=short", "--pretty=format:%cd"]) + } +} + +struct Noop; + +impl BuildInfo for Noop { + fn hash(&self) -> Option<String> { + None + } + fn date(&self) -> Option<String> { + None + } +} diff --git a/testing/geckodriver/doc/ARM.md b/testing/geckodriver/doc/ARM.md new file mode 100644 index 0000000000..5627cbd85e --- /dev/null +++ b/testing/geckodriver/doc/ARM.md @@ -0,0 +1,39 @@ +Self-serving an ARM build +========================= + +Mozilla [announced the intent] to deprecate ARMv7 HF builds of +geckodriver in September 2018. This does not mean you can no longer +use geckodriver on ARM systems, and this document explains how you +can self-service a build for ARMv7 HF. + +Assuming you have already checked out [central], the steps to +cross-compile ARMv7 from a Linux host system is as follows: + + 1. If you don’t have Rust installed: + + # curl https://sh.rustup.rs -sSf | sh + + 2. Install cross-compiler toolchain: + + # apt install gcc-arm-linux-gnueabihf libc6-armhf-cross libc6-dev-armhf-cross + + 3. Create a new shell, or to reuse the existing shell: + + source $HOME/.cargo/env + + 4. Install rustc target toolchain: + + % rustup target install armv7-unknown-linux-gnueabihf + + 5. Put this in testing/geckodriver/.cargo/config: + + [target.armv7-unknown-linux-gnueabihf] + linker = "arm-linux-gnueabihf-gcc" + + 6. Build geckodriver from testing/geckodriver: + + % cd testing/geckodriver + % cargo build --release --target armv7-unknown-linux-gnueabihf + +[announced the intent]: https://lists.mozilla.org/pipermail/tools-marionette/2018-September/000035.html +[central]: https://hg.mozilla.org/mozilla-central/ diff --git a/testing/geckodriver/doc/Bugs.md b/testing/geckodriver/doc/Bugs.md new file mode 100644 index 0000000000..0a4272d0ee --- /dev/null +++ b/testing/geckodriver/doc/Bugs.md @@ -0,0 +1,46 @@ +Reporting bugs +============== + +When opening a new issue or commenting on existing issues, please +make sure discussions are related to concrete technical issues +with geckodriver or Marionette. Questions or discussions are more +appropriate for the [mailing list]. + +For issue reports to be actionable, it must be clear exactly +what the observed and expected behaviours are, and how to set up +the state required to observe the erroneous behaviour. The most +useful thing to provide is a minimal HTML document which permits +the problem to be reproduced along with a [trace-level log] from +geckodriver showing the exact wire protocol calls made. + +Because of the wide variety and different charateristics of clients +used with geckodriver, their stacktraces, logs, and code examples are +typically not very useful as they distract from the actual underlying +cause. **For this reason, we cannot overstate the importance of +always providing the [trace-level log] from geckodriver.** Bugs +relating to a specific client should be filed with that project. + +We welcome you to file issues in the [GitHub issue tracker] once you are +confident it has not already been reported. The [ISSUE_TEMPLATE.md] +contains a helpful checklist for things we will want to know about +the affected system, reproduction steps, and logs. + +geckodriver development follows a rolling release model as +we don’t release patches for older versions. It is therefore +useful to use the tip-of-tree geckodriver binary, or failing this, +the latest release when verifying the problem. geckodriver is only +compatible with the current release channel versions of Firefox, and +it consequently does not help to report bugs that affect outdated +and unsupported Firefoxen. Please always try to verify the issue +in the latest Firefox Nightly before you file your bug. + +Once we are satisfied the issue raised is of sufficiently actionable +character, we will continue with triaging it and file a bug where it +is appropriate. Bugs specific to geckodriver will be filed in the +[`Testing :: geckodriver`] component in Bugzilla. + +[mailing list]: ./#communication +[trace-level log]: TraceLogs.html +[GitHub issue tracker]: https://github.com/mozilla/geckodriver/issues +[ISSUE_TEMPLATE.md]: https://raw.githubusercontent.com/mozilla/geckodriver/master/ISSUE_TEMPLATE.md +[`Testing :: geckodriver`]: https://bugzilla.mozilla.org/buglist.cgi?component=geckodriver diff --git a/testing/geckodriver/doc/Building.md b/testing/geckodriver/doc/Building.md new file mode 100644 index 0000000000..50e2f969fb --- /dev/null +++ b/testing/geckodriver/doc/Building.md @@ -0,0 +1,41 @@ +Building geckodriver +==================== + +geckodriver is written in [Rust], a systems programming language +from Mozilla. Crucially, it relies on the [webdriver crate] to +provide the HTTPD and do most of the heavy lifting of marshalling +the WebDriver protocol. geckodriver translates WebDriver [commands], +[responses], and [errors] to the [Marionette protocol], and acts +as a proxy between [WebDriver] and [Marionette]. + +To build geckodriver: + + % ./mach build testing/geckodriver + +If you use artifact builds you may build geckodriver using cargo, +since mach in this case does not have a compile environment: + + % cd testing/geckodriver + % cargo build + … + Compiling geckodriver v0.21.0 (file:///code/gecko/testing/geckodriver) + … + Finished dev [optimized + debuginfo] target(s) in 7.83s + +Because all Rust code in central shares the same cargo workspace, +the binary will be put in the `$(topsrcdir)/target` directory. + +You can run your freshly built geckodriver this way: + + % ./mach geckodriver -- --other --flags + +See [Testing](Testing.html) for how to run tests. + +[Rust]: https://www.rust-lang.org/ +[webdriver crate]: https://crates.io/crates/webdriver +[commands]: https://docs.rs/webdriver/newest/webdriver/command/ +[responses]: https://docs.rs/webdriver/newest/webdriver/response/ +[errors]: https://docs.rs/webdriver/newest/webdriver/error/enum.ErrorStatus.html +[Marionette protocol]: /testing/marionette/doc/marionette/Protocol.html +[WebDriver]: https://w3c.github.io/webdriver/ +[Marionette]: /testing/marionette/doc/marionette diff --git a/testing/geckodriver/doc/Capabilities.md b/testing/geckodriver/doc/Capabilities.md new file mode 100644 index 0000000000..feec2cbeb0 --- /dev/null +++ b/testing/geckodriver/doc/Capabilities.md @@ -0,0 +1,101 @@ +Firefox capabilities +==================== + +geckodriver has a few capabilities that are specific to Firefox. +Most of these [are documented on MDN](https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities/firefoxOptions). + +We additionally have some capabilities that largely are implementation +concerns that normal users should not care about: + + +`moz:debuggerAddress` +-------------------- + +A boolean value to indicate if Firefox has to be started with the +[Remote Protocol] enabled, which is a low-level debugging interface that +implements a subset of the [Chrome DevTools Protocol] (CDP). + +When enabled the returned `moz:debuggerAddress` capability of the `New Session` +command is the `host:port` combination of a server that supports the following +HTTP endpoints: + +### GET /json/version + +The browser version metadata: + + { + "Browser": "Firefox/84.0a1", + "Protocol-Version": "1.0", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:84.0) Gecko/20100101 Firefox/84.0", + "V8-Version": "1.0", + "WebKit-Version": "1.0", + "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/fe507083-2960-a442-bbd7-7dfe1f111c05" + } + +### GET /json/list + +A list of all available websocket targets: + + [ { + "description": "", + "devtoolsFrontendUrl": null, + "faviconUrl": "", + "id": "ecbf9028-676a-1b40-8596-a5edc0e2875b", + "type": "page", + "url": "https://www.mozilla.org/en-US/", + "browsingContextId": 29, + "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/ecbf9028-676a-1b40-8596-a5edc0e2875b" + } ] + +The contained `webSocketDebuggerUrl` entries can be used to connect to the +websocket and interact with the browser by using the CDP protocol. + +[Remote Protocol]: /testing/remote/doc/ +[Chrome DevTools Protocol]: https://chromedevtools.github.io/devtools-protocol/ + + +`moz:useNonSpecCompliantPointerOrigin` +-------------------------------------- + +A boolean value to indicate how the pointer origin for an action +command will be calculated. + +With Firefox 59 the calculation will be based on the requirements +by the [WebDriver] specification. This means that the pointer origin +is no longer computed based on the top and left position of the +referenced element, but on the in-view center point. + +To temporarily disable the WebDriver conformant behavior use `false` +as value for this capability. + +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` +-------------------- + +A boolean value to indicate which kind of interactability checks +to run when performing a click or sending keys to an elements. For +Firefoxen prior to version 58.0 some legacy code as imported from +an older version of FirefoxDriver was in use. + +With Firefox 58 the interactability checks as required by the +[WebDriver] specification are enabled by default. This means +geckodriver will additionally check if an element is obscured by +another when clicking, and if an element is focusable for sending +keys. + +Because of this change in behaviour, we are aware that some extra +errors could be returned. In most cases the test in question might +have to be updated so it's conform with the new checks. But if the +problem is located in geckodriver, then please raise an issue in +the [issue tracker]. + +To temporarily disable the WebDriver conformant checks use `false` +as value for this capability. + +Please note that this capability exists only temporarily, and that +it will be removed once the interactability checks have been +stabilized. diff --git a/testing/geckodriver/doc/CrashReports.md b/testing/geckodriver/doc/CrashReports.md new file mode 100644 index 0000000000..98820ca1fb --- /dev/null +++ b/testing/geckodriver/doc/CrashReports.md @@ -0,0 +1,71 @@ +Analyzing crash data of Firefox +=============================== + +It's not uncommon that under some special platform configurations and while +running automated tests via Selenium and geckodriver Firefox could crash. In +those cases it is very helpful to retrieve the generated crash data aka +minidump files, and report these to us. + +Retrieve the crash data +----------------------- + +Because geckodriver creates a temporary user profile for Firefox, it also +automatically removes all its folders once the tests have been finished. That +also means that if Firefox crashed the created minidump files are lost. To +prevent that a custom profile has to be used instead. The following code +shows an example by using the Python Selenium bindings on Mac OS: + + import tempfile + + from selenium import webdriver + from selenium.webdriver.firefox.options import Options + + # Custom profile folder to keep the minidump files + profile = tempfile.mkdtemp(".selenium") + print("*** Using profile: {}".format(profile)) + + # Use the above folder as custom profile + opts = Options() + opts.add_argument("-profile") + opts.add_argument(profile) + opts.binary = "/Applications/Firefox.app/Contents/MacOS/firefox" + + driver = webdriver.Firefox(options=opts, + # hard-code the Marionette port so geckodriver can connect + service_args=["--marionette-port", "2828"]) + + # Your test code which crashes Firefox + +Executing the test with Selenium now, which triggers the crash of Firefox +will leave all the files from the user profile around in the above path. + +To retrieve the minidump files navigate to that folder and look for a sub +folder with the name `minidumps`. It should contain at least one series of +files. One file with the `.dmp` extension and another one with `.extra`. +Both of those files are needed. If more crash files are present grab them all. + +Attach the files as best archived as zip file to the created [geckodriver issue] +on Github. + +[geckodriver issue]: https://github.com/mozilla/geckodriver/issues/new + + +Getting details of the crash +---------------------------- + +More advanced users can upload the generated minidump files themselves and +receive details information about the crash. Therefore find the [crash reporter] +folder and copy all the generated minidump files into the `pending` sub directory. +Make sure that both the `.dmp` and `.extra` files are present. + +Once done you can also [view the crash reports]. + +If you submitted a crash please do not forget to also add the link of the +crash report to the geckodriver issue. + +[crash reporter]: https://support.mozilla.org/kb/mozillacrashreporter#w_viewing-reports-outside-of-firefox +[view crash reports]: https://support.mozilla.orgkb/mozillacrashreporter#w_viewing-crash-reports + + + + diff --git a/testing/geckodriver/doc/Flags.md b/testing/geckodriver/doc/Flags.md new file mode 100644 index 0000000000..1fc6285ac1 --- /dev/null +++ b/testing/geckodriver/doc/Flags.md @@ -0,0 +1,145 @@ +Flags +===== + +#### <code>--android-storage <var>ANDROID_STORAGE</var></code> + +Selects the test data location on the Android device, eg. the Firefox profile. +By default `auto` is used. + +<style type="text/css"> + table { width: 100%; margin-bottom: 2em; } + table, th, td { border: solid gray 1px; } + td, th { padding: 10px; text-align: left; vertical-align: middle; } + td:nth-child(1), th:nth-child(1) { width: 10em; text-align: center; } +</style> + +<table> + <thead> + <tr> + <th>Value + <th>Description + </tr> + </thead> + + <tr> + <td>auto + <td>Best suitable location based on whether the device is rooted.<br/> + If the device is rooted <code>internal</code> is used, otherwise <code>app</code>. + <tr> + <td>app + <td><p>Location: <code>/data/data/%androidPackage%/test_root</code></p> + Based on the <code>androidPackage</code> capability that is passed as part of + <code>moz:firefoxOptions</code> 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. + <tr> + <td>internal + <td><p>Location: <code>/data/local/tmp/test_root</code></p> + 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 <code>su</code>. + <tr> + <td>sdcard + <td><p>Location: <code>/mnt/sdcard/test_root</code></p> + This location is not supported on Android 11+ due to the + <a href="https://developer.android.com/about/versions/11/privacy/storage"> + changes related to scoped storage</a>. +</table> + + +#### <code>-b <var>BINARY</var></code> / <code>--binary <var>BINARY</var></code> + +Path to the Firefox binary to use. By default geckodriver tries to +find and use the system installation of Firefox, but that behaviour +can be changed by using this option. Note that the `binary` +capability of the `moz:firefoxOptions` object that is passed when +[creating a new session] will override this option. + +On Linux systems it will use the first _firefox_ binary found +by searching the `PATH` environmental variable, which is roughly +equivalent to calling [whereis(1)] and extracting the second column: + + % whereis firefox + firefox: /usr/bin/firefox /usr/local/firefox + +On macOS, the binary is found by looking for the first _firefox-bin_ +binary in the same fashion as on Linux systems. This means it is +possible to also use `PATH` to control where geckodriver should +find Firefox on macOS. It will then look for _/Applications/Firefox.app_. + +On Windows systems, geckodriver looks for the system Firefox by +scanning the Windows registry. + +[creating a new session]: https://w3c.github.io/webdriver/#new-session +[whereis(1)]: http://www.manpagez.com/man/1/whereis/ + + +#### <code>--connect-existing</code> + +Connect geckodriver to an existing Firefox instance. This means +geckodriver will abstain from the default of starting a new Firefox +session. + +The existing Firefox instance must have [Marionette] enabled. +To enable the remote protocol in Firefox, you can pass the +`-marionette` flag. Unless the `marionette.port` preference +has been user-set, Marionette will listen on port 2828. So when +using `--connect-existing` it is likely you will also have to use +`--marionette-port` to set the correct port. + +[`--marionette-port`]: #marionette-port + + +#### <code>--host <var>HOST</var></code> + +Host to use for the WebDriver server. Defaults to 127.0.0.1. + + +#### <code>--log <var>LEVEL</var></code> + +Set the Gecko and geckodriver log level. Possible values are `fatal`, +`error`, `warn`, `info`, `config`, `debug`, and `trace`. + + +#### <code>--marionette-host <var>HOST</var></code> + +Selects the host for geckodriver’s connection to the [Marionette] +remote protocol. Defaults to 127.0.0.1. + + +#### <code>--marionette-port <var>PORT</var></code> + +Selects the port for geckodriver’s connection to the [Marionette] +remote protocol. + +In the default mode where geckodriver starts and manages the Firefox +process, it will pick a free port assigned by the system and set the +`marionette.port` preference in the profile. + +When `--connect-existing` is used and the Firefox process is not +under geckodriver’s control, it will simply connect to <var>PORT</var>. + +[`--connect-existing`]: #connect-existing + + +#### <code>-p <var>PORT</var></code> / <code>--port <var>PORT</var></code> + +Port to use for the WebDriver server. Defaults to 4444. + +A helpful trick is that it is possible to bind to 0 to get the +system to atomically assign a free port. + + +#### <code>--jsdebugger</code> + +Attach [browser toolbox] debugger when Firefox starts. This is +useful for debugging [Marionette] internals. + +[browser toolbox]: https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox + + +#### <code>-v<var>[v]</var></code> + +Increases the logging verbosity by to debug level when passing +a single `-v`, or to trace level if `-vv` is passed. This is +analogous to passing `--log debug` and `--log trace`, respectively. diff --git a/testing/geckodriver/doc/Notarization.md b/testing/geckodriver/doc/Notarization.md new file mode 100644 index 0000000000..559800dddc --- /dev/null +++ b/testing/geckodriver/doc/Notarization.md @@ -0,0 +1,41 @@ +macOS notarization +================== + +With the introduction of macOS 10.15 “Catalina” Apple introduced +[new notarization requirements] that all software must be signed +and notarized centrally. + +Whilst geckodriver is technically both signed and notarized, the +way we package geckodriver on macOS means the notarization is lost. +Mozilla considers this a known bug with the [geckodriver 0.26.0 +release] and are taking steps to resolve this. You can track the +progress in [bug 1588081]. + +There are some mitigating circumstances: + + * Verification problems only occur when other notarized programs, + such as a web browser, downloads the software from the internet. + + * Arbitrary software downloaded through other means, such as + curl(1) is _not_ affected by this change. + +In other words, if your method for fetching geckodriver on macOS +is through the GitHub web UI using a web browser, the program will +not be able to run unless you manually disable the quarantine check +(explained below). If downloading geckodriver via other means +than a macOS notarized program, you should not be affected. + +To bypass the notarization requirement on macOS if you have downloaded +the geckodriver .tar.gz via a web browser, you can run the following +command in a terminal: + + % xattr -r -d com.apple.quarantine geckodriver + +A problem with notarization will manifest itself through a security +dialogue appearing, explaining that the source of the program is +not trusted. + + +[new notarization requirements]: https://developer.apple.com/news/?id=04102019a +[geckodriver 0.26.0 release]: https://github.com/mozilla/geckodriver/releases/tag/v0.26.0 +[bug 1588081]: https://bugzilla.mozilla.org/show_bug.cgi?id=1588081 diff --git a/testing/geckodriver/doc/Profiles.md b/testing/geckodriver/doc/Profiles.md new file mode 100644 index 0000000000..c7ff656ac8 --- /dev/null +++ b/testing/geckodriver/doc/Profiles.md @@ -0,0 +1,108 @@ +Profiles +======== + +geckodriver uses [profiles] to instrument Firefox’ behaviour. The +user will usually rely on geckodriver to generate a temporary, +throwaway profile. These profiles are deleted when the WebDriver +session expires. + +In cases where the user needs to use custom, prepared profiles, +geckodriver will make modifications to the profile that ensures +correct behaviour. See [_Automation preferences_] below on the +precedence of user-defined preferences in this case. + +Custom profiles can be provided two different ways: + + 1. by appending `--profile /some/location` to the [`args` capability], + which will instruct geckodriver to use the profile _in-place_; + + 2. or by setting the [`profile` capability] to a Base64-encoded + ZIP of the profile directory. + +Note that geckodriver has a [known bug concerning `--profile`] that +prevents the randomised Marionette port from being passed to +geckodriver. To circumvent this issue, make sure you specify the +port manually using `--marionette-port <port>`. + +The second way is compatible with shipping Firefox profiles across +a network, when for example the geckodriver instance is running on +a remote system. This is the case when using Selenium’s `RemoteWebDriver` +concept, where the WebDriver client and the server are running on +two distinct systems. + +[profiles]: https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data +[_Automation preferences_]: #automation-preferences +[`args` capability]: ./Capabilities.html#capability-args +[`profile` capability]: ./Capabilities.html#capability-profile +[known bug concerning `--profile`]: https://github.com/mozilla/geckodriver/issues/1058 + + +Default locations for temporary profiles +---------------------------------------- + +When a custom user profile is not provided with the `-profile` +command-line argument geckodriver generates a temporary, throwaway +profile. This is written to the default system temporary folder +and subsequently removed when the WebDriver session expires. + +The default location for temporary profiles depends on the system. +On Unix systems it uses /tmp, and on Windows it uses the Windows +directory. + +The default location can be overridden. On Unix you set the `TMPDIR` +environment variable. On Windows, the following environment variables +are respected, in order: + + 1. `TMP` + 2. `TEMP` + 3. `USERPROFILE` + +It is not necessary to change the temporary directory system-wide. +All you have to do is make sure it gets set for the environment of +the geckodriver process: + + % TMPDIR=/some/location ./geckodriver + + +Automation preferences +---------------------- + +As indicated in the introduction, geckodriver configures Firefox +so it is well-behaved in automation environments. It uses a +combination of preferences written to the profile prior to launching +Firefox (1), and a set of recommended preferences set on startup (2). + +These can be perused here: + + 1. [testing/geckodriver/src/prefs.rs](https://searchfox.org/mozilla-central/source/testing/geckodriver/src/prefs.rs) + 2. [testing/marionette/components/marionette/marionette.js](https://searchfox.org/mozilla-central/source/testing/marionette/components/marionette.js) + +As mentioned, these are _recommended_ preferences, and any user-defined +preferences in the [user.js file] or as part of the [`prefs` capability] +take precedence. This means for example that the user can tweak +`browser.startup.page` to override the recommended preference for +starting the browser with a blank page. + +The recommended preferences set at runtime (see 2 above) may also +be disabled entirely by setting `marionette.prefs.recommended`. +This may however cause geckodriver to not behave correctly according +to the WebDriver standard, so it should be used with caution. + +Users should take note that the `marionette.port` preference is +special, and will always be overridden when using geckodriver unless +the `--marionette-port <port>` flag is used specifically to instruct +the Marionette server in Firefox which port to use. + +[user.js file]: http://kb.mozillazine.org/User.js_file +[`prefs` capability]: ./Capabilities.html#capability-prefs + + +Temporary profiles not being removed +------------------------------------ + +It is a known bug that geckodriver in some instances fails to remove +the temporary profile, particularly when the session is not explicitly +deleted or the process gets interrupted. See [geckodriver issue +299] for more information. + +[geckodriver issue 299]: https://github.com/mozilla/geckodriver/issues/299 diff --git a/testing/geckodriver/doc/Releasing.md b/testing/geckodriver/doc/Releasing.md new file mode 100644 index 0000000000..39185809d9 --- /dev/null +++ b/testing/geckodriver/doc/Releasing.md @@ -0,0 +1,255 @@ +Releasing geckodriver +===================== + +Releasing geckodriver is not as easy as it once used to be when the +project’s canonical home was on GitHub. Today geckodriver is hosted +in [mozilla-central], and whilst we do want to make future releases +from [Mozilla’s CI infrastructure], we are currently in between two +worlds: development happens in m-c, but releases continue to be made +from GitHub. + +In any case, the steps to release geckodriver are as follows: + +[mozilla-central]: https://hg.mozilla.org/mozilla-central/ +[Mozilla’s CI infrastructure]: https://treeherder.mozilla.org/ + + +Update in-tree dependency crates +-------------------------------- + +geckodriver depends on a number of Rust crates that also live in +central by using relative paths: + + [dependencies] + … + mozdevice = { path = "../mozbase/rust/mozdevice" } + mozprofile = { path = "../mozbase/rust/mozprofile" } + mozrunner = { path = "../mozbase/rust/mozrunner" } + mozversion = { path = "../mozbase/rust/mozversion" } + … + webdriver = { path = "../webdriver" } + +Because we need to export the geckodriver source code to the old +GitHub repository when we release, we first need to publish these +crates if they have had any changes in the interim since the last +release. If they have received no changes, you can skip them: + + - `testing/mozbase/rust/mozdevice` + - `testing/mozbase/rust/mozprofile` + - `testing/mozbase/rust/mozrunner` + - `testing/mozbase/rust/mozversion` + - `testing/webdriver` + +For each crate: + + 1. Bump the version number in Cargo.toml + 2. Update the crate: `cargo update -p <crate name>` + 3. Commit the changes for the modified `Cargo.toml`, and `Cargo.lock` + (can be found in the repositories root folder) + + +Update the change log +--------------------- + +Notable changes to geckodriver are mentioned in [CHANGES.md]. Many +users rely on this, so it’s important that you make it **relevant +to end-users**. For example, we only mention changes that are visible +to users. The change log is not a complete anthology of commits, +as these often will not convey the essence of a change to end-users. +If a feature was added but removed before release, there is no reason +to list it as a change. + +It is good practice to also include relevant information from the +[webdriver] and [rust-mozrunner] crates, since these are the two most +important dependencies of geckodriver and a lot of its functionality +is implemented there. + +We follow the writing style of the existing change log, with +one section per version (with a release date), with subsections +‘Added’, ‘Changed’, and ‘Removed’. If the targeted +Firefox or Selenium versions have changed, it is good to make a +mention of this. Lines are optimally formatted at roughly 72 columns +to make the file readable in a text editor as well as rendered HTML. +fmt(1) does a splendid job at text formatting. + +[CHANGES.md]: https://searchfox.org/mozilla-central/source/testing/geckodriver/CHANGES.md +[rust-mozrunner]: https://searchfox.org/mozilla-central/source/testing/mozbase/rust/mozrunner + + +Update libraries +---------------- + +Make relevant changes to [Cargo.toml] to upgrade dependencies, then run + + % ./mach vendor rust + % ./mach build testing/geckodriver + +to pull down and vendor the upgraded libraries. + +The updates to dependencies should always be made as a separate +commit to not confuse reviewers, because vendoring involves checking +in a lot of extra code already reviewed downstream. + +[Cargo.toml]: https://searchfox.org/mozilla-central/source/testing/geckodriver/Cargo.toml +[Cargo.lock]: https://searchfox.org/mozilla-central/source/Cargo.lock + + +Bump the version number and update the support page +--------------------------------------------------- + +Bump the version number in [Cargo.toml] to the next version. +geckodriver follows [semantic versioning] so it’s a good idea to +familiarise yourself with that before deciding on the version number. + +After you’ve changed the version number, run + + % ./mach build testing/geckodriver + +again to update [Cargo.lock]. + +Now update the [support page] by adding a new row to the versions table, +including the required versions of Selenium, and Firefox. + +Finally commit all those changes. + +[semantic versioning]: http://semver.org/ +[support page]: https://searchfox.org/mozilla-central/source/testing/geckodriver/doc/Support.md + + +Add the changeset id +-------------------- + +To easily allow a release build of geckodriver after cloning the +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 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 +------------------------------------- + +Make sure to wait until the complete patch series from above has been +merged to mozilla-central. Then continue with the following steps. + +Before releasing geckodriver all dependency crates as +[updated earlier](#update-in-tree-dependency-crates) have to be +released first. + +Therefore change into each of the directories for crates with an update +and run the following command to publish the crate: + + % cargo publish + +Note that if a crate has an in-tree dependency make sure to first +change the dependency information. + + +Export to GitHub +---------------- + +The canonical GitHub repository is + + https://github.com/mozilla/geckodriver.git + +so make sure you have a local clone of that. It has three branches: +_master_ which only contains the [README.md]; _old_ which was the +state of the project when it was exported to mozilla-central; and +_release_, from where releases are made. + +Before we copy the code over to the GitHub repository we need to +check out the [release commit that bumped the version number](#add-the-changeset-id) +on mozilla-central: + + % hg update $RELEASE_REVISION + +Or: + + % git checkout $RELEASE_REVISION + +We will now export the contents of [testing/geckodriver] to the +_release_ branch: + + % cd $SRC/geckodriver + % git checkout release + % git pull + % git rm -rf . + % git clean -fxd + % cp -rt $SRC/gecko/testing/geckodriver . + +[README.md]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md +[testing/geckodriver]: https://searchfox.org/mozilla-central/source/testing/geckodriver + + +Manually change in-tree path dependencies +------------------------------------------ + +After the source code has been imported we need to change the dependency +information for the `mozrunner`, `mozprofile`, `mozversion`, and +`webdriver` crates. As explained previously geckodriver depends +on a relative path in the mozilla-central repository to build +with the latest unreleased source code. + +This relative paths do not exist in the GitHub repository and the +build will fail unless we change it to the latest crate versions +from crates.io. That version will either be the crate you published +earlier, or the latest version available if no changes have been +made to it since the last geckodriver release. + + +Commit local changes +-------------------- + +Now commit all the changes you have made locally to the _release_ branch. +It is recommended to setup a [GPG key] for signing the commit, so +that the release commit is marked as `verified`. + + % git add . + % git commit -S -am "import of vX.Y.Z" (signed) + +or if you cannot use signing use: + + % git add . + % git commit -am "import of vX.Y.Z" (unsigned) + +Then push the changes: + + % git push + +As indicated above, the changes you make to this branch will not +be upstreamed back into mozilla-central. It is merely used as a +place for external consumers to build their own version of geckodriver. + +[GPG key]: https://help.github.com/articles/signing-commits/ + + +Make the release +---------------- + +geckodriver needs to be manually released on github.com. Therefore start to +[draft a new release], and make the following changes: + +1. Specify the "Tag version", and select "Release" as target. + +2. Leave the release title empty + +3. Paste the raw Markdown source from [CHANGES.md] into the description field. + This will highlight for end-users what changes were made in that particular + package when they visit the GitHub downloads section. Make sure to check that + all references can be resolved, and if not make sure to add those too. + +4. Find the signed geckodriver archives in the [taskcluster index] by + replacing %changeset% with the full release changeset id. Rename the + individual files so the basename looks like 'geckodriver-v%version%-%platform%'. + Upload them all, including the checksum files for both the Linux platforms. + +[draft a new release]: https://github.com/mozilla/geckodriver/releases/new +[taskcluster index]: https://firefox-ci-tc.services.mozilla.com/tasks/index/gecko.v2.mozilla-central.revision.%changeset%.geckodriver + + +Congratulations! You’ve released geckodriver! + +[releases page]: https://github.com/mozilla/geckodriver/releases diff --git a/testing/geckodriver/doc/Support.md b/testing/geckodriver/doc/Support.md new file mode 100644 index 0000000000..94afa54701 --- /dev/null +++ b/testing/geckodriver/doc/Support.md @@ -0,0 +1,151 @@ +Supported platforms +=================== + +The following table shows a mapping between [geckodriver releases], +and required versions of Selenium and Firefox: + +<style type="text/css"> + table { width: 100%; margin-bottom: 2em; } + table, th, td { border: solid gray 1px; } + td, th { padding: 5px 10px; text-align: center; } +</style> + +<table> + <thead> + <tr> + <th rowspan="2">geckodriver + <th rowspan="2">Selenium + <th colspan="2">Firefox + </tr> + <tr> + <th>min + <th>max + </tr> + </thead> + + <tr> + <td>0.29.0 + <td>≥ 3.11 (3.14 Python) + <td>60 + <td>n/a + <tr> + <td>0.28.0 + <td>≥ 3.11 (3.14 Python) + <td>60 + <td>n/a + <tr> + <td>0.27.0 + <td>≥ 3.11 (3.14 Python) + <td>60 + <td>n/a + <tr> + <td>0.26.0 + <td>≥ 3.11 (3.14 Python) + <td>60 + <td>n/a + <tr> + <td>0.25.0 + <td>≥ 3.11 (3.14 Python) + <td>57 + <td>n/a + <tr> + <td>0.24.0 + <td>≥ 3.11 (3.14 Python) + <td>57 + <td>79 + <tr> + <td>0.23.0 + <td>≥ 3.11 (3.14 Python) + <td>57 + <td>79 + <tr> + <td>0.22.0 + <td>≥ 3.11 (3.14 Python) + <td>57 + <td>79 + <tr> + <td>0.21.0 + <td>≥ 3.11 (3.14 Python) + <td>57 + <td>79 + <tr> + <td>0.20.1 + <td>≥ 3.5 + <td>55 + <td>62 + <tr> + <td>0.20.0 + <td>≥ 3.5 + <td>55 + <td>62 + <tr> + <td>0.19.1 + <td>≥ 3.5 + <td>55 + <td>62 + <tr> + <td>0.19.0 + <td>≥ 3.5 + <td>55 + <td>62 + <tr> + <td>0.18.0 + <td>≥ 3.4 + <td>53 + <td>62 + <tr> + <td>0.17.0 + <td>≥ 3.4 + <td>52 + <td>62 +</table> + +Clients +------- + +[Selenium] users must update to version 3.11 or later to use geckodriver. +Other clients that follow the [W3C WebDriver specification][WebDriver] +are also supported. + +Firefoxen +--------- + +geckodriver is not yet feature complete. This means that it does +not yet offer full conformance with the [WebDriver] standard +or complete compatibility with [Selenium]. You can track the +[implementation status] of the latest [Firefox Nightly] on MDN. +We also keep track of known [Selenium], [remote protocol], and +[specification] problems in our [issue tracker]. + +Support is best in Firefox 57 and greater, although generally the more +recent the Firefox version, the better the experience as they have +more bug fixes and features. Some features will only be available +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 Capabilities](Capabilities.html#android) for more details. + +[geckodriver releases]: https://github.com/mozilla/geckodriver/releases +[Selenium]: https://github.com/seleniumhq/selenium +[WebDriver]: https://w3c.github.io/webdriver/ +[implementation status]: https://bugzilla.mozilla.org/showdependencytree.cgi?id=721859&hide_resolved=1 +[Firefox Nightly]: https://whattrainisitnow.com/ +[remote protocol]: https://github.com/mozilla/geckodriver/issues?q=is%3Aissue+is%3Aopen+label%3Amarionette +[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 diff --git a/testing/geckodriver/doc/Testing.md b/testing/geckodriver/doc/Testing.md new file mode 100644 index 0000000000..f14aff6eee --- /dev/null +++ b/testing/geckodriver/doc/Testing.md @@ -0,0 +1,59 @@ +Testing geckodriver +=================== + +We verify and test geckodriver in a couple of different ways. +Since it is an implementation of the WebDriver web standard, we share +a set of conformance tests with other browser vendors through the +[Web Platform Tests] (WPT) initiative. This lets us ensure web +compatibility between _different_ WebDriver implementations for +different browsers. + +In addition to the WPT tests, geckodriver and webdriver have +unit tests. These are written in Rust, but you must explicitly +tell mach to build these by adding the following line to your [mozconfig]: + + ac_add_options --enable-rust-tests + +Tests can then be run by using `cargo test` in the specific source folder: + + % cd testing/geckodriver/src + % cargo test + +To run the more extensive WPT tests you can use mach, but first +make sure you have built Firefox: + + % ./mach build + % ./mach wpt testing/web-platform/tests/webdriver + +As these are functional integration tests and pop up Firefox windows +sporadically, a helpful tip is to suppress the window whilst you +are running them by using Firefox’ [headless mode]: + + % ./mach wpt --headless testing/web-platform/tests/webdriver + +The `--headless` flag is equivalent to setting the `MOZ_HEADLESS` +output variable. In addition to `MOZ_HEADLESS` there is also +`MOZ_HEADLESS_WIDTH` and `MOZ_HEADLESS_HEIGHT` for controlling the +dimensions of the no-op virtual display. This is similar to using +Xvfb(1) which you may know from the X windowing system, but has +the additional benefit of also working on macOS and Windows. + +As you get in to development of geckodriver and Marionette you will +increasingly grow to understand our love for [trace-level logs]. +They provide us with the input—the HTTP requests—from the client +(in WPT’s case from the tests’ use of a custom WebDriver client), +the translation geckodriver makes to the [Marionette protocol], +the log output from Marionette, its responses back to geckodriver, +and finally the output—or the HTTP response—back to the client. + +The [trace-level logs] can be surfaced by passing on the `-vv` +flag to geckodriver through WPT: + + % ./mach wpt --webdriver-arg=-vv testing/web-platform/tests/webdriver + +[Web Platform Tests]: http://web-platform-tests.org/ +[cargo]: http://doc.crates.io/guide.html +[headless mode]: https://developer.mozilla.org/en-US/Firefox/Headless_mode +[mozconfig]: https://developer.mozilla.org/en-US/docs/Mozilla/Developer_guide/Build_Instructions/Configuring_Build_Options +[trace-level logs]: TraceLogs.html +[Marionette protocol]: https://firefox-source-docs.mozilla.org/testing/marionette/Protocol.html diff --git a/testing/geckodriver/doc/TraceLogs.md b/testing/geckodriver/doc/TraceLogs.md new file mode 100644 index 0000000000..ef472e4725 --- /dev/null +++ b/testing/geckodriver/doc/TraceLogs.md @@ -0,0 +1,173 @@ +Enabling trace logs +=================== + +geckodriver provides different bands of logs for different audiences. +The most important log entries are shown to everyone by default, +and these include which port geckodriver provides the WebDriver +API on, as well as informative warnings, errors, and fatal exceptions. + +The different log bands are, in ascending bandwidth: + +1. `fatal` is reserved for exceptional circumstances when geckodriver + or Firefox cannot recover. This usually entails that either + one or both of the processes will exit. + +2. `error` messages are mistakes in the program code which it is + possible to recover from. + +3. `warn` shows warnings of more informative nature that are not + necessarily problems in geckodriver. This could for example happen + if you use the legacy `desiredCapabilities`/`requiredCapabilities` + objects instead of the new `alwaysMatch`/`firstMatch` structures. + +4. `info` (default) contains information about which port geckodriver + binds to, but also all messages from the lower-bandwidth levels + listed above. + +5. `config` additionally shows the negotiated capabilities after + matching the `alwaysMatch` capabilities with the sequence of + `firstMatch` capabilities. + +6. `debug` is reserved for information that is useful when programming. + +7. `trace`, where in addition to itself, all previous levels + are included. The trace level shows all HTTP requests received + by geckodriver, packets sent to and from the remote protocol in + Firefox, and responses sent back to your client. + +In other words this means that the configured level will coalesce +entries from all lower bands including itself. If you set the log +level to `error`, you will get log entries for both `fatal` and `error`. +Similarly for `trace`, you will get all the logs that are offered. + +To help debug a problem with geckodriver or Firefox, the trace-level +output is vital to understand what is going on. This is why we ask +that trace logs are included when filing bugs gainst geckodriver. +It is only under very special circumstances that a trace log is +not needed, so you will normally find that our first action when +triaging your issue will be to ask you to include one. Do yourself +and us a favour and provide a trace-level log right away. + +To silence geckodriver altogether you may for example either redirect +all output to append to some log files: + + % geckodriver >>geckodriver.log 2>>geckodriver.err.log + +Or a black hole somewhere: + + % geckodriver >/dev/null 2>&1 + +The log level set for geckodriver is propagated to the Marionette +logger in Firefox. Marionette is the remote protocol that geckodriver +uses to implement WebDriver. This means enabling trace logs for +geckodriver will also implicitly enable them for Marionette. + +The log level is set in different ways. Either by using the +`--log <LEVEL>` option, where `LEVEL` is one of the log levels +from the list above, or by using the `-v` (for debug) or `-vv` +(for trace) shorthands. For example, the following command will +enable trace logs for both geckodriver and Marionette: + + % geckodriver -vv + +The second way of setting the log level is through capabilities. +geckodriver accepts a Mozilla-specific configuration object +in [`moz:firefoxOptions`]. This JSON Object, which is further +described in the [README] can hold Firefox-specific configuration, +such as which Firefox binary to use, additional preferences to set, +and of course which log level to use. + +[`moz:firefoxOptions`]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md#firefox-capabilities +[README]: https://searchfox.org/mozilla-central/source/testing/geckodriver/README.md + +Each client has its own way of specifying capabilities, and some clients +include “helpers” for providing browser-specific configuration. +It is often advisable to use these helpers instead of encoding the +JSON Object yourself because it can be difficult to get the exact +details right, but if you choose to, it should look like this: + + {"moz:firefoxOptions": {"log": {"level": "trace"}}} + +Note that most known WebDriver clients, such as those provided by +the Selenium project, do not expose a way to actually _see_ the logs +unless you redirect the log output to a particular file (using the +method shown above) or let the client “inherit” geckodriver’s +output, for example by redirecting the stdout and stderr streams to +its own. The notable exceptions are the Python and Ruby bindings, +which surface geckodriver logs in a remarkable easy and efficient way. + +See the client-specific documentation below for the most idiomatic +way to enable trace logs in your language. We want to expand this +documentation to cover all the best known clients people use with +geckodriver. If you find your language missing, please consider +[submitting a patch]. + +[submitting a patch]: ../CONTRIBUTING.md + + +C# +-- + +The Selenium [C# client] comes with a [`FirefoxOptions`] helper for +constructing the [`moz:firefoxOptions`] capabilities object: + + FirefoxOptions options = new FirefoxOptions(); + options.LogLevel = FirefoxDriverLogLevel.Trace; + IWebDriver driver = new FirefoxDriver(options); + +The log output is directed to stdout. + +[C# client]: https://seleniumhq.github.io/selenium/docs/api/dotnet/ +[`FirefoxOptions`]: https://seleniumhq.github.io/selenium/docs/api/dotnet/html/T_OpenQA_Selenium_Firefox_FirefoxOptions.htm + +Java +---- + +The Selenium [Java client] also comes with +a [`org.openqa.selenium.firefox.FirefoxOptions`] helper for +constructing the [`moz:firefoxOptions`] capabilities object: + + FirefoxOptions options = new FirefoxOptions(); + options.setLogLevel(FirefoxDriverLogLevel.TRACE); + WebDriver driver = new FirefoxDriver(options); + +As with C#, the log output is helpfully propagated to stdout. + +[Java client]: https://seleniumhq.github.io/selenium/docs/api/java/ +[`org.openqa.selenium.firefox.FirefoxOptions`]: https://seleniumhq.github.io/selenium/docs/api/java/org/openqa/selenium/firefox/FirefoxOptions.html + + +Python +------ + +The Selenium [Python client] comes with a +[`selenium.webdriver.firefox.options.Options`] helper that can +be used programmatically to construct the [`moz:firefoxOptions`] +capabilities object: + + from selenium.webdriver import Firefox + from selenium.webdriver.firefox.options import Options + + opts = Options() + opts.log.level = "trace" + driver = Firefox(options=opts) + +The log output is stored in a file called _geckodriver.log_ in your +script’s current working directory. + +[Python client]: https://selenium-python.readthedocs.io/ +[`selenium.webdriver.firefox.options.Options`]: https://github.com/SeleniumHQ/selenium/blob/master/py/selenium/webdriver/firefox/options.py + + +Ruby +---- + +The Selenium [Ruby client] comes with an [`Options`] helper to +generate the correct [`moz:firefoxOptions`] capabilities object: + + Selenium::WebDriver.logger.level = :debug + opts = Selenium::WebDriver::Firefox::Options.new(log_level: :trace) + driver = Selenium::WebDriver.for :firefox, options: opts + +[Ruby client]: https://seleniumhq.github.io/selenium/docs/api/rb/ +[`Options`]: https://seleniumhq.github.io/selenium/docs/api/rb/Selenium/WebDriver/Firefox/Options.html diff --git a/testing/geckodriver/doc/Usage.md b/testing/geckodriver/doc/Usage.md new file mode 100644 index 0000000000..d3ce68a488 --- /dev/null +++ b/testing/geckodriver/doc/Usage.md @@ -0,0 +1,111 @@ +Usage +===== + +geckodriver is an implementation of WebDriver, and WebDriver can +be used for widely different purposes. How you invoke geckodriver +largely depends on your use case. + + +Selenium +-------- + +If you are using geckodriver through [Selenium], you must ensure that +you have version 3.11 or greater. Because geckodriver implements the +[W3C WebDriver standard][WebDriver] and not the same Selenium wire +protocol older drivers are using, you may experience incompatibilities +and migration problems when making the switch from FirefoxDriver to +geckodriver. + +Generally speaking, Selenium 3 enabled geckodriver as the default +WebDriver implementation for Firefox. With the release of Firefox 47, +FirefoxDriver had to be discontinued for its lack of support for the +[new multi-processing architecture in Gecko][e10s]. + +Selenium client bindings will pick up the _geckodriver_ binary executable +from your [system’s `PATH` environmental variable][PATH] unless you +override it by setting the `webdriver.gecko.driver` [Java VM system +property]: + + System.setProperty("webdriver.gecko.driver", "/home/user/bin"); + +Or by passing it as a flag to the [java(1)] launcher: + + % java -Dwebdriver.gecko.driver=/home/user/bin YourApplication + +Your mileage with this approach may vary based on which programming +language bindings you are using. It is in any case generally the case +that geckodriver will be picked up if it is available on the system path. +In a bash compatible shell, you can make other programs aware of its +location by exporting or setting the `PATH` variable: + + % export PATH=$PATH:/home/user/bin + % whereis geckodriver + geckodriver: /home/user/bin/geckodriver + +On Window systems you can change the system path by right-clicking **My +Computer** and choosing **Properties**. In the dialogue that appears, +navigate **Advanced** → **Environmental Variables** → **Path**. + +Or in the Windows console window: + + $ set PATH=%PATH%;C:\bin\geckodriver + + +Standalone +---------- + +Since geckodriver is a separate HTTP server that is a complete remote end +implementation of [WebDriver], it is possible to avoid using the Selenium +remote server if you have no requirements to distribute processes across +a matrix of systems. + +Given a W3C WebDriver conforming client library (or _local end_) you +may interact with the geckodriver HTTP server as if you were speaking +to any Selenium server. + +Using [curl(1)]: + + % geckodriver & + [1] 16010 + % 1491834109194 geckodriver INFO Listening on 127.0.0.1:4444 + % 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/" + % curl -X DELETE http://localhost:4444/session/d4605710-5a4e-4d64-a52a-778bb0c31e00 + {} + % fg + geckodriver + ^C + % + +Using the Python [wdclient] library: + + import webdriver + + with webdriver.Session("127.0.0.1", 4444) as session: + session.url = "https://mozilla.org" + print "The current URL is %s" % session.url + +And to run: + + % geckodriver & + [1] 16054 + % python example.py + 1491835308354 geckodriver INFO Listening on 127.0.0.1:4444 + The current URL is https://www.mozilla.org/en-US/ + % fg + geckodriver + ^C + % + +[Selenium]: http://seleniumhq.org/ +[e10s]: https://developer.mozilla.org/en-US/Firefox/Multiprocess_Firefox +[PATH]: https://en.wikipedia.org/wiki/PATH_(variable) +[Java VM system property]: http://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html +[java(1)]: http://www.manpagez.com/man/1/java/ +[WebDriver]: https://w3c.github.io/webdriver/ +[curl(1)]: http://www.manpagez.com/man/1/curl/ +[wdclient]: https://github.com/web-platform-tests/wpt/tree/master/tools/webdriver diff --git a/testing/geckodriver/doc/index.rst b/testing/geckodriver/doc/index.rst new file mode 100644 index 0000000000..be9fc88004 --- /dev/null +++ b/testing/geckodriver/doc/index.rst @@ -0,0 +1,57 @@ +=========== +geckodriver +=========== + +Proxy for using W3C WebDriver-compatible clients to interact with +Gecko-based browsers. + +This program provides the HTTP API described by the `WebDriver protocol`_. +to communicate with Gecko browsers, such as Firefox. It translates calls +into the :ref:`Firefox remote protocol <Protocol>` by acting as a proxy between the local- +and remote ends. + +You can consult the `change log`_ for a record of all notable changes +to the program. Releases_ are made available on GitHub. + +.. _WebDriver protocol: https://w3c.github.io/webdriver/#protocol +.. _change log: https://github.com/mozilla/geckodriver/releases +.. _Releases: https://github.com/mozilla/geckodriver/releases + + +.. toctree:: + :maxdepth: 1 + + Support.md + WebDriver capabilities <https://developer.mozilla.org/en-US/docs/Web/WebDriver/Capabilities> + Capabilities.md + Usage.md + Flags.md + Profiles.md + Bugs.md + TraceLogs.md + CrashReports.md + Notarization.md + + +For developers +============== +.. toctree:: + :maxdepth: 1 + + Building.md + Testing.md + Releasing.md + ARM.md + + +Communication +============= + +The mailing list for geckodriver discussion is +tools-marionette@lists.mozilla.org (`subscribe`_, `archive`_). + +If you prefer real-time chat, ask your questions +on `#interop:mozilla.org <https://chat.mozilla.org/#/room/#interop:mozilla.org>`__. + +.. _subscribe: https://lists.mozilla.org/listinfo/tools-marionette +.. _archive: https://lists.mozilla.org/pipermail/tools-marionette/ diff --git a/testing/geckodriver/mach_commands.py b/testing/geckodriver/mach_commands.py new file mode 100644 index 0000000000..bf5fe594e3 --- /dev/null +++ b/testing/geckodriver/mach_commands.py @@ -0,0 +1,127 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, print_function, unicode_literals + +import os +import logging + +from mach.decorators import ( + Command, + CommandArgument, + CommandArgumentGroup, + CommandProvider, +) + +from mozbuild.base import MachCommandBase, BinaryNotFoundException + + +@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." + ) + @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.", + ) + 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.", + ) + return 1 + + args = [binpath] + + if params: + args.extend(params) + + if binary is None: + 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}") + return 1 + + args.extend(["--binary", binary]) + + if debug or debugger or debugger_args: + if "INSIDE_EMACS" in os.environ: + 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 + ) + + if debugger: + self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) + if not self.debuggerInfo: + print("Could not find a suitable debugger in your PATH.") + return 1 + + # Parameters come from the CLI. We need to convert them before + # 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("(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) diff --git a/testing/geckodriver/marionette/Cargo.toml b/testing/geckodriver/marionette/Cargo.toml new file mode 100644 index 0000000000..1d18558ace --- /dev/null +++ b/testing/geckodriver/marionette/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "marionette" +version = "0.1.0" +authors = ["Mozilla"] +edition = "2018" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_repr = "0.1" diff --git a/testing/geckodriver/marionette/src/common.rs b/testing/geckodriver/marionette/src/common.rs new file mode 100644 index 0000000000..78bf3afd60 --- /dev/null +++ b/testing/geckodriver/marionette/src/common.rs @@ -0,0 +1,250 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::ser::SerializeMap; +use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct BoolValue { + value: bool, +} + +impl BoolValue { + pub fn new(val: bool) -> Self { + BoolValue { value: val } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Cookie { + pub name: String, + pub value: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + pub domain: Option<String>, + #[serde(default)] + pub secure: bool, + #[serde(default, rename = "httpOnly")] + pub http_only: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub expiry: Option<Date>, + #[serde(skip_serializing_if = "Option::is_none", rename = "sameSite")] + pub same_site: Option<String>, +} + +pub fn to_cookie<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, + T: Serialize, +{ + #[derive(Serialize)] + struct Wrapper<T> { + cookie: T, + } + + Wrapper { cookie: data }.serialize(serializer) +} + +pub fn from_cookie<'de, D, T>(deserializer: D) -> Result<T, D::Error> +where + D: Deserializer<'de>, + T: serde::de::DeserializeOwned, + T: std::fmt::Debug, +{ + #[derive(Debug, Deserialize)] + struct Wrapper<T> { + cookie: T, + } + + let w = Wrapper::deserialize(deserializer)?; + Ok(w.cookie) +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Date(pub u64); + +#[derive(Clone, Debug, PartialEq)] +pub enum Frame { + Index(u16), + Element(String), + Parent, +} + +impl Serialize for Frame { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let mut map = serializer.serialize_map(Some(1))?; + match self { + Frame::Index(nth) => map.serialize_entry("id", nth)?, + Frame::Element(el) => map.serialize_entry("element", el)?, + Frame::Parent => map.serialize_entry("id", &Value::Null)?, + } + map.end() + } +} + +impl<'de> Deserialize<'de> for Frame { + fn deserialize<D>(deserializer: D) -> Result<Frame, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Debug, Deserialize)] + #[serde(rename_all = "lowercase")] + struct JsonFrame { + id: Option<u16>, + element: Option<String>, + } + + let json = JsonFrame::deserialize(deserializer)?; + match (json.id, json.element) { + (Some(_id), Some(_element)) => Err(de::Error::custom("conflicting frame identifiers")), + (Some(id), None) => Ok(Frame::Index(id)), + (None, Some(element)) => Ok(Frame::Element(element)), + (None, None) => Ok(Frame::Parent), + } + } +} + +// TODO(nupur): Bug 1567165 - Make WebElement in Marionette a unit struct +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct WebElement { + #[serde(rename = "element-6066-11e4-a52e-4f735466cecf")] + pub element: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Timeouts { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub implicit: Option<u64>, + #[serde( + default, + rename = "pageLoad", + alias = "page load", + skip_serializing_if = "Option::is_none" + )] + pub page_load: Option<u64>, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[allow(clippy::option_option)] + pub script: Option<Option<u64>>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Window { + pub name: String, + pub handle: String, +} + +pub fn to_name<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, + T: Serialize, +{ + #[derive(Serialize)] + struct Wrapper<T> { + name: T, + } + + Wrapper { name: data }.serialize(serializer) +} + +pub fn from_name<'de, D, T>(deserializer: D) -> Result<T, D::Error> +where + D: Deserializer<'de>, + T: serde::de::DeserializeOwned, + T: std::fmt::Debug, +{ + #[derive(Debug, Deserialize)] + struct Wrapper<T> { + name: T, + } + + let w = Wrapper::deserialize(deserializer)?; + Ok(w.name) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::{assert_de, assert_ser, assert_ser_de, ELEMENT_KEY}; + use serde_json::json; + + #[test] + fn test_cookie_default_values() { + let data = Cookie { + name: "hello".into(), + value: "world".into(), + path: None, + domain: None, + secure: false, + http_only: false, + expiry: None, + same_site: None, + }; + assert_de(&data, json!({"name":"hello", "value":"world"})); + } + + #[test] + fn test_json_frame_index() { + assert_ser_de(&Frame::Index(1234), json!({"id": 1234})); + } + + #[test] + fn test_json_frame_element() { + assert_ser_de(&Frame::Element("elem".into()), json!({"element": "elem"})); + } + + #[test] + fn test_json_frame_parent() { + assert_ser_de(&Frame::Parent, json!({ "id": null })); + } + + #[test] + fn test_web_element() { + let data = WebElement { + element: "foo".into(), + }; + assert_ser_de(&data, json!({ELEMENT_KEY: "foo"})); + } + + #[test] + fn test_timeouts_with_all_params() { + let data = Timeouts { + implicit: Some(1000), + page_load: Some(200000), + script: Some(Some(60000)), + }; + assert_ser_de( + &data, + json!({"implicit":1000,"pageLoad":200000,"script":60000}), + ); + assert_de( + &data, + json!({"implicit":1000,"page load":200000,"script":60000}), + ); + } + + #[test] + fn test_timeouts_with_missing_params() { + let data = Timeouts { + implicit: Some(1000), + page_load: None, + script: None, + }; + assert_ser_de(&data, json!({"implicit":1000})); + } + + #[test] + fn test_timeouts_setting_script_none() { + let data = Timeouts { + implicit: Some(1000), + page_load: None, + script: Some(None), + }; + assert_ser(&data, json!({"implicit":1000, "script":null})); + } +} diff --git a/testing/geckodriver/marionette/src/error.rs b/testing/geckodriver/marionette/src/error.rs new file mode 100644 index 0000000000..5db502eba5 --- /dev/null +++ b/testing/geckodriver/marionette/src/error.rs @@ -0,0 +1,184 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use std::error; +use std::fmt; + +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +#[serde(untagged)] +pub(crate) enum Error { + Marionette(MarionetteError), +} + +impl Error { + pub fn kind(&self) -> ErrorKind { + match *self { + Error::Marionette(ref err) => err.kind, + } + } +} + +impl fmt::Debug for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Marionette(ref err) => fmt + .debug_struct("Marionette") + .field("kind", &err.kind) + .field("message", &err.message) + .field("stacktrace", &err.stack.clone()) + .finish(), + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Marionette(ref err) => write!(fmt, "{}: {}", err.kind, err.message), + } + } +} + +impl error::Error for Error { + fn description(&self) -> &str { + match self { + Error::Marionette(_) => self.kind().as_str(), + } + } +} + +#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +pub struct MarionetteError { + #[serde(rename = "error")] + pub kind: ErrorKind, + #[serde(default = "empty_string")] + pub message: String, + #[serde(rename = "stacktrace", default = "empty_string")] + pub stack: String, +} + +fn empty_string() -> String { + "".to_owned() +} + +impl Into<Error> for MarionetteError { + fn into(self) -> Error { + Error::Marionette(self) + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)] +pub enum ErrorKind { + #[serde(rename = "element click intercepted")] + ElementClickIntercepted, + #[serde(rename = "element not accessible")] + ElementNotAccessible, + #[serde(rename = "element not interactable")] + ElementNotInteractable, + #[serde(rename = "insecure certificate")] + InsecureCertificate, + #[serde(rename = "invalid argument")] + InvalidArgument, + #[serde(rename = "invalid cookie")] + InvalidCookieDomain, + #[serde(rename = "invalid element state")] + InvalidElementState, + #[serde(rename = "invalid selector")] + InvalidSelector, + #[serde(rename = "invalid session id")] + InvalidSessionId, + #[serde(rename = "javascript error")] + JavaScript, + #[serde(rename = "move target out of bounds")] + MoveTargetOutOfBounds, + #[serde(rename = "no such alert")] + NoSuchAlert, + #[serde(rename = "no such element")] + NoSuchElement, + #[serde(rename = "no such frame")] + NoSuchFrame, + #[serde(rename = "no such window")] + NoSuchWindow, + #[serde(rename = "script timeout")] + ScriptTimeout, + #[serde(rename = "session not created")] + SessionNotCreated, + #[serde(rename = "stale element reference")] + StaleElementReference, + #[serde(rename = "timeout")] + Timeout, + #[serde(rename = "unable to set cookie")] + UnableToSetCookie, + #[serde(rename = "unexpected alert open")] + UnexpectedAlertOpen, + #[serde(rename = "unknown command")] + UnknownCommand, + #[serde(rename = "unknown error")] + Unknown, + #[serde(rename = "unsupported operation")] + UnsupportedOperation, + #[serde(rename = "webdriver error")] + WebDriver, +} + +impl ErrorKind { + pub(crate) fn as_str(self) -> &'static str { + use ErrorKind::*; + match self { + ElementClickIntercepted => "element click intercepted", + ElementNotAccessible => "element not accessible", + ElementNotInteractable => "element not interactable", + InsecureCertificate => "insecure certificate", + InvalidArgument => "invalid argument", + InvalidCookieDomain => "invalid cookie", + InvalidElementState => "invalid element state", + InvalidSelector => "invalid selector", + InvalidSessionId => "invalid session id", + JavaScript => "javascript error", + MoveTargetOutOfBounds => "move target out of bounds", + NoSuchAlert => "no such alert", + NoSuchElement => "no such element", + NoSuchFrame => "no such frame", + NoSuchWindow => "no such window", + ScriptTimeout => "script timeout", + SessionNotCreated => "session not created", + StaleElementReference => "stale eelement referencee", + Timeout => "timeout", + UnableToSetCookie => "unable to set cookie", + UnexpectedAlertOpen => "unexpected alert open", + UnknownCommand => "unknown command", + Unknown => "unknown error", + UnsupportedOperation => "unsupported operation", + WebDriver => "webdriver error", + } + } +} + +impl fmt::Display for ErrorKind { + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + write!(fmt, "{}", self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::assert_ser_de; + use serde_json::json; + + #[test] + fn test_json_error() { + let err = MarionetteError { + kind: ErrorKind::Timeout, + message: "".into(), + stack: "".into(), + }; + assert_ser_de( + &err, + json!({"error": "timeout", "message": "", "stacktrace": ""}), + ); + } +} diff --git a/testing/geckodriver/marionette/src/lib.rs b/testing/geckodriver/marionette/src/lib.rs new file mode 100644 index 0000000000..80817c5f5b --- /dev/null +++ b/testing/geckodriver/marionette/src/lib.rs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub mod error; + +pub mod common; +pub mod marionette; +pub mod message; +pub mod result; +pub mod webdriver; + +#[cfg(test)] +mod test; diff --git a/testing/geckodriver/marionette/src/marionette.rs b/testing/geckodriver/marionette/src/marionette.rs new file mode 100644 index 0000000000..c06e2d60c7 --- /dev/null +++ b/testing/geckodriver/marionette/src/marionette.rs @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::{Deserialize, Serialize}; + +use crate::common::BoolValue; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[allow(non_camel_case_types)] +pub enum AppStatus { + eAttemptQuit, + eConsiderQuit, + eForceQuit, + eRestart, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Command { + #[serde(rename = "Marionette:AcceptConnections")] + AcceptConnections(BoolValue), + #[serde(rename = "Marionette:Quit")] + DeleteSession { flags: Vec<AppStatus> }, + #[serde(rename = "Marionette:GetContext")] + GetContext, + #[serde(rename = "Marionette:GetScreenOrientation")] + GetScreenOrientation, +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::assert_ser_de; + use serde_json::json; + + #[test] + fn test_json_command_accept_connections() { + assert_ser_de( + &Command::AcceptConnections(BoolValue::new(false)), + json!({"Marionette:AcceptConnections": {"value": false }}), + ); + } + + #[test] + fn test_json_command_delete_session() { + let data = &Command::DeleteSession { + flags: vec![AppStatus::eForceQuit], + }; + assert_ser_de(data, json!({"Marionette:Quit": {"flags": ["eForceQuit"]}})); + } + + #[test] + fn test_json_command_get_context() { + assert_ser_de(&Command::GetContext, json!("Marionette:GetContext")); + } + + #[test] + fn test_json_command_get_screen_orientation() { + assert_ser_de( + &Command::GetScreenOrientation, + json!("Marionette:GetScreenOrientation"), + ); + } + + #[test] + fn test_json_command_invalid() { + assert!(serde_json::from_value::<Command>(json!("foo")).is_err()); + } +} diff --git a/testing/geckodriver/marionette/src/message.rs b/testing/geckodriver/marionette/src/message.rs new file mode 100644 index 0000000000..33741ca4d8 --- /dev/null +++ b/testing/geckodriver/marionette/src/message.rs @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::de::{self, SeqAccess, Unexpected, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::{Map, Value}; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use std::fmt; + +use crate::error::MarionetteError; +use crate::marionette; +use crate::result::MarionetteResult; +use crate::webdriver; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Command { + WebDriver(webdriver::Command), + Marionette(marionette::Command), +} + +impl Command { + pub fn name(&self) -> String { + let (command_name, _) = self.first_entry(); + command_name + } + + fn params(&self) -> Value { + let (_, params) = self.first_entry(); + params + } + + fn first_entry(&self) -> (String, serde_json::Value) { + match serde_json::to_value(&self).unwrap() { + Value::String(cmd) => (cmd, Value::Object(Map::new())), + Value::Object(items) => { + let mut iter = items.iter(); + let (cmd, params) = iter.next().unwrap(); + (cmd.to_string(), params.clone()) + } + _ => unreachable!(), + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize_repr, Deserialize_repr)] +#[repr(u8)] +enum MessageDirection { + Incoming = 0, + Outgoing = 1, +} + +pub type MessageId = u32; + +#[derive(Debug, Clone, PartialEq)] +pub struct Request(pub MessageId, pub Command); + +impl Request { + pub fn id(&self) -> MessageId { + self.0 + } + + pub fn command(&self) -> &Command { + &self.1 + } + + pub fn params(&self) -> Value { + self.command().params() + } +} + +impl Serialize for Request { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + ( + MessageDirection::Incoming, + self.id(), + self.command().name(), + self.params(), + ) + .serialize(serializer) + } +} + +#[derive(Debug, PartialEq)] +pub enum Response { + Result { + id: MessageId, + result: MarionetteResult, + }, + Error { + id: MessageId, + error: MarionetteError, + }, +} + +impl Serialize for Response { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + match self { + Response::Result { id, result } => { + (MessageDirection::Outgoing, id, Value::Null, &result).serialize(serializer) + } + Response::Error { id, error } => { + (MessageDirection::Outgoing, id, &error, Value::Null).serialize(serializer) + } + } + } +} + +#[derive(Debug, PartialEq, Serialize)] +#[serde(untagged)] +pub enum Message { + Incoming(Request), + Outgoing(Response), +} + +struct MessageVisitor; + +impl<'de> Visitor<'de> for MessageVisitor { + type Value = Message; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("four-element array") + } + + fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> { + let direction = seq + .next_element::<MessageDirection>()? + .ok_or_else(|| de::Error::invalid_length(0, &self))?; + let id: MessageId = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(1, &self))?; + + let msg = match direction { + MessageDirection::Incoming => { + let name: String = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(2, &self))?; + let params: Value = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(3, &self))?; + + let command = match params { + Value::Object(ref items) if !items.is_empty() => { + let command_to_params = { + let mut m = Map::new(); + m.insert(name, params); + Value::Object(m) + }; + serde_json::from_value(command_to_params).map_err(de::Error::custom) + } + Value::Object(_) | Value::Null => { + serde_json::from_value(Value::String(name)).map_err(de::Error::custom) + } + x => Err(de::Error::custom(format!("unknown params type: {}", x))), + }?; + Message::Incoming(Request(id, command)) + } + + MessageDirection::Outgoing => { + let maybe_error: Option<MarionetteError> = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(2, &self))?; + + let response = if let Some(error) = maybe_error { + seq.next_element::<Value>()? + .ok_or_else(|| de::Error::invalid_length(3, &self))? + .as_null() + .ok_or_else(|| de::Error::invalid_type(Unexpected::Unit, &self))?; + Response::Error { id, error } + } else { + let result: MarionetteResult = seq + .next_element()? + .ok_or_else(|| de::Error::invalid_length(3, &self))?; + Response::Result { id, result } + }; + + Message::Outgoing(response) + } + }; + + Ok(msg) + } +} + +impl<'de> Deserialize<'de> for Message { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer.deserialize_seq(MessageVisitor) + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + use crate::common::*; + use crate::error::{ErrorKind, MarionetteError}; + use crate::test::assert_ser_de; + + #[test] + fn test_incoming() { + let json = + json!([0, 42, "WebDriver:FindElement", {"using": "css selector", "value": "value"}]); + let find_element = webdriver::Command::FindElement(webdriver::Locator { + using: webdriver::Selector::CSS, + value: "value".into(), + }); + let req = Request(42, Command::WebDriver(find_element)); + let msg = Message::Incoming(req); + assert_ser_de(&msg, json); + } + + #[test] + fn test_incoming_empty_params() { + let json = json!([0, 42, "WebDriver:GetTimeouts", {}]); + let req = Request(42, Command::WebDriver(webdriver::Command::GetTimeouts)); + let msg = Message::Incoming(req); + assert_ser_de(&msg, json); + } + + #[test] + fn test_incoming_common_params() { + let json = json!([0, 42, "Marionette:AcceptConnections", {"value": false}]); + let params = BoolValue::new(false); + let req = Request( + 42, + Command::Marionette(marionette::Command::AcceptConnections(params)), + ); + let msg = Message::Incoming(req); + assert_ser_de(&msg, json); + } + + #[test] + fn test_incoming_params_derived() { + assert!(serde_json::from_value::<Message>( + json!([0,42,"WebDriver:FindElement",{"using":"foo","value":"foo"}]) + ) + .is_err()); + assert!(serde_json::from_value::<Message>( + json!([0,42,"Marionette:AcceptConnections",{"value":"foo"}]) + ) + .is_err()); + } + + #[test] + fn test_incoming_no_params() { + assert!(serde_json::from_value::<Message>( + json!([0,42,"WebDriver:GetTimeouts",{"value":true}]) + ) + .is_err()); + assert!(serde_json::from_value::<Message>( + json!([0,42,"Marionette:Context",{"value":"foo"}]) + ) + .is_err()); + assert!(serde_json::from_value::<Message>( + json!([0,42,"Marionette:GetScreenOrientation",{"value":true}]) + ) + .is_err()); + } + + #[test] + fn test_outgoing_result() { + let json = json!([1, 42, null, { "value": null }]); + let result = MarionetteResult::Null; + let msg = Message::Outgoing(Response::Result { id: 42, result }); + + assert_ser_de(&msg, json); + } + + #[test] + fn test_outgoing_error() { + let json = + json!([1, 42, {"error": "no such element", "message": "", "stacktrace": ""}, null]); + let error = MarionetteError { + kind: ErrorKind::NoSuchElement, + message: "".into(), + stack: "".into(), + }; + let msg = Message::Outgoing(Response::Error { id: 42, error }); + + assert_ser_de(&msg, json); + } + + #[test] + fn test_invalid_type() { + assert!( + serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts", {}])).is_err() + ); + assert!(serde_json::from_value::<Message>(json!([3, 42, "no such element", {}])).is_err()); + } + + #[test] + fn test_missing_fields() { + // all fields are required + assert!( + serde_json::from_value::<Message>(json!([2, 42, "WebDriver:GetTimeouts"])).is_err() + ); + assert!(serde_json::from_value::<Message>(json!([2, 42])).is_err()); + assert!(serde_json::from_value::<Message>(json!([2])).is_err()); + assert!(serde_json::from_value::<Message>(json!([])).is_err()); + } + + #[test] + fn test_unknown_command() { + assert!(serde_json::from_value::<Message>(json!([0, 42, "hooba", {}])).is_err()); + } + + #[test] + fn test_unknown_error() { + assert!(serde_json::from_value::<Message>(json!([1, 42, "flooba", {}])).is_err()); + } + + #[test] + fn test_message_id_bounds() { + let overflow = i64::from(std::u32::MAX) + 1; + let underflow = -1; + + fn get_timeouts(message_id: i64) -> Value { + json!([0, message_id, "WebDriver:GetTimeouts", {}]) + } + + assert!(serde_json::from_value::<Message>(get_timeouts(overflow)).is_err()); + assert!(serde_json::from_value::<Message>(get_timeouts(underflow)).is_err()); + } +} diff --git a/testing/geckodriver/marionette/src/result.rs b/testing/geckodriver/marionette/src/result.rs new file mode 100644 index 0000000000..95817c15f0 --- /dev/null +++ b/testing/geckodriver/marionette/src/result.rs @@ -0,0 +1,223 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::de; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::Value; + +use crate::common::{Cookie, Timeouts, WebElement}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct NewWindow { + handle: String, + #[serde(rename = "type")] + type_hint: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct WindowRect { + pub x: i32, + pub y: i32, + pub width: i32, + pub height: i32, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ElementRect { + pub x: f64, + pub y: f64, + pub width: f64, + pub height: f64, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MarionetteResult { + #[serde(deserialize_with = "from_value", serialize_with = "to_value")] + Bool(bool), + #[serde(deserialize_with = "from_value", serialize_with = "to_empty_value")] + Null, + NewWindow(NewWindow), + WindowRect(WindowRect), + ElementRect(ElementRect), + #[serde(deserialize_with = "from_value", serialize_with = "to_value")] + String(String), + Strings(Vec<String>), + #[serde(deserialize_with = "from_value", serialize_with = "to_value")] + WebElement(WebElement), + WebElements(Vec<WebElement>), + Cookies(Vec<Cookie>), + Timeouts(Timeouts), +} + +fn to_value<T, S>(data: T, serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, + T: Serialize, +{ + #[derive(Serialize)] + struct Wrapper<T> { + value: T, + } + + Wrapper { value: data }.serialize(serializer) +} + +fn to_empty_value<S>(serializer: S) -> Result<S::Ok, S::Error> +where + S: Serializer, +{ + #[derive(Serialize)] + struct Wrapper { + value: Value, + } + + Wrapper { value: Value::Null }.serialize(serializer) +} + +fn from_value<'de, D, T>(deserializer: D) -> Result<T, D::Error> +where + D: Deserializer<'de>, + T: serde::de::DeserializeOwned, + T: std::fmt::Debug, +{ + #[derive(Debug, Deserialize)] + struct Wrapper<T> { + value: T, + } + + let v = Value::deserialize(deserializer)?; + if v.is_object() { + let w = serde_json::from_value::<Wrapper<T>>(v).map_err(de::Error::custom)?; + Ok(w.value) + } else { + Err(de::Error::custom("Cannot be deserialized to struct")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::{assert_de, assert_ser_de, ELEMENT_KEY}; + use serde_json::json; + + #[test] + fn test_boolean_response() { + assert_ser_de(&MarionetteResult::Bool(true), json!({"value": true})); + } + + #[test] + fn test_cookies_response() { + let mut data = Vec::new(); + data.push(Cookie { + name: "foo".into(), + value: "bar".into(), + path: Some("/common".into()), + domain: Some("web-platform.test".into()), + secure: false, + http_only: false, + expiry: None, + same_site: Some("Strict".into()), + }); + assert_ser_de( + &MarionetteResult::Cookies(data), + json!([{"name":"foo","value":"bar","path":"/common","domain":"web-platform.test","secure":false,"httpOnly":false,"sameSite":"Strict"}]), + ); + } + + #[test] + fn test_new_window_response() { + let data = NewWindow { + handle: "6442450945".into(), + type_hint: "tab".into(), + }; + let json = json!({"handle": "6442450945", "type": "tab"}); + assert_ser_de(&MarionetteResult::NewWindow(data), json); + } + + #[test] + fn test_web_element_response() { + let data = WebElement { + element: "foo".into(), + }; + assert_ser_de( + &MarionetteResult::WebElement(data), + json!({"value": {ELEMENT_KEY: "foo"}}), + ); + } + + #[test] + fn test_web_elements_response() { + let data = vec![ + WebElement { + element: "foo".into(), + }, + WebElement { + element: "bar".into(), + }, + ]; + assert_ser_de( + &MarionetteResult::WebElements(data), + json!([{ELEMENT_KEY: "foo"}, {ELEMENT_KEY: "bar"}]), + ); + } + + #[test] + fn test_timeouts_response() { + let data = Timeouts { + implicit: Some(1000), + page_load: Some(200000), + script: Some(Some(60000)), + }; + assert_ser_de( + &MarionetteResult::Timeouts(data), + json!({"implicit":1000,"pageLoad":200000,"script":60000}), + ); + } + + #[test] + fn test_string_response() { + assert_ser_de( + &MarionetteResult::String("foo".into()), + json!({"value": "foo"}), + ); + } + + #[test] + fn test_strings_response() { + assert_ser_de( + &MarionetteResult::Strings(vec!["2147483649".to_string()]), + json!(["2147483649"]), + ); + } + + #[test] + fn test_null_response() { + assert_ser_de(&MarionetteResult::Null, json!({ "value": null })); + } + + #[test] + fn test_window_rect_response() { + let data = WindowRect { + x: 100, + y: 100, + width: 800, + height: 600, + }; + let json = json!({"x": 100, "y": 100, "width": 800, "height": 600}); + assert_ser_de(&MarionetteResult::WindowRect(data), json); + } + + #[test] + fn test_element_rect_response() { + let data = ElementRect { + x: 8.0, + y: 8.0, + width: 148.6666717529297, + height: 22.0, + }; + let json = json!({"x": 8, "y": 8, "width": 148.6666717529297, "height": 22}); + assert_de(&MarionetteResult::ElementRect(data), json); + } +} diff --git a/testing/geckodriver/marionette/src/test.rs b/testing/geckodriver/marionette/src/test.rs new file mode 100644 index 0000000000..3b20bb0917 --- /dev/null +++ b/testing/geckodriver/marionette/src/test.rs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub static ELEMENT_KEY: &'static str = "element-6066-11e4-a52e-4f735466cecf"; + +pub fn assert_ser_de<T>(data: &T, json: serde_json::Value) +where + T: std::fmt::Debug, + T: std::cmp::PartialEq, + T: serde::de::DeserializeOwned, + T: serde::Serialize, +{ + assert_eq!(serde_json::to_value(data).unwrap(), json); + assert_eq!(data, &serde_json::from_value::<T>(json).unwrap()); +} + +#[allow(dead_code)] +pub fn assert_ser<T>(data: &T, json: serde_json::Value) +where + T: std::fmt::Debug, + T: std::cmp::PartialEq, + T: serde::Serialize, +{ + assert_eq!(serde_json::to_value(data).unwrap(), json); +} + +pub fn assert_de<T>(data: &T, json: serde_json::Value) +where + T: std::fmt::Debug, + T: std::cmp::PartialEq, + T: serde::de::DeserializeOwned, +{ + assert_eq!(data, &serde_json::from_value::<T>(json).unwrap()); +} diff --git a/testing/geckodriver/marionette/src/webdriver.rs b/testing/geckodriver/marionette/src/webdriver.rs new file mode 100644 index 0000000000..b1069ed4ff --- /dev/null +++ b/testing/geckodriver/marionette/src/webdriver.rs @@ -0,0 +1,456 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::common::{from_cookie, from_name, to_cookie, to_name, Cookie, Frame, Timeouts, Window}; + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Url { + pub url: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct LegacyWebElement { + pub id: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Locator { + pub using: Selector, + pub value: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Selector { + #[serde(rename = "css selector")] + CSS, + #[serde(rename = "link text")] + LinkText, + #[serde(rename = "partial link text")] + PartialLinkText, + #[serde(rename = "tag name")] + TagName, + #[serde(rename = "xpath")] + XPath, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct NewWindow { + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub type_hint: Option<String>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct WindowRect { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub x: Option<i32>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub y: Option<i32>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub width: Option<i32>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub height: Option<i32>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Keys { + pub text: String, + pub value: Vec<String>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(default, rename_all = "camelCase")] +pub struct PrintParameters { + pub orientation: PrintOrientation, + pub scale: f64, + pub background: bool, + pub page: PrintPage, + pub margin: PrintMargins, + pub page_ranges: Vec<String>, + pub shrink_to_fit: bool, +} + +impl Default for PrintParameters { + fn default() -> Self { + PrintParameters { + orientation: PrintOrientation::default(), + scale: 1.0, + background: false, + page: PrintPage::default(), + margin: PrintMargins::default(), + page_ranges: Vec::new(), + shrink_to_fit: true, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PrintOrientation { + Landscape, + Portrait, +} + +impl Default for PrintOrientation { + fn default() -> Self { + PrintOrientation::Portrait + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PrintPage { + pub width: f64, + pub height: f64, +} + +impl Default for PrintPage { + fn default() -> Self { + PrintPage { + width: 21.59, + height: 27.94, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct PrintMargins { + pub top: f64, + pub bottom: f64, + pub left: f64, + pub right: f64, +} + +impl Default for PrintMargins { + fn default() -> Self { + PrintMargins { + top: 1.0, + bottom: 1.0, + left: 1.0, + right: 1.0, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct ScreenshotOptions { + pub id: Option<String>, + pub highlights: Vec<Option<String>>, + pub full: bool, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Script { + pub script: String, + pub args: Option<Vec<Value>>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum Command { + // Needs to be updated to "WebDriver:AcceptAlert" for Firefox 63 + #[serde(rename = "WebDriver:AcceptDialog")] + AcceptAlert, + #[serde( + rename = "WebDriver:AddCookie", + serialize_with = "to_cookie", + deserialize_with = "from_cookie" + )] + AddCookie(Cookie), + #[serde(rename = "WebDriver:CloseWindow")] + CloseWindow, + #[serde( + rename = "WebDriver:DeleteCookie", + serialize_with = "to_name", + deserialize_with = "from_name" + )] + DeleteCookie(String), + #[serde(rename = "WebDriver:DeleteAllCookies")] + DeleteCookies, + #[serde(rename = "WebDriver:DismissAlert")] + DismissAlert, + #[serde(rename = "WebDriver:ElementClear")] + ElementClear(LegacyWebElement), + #[serde(rename = "WebDriver:ElementClick")] + ElementClick(LegacyWebElement), + #[serde(rename = "WebDriver:ElementSendKeys")] + ElementSendKeys { + id: String, + text: String, + value: Vec<String>, + }, + #[serde(rename = "WebDriver:ExecuteAsyncScript")] + ExecuteAsyncScript(Script), + #[serde(rename = "WebDriver:ExecuteScript")] + ExecuteScript(Script), + #[serde(rename = "WebDriver:FindElement")] + FindElement(Locator), + #[serde(rename = "WebDriver:FindElements")] + FindElements(Locator), + #[serde(rename = "WebDriver:FindElement")] + FindElementElement { + element: String, + using: Selector, + value: String, + }, + #[serde(rename = "WebDriver:FindElements")] + FindElementElements { + element: String, + using: Selector, + value: String, + }, + #[serde(rename = "WebDriver:FullscreenWindow")] + FullscreenWindow, + #[serde(rename = "WebDriver:Navigate")] + Get(Url), + #[serde(rename = "WebDriver:GetActiveElement")] + GetActiveElement, + #[serde(rename = "WebDriver:GetAlertText")] + GetAlertText, + #[serde(rename = "WebDriver:GetCookies")] + GetCookies, + #[serde(rename = "WebDriver:GetElementCSSValue")] + GetCSSValue { + id: String, + #[serde(rename = "propertyName")] + property: String, + }, + #[serde(rename = "WebDriver:GetCurrentURL")] + GetCurrentUrl, + #[serde(rename = "WebDriver:GetElementAttribute")] + GetElementAttribute { id: String, name: String }, + #[serde(rename = "WebDriver:GetElementProperty")] + GetElementProperty { id: String, name: String }, + #[serde(rename = "WebDriver:GetElementRect")] + GetElementRect(LegacyWebElement), + #[serde(rename = "WebDriver:GetElementTagName")] + GetElementTagName(LegacyWebElement), + #[serde(rename = "WebDriver:GetElementText")] + GetElementText(LegacyWebElement), + #[serde(rename = "WebDriver:GetPageSource")] + GetPageSource, + #[serde(rename = "WebDriver:GetTimeouts")] + GetTimeouts, + #[serde(rename = "WebDriver:GetTitle")] + GetTitle, + #[serde(rename = "WebDriver:GetWindowHandle")] + GetWindowHandle, + #[serde(rename = "WebDriver:GetWindowHandles")] + GetWindowHandles, + #[serde(rename = "WebDriver:GetWindowRect")] + GetWindowRect, + #[serde(rename = "WebDriver:Back")] + GoBack, + #[serde(rename = "WebDriver:Forward")] + GoForward, + #[serde(rename = "WebDriver:IsElementDisplayed")] + IsDisplayed(LegacyWebElement), + #[serde(rename = "WebDriver:IsElementEnabled")] + IsEnabled(LegacyWebElement), + #[serde(rename = "WebDriver:IsElementSelected")] + IsSelected(LegacyWebElement), + #[serde(rename = "WebDriver:MaximizeWindow")] + MaximizeWindow, + #[serde(rename = "WebDriver:MinimizeWindow")] + MinimizeWindow, + #[serde(rename = "WebDriver:NewWindow")] + NewWindow(NewWindow), + #[serde(rename = "WebDriver:Print")] + Print(PrintParameters), + #[serde(rename = "WebDriver:Refresh")] + Refresh, + #[serde(rename = "WebDriver:ReleaseActions")] + ReleaseActions, + #[serde(rename = "WebDriver:SendAlertText")] + SendAlertText(Keys), + #[serde(rename = "WebDriver:SetTimeouts")] + SetTimeouts(Timeouts), + #[serde(rename = "WebDriver:SetWindowRect")] + SetWindowRect(WindowRect), + #[serde(rename = "WebDriver:SwitchToFrame")] + SwitchToFrame(Frame), + #[serde(rename = "WebDriver:SwitchToParentFrame")] + SwitchToParentFrame, + #[serde(rename = "WebDriver:SwitchToWindow")] + SwitchToWindow(Window), + #[serde(rename = "WebDriver:TakeScreenshot")] + TakeElementScreenshot(ScreenshotOptions), + #[serde(rename = "WebDriver:TakeScreenshot")] + TakeFullScreenshot(ScreenshotOptions), + #[serde(rename = "WebDriver:TakeScreenshot")] + TakeScreenshot(ScreenshotOptions), +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::Date; + use crate::test::{assert_ser, assert_ser_de}; + use serde_json::json; + + #[test] + fn test_json_screenshot() { + let data = ScreenshotOptions { + id: None, + highlights: vec![], + full: false, + }; + let json = json!({"full":false,"highlights":[],"id":null}); + assert_ser_de(&data, json); + } + + #[test] + fn test_json_selector_css() { + assert_ser_de(&Selector::CSS, json!("css selector")); + } + + #[test] + fn test_json_selector_link_text() { + assert_ser_de(&Selector::LinkText, json!("link text")); + } + + #[test] + fn test_json_selector_partial_link_text() { + assert_ser_de(&Selector::PartialLinkText, json!("partial link text")); + } + + #[test] + fn test_json_selector_tag_name() { + assert_ser_de(&Selector::TagName, json!("tag name")); + } + + #[test] + fn test_json_selector_xpath() { + assert_ser_de(&Selector::XPath, json!("xpath")); + } + + #[test] + fn test_json_selector_invalid() { + assert!(serde_json::from_value::<Selector>(json!("foo")).is_err()); + } + + #[test] + fn test_json_locator() { + let json = json!({ + "using": "partial link text", + "value": "link text", + }); + let data = Locator { + using: Selector::PartialLinkText, + value: "link text".into(), + }; + + assert_ser_de(&data, json); + } + + #[test] + fn test_json_keys() { + let data = Keys { + text: "Foo".into(), + value: vec!["F".into(), "o".into(), "o".into()], + }; + let json = json!({"text": "Foo", "value": ["F", "o", "o"]}); + assert_ser_de(&data, json); + } + + #[test] + fn test_json_new_window() { + let data = NewWindow { + type_hint: Some("foo".into()), + }; + assert_ser_de(&data, json!({ "type": "foo" })); + } + + #[test] + fn test_json_window_rect() { + let data = WindowRect { + x: Some(123), + y: None, + width: None, + height: None, + }; + assert_ser_de(&data, json!({"x": 123})); + } + + #[test] + fn test_command_with_params() { + let locator = Locator { + using: Selector::CSS, + value: "value".into(), + }; + let json = json!({"WebDriver:FindElement": {"using": "css selector", "value": "value"}}); + assert_ser_de(&Command::FindElement(locator), json); + } + + #[test] + fn test_command_with_wrapper_params() { + let cookie = Cookie { + name: "hello".into(), + value: "world".into(), + path: None, + domain: None, + secure: false, + http_only: false, + expiry: Some(Date(1564488092)), + same_site: None, + }; + let json = json!({"WebDriver:AddCookie": {"cookie": {"name": "hello", "value": "world", "secure": false, "httpOnly": false, "expiry": 1564488092}}}); + assert_ser_de(&Command::AddCookie(cookie), json); + } + + #[test] + fn test_empty_commands() { + assert_ser_de(&Command::GetTimeouts, json!("WebDriver:GetTimeouts")); + } + + #[test] + fn test_json_command_invalid() { + assert!(serde_json::from_value::<Command>(json!("foo")).is_err()); + } + + #[test] + fn test_json_delete_cookie_command() { + let json = json!({"WebDriver:DeleteCookie": {"name": "foo"}}); + assert_ser_de(&Command::DeleteCookie("foo".into()), json); + } + + #[test] + fn test_json_new_window_command() { + let data = NewWindow { + type_hint: Some("foo".into()), + }; + let json = json!({"WebDriver:NewWindow": {"type": "foo"}}); + assert_ser_de(&Command::NewWindow(data), json); + } + + #[test] + fn test_json_new_window_command_with_none_value() { + let data = NewWindow { type_hint: None }; + let json = json!({"WebDriver:NewWindow": {}}); + assert_ser_de(&Command::NewWindow(data), json); + } + + #[test] + fn test_json_command_as_struct() { + assert_ser( + &Command::FindElementElement { + element: "foo".into(), + using: Selector::XPath, + value: "bar".into(), + }, + json!({"WebDriver:FindElement": {"element": "foo", "using": "xpath", "value": "bar" }}), + ); + } + + #[test] + fn test_json_get_css_value() { + assert_ser_de( + &Command::GetCSSValue { + id: "foo".into(), + property: "bar".into(), + }, + json!({"WebDriver:GetElementCSSValue": {"id": "foo", "propertyName": "bar"}}), + ); + } +} diff --git a/testing/geckodriver/moz.build b/testing/geckodriver/moz.build new file mode 100644 index 0000000000..9cda32b430 --- /dev/null +++ b/testing/geckodriver/moz.build @@ -0,0 +1,27 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +RUST_PROGRAMS += ["geckodriver"] +# Some Rust build scripts compile C/C++ sources, don't error on warnings for them. +AllowCompilerWarnings() + +RUST_TESTS = [ + "geckodriver", + "webdriver", + "marionette", + # TODO: Move to mozbase/rust/moz.build once those crates can be + # tested separately. + "mozdevice", + "mozprofile", + "mozrunner", + "mozversion", +] + +with Files("**"): + BUG_COMPONENT = ("Testing", "geckodriver") + +SPHINX_TREES["/testing/geckodriver"] = "doc" + +with Files("doc/**"): + SCHEDULES.exclusive = ["docs"] diff --git a/testing/geckodriver/src/android.rs b/testing/geckodriver/src/android.rs new file mode 100644 index 0000000000..6aaf58ec6b --- /dev/null +++ b/testing/geckodriver/src/android.rs @@ -0,0 +1,470 @@ +use crate::capabilities::AndroidOptions; +use mozdevice::{AndroidStorage, Device, Host}; +use mozprofile::profile::Profile; +use serde::Serialize; +use serde_yaml::{Mapping, Value}; +use std::fmt; +use std::io; +use std::path::PathBuf; +use std::time; +use webdriver::error::{ErrorStatus, WebDriverError}; + +// TODO: avoid port clashes across GeckoView-vehicles. +// For now, we always use target port 2829, leading to issues like bug 1533704. +const TARGET_PORT: u16 = 2829; + +const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML +## +## Auto-generated by geckodriver. +## See https://mozilla.github.io/geckoview/consumer/docs/automation. +"#; + +pub type Result<T> = std::result::Result<T, AndroidError>; + +#[derive(Debug)] +pub enum AndroidError { + ActivityNotFound(String), + Device(mozdevice::DeviceError), + IO(io::Error), + PackageNotFound(String), + Serde(serde_yaml::Error), +} + +impl fmt::Display for AndroidError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + AndroidError::ActivityNotFound(ref package) => { + write!(f, "Activity for package '{}' not found", package) + } + AndroidError::Device(ref message) => message.fmt(f), + AndroidError::IO(ref message) => message.fmt(f), + AndroidError::PackageNotFound(ref package) => { + write!(f, "Package '{}' not found", package) + } + AndroidError::Serde(ref message) => message.fmt(f), + } + } +} + +impl From<io::Error> for AndroidError { + fn from(value: io::Error) -> AndroidError { + AndroidError::IO(value) + } +} + +impl From<mozdevice::DeviceError> for AndroidError { + fn from(value: mozdevice::DeviceError) -> AndroidError { + AndroidError::Device(value) + } +} + +impl From<serde_yaml::Error> for AndroidError { + fn from(value: serde_yaml::Error) -> AndroidError { + AndroidError::Serde(value) + } +} + +impl From<AndroidError> for WebDriverError { + fn from(value: AndroidError) -> WebDriverError { + WebDriverError::new(ErrorStatus::UnknownError, value.to_string()) + } +} + +/// A remote Gecko instance. +/// +/// Host refers to the device running `geckodriver`. Target refers to the +/// Android device running Gecko in a GeckoView-based vehicle. +#[derive(Debug)] +pub struct AndroidProcess { + pub device: Device, + pub package: String, + pub activity: String, +} + +impl AndroidProcess { + pub fn new( + device: Device, + package: String, + activity: String, + ) -> mozdevice::Result<AndroidProcess> { + Ok(AndroidProcess { + device, + package, + activity, + }) + } +} + +#[derive(Debug)] +pub struct AndroidHandler { + pub config: PathBuf, + pub options: AndroidOptions, + pub process: AndroidProcess, + pub profile: PathBuf, + pub test_root: PathBuf, + + // For port forwarding host => target + pub host_port: u16, + pub target_port: u16, +} + +impl Drop for AndroidHandler { + fn drop(&mut self) { + // Try to clean up various settings + let clear_command = format!("am clear-debug-app {}", self.process.package); + match self + .process + .device + .execute_host_shell_command(&clear_command) + { + Ok(_) => debug!("Disabled reading from configuration file"), + Err(e) => error!("Failed disabling from configuration file: {}", e), + } + + match self.process.device.remove(&self.config) { + Ok(_) => debug!("Deleted GeckoView configuration file"), + Err(e) => error!("Failed deleting GeckoView configuration file: {}", e), + } + + match self.process.device.kill_forward_port(self.host_port) { + Ok(_) => debug!( + "Android port forward ({} -> {}) stopped", + &self.host_port, &self.target_port + ), + Err(e) => error!( + "Android port forward ({} -> {}) failed to stop: {}", + &self.host_port, &self.target_port, e + ), + } + } +} + +impl AndroidHandler { + pub fn new(options: &AndroidOptions, host_port: u16) -> Result<AndroidHandler> { + // We need to push profile.pathbuf to a safe space on the device. + // Make it per-Android package to avoid clashes and confusion. + // This naming scheme follows GeckoView's configuration file naming scheme, + // see bug 1533385. + + let host = Host { + host: None, + port: None, + read_timeout: Some(time::Duration::from_millis(5000)), + write_timeout: Some(time::Duration::from_millis(5000)), + }; + + let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?; + + // Set up port forward. Port forwarding will be torn down, if possible, + device.forward_port(host_port, TARGET_PORT)?; + debug!( + "Android port forward ({} -> {}) started", + host_port, TARGET_PORT + ); + + let test_root = match device.storage { + AndroidStorage::App => { + device.run_as_package = Some(options.package.to_owned()); + let mut buf = PathBuf::from("/data/data"); + buf.push(&options.package); + buf.push("test_root"); + buf + } + AndroidStorage::Internal => PathBuf::from("/data/local/tmp/test_root"), + AndroidStorage::Sdcard => PathBuf::from("/mnt/sdcard/test_root"), + }; + + debug!( + "Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}", + options, + device.storage, + test_root.display(), + device.run_as_package + ); + + let mut profile = test_root.clone(); + profile.push(format!("{}-geckodriver-profile", &options.package)); + + // Check if the specified package is installed + let response = + device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?; + let packages = response + .trim() + .split_terminator('\n') + .filter(|line| line.starts_with("package:")) + .map(|line| line.rsplit(':').next().expect("Package name found")) + .collect::<Vec<&str>>(); + if !packages.contains(&options.package.as_str()) { + return Err(AndroidError::PackageNotFound(options.package.clone())); + } + + let config = PathBuf::from(format!( + "/data/local/tmp/{}-geckoview-config.yaml", + &options.package + )); + + // If activity hasn't been specified default to the main activity of the package + let activity = match options.activity { + Some(ref activity) => activity.clone(), + None => { + let response = device.execute_host_shell_command(&format!( + "cmd package resolve-activity --brief {}", + &options.package + ))?; + let activities = response + .split_terminator('\n') + .filter(|line| line.starts_with(&options.package)) + .map(|line| line.rsplit('/').next().unwrap()) + .collect::<Vec<&str>>(); + if activities.is_empty() { + return Err(AndroidError::ActivityNotFound(options.package.clone())); + } + + activities[0].to_owned() + } + }; + + let process = AndroidProcess::new(device, options.package.clone(), activity)?; + + Ok(AndroidHandler { + options: options.clone(), + config, + process, + profile, + test_root, + host_port, + target_port: TARGET_PORT, + }) + } + + pub fn generate_config_file<I, K, V>(&self, envs: I) -> Result<String> + where + I: IntoIterator<Item = (K, V)>, + K: ToString, + V: ToString, + { + // To configure GeckoView, we use the automation techniques documented at + // https://mozilla.github.io/geckoview/consumer/docs/automation. + #[derive(Serialize, Deserialize, PartialEq, Debug)] + pub struct Config { + pub env: Mapping, + pub args: Value, + } + + // TODO: Allow to write custom arguments and preferences from moz:firefoxOptions + let mut config = Config { + args: Value::Sequence(vec![ + Value::String("--marionette".into()), + Value::String("--profile".into()), + Value::String(self.profile.display().to_string()), + ]), + env: Mapping::new(), + }; + + for (key, value) in envs { + config.env.insert( + Value::String(key.to_string()), + Value::String(value.to_string()), + ); + } + + config.env.insert( + Value::String("MOZ_CRASHREPORTER".to_owned()), + Value::String("1".to_owned()), + ); + config.env.insert( + Value::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()), + Value::String("1".to_owned()), + ); + config.env.insert( + Value::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()), + Value::String("1".to_owned()), + ); + + let mut contents: Vec<String> = vec![CONFIG_FILE_HEADING.to_owned()]; + contents.push(serde_yaml::to_string(&config)?); + + Ok(contents.concat()) + } + + pub fn prepare<I, K, V>(&self, profile: &Profile, env: I) -> Result<()> + where + I: IntoIterator<Item = (K, V)>, + K: ToString, + V: ToString, + { + self.process.device.clear_app_data(&self.process.package)?; + + // These permissions, at least, are required to read profiles in /mnt/sdcard. + for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] { + self.process.device.execute_host_shell_command(&format!( + "pm grant {} android.permission.{}", + &self.process.package, perm + ))?; + } + + // Make sure to create the test root. + self.process.device.create_dir(&self.test_root)?; + self.process.device.chmod(&self.test_root, "777", true)?; + + // Replace the profile + self.process.device.remove(&self.profile)?; + self.process + .device + .push_dir(&profile.path, &self.profile, 0o777)?; + + let contents = self.generate_config_file(env)?; + debug!("Content of generated GeckoView config file:\n{}", contents); + let reader = &mut io::BufReader::new(contents.as_bytes()); + + debug!( + "Pushing GeckoView configuration file to {}", + self.config.display() + ); + self.process.device.push(reader, &self.config, 0o777)?; + + // Tell GeckoView to read configuration even when `android:debuggable="false"`. + self.process.device.execute_host_shell_command(&format!( + "am set-debug-app --persistent {}", + self.process.package + ))?; + + Ok(()) + } + + pub fn launch(&self) -> Result<()> { + // TODO: Remove the usage of intent arguments once Fennec is no longer + // supported. Packages which are using GeckoView always read the arguments + // via the YAML configuration file. + let mut intent_arguments = self + .options + .intent_arguments + .clone() + .unwrap_or_else(|| Vec::with_capacity(3)); + intent_arguments.push("--es".to_owned()); + intent_arguments.push("args".to_owned()); + intent_arguments.push(format!("--marionette --profile {}", self.profile.display())); + + debug!( + "Launching {}/{}", + self.process.package, self.process.activity + ); + self.process + .device + .launch( + &self.process.package, + &self.process.activity, + &intent_arguments, + ) + .map_err(|e| { + let message = format!( + "Could not launch Android {}/{}: {}", + self.process.package, self.process.activity, e + ); + mozdevice::DeviceError::Adb(message) + })?; + + Ok(()) + } + + pub fn force_stop(&self) -> Result<()> { + debug!( + "Force stopping the Android package: {}", + &self.process.package + ); + self.process.device.force_stop(&self.process.package)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + // To successfully run those tests the geckoview_example package needs to + // be installed on the device or emulator. After setting up the build + // environment (https://mzl.la/3muLv5M), the following mach commands have to + // be executed: + // + // $ ./mach build && ./mach install + // + // Currently the mozdevice API is not safe for multiple requests at the same + // time. It is recommended to run each of the unit tests on its own. Also adb + // specific tests cannot be run in CI yet. To check those locally, also run + // the ignored tests. + // + // Use the following command to accomplish that: + // + // $ cargo test -- --ignored --test-threads=1 + + use crate::android::AndroidHandler; + use crate::capabilities::AndroidOptions; + use mozdevice::{AndroidStorage, AndroidStorageInput}; + use std::path::PathBuf; + + fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) { + let options = AndroidOptions::new(package.to_owned(), storage); + let handler = AndroidHandler::new(&options, 4242).expect("has valid Android handler"); + + assert_eq!(handler.options, options); + assert_eq!(handler.process.package, package); + + let expected_config_path = PathBuf::from(format!( + "/data/local/tmp/{}-geckoview-config.yaml", + &package + )); + assert_eq!(handler.config, expected_config_path); + + if handler.process.device.storage == AndroidStorage::App { + assert_eq!( + handler.process.device.run_as_package, + Some(package.to_owned()) + ); + } else { + assert_eq!(handler.process.device.run_as_package, None); + } + + let test_root = match handler.process.device.storage { + AndroidStorage::App => { + let mut buf = PathBuf::from("/data/data"); + buf.push(&package); + buf.push("test_root"); + buf + } + AndroidStorage::Internal => PathBuf::from("/data/local/tmp/test_root"), + AndroidStorage::Sdcard => PathBuf::from("/mnt/sdcard/test_root"), + }; + assert_eq!(handler.test_root, test_root); + + let mut profile = test_root.clone(); + profile.push(format!("{}-geckodriver-profile", &package)); + assert_eq!(handler.profile, profile); + } + + #[test] + #[ignore] + fn android_handler_storage_as_app() { + let package = "org.mozilla.geckoview_example"; + run_handler_storage_test(&package, AndroidStorageInput::App); + } + + #[test] + #[ignore] + fn android_handler_storage_as_auto() { + let package = "org.mozilla.geckoview_example"; + run_handler_storage_test(package, AndroidStorageInput::Auto); + } + + #[test] + #[ignore] + fn android_handler_storage_as_internal() { + let package = "org.mozilla.geckoview_example"; + run_handler_storage_test(package, AndroidStorageInput::Internal); + } + + #[test] + #[ignore] + fn android_handler_storage_as_sdcard() { + let package = "org.mozilla.geckoview_example"; + run_handler_storage_test(package, AndroidStorageInput::Sdcard); + } +} diff --git a/testing/geckodriver/src/build.rs b/testing/geckodriver/src/build.rs new file mode 100644 index 0000000000..7ba3144755 --- /dev/null +++ b/testing/geckodriver/src/build.rs @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use serde_json::Value; +use std::fmt; + +include!(concat!(env!("OUT_DIR"), "/build-info.rs")); + +pub struct BuildInfo; + +impl BuildInfo { + pub fn version() -> &'static str { + crate_version!() + } + + pub fn hash() -> Option<&'static str> { + COMMIT_HASH + } + + pub fn date() -> Option<&'static str> { + COMMIT_DATE + } +} + +impl fmt::Display for BuildInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", BuildInfo::version())?; + match (BuildInfo::hash(), BuildInfo::date()) { + (Some(hash), Some(date)) => write!(f, " ({} {})", hash, date)?, + (Some(hash), None) => write!(f, " ({})", hash)?, + _ => {} + } + Ok(()) + } +} + +// TODO(Henrik): Change into From +//std::convert::From<&str>` is not implemented for `rustc_serialize::json::Json +impl Into<Value> for BuildInfo { + fn into(self) -> Value { + Value::String(BuildInfo::version().to_string()) + } +} + +/// Returns build-time information about geckodriver. +pub fn build_info() -> BuildInfo { + BuildInfo {} +} diff --git a/testing/geckodriver/src/capabilities.rs b/testing/geckodriver/src/capabilities.rs new file mode 100644 index 0000000000..e21651ea61 --- /dev/null +++ b/testing/geckodriver/src/capabilities.rs @@ -0,0 +1,1088 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::command::LogOptions; +use crate::logging::Level; +use base64; +use mozdevice::AndroidStorageInput; +use mozprofile::preferences::Pref; +use mozprofile::profile::Profile; +use mozrunner::runner::platform::firefox_default_path; +use mozversion::{self, firefox_binary_version, firefox_version, Version}; +use regex::bytes::Regex; +use serde_json::{Map, Value}; +use std::collections::BTreeMap; +use std::default::Default; +use std::fmt::{self, Display}; +use std::fs; +use std::io; +use std::io::BufWriter; +use std::io::Cursor; +use std::path::{Path, PathBuf}; +use std::str::{self, FromStr}; +use webdriver::capabilities::{BrowserCapabilities, Capabilities}; +use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; +use zip; + +#[derive(Clone, Debug)] +enum VersionError { + VersionError(mozversion::Error), + MissingBinary, +} + +impl From<mozversion::Error> for VersionError { + fn from(err: mozversion::Error) -> VersionError { + VersionError::VersionError(err) + } +} + +impl Display for VersionError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + VersionError::VersionError(ref x) => x.fmt(f), + VersionError::MissingBinary => "No binary provided".fmt(f), + } + } +} + +impl From<VersionError> for WebDriverError { + fn from(err: VersionError) -> WebDriverError { + WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string()) + } +} + +/// Provides matching of `moz:firefoxOptions` and resolutionnized of which Firefox +/// binary to use. +/// +/// `FirefoxCapabilities` is constructed with the fallback binary, should +/// `moz:firefoxOptions` not contain a binary entry. This may either be the +/// system Firefox installation or an override, for example given to the +/// `--binary` flag of geckodriver. +pub struct FirefoxCapabilities<'a> { + pub chosen_binary: Option<PathBuf>, + fallback_binary: Option<&'a PathBuf>, + version_cache: BTreeMap<PathBuf, Result<Version, VersionError>>, +} + +impl<'a> FirefoxCapabilities<'a> { + pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> { + FirefoxCapabilities { + chosen_binary: None, + fallback_binary, + version_cache: BTreeMap::new(), + } + } + + fn set_binary(&mut self, capabilities: &Map<String, Value>) { + self.chosen_binary = capabilities + .get("moz:firefoxOptions") + .and_then(|x| x.get("binary")) + .and_then(|x| x.as_str()) + .map(PathBuf::from) + .or_else(|| self.fallback_binary.cloned()) + .or_else(firefox_default_path); + } + + fn version(&mut self, binary: Option<&Path>) -> Result<Version, VersionError> { + if let Some(binary) = binary { + if let Some(cache_value) = self.version_cache.get(binary) { + return cache_value.clone(); + } + let rv = self + .version_from_ini(binary) + .or_else(|_| self.version_from_binary(binary)); + if let Ok(ref version) = rv { + debug!("Found version {}", version); + } else { + debug!("Failed to get binary version"); + } + self.version_cache.insert(binary.to_path_buf(), rv.clone()); + rv + } else { + Err(VersionError::MissingBinary) + } + } + + fn version_from_ini(&self, binary: &Path) -> Result<Version, VersionError> { + debug!("Trying to read firefox version from ini files"); + let version = firefox_version(binary)?; + if let Some(version_string) = version.version_string { + Version::from_str(&version_string).map_err(|err| err.into()) + } else { + Err(VersionError::VersionError( + mozversion::Error::MetadataError("Missing version string".into()), + )) + } + } + + fn version_from_binary(&self, binary: &Path) -> Result<Version, VersionError> { + debug!("Trying to read firefox version from binary"); + Ok(firefox_binary_version(binary)?) + } +} + +impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> { + fn init(&mut self, capabilities: &Capabilities) { + self.set_binary(capabilities); + } + + fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> { + Ok(Some("firefox".into())) + } + + fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> { + let binary = self.chosen_binary.clone(); + self.version(binary.as_ref().map(|x| x.as_ref())) + .map_err(|err| err.into()) + .map(|x| Some(x.to_string())) + } + + fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> { + Ok(if cfg!(target_os = "windows") { + Some("windows".into()) + } else if cfg!(target_os = "macos") { + Some("mac".into()) + } else if cfg!(target_os = "linux") { + Some("linux".into()) + } else { + None + }) + } + + fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn compare_browser_version( + &mut self, + version: &str, + comparison: &str, + ) -> WebDriverResult<bool> { + Version::from_str(version) + .map_err(|err| VersionError::from(err))? + .matches(comparison) + .map_err(|err| VersionError::from(err).into()) + } + + fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } + + fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> { + if !name.starts_with("moz:") { + return Ok(()); + } + match name { + "moz:firefoxOptions" => { + let data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "moz:firefoxOptions is not an object" + ); + for (key, value) in data.iter() { + match &**key { + "androidActivity" + | "androidDeviceSerial" + | "androidPackage" + | "profile" => { + if !value.is_string() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} is not a string", &**key), + )); + } + } + "androidIntentArguments" | "args" => { + if !try_opt!( + value.as_array(), + ErrorStatus::InvalidArgument, + format!("{} is not an array", &**key) + ) + .iter() + .all(|value| value.is_string()) + { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} entry is not a string", &**key), + )); + } + } + "binary" => { + if let Some(binary) = value.as_str() { + if !data.contains_key("androidPackage") + && self.version(Some(Path::new(binary))).is_err() + { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} is not a Firefox executable", &**key), + )); + } + } else { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("{} is not a string", &**key), + )); + } + } + "env" => { + let env_data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "env value is not an object" + ); + if !env_data.values().all(Value::is_string) { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Environment values were not all strings", + )); + } + } + "log" => { + let log_data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "log value is not an object" + ); + for (log_key, log_value) in log_data.iter() { + match &**log_key { + "level" => { + let level = try_opt!( + log_value.as_str(), + ErrorStatus::InvalidArgument, + "log level is not a string" + ); + if Level::from_str(level).is_err() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Not a valid log level: {}", level), + )); + } + } + x => { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Invalid log field {}", x), + )) + } + } + } + } + "prefs" => { + let prefs_data = try_opt!( + value.as_object(), + ErrorStatus::InvalidArgument, + "prefs value is not an object" + ); + let is_pref_value_type = |x: &Value| { + x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean() + }; + if !prefs_data.values().all(is_pref_value_type) { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Preference values not all string or integer or boolean", + )); + } + } + x => { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Invalid moz:firefoxOptions field {}", x), + )) + } + } + } + } + "moz:useNonSpecCompliantPointerOrigin" => { + if !value.is_boolean() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:useNonSpecCompliantPointerOrigin is not a boolean", + )); + } + } + "moz:webdriverClick" => { + if !value.is_boolean() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:webdriverClick is not a boolean", + )); + } + } + "moz:debuggerAddress" => { + if !value.is_boolean() { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:debuggerAddress is not a boolean", + )); + } + } + _ => { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + format!("Unrecognised option {}", name), + )) + } + } + Ok(()) + } + + fn accept_custom(&mut self, _: &str, _: &Value, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) + } +} + +/// Android-specific options in the `moz:firefoxOptions` struct. +/// These map to "androidCamelCase", following [chromedriver's Android-specific +/// Capabilities](http://chromedriver.chromium.org/getting-started/getting-started---android). +#[derive(Default, Clone, Debug, PartialEq)] +pub struct AndroidOptions { + pub activity: Option<String>, + pub device_serial: Option<String>, + pub intent_arguments: Option<Vec<String>>, + pub package: String, + pub storage: AndroidStorageInput, +} + +impl AndroidOptions { + pub fn new(package: String, storage: AndroidStorageInput) -> AndroidOptions { + AndroidOptions { + package, + storage, + ..Default::default() + } + } +} + +/// Rust representation of `moz:firefoxOptions`. +/// +/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes +/// the encoded profile, the binary arguments, log settings, and additional +/// preferences to be checked and unmarshaled from the `moz:firefoxOptions` +/// JSON Object into a Rust representation. +#[derive(Default, Debug)] +pub struct FirefoxOptions { + pub binary: Option<PathBuf>, + pub profile: Option<Profile>, + pub args: Option<Vec<String>>, + pub env: Option<Vec<(String, String)>>, + pub log: LogOptions, + pub prefs: Vec<(String, Pref)>, + pub android: Option<AndroidOptions>, +} + +impl FirefoxOptions { + pub fn new() -> FirefoxOptions { + Default::default() + } + + pub fn from_capabilities( + binary_path: Option<PathBuf>, + android_storage: AndroidStorageInput, + matched: &mut Capabilities, + ) -> WebDriverResult<FirefoxOptions> { + let mut rv = FirefoxOptions::new(); + rv.binary = binary_path; + + if let Some(json) = matched.remove("moz:firefoxOptions") { + let options = json.as_object().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "'moz:firefoxOptions' \ + capability is not an object", + ) + })?; + + rv.android = FirefoxOptions::load_android(android_storage, &options)?; + rv.args = FirefoxOptions::load_args(&options)?; + rv.env = FirefoxOptions::load_env(&options)?; + rv.log = FirefoxOptions::load_log(&options)?; + rv.prefs = FirefoxOptions::load_prefs(&options)?; + rv.profile = FirefoxOptions::load_profile(&options)?; + } + + if let Some(json) = matched.remove("moz:debuggerAddress") { + let use_web_socket = json.as_bool().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "moz:debuggerAddress is not a boolean", + ) + })?; + + if use_web_socket { + let mut remote_args = Vec::new(); + remote_args.push("--remote-debugging-port".to_owned()); + remote_args.push("0".to_owned()); + + if let Some(ref mut args) = rv.args { + args.append(&mut remote_args); + } else { + rv.args = Some(remote_args); + } + + // Force Fission disabled until Remote Agent is compatible, + // and preference hasn't been already set + let has_fission_pref = rv.prefs.iter().find(|&x| x.0 == "fission.autostart"); + if has_fission_pref.is_none() { + rv.prefs + .push(("fission.autostart".to_owned(), Pref::new(false))); + } + } + } + + Ok(rv) + } + + fn load_profile(options: &Capabilities) -> WebDriverResult<Option<Profile>> { + if let Some(profile_json) = options.get("profile") { + let profile_base64 = profile_json.as_str().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string") + })?; + let profile_zip = &*base64::decode(profile_base64)?; + + // Create an emtpy profile directory + let profile = Profile::new()?; + unzip_buffer( + profile_zip, + profile + .temp_dir + .as_ref() + .expect("Profile doesn't have a path") + .path(), + )?; + + Ok(Some(profile)) + } else { + Ok(None) + } + } + + fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> { + if let Some(args_json) = options.get("args") { + let args_array = args_json.as_array().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Arguments were not an \ + array", + ) + })?; + let args = args_array + .iter() + .map(|x| x.as_str().map(|x| x.to_owned())) + .collect::<Option<Vec<String>>>() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Arguments entries were not all strings", + ) + })?; + Ok(Some(args)) + } else { + Ok(None) + } + } + + pub fn load_env(options: &Capabilities) -> WebDriverResult<Option<Vec<(String, String)>>> { + if let Some(env_data) = options.get("env") { + let env = env_data.as_object().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Env was not an object") + })?; + let mut rv = Vec::with_capacity(env.len()); + for (key, value) in env.iter() { + rv.push(( + key.clone(), + value + .as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Env value is not a string", + ) + })? + .to_string(), + )); + } + Ok(Some(rv)) + } else { + Ok(None) + } + } + + fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> { + if let Some(json) = options.get("log") { + let log = json.as_object().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Log section is not an object") + })?; + + let level = match log.get("level") { + Some(json) => { + let s = json.as_str().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "Log level is not a string", + ) + })?; + Some(Level::from_str(s).ok().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Log level is unknown") + })?) + } + None => None, + }; + + Ok(LogOptions { level }) + } else { + Ok(Default::default()) + } + } + + pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> { + if let Some(prefs_data) = options.get("prefs") { + let prefs = prefs_data.as_object().ok_or_else(|| { + WebDriverError::new(ErrorStatus::InvalidArgument, "Prefs were not an object") + })?; + let mut rv = Vec::with_capacity(prefs.len()); + for (key, value) in prefs.iter() { + rv.push((key.clone(), pref_from_json(value)?)); + } + Ok(rv) + } else { + Ok(vec![]) + } + } + + pub fn load_android( + storage: AndroidStorageInput, + options: &Capabilities, + ) -> WebDriverResult<Option<AndroidOptions>> { + if let Some(package_json) = options.get("androidPackage") { + let package = package_json + .as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidPackage is not a string", + ) + })? + .to_owned(); + + // https://developer.android.com/studio/build/application-id + let package_regexp = + Regex::new(r#"^([a-zA-Z][a-zA-Z0-9_]*\.){1,}([a-zA-Z][a-zA-Z0-9_]*)$"#).unwrap(); + if !package_regexp.is_match(package.as_bytes()) { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "Not a valid androidPackage name", + )); + } + + let mut android = AndroidOptions::new(package, storage); + + android.activity = match options.get("androidActivity") { + Some(json) => { + let activity = json + .as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidActivity is not a string", + ) + })? + .to_owned(); + + if activity.contains("/") { + return Err(WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidActivity should not contain '/", + )); + } + + Some(activity) + } + None => None, + }; + + android.device_serial = match options.get("androidDeviceSerial") { + Some(json) => Some( + json.as_str() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidDeviceSerial is not a string", + ) + })? + .to_owned(), + ), + None => None, + }; + + android.intent_arguments = match options.get("androidIntentArguments") { + Some(json) => { + let args_array = json.as_array().ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidIntentArguments is not an array", + ) + })?; + let args = args_array + .iter() + .map(|x| x.as_str().map(|x| x.to_owned())) + .collect::<Option<Vec<String>>>() + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::InvalidArgument, + "androidIntentArguments entries are not all strings", + ) + })?; + + Some(args) + } + None => None, + }; + + Ok(Some(android)) + } else { + Ok(None) + } + } +} + +fn pref_from_json(value: &Value) -> WebDriverResult<Pref> { + match *value { + Value::String(ref x) => Ok(Pref::new(x.clone())), + Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())), + Value::Bool(x) => Ok(Pref::new(x)), + _ => Err(WebDriverError::new( + ErrorStatus::UnknownError, + "Could not convert pref value to string, boolean, or integer", + )), + } +} + +fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> { + let reader = Cursor::new(buf); + let mut zip = zip::ZipArchive::new(reader) + .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile"))?; + + for i in 0..zip.len() { + let mut file = zip.by_index(i).map_err(|_| { + WebDriverError::new( + ErrorStatus::UnknownError, + "Processing profile zip file failed", + ) + })?; + let unzip_path = { + let name = file.name(); + let is_dir = name.ends_with('/'); + let rel_path = Path::new(name); + let dest_path = dest_dir.join(rel_path); + + { + let create_dir = if is_dir { + Some(dest_path.as_path()) + } else { + dest_path.parent() + }; + if let Some(dir) = create_dir { + if !dir.exists() { + debug!("Creating profile directory tree {}", dir.to_string_lossy()); + fs::create_dir_all(dir)?; + } + } + } + + if is_dir { + None + } else { + Some(dest_path) + } + }; + + if let Some(unzip_path) = unzip_path { + debug!("Extracting profile to {}", unzip_path.to_string_lossy()); + let dest = fs::File::create(unzip_path)?; + if file.size() > 0 { + let mut writer = BufWriter::new(dest); + io::copy(&mut file, &mut writer)?; + } + } + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + extern crate mozprofile; + + use self::mozprofile::preferences::Pref; + use super::*; + use crate::marionette::MarionetteHandler; + use mozdevice::AndroidStorageInput; + use serde_json::json; + use std::default::Default; + use std::fs::File; + use std::io::Read; + + use webdriver::capabilities::Capabilities; + + fn example_profile() -> Value { + let mut profile_data = Vec::with_capacity(1024); + let mut profile = File::open("src/tests/profile.zip").unwrap(); + profile.read_to_end(&mut profile_data).unwrap(); + Value::String(base64::encode(&profile_data)) + } + + fn make_options(firefox_opts: Capabilities) -> WebDriverResult<FirefoxOptions> { + let mut caps = Capabilities::new(); + caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts)); + + FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + } + + #[test] + fn fx_options_default() { + let opts = FirefoxOptions::new(); + assert_eq!(opts.android, None); + assert_eq!(opts.args, None); + assert_eq!(opts.binary, None); + assert_eq!(opts.log, LogOptions { level: None }); + assert_eq!(opts.prefs, vec![]); + // Profile doesn't support PartialEq + // assert_eq!(opts.profile, None); + } + + #[test] + fn fx_options_from_capabilities_no_binary_and_caps() { + let mut caps = Capabilities::new(); + + let opts = + FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps).unwrap(); + assert_eq!(opts.android, None); + assert_eq!(opts.args, None); + assert_eq!(opts.binary, None); + assert_eq!(opts.log, LogOptions { level: None }); + assert_eq!(opts.prefs, vec![]); + } + + #[test] + fn fx_options_from_capabilities_with_binary_and_caps() { + let mut caps = Capabilities::new(); + caps.insert( + "moz:firefoxOptions".into(), + Value::Object(Capabilities::new()), + ); + + let binary = PathBuf::from("foo"); + + let opts = FirefoxOptions::from_capabilities( + Some(binary.clone()), + AndroidStorageInput::Auto, + &mut caps, + ) + .unwrap(); + assert_eq!(opts.android, None); + assert_eq!(opts.args, None); + assert_eq!(opts.binary, Some(binary)); + assert_eq!(opts.log, LogOptions { level: None }); + assert_eq!(opts.prefs, vec![]); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_not_set() { + let mut caps = Capabilities::new(); + + let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect("Valid Firefox options"); + + assert!( + opts.args.is_none(), + "CLI arguments for Firefox unexpectedly found" + ); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_false() { + let mut caps = Capabilities::new(); + caps.insert("moz:debuggerAddress".into(), json!(false)); + + let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect("Valid Firefox options"); + + assert!( + opts.args.is_none(), + "CLI arguments for remote protocol unexpectedly found" + ); + } + + #[test] + fn fx_options_from_capabilities_with_debugger_address_true() { + let mut caps = Capabilities::new(); + caps.insert("moz:debuggerAddress".into(), json!(true)); + + let opts = FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect("Valid Firefox options"); + + if let Some(args) = opts.args { + let mut iter = args.iter(); + assert!(iter + .find(|&arg| arg == &"--remote-debugging-port".to_owned()) + .is_some()); + assert_eq!(iter.next(), Some(&"0".to_owned())); + } else { + assert!(false, "CLI arguments for remote protocol not found"); + } + + assert!(opts + .prefs + .iter() + .any(|pref| pref == &("fission.autostart".to_owned(), Pref::new(false)))); + } + + #[test] + fn fx_options_from_capabilities_with_invalid_caps() { + let mut caps = Capabilities::new(); + caps.insert("moz:firefoxOptions".into(), json!(42)); + + FirefoxOptions::from_capabilities(None, AndroidStorageInput::Auto, &mut caps) + .expect_err("Firefox options need to be of type object"); + } + + #[test] + fn fx_options_android_no_package() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidAvtivity".into(), json!("foo")); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + assert_eq!(opts.android, None); + } + + #[test] + fn fx_options_android_package_valid_value() { + for value in ["foo.bar", "foo.bar.cheese.is.good", "Foo.Bar_9"].iter() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!(value)); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + assert_eq!( + opts.android, + Some(AndroidOptions::new( + value.to_string(), + AndroidStorageInput::Auto + )) + ); + } + } + + #[test] + fn fx_options_android_package_invalid_type() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_package_invalid_value() { + for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!(value)); + make_options(firefox_opts).expect_err("invalid firefox options"); + } + } + + #[test] + fn fx_options_android_activity_valid_value() { + for value in ["cheese", "Cheese_9"].iter() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidActivity".into(), json!(value)); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let android_opts = AndroidOptions { + package: "foo.bar".to_owned(), + activity: Some(value.to_string()), + ..Default::default() + }; + assert_eq!(opts.android, Some(android_opts)); + } + } + + #[test] + fn fx_options_android_activity_invalid_type() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidActivity".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_activity_invalid_value() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese")); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_device_serial() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidDeviceSerial".into(), json!("cheese")); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let android_opts = AndroidOptions { + package: "foo.bar".to_owned(), + device_serial: Some("cheese".to_owned()), + ..Default::default() + }; + assert_eq!(opts.android, Some(android_opts)); + } + + #[test] + fn fx_options_android_serial_invalid() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidDeviceSerial".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_intent_arguments() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"])); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let android_opts = AndroidOptions { + package: "foo.bar".to_owned(), + intent_arguments: Some(vec!["lorem".to_owned(), "ipsum".to_owned()]), + ..Default::default() + }; + assert_eq!(opts.android, Some(android_opts)); + } + + #[test] + fn fx_options_android_intent_arguments_no_array() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidIntentArguments".into(), json!(42)); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_android_intent_arguments_invalid_value() { + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("androidPackage".into(), json!("foo.bar")); + firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42])); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_env() { + let mut env: Map<String, Value> = Map::new(); + env.insert("TEST_KEY_A".into(), Value::String("test_value_a".into())); + env.insert("TEST_KEY_B".into(), Value::String("test_value_b".into())); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("env".into(), env.into()); + + let mut opts = make_options(firefox_opts).expect("valid firefox options"); + for sorted in opts.env.iter_mut() { + sorted.sort() + } + assert_eq!( + opts.env, + Some(vec![ + ("TEST_KEY_A".into(), "test_value_a".into()), + ("TEST_KEY_B".into(), "test_value_b".into()), + ]) + ); + } + + #[test] + fn fx_options_env_invalid_container() { + let env = Value::Number(1.into()); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("env".into(), env.into()); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn fx_options_env_invalid_value() { + let mut env: Map<String, Value> = Map::new(); + env.insert("TEST_KEY".into(), Value::Number(1.into())); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("env".into(), env.into()); + + make_options(firefox_opts).expect_err("invalid firefox options"); + } + + #[test] + fn test_profile() { + let encoded_profile = example_profile(); + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("profile".into(), encoded_profile); + + let opts = make_options(firefox_opts).expect("valid firefox options"); + let mut profile = opts.profile.expect("valid firefox profile"); + let prefs = profile.user_prefs().expect("valid preferences"); + + println!("{:#?}", prefs.prefs); + + assert_eq!( + prefs.get("startup.homepage_welcome_url"), + Some(&Pref::new("data:text/html,PASS")) + ); + } + + #[test] + fn test_prefs() { + let encoded_profile = example_profile(); + let mut prefs: Map<String, Value> = Map::new(); + prefs.insert( + "browser.display.background_color".into(), + Value::String("#00ff00".into()), + ); + + let mut firefox_opts = Capabilities::new(); + firefox_opts.insert("profile".into(), encoded_profile); + firefox_opts.insert("prefs".into(), Value::Object(prefs)); + + let opts = make_options(firefox_opts).expect("valid profile and prefs"); + let mut profile = opts.profile.expect("valid firefox profile"); + + let handler = MarionetteHandler::new(Default::default()); + handler + .set_prefs(2828, &mut profile, true, opts.prefs) + .expect("set preferences"); + + let prefs_set = profile.user_prefs().expect("valid user preferences"); + println!("{:#?}", prefs_set.prefs); + + assert_eq!( + prefs_set.get("startup.homepage_welcome_url"), + Some(&Pref::new("data:text/html,PASS")) + ); + assert_eq!( + prefs_set.get("browser.display.background_color"), + Some(&Pref::new("#00ff00")) + ); + assert_eq!(prefs_set.get("marionette.port"), Some(&Pref::new(2828))); + } +} diff --git a/testing/geckodriver/src/command.rs b/testing/geckodriver/src/command.rs new file mode 100644 index 0000000000..f5bc27ad81 --- /dev/null +++ b/testing/geckodriver/src/command.rs @@ -0,0 +1,342 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::logging; +use base64; +use hyper::Method; +use serde::de::{self, Deserialize, Deserializer}; +use serde_json::{self, Value}; +use std::env; +use std::fs::File; +use std::io::prelude::*; +use uuid::Uuid; +use webdriver::command::{WebDriverCommand, WebDriverExtensionCommand}; +use webdriver::error::WebDriverResult; +use webdriver::httpapi::WebDriverExtensionRoute; +use webdriver::Parameters; + +pub const CHROME_ELEMENT_KEY: &str = "chromeelement-9fc5-4b51-a3c8-01716eedeb04"; + +pub fn extension_routes() -> Vec<(Method, &'static str, GeckoExtensionRoute)> { + return vec![ + ( + Method::GET, + "/session/{sessionId}/moz/context", + GeckoExtensionRoute::GetContext, + ), + ( + Method::POST, + "/session/{sessionId}/moz/context", + GeckoExtensionRoute::SetContext, + ), + ( + Method::POST, + "/session/{sessionId}/moz/addon/install", + GeckoExtensionRoute::InstallAddon, + ), + ( + Method::POST, + "/session/{sessionId}/moz/addon/uninstall", + GeckoExtensionRoute::UninstallAddon, + ), + ( + Method::GET, + "/session/{sessionId}/moz/screenshot/full", + GeckoExtensionRoute::TakeFullScreenshot, + ), + ]; +} + +#[derive(Clone, PartialEq)] +pub enum GeckoExtensionRoute { + GetContext, + SetContext, + InstallAddon, + UninstallAddon, + TakeFullScreenshot, +} + +impl WebDriverExtensionRoute for GeckoExtensionRoute { + type Command = GeckoExtensionCommand; + + fn command( + &self, + _params: &Parameters, + body_data: &Value, + ) -> WebDriverResult<WebDriverCommand<GeckoExtensionCommand>> { + use self::GeckoExtensionRoute::*; + + let command = match *self { + GetContext => GeckoExtensionCommand::GetContext, + SetContext => { + GeckoExtensionCommand::SetContext(serde_json::from_value(body_data.clone())?) + } + InstallAddon => { + GeckoExtensionCommand::InstallAddon(serde_json::from_value(body_data.clone())?) + } + UninstallAddon => { + GeckoExtensionCommand::UninstallAddon(serde_json::from_value(body_data.clone())?) + } + TakeFullScreenshot => GeckoExtensionCommand::TakeFullScreenshot, + }; + + Ok(WebDriverCommand::Extension(command)) + } +} + +#[derive(Clone)] +pub enum GeckoExtensionCommand { + GetContext, + SetContext(GeckoContextParameters), + InstallAddon(AddonInstallParameters), + UninstallAddon(AddonUninstallParameters), + TakeFullScreenshot, +} + +impl WebDriverExtensionCommand for GeckoExtensionCommand { + fn parameters_json(&self) -> Option<Value> { + use self::GeckoExtensionCommand::*; + match self { + GetContext => None, + InstallAddon(x) => Some(serde_json::to_value(x).unwrap()), + SetContext(x) => Some(serde_json::to_value(x).unwrap()), + UninstallAddon(x) => Some(serde_json::to_value(x).unwrap()), + TakeFullScreenshot => None, + } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize)] +pub struct AddonInstallParameters { + pub path: String, + pub temporary: Option<bool>, +} + +impl<'de> Deserialize<'de> for AddonInstallParameters { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Debug, Deserialize)] + #[serde(deny_unknown_fields)] + struct Base64 { + addon: String, + temporary: Option<bool>, + }; + + #[derive(Debug, Deserialize)] + #[serde(deny_unknown_fields)] + struct Path { + path: String, + temporary: Option<bool>, + }; + + #[derive(Debug, Deserialize)] + #[serde(untagged)] + enum Helper { + Base64(Base64), + Path(Path), + }; + + let params = match Helper::deserialize(deserializer)? { + Helper::Path(ref mut data) => AddonInstallParameters { + path: data.path.clone(), + temporary: data.temporary, + }, + Helper::Base64(ref mut data) => { + let content = base64::decode(&data.addon).map_err(de::Error::custom)?; + + let path = env::temp_dir() + .as_path() + .join(format!("addon-{}.xpi", Uuid::new_v4())); + let mut xpi_file = File::create(&path).map_err(de::Error::custom)?; + xpi_file + .write(content.as_slice()) + .map_err(de::Error::custom)?; + + let path = match path.to_str() { + Some(path) => path.to_string(), + None => return Err(de::Error::custom("could not write addon to file")), + }; + + AddonInstallParameters { + path, + temporary: data.temporary, + } + } + }; + + Ok(params) + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct AddonUninstallParameters { + pub id: String, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum GeckoContext { + Content, + Chrome, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct GeckoContextParameters { + pub context: GeckoContext, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct XblLocatorParameters { + pub name: String, + pub value: String, +} + +#[derive(Default, Debug, PartialEq)] +pub struct LogOptions { + pub level: Option<logging::Level>, +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + use crate::test::assert_de; + + #[test] + fn test_json_addon_install_parameters_invalid() { + assert!(serde_json::from_str::<AddonInstallParameters>("").is_err()); + assert!(serde_json::from_value::<AddonInstallParameters>(json!(null)).is_err()); + assert!(serde_json::from_value::<AddonInstallParameters>(json!({})).is_err()); + } + + #[test] + fn test_json_addon_install_parameters_with_path_and_temporary() { + let params = AddonInstallParameters { + path: "/path/to.xpi".to_string(), + temporary: Some(true), + }; + assert_de(¶ms, json!({"path": "/path/to.xpi", "temporary": true})); + } + + #[test] + fn test_json_addon_install_parameters_with_path() { + let params = AddonInstallParameters { + path: "/path/to.xpi".to_string(), + temporary: None, + }; + assert_de(¶ms, json!({"path": "/path/to.xpi"})); + } + + #[test] + fn test_json_addon_install_parameters_with_path_invalid_type() { + let json = json!({"path": true, "temporary": true}); + assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err()); + } + + #[test] + fn test_json_addon_install_parameters_with_path_and_temporary_invalid_type() { + let json = json!({"path": "/path/to.xpi", "temporary": "foo"}); + assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err()); + } + + #[test] + fn test_json_addon_install_parameters_with_addon() { + let json = json!({"addon": "aGVsbG8=", "temporary": true}); + let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap(); + + assert_eq!(data.temporary, Some(true)); + let mut file = File::open(data.path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + assert_eq!(contents, "hello"); + } + + #[test] + fn test_json_addon_install_parameters_with_addon_only() { + let json = json!({"addon": "aGVsbG8="}); + let data = serde_json::from_value::<AddonInstallParameters>(json).unwrap(); + + assert_eq!(data.temporary, None); + let mut file = File::open(data.path).unwrap(); + let mut contents = String::new(); + file.read_to_string(&mut contents).unwrap(); + assert_eq!(contents, "hello"); + } + + #[test] + fn test_json_addon_install_parameters_with_addon_invalid_type() { + let json = json!({"addon": true, "temporary": true}); + assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err()); + } + + #[test] + fn test_json_addon_install_parameters_with_addon_and_temporary_invalid_type() { + let json = json!({"addon": "aGVsbG8=", "temporary": "foo"}); + assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err()); + } + + #[test] + fn test_json_install_parameters_with_temporary_only() { + let json = json!({"temporary": true}); + assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err()); + } + + #[test] + fn test_json_addon_install_parameters_with_both_path_and_addon() { + let json = json!({ + "path": "/path/to.xpi", + "addon": "aGVsbG8=", + "temporary": true, + }); + assert!(serde_json::from_value::<AddonInstallParameters>(json).is_err()); + } + + #[test] + fn test_json_addon_uninstall_parameters_invalid() { + assert!(serde_json::from_str::<AddonUninstallParameters>("").is_err()); + assert!(serde_json::from_value::<AddonUninstallParameters>(json!(null)).is_err()); + assert!(serde_json::from_value::<AddonUninstallParameters>(json!({})).is_err()); + } + + #[test] + fn test_json_addon_uninstall_parameters() { + let params = AddonUninstallParameters { + id: "foo".to_string(), + }; + assert_de(¶ms, json!({"id": "foo"})); + } + + #[test] + fn test_json_addon_uninstall_parameters_id_invalid_type() { + let json = json!({"id": true}); + assert!(serde_json::from_value::<AddonUninstallParameters>(json).is_err()); + } + + #[test] + fn test_json_gecko_context_parameters_content() { + let params = GeckoContextParameters { + context: GeckoContext::Content, + }; + assert_de(¶ms, json!({"context": "content"})); + } + + #[test] + fn test_json_gecko_context_parameters_chrome() { + let params = GeckoContextParameters { + context: GeckoContext::Chrome, + }; + assert_de(¶ms, json!({"context": "chrome"})); + } + + #[test] + fn test_json_gecko_context_parameters_context_invalid() { + type P = GeckoContextParameters; + assert!(serde_json::from_value::<P>(json!({})).is_err()); + assert!(serde_json::from_value::<P>(json!({ "context": null })).is_err()); + assert!(serde_json::from_value::<P>(json!({"context": "foo"})).is_err()); + } +} diff --git a/testing/geckodriver/src/logging.rs b/testing/geckodriver/src/logging.rs new file mode 100644 index 0000000000..7721bb7770 --- /dev/null +++ b/testing/geckodriver/src/logging.rs @@ -0,0 +1,351 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Gecko-esque logger implementation for the [`log`] crate. +//! +//! The [`log`] crate provides a single logging API that abstracts over the +//! actual logging implementation. This module uses the logging API +//! to provide a log implementation that shares many aesthetical traits with +//! [Log.jsm] from Gecko. +//! +//! Using the [`error!`], [`warn!`], [`info!`], [`debug!`], and +//! [`trace!`] macros from `log` will output a timestamp field, followed by the +//! log level, and then the message. The fields are separated by a tab +//! character, making the output suitable for further text processing with +//! `awk(1)`. +//! +//! This module shares the same API as `log`, except it provides additional +//! entry functions [`init`] and [`init_with_level`] and additional log levels +//! `Level::Fatal` and `Level::Config`. Converting these into the +//! [`log::Level`] is lossy so that `Level::Fatal` becomes `log::Level::Error` +//! and `Level::Config` becomes `log::Level::Debug`. +//! +//! [`log`]: https://docs.rs/log/newest/log/ +//! [Log.jsm]: https://developer.mozilla.org/en/docs/Mozilla/JavaScript_code_modules/Log.jsm +//! [`error!`]: https://docs.rs/log/newest/log/macro.error.html +//! [`warn!`]: https://docs.rs/log/newest/log/macro.warn.html +//! [`info!`]: https://docs.rs/log/newest/log/macro.info.html +//! [`debug!`]: https://docs.rs/log/newest/log/macro.debug.html +//! [`trace!`]: https://docs.rs/log/newest/log/macro.trace.html +//! [`init`]: fn.init.html +//! [`init_with_level`]: fn.init_with_level.html + +use std::fmt; +use std::io; +use std::io::Write; +use std::str; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use chrono; +use log; +use mozprofile::preferences::Pref; + +static MAX_LOG_LEVEL: AtomicUsize = AtomicUsize::new(0); +const LOGGED_TARGETS: &[&str] = &[ + "geckodriver", + "mozdevice", + "mozprofile", + "mozrunner", + "mozversion", + "webdriver", +]; + +/// Logger levels from [Log.jsm]. +/// +/// [Log.jsm]: https://developer.mozilla.org/en/docs/Mozilla/JavaScript_code_modules/Log.jsm +#[repr(usize)] +#[derive(Clone, Copy, Eq, Debug, Hash, PartialEq)] +pub enum Level { + Fatal = 70, + Error = 60, + Warn = 50, + Info = 40, + Config = 30, + Debug = 20, + Trace = 10, +} + +impl From<usize> for Level { + fn from(n: usize) -> Level { + use self::Level::*; + match n { + 70 => Fatal, + 60 => Error, + 50 => Warn, + 40 => Info, + 30 => Config, + 20 => Debug, + 10 => Trace, + _ => Info, + } + } +} + +impl fmt::Display for Level { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use self::Level::*; + let s = match *self { + Fatal => "FATAL", + Error => "ERROR", + Warn => "WARN", + Info => "INFO", + Config => "CONFIG", + Debug => "DEBUG", + Trace => "TRACE", + }; + write!(f, "{}", s) + } +} + +impl str::FromStr for Level { + type Err = (); + + fn from_str(s: &str) -> Result<Level, ()> { + use self::Level::*; + match s.to_lowercase().as_ref() { + "fatal" => Ok(Fatal), + "error" => Ok(Error), + "warn" => Ok(Warn), + "info" => Ok(Info), + "config" => Ok(Config), + "debug" => Ok(Debug), + "trace" => Ok(Trace), + _ => Err(()), + } + } +} + +impl Into<log::Level> for Level { + fn into(self) -> log::Level { + use self::Level::*; + match self { + Fatal | Error => log::Level::Error, + Warn => log::Level::Warn, + Info => log::Level::Info, + Config | Debug => log::Level::Debug, + Trace => log::Level::Trace, + } + } +} + +impl Into<Pref> for Level { + fn into(self) -> Pref { + use self::Level::*; + Pref::new(match self { + Fatal => "Fatal", + Error => "Error", + Warn => "Warn", + Info => "Info", + Config => "Config", + Debug => "Debug", + Trace => "Trace", + }) + } +} + +impl From<log::Level> for Level { + fn from(log_level: log::Level) -> Level { + use log::Level::*; + match log_level { + Error => Level::Error, + Warn => Level::Warn, + Info => Level::Info, + Debug => Level::Debug, + Trace => Level::Trace, + } + } +} + +struct Logger; + +impl log::Log for Logger { + fn enabled(&self, meta: &log::Metadata) -> bool { + LOGGED_TARGETS.iter().any(|&x| meta.target().starts_with(x)) + && meta.level() <= log::max_level() + } + + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + let ts = format_ts(chrono::Local::now()); + println!( + "{}\t{}\t{}\t{}", + ts, + record.target(), + record.level(), + record.args() + ); + } + } + + fn flush(&self) { + io::stdout().flush().unwrap(); + } +} + +/// Initialises the logging subsystem with the default log level. +pub fn init() -> Result<(), log::SetLoggerError> { + init_with_level(Level::Info) +} + +/// Initialises the logging subsystem. +pub fn init_with_level(level: Level) -> Result<(), log::SetLoggerError> { + let logger = Logger {}; + set_max_level(level); + log::set_boxed_logger(Box::new(logger))?; + Ok(()) +} + +/// Returns the current maximum log level. +pub fn max_level() -> Level { + MAX_LOG_LEVEL.load(Ordering::Relaxed).into() +} + +/// Sets the global maximum log level. +pub fn set_max_level(level: Level) { + MAX_LOG_LEVEL.store(level as usize, Ordering::SeqCst); + + let slevel: log::Level = level.into(); + log::set_max_level(slevel.to_level_filter()) +} + +/// Produces a 13-digit Unix Epoch timestamp similar to Gecko. +fn format_ts(ts: chrono::DateTime<chrono::Local>) -> String { + format!("{}{:03}", ts.timestamp(), ts.timestamp_subsec_millis()) +} + +#[cfg(test)] +mod tests { + use super::{format_ts, init_with_level, max_level, set_max_level, Level}; + + use std::str::FromStr; + use std::sync::Mutex; + + use chrono; + use log; + use mozprofile::preferences::{Pref, PrefValue}; + + lazy_static! { + static ref LEVEL_MUTEX: Mutex<()> = Mutex::new(()); + } + + #[test] + fn test_level_repr() { + assert_eq!(Level::Fatal as usize, 70); + assert_eq!(Level::Error as usize, 60); + assert_eq!(Level::Warn as usize, 50); + assert_eq!(Level::Info as usize, 40); + assert_eq!(Level::Config as usize, 30); + assert_eq!(Level::Debug as usize, 20); + assert_eq!(Level::Trace as usize, 10); + } + + #[test] + fn test_level_eq() { + assert_eq!(Level::Fatal, Level::Fatal); + assert_eq!(Level::Error, Level::Error); + assert_eq!(Level::Warn, Level::Warn); + assert_eq!(Level::Info, Level::Info); + assert_eq!(Level::Config, Level::Config); + assert_eq!(Level::Debug, Level::Debug); + assert_eq!(Level::Trace, Level::Trace); + } + + #[test] + fn test_level_from_log() { + assert_eq!(Level::from(log::Level::Error), Level::Error); + assert_eq!(Level::from(log::Level::Warn), Level::Warn); + assert_eq!(Level::from(log::Level::Info), Level::Info); + assert_eq!(Level::from(log::Level::Debug), Level::Debug); + assert_eq!(Level::from(log::Level::Trace), Level::Trace); + } + + #[test] + fn test_level_into_log() { + assert_eq!(Into::<log::Level>::into(Level::Fatal), log::Level::Error); + assert_eq!(Into::<log::Level>::into(Level::Error), log::Level::Error); + assert_eq!(Into::<log::Level>::into(Level::Warn), log::Level::Warn); + assert_eq!(Into::<log::Level>::into(Level::Info), log::Level::Info); + assert_eq!(Into::<log::Level>::into(Level::Config), log::Level::Debug); + assert_eq!(Into::<log::Level>::into(Level::Debug), log::Level::Debug); + assert_eq!(Into::<log::Level>::into(Level::Trace), log::Level::Trace); + } + + #[test] + fn test_level_into_pref() { + let tests = [ + (Level::Fatal, "Fatal"), + (Level::Error, "Error"), + (Level::Warn, "Warn"), + (Level::Info, "Info"), + (Level::Config, "Config"), + (Level::Debug, "Debug"), + (Level::Trace, "Trace"), + ]; + + for &(lvl, s) in tests.iter() { + let expected = Pref { + value: PrefValue::String(s.to_string()), + sticky: false, + }; + assert_eq!(Into::<Pref>::into(lvl), expected); + } + } + + #[test] + fn test_level_from_str() { + assert_eq!(Level::from_str("fatal"), Ok(Level::Fatal)); + assert_eq!(Level::from_str("error"), Ok(Level::Error)); + assert_eq!(Level::from_str("warn"), Ok(Level::Warn)); + assert_eq!(Level::from_str("info"), Ok(Level::Info)); + assert_eq!(Level::from_str("config"), Ok(Level::Config)); + assert_eq!(Level::from_str("debug"), Ok(Level::Debug)); + assert_eq!(Level::from_str("trace"), Ok(Level::Trace)); + + assert_eq!(Level::from_str("INFO"), Ok(Level::Info)); + + assert!(Level::from_str("foo").is_err()); + } + + #[test] + fn test_level_to_str() { + assert_eq!(Level::Fatal.to_string(), "FATAL"); + assert_eq!(Level::Error.to_string(), "ERROR"); + assert_eq!(Level::Warn.to_string(), "WARN"); + assert_eq!(Level::Info.to_string(), "INFO"); + assert_eq!(Level::Config.to_string(), "CONFIG"); + assert_eq!(Level::Debug.to_string(), "DEBUG"); + assert_eq!(Level::Trace.to_string(), "TRACE"); + } + + #[test] + fn test_max_level() { + let _guard = LEVEL_MUTEX.lock(); + set_max_level(Level::Info); + assert_eq!(max_level(), Level::Info); + } + + #[test] + fn test_set_max_level() { + let _guard = LEVEL_MUTEX.lock(); + set_max_level(Level::Error); + assert_eq!(max_level(), Level::Error); + set_max_level(Level::Fatal); + assert_eq!(max_level(), Level::Fatal); + } + + #[test] + fn test_init_with_level() { + let _guard = LEVEL_MUTEX.lock(); + init_with_level(Level::Debug).unwrap(); + assert_eq!(max_level(), Level::Debug); + assert!(init_with_level(Level::Warn).is_err()); + } + + #[test] + fn test_format_ts() { + let ts = chrono::Local::now(); + let s = format_ts(ts); + assert_eq!(s.len(), 13); + } +} diff --git a/testing/geckodriver/src/main.rs b/testing/geckodriver/src/main.rs new file mode 100644 index 0000000000..4dd6c668d9 --- /dev/null +++ b/testing/geckodriver/src/main.rs @@ -0,0 +1,351 @@ +#![forbid(unsafe_code)] + +extern crate chrono; +#[macro_use] +extern crate clap; +#[macro_use] +extern crate lazy_static; +extern crate hyper; +extern crate marionette as marionette_rs; +extern crate mozdevice; +extern crate mozprofile; +extern crate mozrunner; +extern crate mozversion; +extern crate regex; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; +extern crate serde_yaml; +extern crate uuid; +extern crate webdriver; +extern crate zip; + +#[macro_use] +extern crate log; + +use std::env; +use std::fmt; +use std::io; +use std::net::{IpAddr, SocketAddr}; +use std::path::PathBuf; +use std::result; +use std::str::FromStr; + +use clap::{App, Arg}; + +macro_rules! try_opt { + ($expr:expr, $err_type:expr, $err_msg:expr) => {{ + match $expr { + Some(x) => x, + None => return Err(WebDriverError::new($err_type, $err_msg)), + } + }}; +} + +mod android; +mod build; +mod capabilities; +mod command; +mod logging; +mod marionette; +mod prefs; + +#[cfg(test)] +pub mod test; + +use crate::command::extension_routes; +use crate::logging::Level; +use crate::marionette::{MarionetteHandler, MarionetteSettings}; +use mozdevice::AndroidStorageInput; + +const EXIT_SUCCESS: i32 = 0; +const EXIT_USAGE: i32 = 64; +const EXIT_UNAVAILABLE: i32 = 69; + +enum FatalError { + Parsing(clap::Error), + Usage(String), + Server(io::Error), +} + +impl FatalError { + fn exit_code(&self) -> i32 { + use FatalError::*; + match *self { + Parsing(_) | Usage(_) => EXIT_USAGE, + Server(_) => EXIT_UNAVAILABLE, + } + } + + fn help_included(&self) -> bool { + match *self { + FatalError::Parsing(_) => true, + _ => false, + } + } +} + +impl From<clap::Error> for FatalError { + fn from(err: clap::Error) -> FatalError { + FatalError::Parsing(err) + } +} + +impl From<io::Error> for FatalError { + fn from(err: io::Error) -> FatalError { + FatalError::Server(err) + } +} + +// harmonise error message from clap to avoid duplicate "error:" prefix +impl fmt::Display for FatalError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use FatalError::*; + let s = match *self { + Parsing(ref err) => err.to_string(), + Usage(ref s) => format!("error: {}", s), + Server(ref err) => format!("error: {}", err.to_string()), + }; + write!(f, "{}", s) + } +} + +macro_rules! usage { + ($msg:expr) => { + return Err(FatalError::Usage($msg.to_string())); + }; + + ($fmt:expr, $($arg:tt)+) => { + return Err(FatalError::Usage(format!($fmt, $($arg)+))); + }; +} + +type ProgramResult<T> = result::Result<T, FatalError>; + +enum Operation { + Help, + Version, + Server { + log_level: Option<Level>, + address: SocketAddr, + settings: MarionetteSettings, + }, +} + +fn parse_args(app: &mut App) -> ProgramResult<Operation> { + let matches = app.get_matches_from_safe_borrow(env::args())?; + + let log_level = if matches.is_present("log_level") { + Level::from_str(matches.value_of("log_level").unwrap()).ok() + } else { + Some(match matches.occurrences_of("verbosity") { + 0 => Level::Info, + 1 => Level::Debug, + _ => Level::Trace, + }) + }; + + let host = matches.value_of("webdriver_host").unwrap(); + let port = { + let s = matches.value_of("webdriver_port").unwrap(); + match u16::from_str(s) { + Ok(n) => n, + Err(e) => usage!("invalid --port: {}: {}", e, s), + } + }; + let address = match IpAddr::from_str(host) { + Ok(addr) => SocketAddr::new(addr, port), + Err(e) => usage!("{}: {}:{}", e, host, port), + }; + + let android_storage = value_t!(matches, "android_storage", AndroidStorageInput)?; + + let binary = matches.value_of("binary").map(PathBuf::from); + + let marionette_host = matches.value_of("marionette_host").unwrap(); + let marionette_port = match matches.value_of("marionette_port") { + Some(s) => match u16::from_str(s) { + Ok(n) => Some(n), + Err(e) => usage!("invalid --marionette-port: {}", e), + }, + None => None, + }; + + let op = if matches.is_present("help") { + Operation::Help + } else if matches.is_present("version") { + Operation::Version + } else { + let settings = MarionetteSettings { + host: marionette_host.to_string(), + port: marionette_port, + binary, + connect_existing: matches.is_present("connect_existing"), + jsdebugger: matches.is_present("jsdebugger"), + android_storage, + }; + Operation::Server { + log_level, + address, + settings, + } + }; + + Ok(op) +} + +fn inner_main(app: &mut App) -> ProgramResult<()> { + match parse_args(app)? { + Operation::Help => print_help(app), + Operation::Version => print_version(), + + Operation::Server { + log_level, + address, + settings, + } => { + if let Some(ref level) = log_level { + logging::init_with_level(*level).unwrap(); + } else { + logging::init().unwrap(); + } + + let handler = MarionetteHandler::new(settings); + let listening = webdriver::server::start(address, handler, extension_routes())?; + info!("Listening on {}", listening.socket); + } + } + + Ok(()) +} + +fn main() { + use std::process::exit; + + let mut app = make_app(); + + // use std::process:Termination when it graduates + exit(match inner_main(&mut app) { + Ok(_) => EXIT_SUCCESS, + + Err(e) => { + eprintln!("{}: {}", get_program_name(), e); + if !e.help_included() { + print_help(&mut app); + } + + e.exit_code() + } + }); +} + +fn make_app<'a, 'b>() -> App<'a, 'b> { + App::new(format!("geckodriver {}", build::build_info())) + .about("WebDriver implementation for Firefox") + .arg( + Arg::with_name("webdriver_host") + .long("host") + .takes_value(true) + .value_name("HOST") + .default_value("127.0.0.1") + .help("Host IP to use for WebDriver server"), + ) + .arg( + Arg::with_name("webdriver_port") + .short("p") + .long("port") + .takes_value(true) + .value_name("PORT") + .default_value("4444") + .help("Port to use for WebDriver server"), + ) + .arg( + Arg::with_name("binary") + .short("b") + .long("binary") + .takes_value(true) + .value_name("BINARY") + .help("Path to the Firefox binary"), + ) + .arg( + Arg::with_name("marionette_host") + .long("marionette-host") + .takes_value(true) + .value_name("HOST") + .default_value("127.0.0.1") + .help("Host to use to connect to Gecko"), + ) + .arg( + Arg::with_name("marionette_port") + .long("marionette-port") + .takes_value(true) + .value_name("PORT") + .help("Port to use to connect to Gecko [default: system-allocated port]"), + ) + .arg( + Arg::with_name("connect_existing") + .long("connect-existing") + .requires("marionette_port") + .help("Connect to an existing Firefox instance"), + ) + .arg( + Arg::with_name("jsdebugger") + .long("jsdebugger") + .help("Attach browser toolbox debugger for Firefox"), + ) + .arg( + Arg::with_name("verbosity") + .multiple(true) + .conflicts_with("log_level") + .short("v") + .help("Log level verbosity (-v for debug and -vv for trace level)"), + ) + .arg( + Arg::with_name("log_level") + .long("log") + .takes_value(true) + .value_name("LEVEL") + .possible_values(&["fatal", "error", "warn", "info", "config", "debug", "trace"]) + .help("Set Gecko log level"), + ) + .arg( + Arg::with_name("help") + .short("h") + .long("help") + .help("Prints this message"), + ) + .arg( + Arg::with_name("version") + .short("V") + .long("version") + .help("Prints version and copying information"), + ) + .arg( + Arg::with_name("android_storage") + .long("android-storage") + .possible_values(&["auto", "app", "internal", "sdcard"]) + .default_value("auto") + .value_name("ANDROID_STORAGE") + .help("Selects storage location to be used for test data."), + ) +} + +fn get_program_name() -> String { + env::args().next().unwrap() +} + +fn print_help(app: &mut App) { + app.print_help().ok(); + println!(); +} + +fn print_version() { + println!("geckodriver {}", build::build_info()); + println!(); + println!("The source code of this program is available from"); + println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central."); + println!(); + println!("This program is subject to the terms of the Mozilla Public License 2.0."); + println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/."); +} diff --git a/testing/geckodriver/src/marionette.rs b/testing/geckodriver/src/marionette.rs new file mode 100644 index 0000000000..99a82f3a89 --- /dev/null +++ b/testing/geckodriver/src/marionette.rs @@ -0,0 +1,1749 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use crate::android::AndroidHandler; +use crate::command::{ + AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters, + GeckoExtensionCommand, GeckoExtensionRoute, CHROME_ELEMENT_KEY, +}; +use marionette_rs::common::{ + Cookie as MarionetteCookie, Date as MarionetteDate, Frame as MarionetteFrame, + Timeouts as MarionetteTimeouts, WebElement as MarionetteWebElement, Window, +}; +use marionette_rs::marionette::AppStatus; +use marionette_rs::message::{Command, Message, MessageId, Request}; +use marionette_rs::webdriver::{ + Command as MarionetteWebDriverCommand, Keys as MarionetteKeys, LegacyWebElement, + Locator as MarionetteLocator, NewWindow as MarionetteNewWindow, + PrintMargins as MarionettePrintMargins, PrintOrientation as MarionettePrintOrientation, + PrintPage as MarionettePrintPage, PrintParameters as MarionettePrintParameters, + ScreenshotOptions, Script as MarionetteScript, Selector as MarionetteSelector, + Url as MarionetteUrl, WindowRect as MarionetteWindowRect, +}; +use mozdevice::AndroidStorageInput; +use mozprofile::preferences::Pref; +use mozprofile::profile::Profile; +use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess}; +use serde::de::{self, Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use serde_json::{self, Map, Value}; +use std::io::prelude::*; +use std::io::Error as IoError; +use std::io::ErrorKind; +use std::io::Result as IoResult; +use std::net::{TcpListener, TcpStream}; +use std::path::PathBuf; +use std::sync::Mutex; +use std::thread; +use std::time; +use webdriver::capabilities::CapabilitiesMatching; +use webdriver::command::WebDriverCommand::{ + AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert, + ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension, + FindElement, FindElementElement, FindElementElements, FindElements, FullscreenWindow, Get, + GetActiveElement, GetAlertText, GetCSSValue, GetCookies, GetCurrentUrl, GetElementAttribute, + GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie, + GetPageSource, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles, GetWindowRect, GoBack, + GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow, MinimizeWindow, NewSession, + NewWindow, PerformActions, Print, Refresh, ReleaseActions, SendAlertText, SetTimeouts, + SetWindowRect, Status, SwitchToFrame, SwitchToParentFrame, SwitchToWindow, + TakeElementScreenshot, TakeScreenshot, +}; +use webdriver::command::{ + ActionsParameters, AddCookieParameters, GetNamedCookieParameters, GetParameters, + JavascriptCommandParameters, LocatorParameters, NewSessionParameters, NewWindowParameters, + PrintMargins, PrintOrientation, PrintPage, PrintParameters, SendKeysParameters, + SwitchToFrameParameters, SwitchToWindowParameters, TimeoutsParameters, WindowRectParameters, +}; +use webdriver::command::{WebDriverCommand, WebDriverMessage}; +use webdriver::common::{ + Cookie, Date, FrameId, LocatorStrategy, WebElement, ELEMENT_KEY, FRAME_KEY, WINDOW_KEY, +}; +use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; +use webdriver::response::{ + CloseWindowResponse, CookieResponse, CookiesResponse, ElementRectResponse, NewSessionResponse, + NewWindowResponse, TimeoutsResponse, ValueResponse, WebDriverResponse, WindowRectResponse, +}; +use webdriver::server::{Session, WebDriverHandler}; + +use crate::build; +use crate::capabilities::{FirefoxCapabilities, FirefoxOptions}; +use crate::logging; +use crate::prefs; + +/// A running Gecko instance. +#[derive(Debug)] +pub enum Browser { + /// A local Firefox process, running on this (host) device. + Host(FirefoxProcess), + + /// A remote instance, running on a (target) Android device. + Target(AndroidHandler), +} + +#[derive(Debug, PartialEq, Deserialize)] +pub struct MarionetteHandshake { + #[serde(rename = "marionetteProtocol")] + protocol: u16, + #[serde(rename = "applicationType")] + application_type: String, +} + +#[derive(Default)] +pub struct MarionetteSettings { + pub host: String, + pub port: Option<u16>, + pub binary: Option<PathBuf>, + pub connect_existing: bool, + + /// Brings up the Browser Toolbox when starting Firefox, + /// letting you debug internals. + pub jsdebugger: bool, + + pub android_storage: AndroidStorageInput, +} + +#[derive(Default)] +pub struct MarionetteHandler { + pub connection: Mutex<Option<MarionetteConnection>>, + pub settings: MarionetteSettings, + pub browser: Option<Browser>, +} + +impl MarionetteHandler { + pub fn new(settings: MarionetteSettings) -> MarionetteHandler { + MarionetteHandler { + connection: Mutex::new(None), + settings, + browser: None, + } + } + + pub fn create_connection( + &mut self, + session_id: &Option<String>, + new_session_parameters: &NewSessionParameters, + ) -> WebDriverResult<Map<String, Value>> { + let (options, capabilities) = { + let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref()); + let mut capabilities = new_session_parameters + .match_browser(&mut fx_capabilities)? + .ok_or_else(|| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + "Unable to find a matching set of capabilities", + ) + })?; + + let options = FirefoxOptions::from_capabilities( + fx_capabilities.chosen_binary, + self.settings.android_storage, + &mut capabilities, + )?; + (options, capabilities) + }; + + if let Some(l) = options.log.level { + logging::set_max_level(l); + } + + let host = self.settings.host.to_owned(); + let port = self.settings.port.unwrap_or(get_free_port(&host)?); + + match options.android { + Some(_) => { + // TODO: support connecting to running Apps. There's no real obstruction here, + // just some details about port forwarding to work through. We can't follow + // `chromedriver` here since it uses an abstract socket rather than a TCP socket: + // see bug 1240830 for thoughts on doing that for Marionette. + if self.settings.connect_existing { + return Err(WebDriverError::new( + ErrorStatus::SessionNotCreated, + "Cannot connect to an existing Android App yet", + )); + } + + self.start_android(port, options)?; + } + None => { + if !self.settings.connect_existing { + self.start_browser(port, options)?; + } + } + } + + let mut connection = MarionetteConnection::new(host, port, session_id.clone()); + connection.connect(&mut self.browser).or_else(|e| { + match self.browser { + Some(Browser::Host(ref mut runner)) => { + runner.kill()?; + } + Some(Browser::Target(ref mut handler)) => { + handler.force_stop().map_err(|e| { + WebDriverError::new(ErrorStatus::UnknownError, e.to_string()) + })?; + } + _ => {} + } + + Err(e) + })?; + self.connection = Mutex::new(Some(connection)); + Ok(capabilities) + } + + fn start_android(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> { + let android_options = options.android.unwrap(); + + let handler = AndroidHandler::new(&android_options, port)?; + + // Profile management. + let is_custom_profile = options.profile.is_some(); + + let mut profile = options.profile.unwrap_or(Profile::new()?); + + self.set_prefs( + handler.target_port, + &mut profile, + is_custom_profile, + options.prefs, + ) + .map_err(|e| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to set preferences: {}", e), + ) + })?; + + handler + .prepare(&profile, options.env.unwrap_or_default()) + .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?; + + handler + .launch() + .map_err(|e| WebDriverError::new(ErrorStatus::UnknownError, e.to_string()))?; + + self.browser = Some(Browser::Target(handler)); + + Ok(()) + } + + fn start_browser(&mut self, port: u16, options: FirefoxOptions) -> WebDriverResult<()> { + let binary = options.binary.ok_or_else(|| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + "Expected browser binary location, but unable to find \ + binary in default location, no \ + 'moz:firefoxOptions.binary' capability provided, and \ + no binary flag set on the command line", + ) + })?; + + let is_custom_profile = options.profile.is_some(); + + let mut profile = match options.profile { + Some(x) => x, + None => Profile::new()?, + }; + + self.set_prefs(port, &mut profile, is_custom_profile, options.prefs) + .map_err(|e| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to set preferences: {}", e), + ) + })?; + + let mut runner = FirefoxRunner::new(&binary, profile); + + runner.arg("--marionette"); + if self.settings.jsdebugger { + runner.arg("--jsdebugger"); + } + if let Some(args) = options.args.as_ref() { + runner.args(args); + } + + // https://developer.mozilla.org/docs/Environment_variables_affecting_crash_reporting + runner + .env("MOZ_CRASHREPORTER", "1") + .env("MOZ_CRASHREPORTER_NO_REPORT", "1") + .env("MOZ_CRASHREPORTER_SHUTDOWN", "1"); + + let browser_proc = runner.start().map_err(|e| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to start browser {}: {}", binary.display(), e), + ) + })?; + self.browser = Some(Browser::Host(browser_proc)); + + Ok(()) + } + + pub fn set_prefs( + &self, + port: u16, + profile: &mut Profile, + custom_profile: bool, + extra_prefs: Vec<(String, Pref)>, + ) -> WebDriverResult<()> { + let prefs = profile.user_prefs().map_err(|_| { + WebDriverError::new( + ErrorStatus::UnknownError, + "Unable to read profile preferences file", + ) + })?; + + for &(ref name, ref value) in prefs::DEFAULT.iter() { + if !custom_profile || !prefs.contains_key(name) { + prefs.insert((*name).to_string(), (*value).clone()); + } + } + + prefs.insert_slice(&extra_prefs[..]); + + if self.settings.jsdebugger { + prefs.insert("devtools.browsertoolbox.panel", Pref::new("jsdebugger")); + prefs.insert("devtools.debugger.remote-enabled", Pref::new(true)); + prefs.insert("devtools.chrome.enabled", Pref::new(true)); + prefs.insert("devtools.debugger.prompt-connection", Pref::new(false)); + prefs.insert("marionette.debugging.clicktostart", Pref::new(true)); + } + + prefs.insert("marionette.log.level", logging::max_level().into()); + prefs.insert("marionette.port", Pref::new(port)); + + prefs.write().map_err(|e| { + WebDriverError::new( + ErrorStatus::UnknownError, + format!("Unable to write Firefox profile: {}", e), + ) + }) + } +} + +impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler { + fn handle_command( + &mut self, + _: &Option<Session>, + msg: WebDriverMessage<GeckoExtensionRoute>, + ) -> WebDriverResult<WebDriverResponse> { + let mut resolved_capabilities = None; + { + let mut capabilities_options = None; + // First handle the status message which doesn't actually require a marionette + // connection or message + if let Status = msg.command { + let (ready, message) = self + .connection + .lock() + .map(|ref connection| { + connection + .as_ref() + .map(|_| (false, "Session already started")) + .unwrap_or((true, "")) + }) + .unwrap_or((false, "geckodriver internal error")); + let mut value = Map::new(); + value.insert("ready".to_string(), Value::Bool(ready)); + value.insert("message".to_string(), Value::String(message.into())); + return Ok(WebDriverResponse::Generic(ValueResponse(Value::Object( + value, + )))); + } + + match self.connection.lock() { + Ok(ref connection) => { + if connection.is_none() { + match msg.command { + NewSession(ref capabilities) => { + capabilities_options = Some(capabilities); + } + _ => { + return Err(WebDriverError::new( + ErrorStatus::InvalidSessionId, + "Tried to run command without establishing a connection", + )); + } + } + } + } + Err(_) => { + return Err(WebDriverError::new( + ErrorStatus::UnknownError, + "Failed to aquire Marionette connection", + )) + } + } + if let Some(capabilities) = capabilities_options { + resolved_capabilities = + Some(self.create_connection(&msg.session_id, &capabilities)?); + } + } + + match self.connection.lock() { + Ok(ref mut connection) => { + match connection.as_mut() { + Some(conn) => { + conn.send_command(resolved_capabilities, &msg) + .map_err(|mut err| { + // Shutdown the browser if no session can + // be established due to errors. + if let NewSession(_) = msg.command { + err.delete_session = true; + } + err + }) + } + None => panic!("Connection missing"), + } + } + Err(_) => Err(WebDriverError::new( + ErrorStatus::UnknownError, + "Failed to aquire Marionette connection", + )), + } + } + + fn delete_session(&mut self, session: &Option<Session>) { + if let Some(ref s) = *session { + let delete_session = WebDriverMessage { + session_id: Some(s.id.clone()), + command: WebDriverCommand::DeleteSession, + }; + let _ = self.handle_command(session, delete_session); + } + + if let Ok(ref mut connection) = self.connection.lock() { + if let Some(conn) = connection.as_mut() { + conn.close(); + } + } + + match self.browser { + Some(Browser::Host(ref mut runner)) => { + // TODO(https://bugzil.la/1443922): + // Use toolkit.asyncshutdown.crash_timout pref + match runner.wait(time::Duration::from_secs(70)) { + Ok(x) => debug!("Browser process stopped: {}", x), + Err(e) => error!("Failed to stop browser process: {}", e), + } + } + Some(Browser::Target(ref mut handler)) => { + // Try to force-stop the process on the target device + match handler.force_stop() { + Ok(_) => debug!("Android package force-stopped"), + Err(e) => error!("Failed to force-stop Android package: {}", e), + } + } + None => {} + } + + self.connection = Mutex::new(None); + self.browser = None; + } +} + +pub struct MarionetteSession { + pub session_id: String, + protocol: Option<u16>, + application_type: Option<String>, + command_id: MessageId, +} + +impl MarionetteSession { + pub fn new(session_id: Option<String>) -> MarionetteSession { + let initital_id = session_id.unwrap_or_else(|| "".to_string()); + MarionetteSession { + session_id: initital_id, + protocol: None, + application_type: None, + command_id: 0, + } + } + + pub fn update( + &mut self, + msg: &WebDriverMessage<GeckoExtensionRoute>, + resp: &MarionetteResponse, + ) -> WebDriverResult<()> { + if let NewSession(_) = msg.command { + let session_id = try_opt!( + try_opt!( + resp.result.get("sessionId"), + ErrorStatus::SessionNotCreated, + "Unable to get session id" + ) + .as_str(), + ErrorStatus::SessionNotCreated, + "Unable to convert session id to string" + ); + self.session_id = session_id.to_string().clone(); + }; + Ok(()) + } + + /// Converts a Marionette JSON response into a `WebElement`. + /// + /// Note that it currently coerces all chrome elements, web frames, and web + /// windows also into web elements. This will change at a later point. + fn to_web_element(&self, json_data: &Value) -> WebDriverResult<WebElement> { + let data = try_opt!( + json_data.as_object(), + ErrorStatus::UnknownError, + "Failed to convert data to an object" + ); + + let chrome_element = data.get(CHROME_ELEMENT_KEY); + let element = data.get(ELEMENT_KEY); + let frame = data.get(FRAME_KEY); + let window = data.get(WINDOW_KEY); + + let value = try_opt!( + element.or(chrome_element).or(frame).or(window), + ErrorStatus::UnknownError, + "Failed to extract web element from Marionette response" + ); + let id = try_opt!( + value.as_str(), + ErrorStatus::UnknownError, + "Failed to convert web element reference value to string" + ) + .to_string(); + Ok(WebElement(id)) + } + + pub fn next_command_id(&mut self) -> MessageId { + self.command_id += 1; + self.command_id + } + + pub fn response( + &mut self, + msg: &WebDriverMessage<GeckoExtensionRoute>, + resp: MarionetteResponse, + ) -> WebDriverResult<WebDriverResponse> { + use self::GeckoExtensionCommand::*; + + if resp.id != self.command_id { + return Err(WebDriverError::new( + ErrorStatus::UnknownError, + format!( + "Marionette responses arrived out of sequence, expected {}, got {}", + self.command_id, resp.id + ), + )); + } + + if let Some(error) = resp.error { + return Err(error.into()); + } + + self.update(msg, &resp)?; + + Ok(match msg.command { + // Everything that doesn't have a response value + Get(_) + | GoBack + | GoForward + | Refresh + | SetTimeouts(_) + | SwitchToWindow(_) + | SwitchToFrame(_) + | SwitchToParentFrame + | AddCookie(_) + | DeleteCookies + | DeleteCookie(_) + | DismissAlert + | AcceptAlert + | SendAlertText(_) + | ElementClick(_) + | ElementClear(_) + | ElementSendKeys(_, _) + | PerformActions(_) + | ReleaseActions => WebDriverResponse::Void, + // Things that simply return the contents of the marionette "value" property + GetCurrentUrl + | GetTitle + | GetPageSource + | GetWindowHandle + | IsDisplayed(_) + | IsSelected(_) + | GetElementAttribute(_, _) + | GetElementProperty(_, _) + | GetCSSValue(_, _) + | GetElementText(_) + | GetElementTagName(_) + | IsEnabled(_) + | ExecuteScript(_) + | ExecuteAsyncScript(_) + | GetAlertText + | TakeScreenshot + | Print(_) + | TakeElementScreenshot(_) => { + WebDriverResponse::Generic(resp.into_value_response(true)?) + } + GetTimeouts => { + let script = match try_opt!( + resp.result.get("script"), + ErrorStatus::UnknownError, + "Missing field: script" + ) { + Value::Null => None, + n => try_opt!( + Some(n.as_u64()), + ErrorStatus::UnknownError, + "Failed to interpret script timeout duration as u64" + ), + }; + // Check for the spec-compliant "pageLoad", but also for "page load", + // which was sent by Firefox 52 and earlier. + let page_load = try_opt!( + try_opt!( + resp.result + .get("pageLoad") + .or_else(|| resp.result.get("page load")), + ErrorStatus::UnknownError, + "Missing field: pageLoad" + ) + .as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret page load duration as u64" + ); + let implicit = try_opt!( + try_opt!( + resp.result.get("implicit"), + ErrorStatus::UnknownError, + "Missing field: implicit" + ) + .as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret implicit search duration as u64" + ); + + WebDriverResponse::Timeouts(TimeoutsResponse { + script, + page_load, + implicit, + }) + } + Status => panic!("Got status command that should already have been handled"), + GetWindowHandles => WebDriverResponse::Generic(resp.into_value_response(false)?), + NewWindow(_) => { + let handle: String = try_opt!( + try_opt!( + resp.result.get("handle"), + ErrorStatus::UnknownError, + "Failed to find handle field" + ) + .as_str(), + ErrorStatus::UnknownError, + "Failed to interpret handle as string" + ) + .into(); + let typ: String = try_opt!( + try_opt!( + resp.result.get("type"), + ErrorStatus::UnknownError, + "Failed to find type field" + ) + .as_str(), + ErrorStatus::UnknownError, + "Failed to interpret type as string" + ) + .into(); + + WebDriverResponse::NewWindow(NewWindowResponse { handle, typ }) + } + CloseWindow => { + let data = try_opt!( + resp.result.as_array(), + ErrorStatus::UnknownError, + "Failed to interpret value as array" + ); + let handles = data + .iter() + .map(|x| { + Ok(try_opt!( + x.as_str(), + ErrorStatus::UnknownError, + "Failed to interpret window handle as string" + ) + .to_owned()) + }) + .collect::<Result<Vec<_>, _>>()?; + WebDriverResponse::CloseWindow(CloseWindowResponse(handles)) + } + GetElementRect(_) => { + let x = try_opt!( + try_opt!( + resp.result.get("x"), + ErrorStatus::UnknownError, + "Failed to find x field" + ) + .as_f64(), + ErrorStatus::UnknownError, + "Failed to interpret x as float" + ); + + let y = try_opt!( + try_opt!( + resp.result.get("y"), + ErrorStatus::UnknownError, + "Failed to find y field" + ) + .as_f64(), + ErrorStatus::UnknownError, + "Failed to interpret y as float" + ); + + let width = try_opt!( + try_opt!( + resp.result.get("width"), + ErrorStatus::UnknownError, + "Failed to find width field" + ) + .as_f64(), + ErrorStatus::UnknownError, + "Failed to interpret width as float" + ); + + let height = try_opt!( + try_opt!( + resp.result.get("height"), + ErrorStatus::UnknownError, + "Failed to find height field" + ) + .as_f64(), + ErrorStatus::UnknownError, + "Failed to interpret width as float" + ); + + let rect = ElementRectResponse { + x, + y, + width, + height, + }; + WebDriverResponse::ElementRect(rect) + } + FullscreenWindow | MinimizeWindow | MaximizeWindow | GetWindowRect + | SetWindowRect(_) => { + let width = try_opt!( + try_opt!( + resp.result.get("width"), + ErrorStatus::UnknownError, + "Failed to find width field" + ) + .as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret width as positive integer" + ); + + let height = try_opt!( + try_opt!( + resp.result.get("height"), + ErrorStatus::UnknownError, + "Failed to find heigenht field" + ) + .as_u64(), + ErrorStatus::UnknownError, + "Failed to interpret height as positive integer" + ); + + let x = try_opt!( + try_opt!( + resp.result.get("x"), + ErrorStatus::UnknownError, + "Failed to find x field" + ) + .as_i64(), + ErrorStatus::UnknownError, + "Failed to interpret x as integer" + ); + + let y = try_opt!( + try_opt!( + resp.result.get("y"), + ErrorStatus::UnknownError, + "Failed to find y field" + ) + .as_i64(), + ErrorStatus::UnknownError, + "Failed to interpret y as integer" + ); + + let rect = WindowRectResponse { + x: x as i32, + y: y as i32, + width: width as i32, + height: height as i32, + }; + WebDriverResponse::WindowRect(rect) + } + GetCookies => { + let cookies: Vec<Cookie> = serde_json::from_value(resp.result)?; + WebDriverResponse::Cookies(CookiesResponse(cookies)) + } + GetNamedCookie(ref name) => { + let mut cookies: Vec<Cookie> = serde_json::from_value(resp.result)?; + cookies.retain(|x| x.name == *name); + let cookie = try_opt!( + cookies.pop(), + ErrorStatus::NoSuchCookie, + format!("No cookie with name {}", name) + ); + WebDriverResponse::Cookie(CookieResponse(cookie)) + } + FindElement(_) | FindElementElement(_, _) => { + let element = self.to_web_element(try_opt!( + resp.result.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field" + ))?; + WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?)) + } + FindElements(_) | FindElementElements(_, _) => { + let element_vec = try_opt!( + resp.result.as_array(), + ErrorStatus::UnknownError, + "Failed to interpret value as array" + ); + let elements = element_vec + .iter() + .map(|x| self.to_web_element(x)) + .collect::<Result<Vec<_>, _>>()?; + + // TODO(Henrik): How to remove unwrap? + WebDriverResponse::Generic(ValueResponse(Value::Array( + elements + .iter() + .map(|x| serde_json::to_value(x).unwrap()) + .collect(), + ))) + } + GetActiveElement => { + let element = self.to_web_element(try_opt!( + resp.result.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field" + ))?; + WebDriverResponse::Generic(ValueResponse(serde_json::to_value(element)?)) + } + NewSession(_) => { + let session_id = try_opt!( + try_opt!( + resp.result.get("sessionId"), + ErrorStatus::InvalidSessionId, + "Failed to find sessionId field" + ) + .as_str(), + ErrorStatus::InvalidSessionId, + "sessionId is not a string" + ); + + let mut capabilities = try_opt!( + try_opt!( + resp.result.get("capabilities"), + ErrorStatus::UnknownError, + "Failed to find capabilities field" + ) + .as_object(), + ErrorStatus::UnknownError, + "capabilities field is not an object" + ) + .clone(); + + capabilities.insert("moz:geckodriverVersion".into(), build::build_info().into()); + + WebDriverResponse::NewSession(NewSessionResponse::new( + session_id.to_string(), + Value::Object(capabilities.clone()), + )) + } + DeleteSession => WebDriverResponse::DeleteSession, + Extension(ref extension) => match extension { + GetContext => WebDriverResponse::Generic(resp.into_value_response(true)?), + SetContext(_) => WebDriverResponse::Void, + InstallAddon(_) => WebDriverResponse::Generic(resp.into_value_response(true)?), + UninstallAddon(_) => WebDriverResponse::Void, + TakeFullScreenshot => WebDriverResponse::Generic(resp.into_value_response(true)?), + }, + }) + } +} + +fn try_convert_to_marionette_message( + msg: &WebDriverMessage<GeckoExtensionRoute>, +) -> WebDriverResult<Option<Command>> { + use self::GeckoExtensionCommand::*; + use self::WebDriverCommand::*; + + Ok(match msg.command { + AcceptAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::AcceptAlert)), + AddCookie(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::AddCookie( + x.to_marionette()?, + ))), + CloseWindow => Some(Command::WebDriver(MarionetteWebDriverCommand::CloseWindow)), + DeleteCookie(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::DeleteCookie(x.clone()), + )), + DeleteCookies => Some(Command::WebDriver( + MarionetteWebDriverCommand::DeleteCookies, + )), + DeleteSession => Some(Command::Marionette( + marionette_rs::marionette::Command::DeleteSession { + flags: vec![AppStatus::eForceQuit], + }, + )), + DismissAlert => Some(Command::WebDriver(MarionetteWebDriverCommand::DismissAlert)), + ElementClear(ref e) => Some(Command::WebDriver( + MarionetteWebDriverCommand::ElementClear(e.to_marionette()?), + )), + ElementClick(ref e) => Some(Command::WebDriver( + MarionetteWebDriverCommand::ElementClick(e.to_marionette()?), + )), + ElementSendKeys(ref e, ref x) => { + let keys = x.to_marionette()?; + Some(Command::WebDriver( + MarionetteWebDriverCommand::ElementSendKeys { + id: e.clone().to_string(), + text: keys.text.clone(), + value: keys.value.clone(), + }, + )) + } + ExecuteAsyncScript(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::ExecuteAsyncScript(x.to_marionette()?), + )), + ExecuteScript(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::ExecuteScript(x.to_marionette()?), + )), + FindElement(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::FindElement( + x.to_marionette()?, + ))), + FindElements(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::FindElements(x.to_marionette()?), + )), + FindElementElement(ref e, ref x) => { + let locator = x.to_marionette()?; + Some(Command::WebDriver( + MarionetteWebDriverCommand::FindElementElement { + element: e.clone().to_string(), + using: locator.using.clone(), + value: locator.value.clone(), + }, + )) + } + FindElementElements(ref e, ref x) => { + let locator = x.to_marionette()?; + Some(Command::WebDriver( + MarionetteWebDriverCommand::FindElementElements { + element: e.clone().to_string(), + using: locator.using.clone(), + value: locator.value.clone(), + }, + )) + } + FullscreenWindow => Some(Command::WebDriver( + MarionetteWebDriverCommand::FullscreenWindow, + )), + Get(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Get( + x.to_marionette()?, + ))), + GetActiveElement => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetActiveElement, + )), + GetAlertText => Some(Command::WebDriver(MarionetteWebDriverCommand::GetAlertText)), + GetCookies | GetNamedCookie(_) => { + Some(Command::WebDriver(MarionetteWebDriverCommand::GetCookies)) + } + GetCSSValue(ref e, ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetCSSValue { + id: e.clone().to_string(), + property: x.clone(), + }, + )), + GetCurrentUrl => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetCurrentUrl, + )), + GetElementAttribute(ref e, ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetElementAttribute { + id: e.clone().to_string(), + name: x.clone(), + }, + )), + GetElementProperty(ref e, ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetElementProperty { + id: e.clone().to_string(), + name: x.clone(), + }, + )), + GetElementRect(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetElementRect(x.to_marionette()?), + )), + GetElementTagName(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetElementTagName(x.to_marionette()?), + )), + GetElementText(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetElementText(x.to_marionette()?), + )), + GetPageSource => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetPageSource, + )), + GetTitle => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTitle)), + GetWindowHandle => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetWindowHandle, + )), + GetWindowHandles => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetWindowHandles, + )), + GetWindowRect => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetWindowRect, + )), + GetTimeouts => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTimeouts)), + GoBack => Some(Command::WebDriver(MarionetteWebDriverCommand::GoBack)), + GoForward => Some(Command::WebDriver(MarionetteWebDriverCommand::GoForward)), + IsDisplayed(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsDisplayed( + x.to_marionette()?, + ))), + IsEnabled(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsEnabled( + x.to_marionette()?, + ))), + IsSelected(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::IsSelected( + x.to_marionette()?, + ))), + MaximizeWindow => Some(Command::WebDriver( + MarionetteWebDriverCommand::MaximizeWindow, + )), + MinimizeWindow => Some(Command::WebDriver( + MarionetteWebDriverCommand::MinimizeWindow, + )), + NewWindow(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::NewWindow( + x.to_marionette()?, + ))), + Print(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::Print( + x.to_marionette()?, + ))), + Refresh => Some(Command::WebDriver(MarionetteWebDriverCommand::Refresh)), + ReleaseActions => Some(Command::WebDriver( + MarionetteWebDriverCommand::ReleaseActions, + )), + SendAlertText(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::SendAlertText(x.to_marionette()?), + )), + SetTimeouts(ref x) => Some(Command::WebDriver(MarionetteWebDriverCommand::SetTimeouts( + x.to_marionette()?, + ))), + SetWindowRect(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::SetWindowRect(x.to_marionette()?), + )), + SwitchToFrame(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::SwitchToFrame(x.to_marionette()?), + )), + SwitchToParentFrame => Some(Command::WebDriver( + MarionetteWebDriverCommand::SwitchToParentFrame, + )), + SwitchToWindow(ref x) => Some(Command::WebDriver( + MarionetteWebDriverCommand::SwitchToWindow(x.to_marionette()?), + )), + TakeElementScreenshot(ref e) => { + let screenshot = ScreenshotOptions { + id: Some(e.clone().to_string()), + highlights: vec![], + full: false, + }; + Some(Command::WebDriver( + MarionetteWebDriverCommand::TakeElementScreenshot(screenshot), + )) + } + TakeScreenshot => { + let screenshot = ScreenshotOptions { + id: None, + highlights: vec![], + full: false, + }; + Some(Command::WebDriver( + MarionetteWebDriverCommand::TakeScreenshot(screenshot), + )) + } + Extension(ref extension) => match extension { + TakeFullScreenshot => { + let screenshot = ScreenshotOptions { + id: None, + highlights: vec![], + full: true, + }; + Some(Command::WebDriver( + MarionetteWebDriverCommand::TakeFullScreenshot(screenshot), + )) + } + _ => None, + }, + _ => None, + }) +} + +#[derive(Debug, PartialEq)] +pub struct MarionetteCommand { + pub id: MessageId, + pub name: String, + pub params: Map<String, Value>, +} + +impl Serialize for MarionetteCommand { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + let data = (&0, &self.id, &self.name, &self.params); + data.serialize(serializer) + } +} + +impl MarionetteCommand { + fn new(id: MessageId, name: String, params: Map<String, Value>) -> MarionetteCommand { + MarionetteCommand { id, name, params } + } + + fn encode_msg<T>(msg: T) -> WebDriverResult<String> + where + T: serde::Serialize, + { + let data = serde_json::to_string(&msg)?; + + Ok(format!("{}:{}", data.len(), data)) + } + + fn from_webdriver_message( + id: MessageId, + capabilities: Option<Map<String, Value>>, + msg: &WebDriverMessage<GeckoExtensionRoute>, + ) -> WebDriverResult<String> { + use self::GeckoExtensionCommand::*; + + if let Some(cmd) = try_convert_to_marionette_message(msg)? { + let req = Message::Incoming(Request(id, cmd)); + MarionetteCommand::encode_msg(req) + } else { + let (opt_name, opt_parameters) = match msg.command { + Status => panic!("Got status command that should already have been handled"), + NewSession(_) => { + let caps = capabilities + .expect("Tried to create new session without processing capabilities"); + + let mut data = Map::new(); + for (k, v) in caps.iter() { + data.insert(k.to_string(), serde_json::to_value(v)?); + } + + (Some("WebDriver:NewSession"), Some(Ok(data))) + } + PerformActions(ref x) => { + (Some("WebDriver:PerformActions"), Some(x.to_marionette())) + } + Extension(ref extension) => match extension { + GetContext => (Some("Marionette:GetContext"), None), + InstallAddon(x) => (Some("Addon:Install"), Some(x.to_marionette())), + SetContext(x) => (Some("Marionette:SetContext"), Some(x.to_marionette())), + UninstallAddon(x) => (Some("Addon:Uninstall"), Some(x.to_marionette())), + _ => (None, None), + }, + _ => (None, None), + }; + + let name = try_opt!( + opt_name, + ErrorStatus::UnsupportedOperation, + "Operation not supported" + ); + let parameters = opt_parameters.unwrap_or_else(|| Ok(Map::new()))?; + + let req = MarionetteCommand::new(id, name.into(), parameters); + MarionetteCommand::encode_msg(req) + } + } +} + +#[derive(Debug, PartialEq)] +pub struct MarionetteResponse { + pub id: MessageId, + pub error: Option<MarionetteError>, + pub result: Value, +} + +impl<'de> Deserialize<'de> for MarionetteResponse { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + struct ResponseWrapper { + msg_type: u64, + id: MessageId, + error: Option<MarionetteError>, + result: Value, + } + + let wrapper: ResponseWrapper = Deserialize::deserialize(deserializer)?; + + if wrapper.msg_type != 1 { + return Err(de::Error::custom( + "Expected '1' in first element of response", + )); + }; + + Ok(MarionetteResponse { + id: wrapper.id, + error: wrapper.error, + result: wrapper.result, + }) + } +} + +impl MarionetteResponse { + fn into_value_response(self, value_required: bool) -> WebDriverResult<ValueResponse> { + let value: &Value = if value_required { + try_opt!( + self.result.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field" + ) + } else { + &self.result + }; + + Ok(ValueResponse(value.clone())) + } +} + +#[derive(Debug, PartialEq, Serialize, Deserialize)] +pub struct MarionetteError { + #[serde(rename = "error")] + pub code: String, + pub message: String, + pub stacktrace: Option<String>, +} + +impl Into<WebDriverError> for MarionetteError { + fn into(self) -> WebDriverError { + let status = ErrorStatus::from(self.code); + let message = self.message; + + if let Some(stack) = self.stacktrace { + WebDriverError::new_with_stack(status, message, stack) + } else { + WebDriverError::new(status, message) + } + } +} + +fn get_free_port(host: &str) -> IoResult<u16> { + TcpListener::bind((host, 0)) + .and_then(|stream| stream.local_addr()) + .map(|x| x.port()) +} + +pub struct MarionetteConnection { + host: String, + port: u16, + stream: Option<TcpStream>, + pub session: MarionetteSession, +} + +impl MarionetteConnection { + pub fn new(host: String, port: u16, session_id: Option<String>) -> MarionetteConnection { + let session = MarionetteSession::new(session_id); + MarionetteConnection { + host, + port, + stream: None, + session, + } + } + + pub fn connect(&mut self, browser: &mut Option<Browser>) -> WebDriverResult<()> { + let timeout = time::Duration::from_secs(60); + let poll_interval = time::Duration::from_millis(100); + let now = time::Instant::now(); + + debug!( + "Waiting {}s to connect to browser on {}:{}", + timeout.as_secs(), + self.host, + self.port + ); + + loop { + // immediately abort connection attempts if process disappears + if let Some(Browser::Host(ref mut runner)) = *browser { + let exit_status = match runner.try_wait() { + Ok(Some(status)) => Some( + status + .code() + .map(|c| c.to_string()) + .unwrap_or_else(|| "signal".into()), + ), + Ok(None) => None, + Err(_) => Some("{unknown}".into()), + }; + if let Some(s) = exit_status { + return Err(WebDriverError::new( + ErrorStatus::UnknownError, + format!("Process unexpectedly closed with status {}", s), + )); + } + } + + let try_connect = || -> WebDriverResult<(TcpStream, MarionetteHandshake)> { + let mut stream = TcpStream::connect((&self.host[..], self.port))?; + let data = MarionetteConnection::handshake(&mut stream)?; + + Ok((stream, data)) + }; + + match try_connect() { + Ok((stream, data)) => { + debug!( + "Connection to Marionette established on {}:{}.", + self.host, self.port, + ); + + self.stream = Some(stream); + self.session.application_type = Some(data.application_type); + self.session.protocol = Some(data.protocol); + break; + } + Err(e) => { + if now.elapsed() < timeout { + thread::sleep(poll_interval); + } else { + return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string())); + } + } + } + } + + Ok(()) + } + + fn handshake(stream: &mut TcpStream) -> WebDriverResult<MarionetteHandshake> { + let resp = (match stream.read_timeout() { + Ok(timeout) => { + // If platform supports changing the read timeout of the stream, + // use a short one only for the handshake with Marionette. + stream + .set_read_timeout(Some(time::Duration::from_millis(100))) + .ok(); + let data = MarionetteConnection::read_resp(stream); + stream.set_read_timeout(timeout).ok(); + + data + } + _ => MarionetteConnection::read_resp(stream), + }) + .map_err(|e| { + WebDriverError::new( + ErrorStatus::UnknownError, + format!("Socket timeout reading Marionette handshake data: {}", e), + ) + })?; + + let data = serde_json::from_str::<MarionetteHandshake>(&resp)?; + + if data.application_type != "gecko" { + return Err(WebDriverError::new( + ErrorStatus::UnknownError, + format!("Unrecognized application type {}", data.application_type), + )); + } + + if data.protocol != 3 { + return Err(WebDriverError::new( + ErrorStatus::UnknownError, + format!( + "Unsupported Marionette protocol version {}, required 3", + data.protocol + ), + )); + } + + Ok(data) + } + + pub fn close(&self) {} + + pub fn send_command( + &mut self, + capabilities: Option<Map<String, Value>>, + msg: &WebDriverMessage<GeckoExtensionRoute>, + ) -> WebDriverResult<WebDriverResponse> { + let id = self.session.next_command_id(); + let enc_cmd = MarionetteCommand::from_webdriver_message(id, capabilities, msg)?; + let resp_data = self.send(enc_cmd)?; + let data: MarionetteResponse = serde_json::from_str(&resp_data)?; + + self.session.response(msg, data) + } + + fn send(&mut self, data: String) -> WebDriverResult<String> { + let stream = match self.stream { + Some(ref mut stream) => { + if stream.write(&*data.as_bytes()).is_err() { + let mut err = WebDriverError::new( + ErrorStatus::UnknownError, + "Failed to write request to stream", + ); + err.delete_session = true; + return Err(err); + } + + stream + } + None => { + let mut err = WebDriverError::new( + ErrorStatus::UnknownError, + "Tried to write before opening stream", + ); + err.delete_session = true; + return Err(err); + } + }; + + match MarionetteConnection::read_resp(stream) { + Ok(resp) => Ok(resp), + Err(_) => { + let mut err = WebDriverError::new( + ErrorStatus::UnknownError, + "Failed to decode response from marionette", + ); + err.delete_session = true; + Err(err) + } + } + } + + fn read_resp(stream: &mut TcpStream) -> IoResult<String> { + let mut bytes = 0usize; + + loop { + let buf = &mut [0 as u8]; + let num_read = stream.read(buf)?; + let byte = match num_read { + 0 => { + return Err(IoError::new( + ErrorKind::Other, + "EOF reading marionette message", + )) + } + 1 => buf[0] as char, + _ => panic!("Expected one byte got more"), + }; + match byte { + '0'..='9' => { + bytes *= 10; + bytes += byte as usize - '0' as usize; + } + ':' => break, + _ => {} + } + } + + let buf = &mut [0 as u8; 8192]; + let mut payload = Vec::with_capacity(bytes); + let mut total_read = 0; + while total_read < bytes { + let num_read = stream.read(buf)?; + if num_read == 0 { + return Err(IoError::new( + ErrorKind::Other, + "EOF reading marionette message", + )); + } + total_read += num_read; + for x in &buf[..num_read] { + payload.push(*x); + } + } + + // TODO(jgraham): Need to handle the error here + Ok(String::from_utf8(payload).unwrap()) + } +} + +trait ToMarionette<T> { + fn to_marionette(&self) -> WebDriverResult<T>; +} + +impl ToMarionette<Map<String, Value>> for AddonInstallParameters { + fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> { + let mut data = Map::new(); + data.insert("path".to_string(), serde_json::to_value(&self.path)?); + if self.temporary.is_some() { + data.insert( + "temporary".to_string(), + serde_json::to_value(&self.temporary)?, + ); + } + Ok(data) + } +} + +impl ToMarionette<Map<String, Value>> for AddonUninstallParameters { + fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> { + let mut data = Map::new(); + data.insert("id".to_string(), Value::String(self.id.clone())); + Ok(data) + } +} + +impl ToMarionette<Map<String, Value>> for GeckoContextParameters { + fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> { + let mut data = Map::new(); + data.insert( + "value".to_owned(), + serde_json::to_value(self.context.clone())?, + ); + Ok(data) + } +} + +impl ToMarionette<MarionettePrintParameters> for PrintParameters { + fn to_marionette(&self) -> WebDriverResult<MarionettePrintParameters> { + Ok(MarionettePrintParameters { + orientation: self.orientation.to_marionette()?, + scale: self.scale, + background: self.background, + page: self.page.to_marionette()?, + margin: self.margin.to_marionette()?, + page_ranges: self.page_ranges.clone(), + shrink_to_fit: self.shrink_to_fit, + }) + } +} + +impl ToMarionette<MarionettePrintOrientation> for PrintOrientation { + fn to_marionette(&self) -> WebDriverResult<MarionettePrintOrientation> { + Ok(match self { + PrintOrientation::Landscape => MarionettePrintOrientation::Landscape, + PrintOrientation::Portrait => MarionettePrintOrientation::Portrait, + }) + } +} + +impl ToMarionette<MarionettePrintPage> for PrintPage { + fn to_marionette(&self) -> WebDriverResult<MarionettePrintPage> { + Ok(MarionettePrintPage { + width: self.width, + height: self.height, + }) + } +} + +impl ToMarionette<MarionettePrintMargins> for PrintMargins { + fn to_marionette(&self) -> WebDriverResult<MarionettePrintMargins> { + Ok(MarionettePrintMargins { + top: self.top, + bottom: self.bottom, + left: self.left, + right: self.right, + }) + } +} + +impl ToMarionette<Map<String, Value>> for ActionsParameters { + fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> { + Ok(try_opt!( + serde_json::to_value(self)?.as_object(), + ErrorStatus::UnknownError, + "Expected an object" + ) + .clone()) + } +} + +impl ToMarionette<MarionetteCookie> for AddCookieParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteCookie> { + Ok(MarionetteCookie { + name: self.name.clone(), + value: self.value.clone(), + path: self.path.clone(), + domain: self.domain.clone(), + secure: self.secure, + http_only: self.httpOnly, + expiry: match &self.expiry { + Some(date) => Some(date.to_marionette()?), + None => None, + }, + same_site: self.sameSite.clone(), + }) + } +} + +impl ToMarionette<MarionetteDate> for Date { + fn to_marionette(&self) -> WebDriverResult<MarionetteDate> { + Ok(MarionetteDate(self.0)) + } +} + +impl ToMarionette<Map<String, Value>> for GetNamedCookieParameters { + fn to_marionette(&self) -> WebDriverResult<Map<String, Value>> { + Ok(try_opt!( + serde_json::to_value(self)?.as_object(), + ErrorStatus::UnknownError, + "Expected an object" + ) + .clone()) + } +} + +impl ToMarionette<MarionetteUrl> for GetParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteUrl> { + Ok(MarionetteUrl { + url: self.url.clone(), + }) + } +} + +impl ToMarionette<MarionetteScript> for JavascriptCommandParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteScript> { + Ok(MarionetteScript { + script: self.script.clone(), + args: self.args.clone(), + }) + } +} + +impl ToMarionette<MarionetteLocator> for LocatorParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteLocator> { + Ok(MarionetteLocator { + using: self.using.to_marionette()?, + value: self.value.clone(), + }) + } +} + +impl ToMarionette<MarionetteSelector> for LocatorStrategy { + fn to_marionette(&self) -> WebDriverResult<MarionetteSelector> { + use self::LocatorStrategy::*; + match self { + CSSSelector => Ok(MarionetteSelector::CSS), + LinkText => Ok(MarionetteSelector::LinkText), + PartialLinkText => Ok(MarionetteSelector::PartialLinkText), + TagName => Ok(MarionetteSelector::TagName), + XPath => Ok(MarionetteSelector::XPath), + } + } +} + +impl ToMarionette<MarionetteNewWindow> for NewWindowParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteNewWindow> { + Ok(MarionetteNewWindow { + type_hint: self.type_hint.clone(), + }) + } +} + +impl ToMarionette<MarionetteKeys> for SendKeysParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteKeys> { + Ok(MarionetteKeys { + text: self.text.clone(), + value: self + .text + .chars() + .map(|x| x.to_string()) + .collect::<Vec<String>>(), + }) + } +} + +impl ToMarionette<MarionetteFrame> for SwitchToFrameParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteFrame> { + Ok(match &self.id { + Some(x) => match x { + FrameId::Short(n) => MarionetteFrame::Index(n.clone()), + FrameId::Element(el) => MarionetteFrame::Element(el.0.clone()), + }, + None => MarionetteFrame::Parent, + }) + } +} + +impl ToMarionette<Window> for SwitchToWindowParameters { + fn to_marionette(&self) -> WebDriverResult<Window> { + Ok(Window { + name: self.handle.clone(), + handle: self.handle.clone(), + }) + } +} + +impl ToMarionette<MarionetteTimeouts> for TimeoutsParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteTimeouts> { + Ok(MarionetteTimeouts { + implicit: self.implicit, + page_load: self.page_load, + script: self.script, + }) + } +} + +impl ToMarionette<LegacyWebElement> for WebElement { + fn to_marionette(&self) -> WebDriverResult<LegacyWebElement> { + Ok(LegacyWebElement { + id: self.to_string(), + }) + } +} + +impl ToMarionette<MarionetteWebElement> for WebElement { + fn to_marionette(&self) -> WebDriverResult<MarionetteWebElement> { + Ok(MarionetteWebElement { + element: self.to_string(), + }) + } +} + +impl ToMarionette<MarionetteWindowRect> for WindowRectParameters { + fn to_marionette(&self) -> WebDriverResult<MarionetteWindowRect> { + Ok(MarionetteWindowRect { + x: self.x, + y: self.y, + width: self.width, + height: self.height, + }) + } +} + +#[cfg(test)] +mod tests { + use super::{MarionetteHandler, MarionetteSettings}; + use mozprofile::preferences::PrefValue; + use mozprofile::profile::Profile; + + // This is not a pretty test, mostly due to the nature of + // mozprofile's and MarionetteHandler's APIs, but we have had + // several regressions related to marionette.log.level. + #[test] + fn test_marionette_log_level() { + let mut profile = Profile::new().unwrap(); + let handler = MarionetteHandler::new(MarionetteSettings::default()); + handler.set_prefs(2828, &mut profile, false, vec![]).ok(); + let user_prefs = profile.user_prefs().unwrap(); + + let pref = user_prefs.get("marionette.log.level").unwrap(); + let value = match pref.value { + PrefValue::String(ref s) => s, + _ => panic!(), + }; + for (i, ch) in value.chars().enumerate() { + if i == 0 { + assert!(ch.is_uppercase()); + } else { + assert!(ch.is_lowercase()); + } + } + } +} diff --git a/testing/geckodriver/src/prefs.rs b/testing/geckodriver/src/prefs.rs new file mode 100644 index 0000000000..075d0c6809 --- /dev/null +++ b/testing/geckodriver/src/prefs.rs @@ -0,0 +1,160 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use mozprofile::preferences::Pref; + +// ALL CHANGES TO THIS FILE MUST HAVE REVIEW FROM A GECKODRIVER PEER! +// +// All preferences in this file are not immediately effective, and +// require a restart of Firefox, or have to be set in the profile before +// Firefox gets started the first time. If a preference has to be added, +// which is immediately effective, it needs to be done in Marionette +// (marionette.js). +// +// Note: geckodriver is used out-of-tree with various builds of Firefox. +// Removing a preference from this file will cause regressions, +// so please be careful and get review from a Testing :: geckodriver peer +// before you make any changes to this file. +lazy_static! { + pub static ref DEFAULT: Vec<(&'static str, Pref)> = vec![ + // Make sure Shield doesn't hit the network. + ("app.normandy.api_url", Pref::new("")), + + // Disable Firefox old build background check + ("app.update.checkInstallTime", Pref::new(false)), + + // Disable automatically upgrading Firefox + // + // Note: Possible update tests could reset or flip the value to allow + // updates to be downloaded and applied. + ("app.update.disabledForTesting", Pref::new(true)), + // !!! For backward compatibility up to Firefox 64. Only remove + // when this Firefox version is no longer supported by geckodriver !!! + ("app.update.auto", Pref::new(false)), + + // Enable the dump function, which sends messages to the system + // console + ("browser.dom.window.dump.enabled", Pref::new(true)), + ("devtools.console.stdout.chrome", Pref::new(true)), + + // Disable safebrowsing components + ("browser.safebrowsing.blockedURIs.enabled", Pref::new(false)), + ("browser.safebrowsing.downloads.enabled", Pref::new(false)), + ("browser.safebrowsing.passwords.enabled", Pref::new(false)), + ("browser.safebrowsing.malware.enabled", Pref::new(false)), + ("browser.safebrowsing.phishing.enabled", Pref::new(false)), + + // Do not restore the last open set of tabs if the browser crashed + ("browser.sessionstore.resume_from_crash", Pref::new(false)), + + // Skip check for default browser on startup + ("browser.shell.checkDefaultBrowser", Pref::new(false)), + + // Do not redirect user when a milestone upgrade of Firefox + // is detected + ("browser.startup.homepage_override.mstone", Pref::new("ignore")), + + // Start with a blank page (about:blank) + ("browser.startup.page", Pref::new(0)), + + // Do not close the window when the last tab gets closed + // TODO: Remove once minimum supported Firefox release is 61. + ("browser.tabs.closeWindowWithLastTab", Pref::new(false)), + + // Do not warn when closing all open tabs + // TODO: Remove once minimum supported Firefox release is 61. + ("browser.tabs.warnOnClose", Pref::new(false)), + + // Disable the UI tour + ("browser.uitour.enabled", Pref::new(false)), + + // Do not warn on quitting Firefox + ("browser.warnOnQuit", Pref::new(false)), + + // Defensively disable data reporting systems + ("datareporting.healthreport.documentServerURI", Pref::new("http://%(server)s/dummy/healthreport/")), + ("datareporting.healthreport.logging.consoleEnabled", Pref::new(false)), + ("datareporting.healthreport.service.enabled", Pref::new(false)), + ("datareporting.healthreport.service.firstRun", Pref::new(false)), + ("datareporting.healthreport.uploadEnabled", Pref::new(false)), + + // Do not show datareporting policy notifications which can + // interfere with tests + ("datareporting.policy.dataSubmissionEnabled", Pref::new(false)), + ("datareporting.policy.dataSubmissionPolicyBypassNotification", Pref::new(true)), + + // Disable the ProcessHangMonitor + ("dom.ipc.reportProcessHangs", Pref::new(false)), + + // Only load extensions from the application and user profile + // AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_APPLICATION + ("extensions.autoDisableScopes", Pref::new(0)), + ("extensions.enabledScopes", Pref::new(5)), + + // Disable intalling any distribution extensions or add-ons + ("extensions.installDistroAddons", Pref::new(false)), + + // Turn off extension updates so they do not bother tests + ("extensions.update.enabled", Pref::new(false)), + ("extensions.update.notifyUser", Pref::new(false)), + + // Allow the application to have focus even it runs in the + // background + ("focusmanager.testmode", Pref::new(true)), + + // Disable useragent updates + ("general.useragent.updates.enabled", Pref::new(false)), + + // Always use network provider for geolocation tests so we bypass + // the macOS dialog raised by the corelocation provider + ("geo.provider.testing", Pref::new(true)), + + // Do not scan wi-fi + ("geo.wifi.scan", Pref::new(false)), + + // No hang monitor + ("hangmonitor.timeout", Pref::new(0)), + + // Disable idle-daily notifications to avoid expensive operations + // that may cause unexpected test timeouts. + ("idle.lastDailyNotification", Pref::new(-1)), + + // Show chrome errors and warnings in the error console + ("javascript.options.showInConsole", Pref::new(true)), + + // Disable download and usage of OpenH264, and Widevine plugins + ("media.gmp-manager.updateEnabled", Pref::new(false)), + + // Disable the GFX sanity window + ("media.sanity-test.disabled", Pref::new(true)), + + // Do not prompt with long usernames or passwords in URLs + // TODO: Remove once minimum supported Firefox release is 61. + ("network.http.phishy-userpass-length", Pref::new(255)), + + // Do not automatically switch between offline and online + ("network.manage-offline-status", Pref::new(false)), + + // Make sure SNTP requests do not hit the network + ("network.sntp.pools", Pref::new("%(server)s")), + + // Disable Flash. The plugin container it is run in is + // causing problems when quitting Firefox from geckodriver, + // c.f. https://github.com/mozilla/geckodriver/issues/225. + ("plugin.state.flash", Pref::new(0)), + + // Don't do network connections for mitm priming + ("security.certerrors.mitm.priming.enabled", Pref::new(false)), + + // Ensure blocklist updates don't hit the network + ("services.settings.server", Pref::new("http://%(server)s/dummy/blocklist/")), + + // Disable first run pages + ("startup.homepage_welcome_url", Pref::new("about:blank")), + ("startup.homepage_welcome_url.additional", Pref::new("")), + + // Prevent starting into safe mode after application crashes + ("toolkit.startup.max_resumed_crashes", Pref::new(-1)), + ]; +} diff --git a/testing/geckodriver/src/test.rs b/testing/geckodriver/src/test.rs new file mode 100644 index 0000000000..e664aadf08 --- /dev/null +++ b/testing/geckodriver/src/test.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +pub fn assert_de<T>(data: &T, json: serde_json::Value) +where + T: std::fmt::Debug, + T: std::cmp::PartialEq, + T: serde::de::DeserializeOwned, +{ + assert_eq!(data, &serde_json::from_value::<T>(json).unwrap()); +} diff --git a/testing/geckodriver/src/tests/profile.zip b/testing/geckodriver/src/tests/profile.zip Binary files differnew file mode 100644 index 0000000000..286b118183 --- /dev/null +++ b/testing/geckodriver/src/tests/profile.zip |