blob: 6a494c89373ae8de2aae7d0318f588ab2a427811 [file] [log] [blame]
import os
import platform
import re
import shutil
import stat
import subprocess
import tempfile
from abc import ABCMeta, abstractmethod
from datetime import datetime, timedelta
from distutils.spawn import find_executable
from urllib.parse import urlsplit
import requests
from .utils import call, get, rmtree, untar, unzip, get_download_to_descriptor, sha256sum
uname = platform.uname()
# the rootUrl for the firefox-ci deployment of Taskcluster
FIREFOX_CI_ROOT_URL = 'https://firefox-ci-tc.services.mozilla.com'
def _get_fileversion(binary, logger=None):
command = "(Get-Item '%s').VersionInfo.FileVersion" % binary.replace("'", "''")
try:
return call("powershell.exe", command).strip()
except (subprocess.CalledProcessError, OSError):
if logger is not None:
logger.warning("Failed to call %s in PowerShell" % command)
return None
def get_ext(filename):
"""Get the extension from a filename with special handling for .tar.foo"""
name, ext = os.path.splitext(filename)
if name.endswith(".tar"):
ext = ".tar%s" % ext
return ext
def get_taskcluster_artifact(index, path):
TC_INDEX_BASE = FIREFOX_CI_ROOT_URL + "/api/index/v1/"
resp = get(TC_INDEX_BASE + "task/%s/artifacts/%s" % (index, path))
resp.raise_for_status()
return resp
class Browser(object):
__metaclass__ = ABCMeta
def __init__(self, logger):
self.logger = logger
def _get_dest(self, dest, channel):
if dest is None:
# os.getcwd() doesn't include the venv path
dest = os.path.join(os.getcwd(), "_venv")
dest = os.path.join(dest, "browsers", channel)
if not os.path.exists(dest):
os.makedirs(dest)
return dest
@abstractmethod
def download(self, dest=None, channel=None, rename=None):
"""Download a package or installer for the browser
:param dest: Directory in which to put the dowloaded package
:param channel: Browser channel to download
:param rename: Optional name for the downloaded package; the original
extension is preserved.
:return: The path to the downloaded package/installer
"""
return NotImplemented
@abstractmethod
def install(self, dest=None, channel=None):
"""Download and install the browser.
This method usually calls download().
:param dest: Directory in which to install the browser
:param channel: Browser channel to install
:return: The path to the installed browser
"""
return NotImplemented
@abstractmethod
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
"""Download and install the WebDriver implementation for this browser.
:param dest: Directory in which to install the WebDriver
:param channel: Browser channel to install
:param browser_binary: The path to the browser binary
:return: The path to the installed WebDriver
"""
return NotImplemented
@abstractmethod
def find_binary(self, venv_path=None, channel=None):
"""Find the binary of the browser.
If the WebDriver for the browser is able to find the binary itself, this
method doesn't need to be implemented, in which case NotImplementedError
is suggested to be raised to prevent accidental use.
"""
return NotImplemented
@abstractmethod
def find_webdriver(self, venv_path=None, channel=None):
"""Find the binary of the WebDriver."""
return NotImplemented
@abstractmethod
def version(self, binary=None, webdriver_binary=None):
"""Retrieve the release version of the installed browser."""
return NotImplemented
@abstractmethod
def requirements(self):
"""Name of the browser-specific wptrunner requirements file"""
return NotImplemented
class Firefox(Browser):
"""Firefox-specific interface.
Includes installation, webdriver installation, and wptrunner setup methods.
"""
product = "firefox"
binary = "browsers/firefox/firefox"
requirements = "requirements_firefox.txt"
platform = {
"Linux": "linux",
"Windows": "win",
"Darwin": "macos"
}.get(uname[0])
application_name = {
"stable": "Firefox.app",
"beta": "Firefox.app",
"nightly": "Firefox Nightly.app"
}
def platform_string_geckodriver(self):
if self.platform is None:
raise ValueError("Unable to construct a valid Geckodriver package name for current platform")
if self.platform in ("linux", "win"):
bits = "64" if uname[4] == "x86_64" else "32"
else:
bits = ""
return "%s%s" % (self.platform, bits)
def download(self, dest=None, channel="nightly", rename=None):
product = {
"nightly": "firefox-nightly-latest-ssl",
"beta": "firefox-beta-latest-ssl",
"stable": "firefox-latest-ssl"
}
os_builds = {
("linux", "x86"): "linux",
("linux", "x86_64"): "linux64",
("win", "x86"): "win",
("win", "AMD64"): "win64",
("macos", "x86_64"): "osx",
}
os_key = (self.platform, uname[4])
if dest is None:
dest = self._get_dest(None, channel)
if channel not in product:
raise ValueError("Unrecognised release channel: %s" % channel)
if os_key not in os_builds:
raise ValueError("Unsupported platform: %s %s" % os_key)
url = "https://download.mozilla.org/?product=%s&os=%s&lang=en-US" % (product[channel],
os_builds[os_key])
self.logger.info("Downloading Firefox from %s" % url)
resp = get(url)
filename = None
content_disposition = resp.headers.get('content-disposition')
if content_disposition:
filenames = re.findall("filename=(.+)", content_disposition)
if filenames:
filename = filenames[0]
if not filename:
filename = urlsplit(resp.url).path.rsplit("/", 1)[1]
if not filename:
filename = "firefox.tar.bz2"
if rename:
filename = "%s%s" % (rename, get_ext(filename))
installer_path = os.path.join(dest, filename)
with open(installer_path, "wb") as f:
f.write(resp.content)
return installer_path
def install(self, dest=None, channel="nightly"):
"""Install Firefox."""
import mozinstall
dest = self._get_dest(dest, channel)
filename = os.path.basename(dest)
installer_path = self.download(dest, channel)
try:
mozinstall.install(installer_path, dest)
except mozinstall.mozinstall.InstallError:
if self.platform == "macos" and os.path.exists(os.path.join(dest, self.application_name.get(channel, "Firefox Nightly.app"))):
# mozinstall will fail if nightly is already installed in the venv because
# mac installation uses shutil.copy_tree
mozinstall.uninstall(os.path.join(dest, self.application_name.get(channel, "Firefox Nightly.app")))
mozinstall.install(filename, dest)
else:
raise
os.remove(installer_path)
return self.find_binary_path(dest)
def find_binary_path(self, path=None, channel="nightly"):
"""Looks for the firefox binary in the virtual environment"""
if path is None:
# os.getcwd() doesn't include the venv path
path = os.path.join(os.getcwd(), "_venv", "browsers", channel)
binary = None
if self.platform == "linux":
binary = find_executable("firefox", os.path.join(path, "firefox"))
elif self.platform == "win":
import mozinstall
try:
binary = mozinstall.get_binary(path, "firefox")
except mozinstall.InvalidBinary:
# ignore the case where we fail to get a binary
pass
elif self.platform == "macos":
binary = find_executable("firefox", os.path.join(path, self.application_name.get(channel, "Firefox Nightly.app"),
"Contents", "MacOS"))
return binary
def find_binary(self, venv_path=None, channel="nightly"):
if venv_path is None:
venv_path = os.path.join(os.getcwd(), "_venv")
path = os.path.join(venv_path, "browsers", channel)
binary = self.find_binary_path(path, channel)
if not binary and self.platform == "win":
winpaths = [os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Mozilla Firefox"),
os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Mozilla Firefox")]
for winpath in winpaths:
binary = self.find_binary_path(winpath, channel)
if binary is not None:
break
if not binary and self.platform == "macos":
macpaths = ["/Applications/Firefox Nightly.app/Contents/MacOS",
os.path.expanduser("~/Applications/Firefox Nightly.app/Contents/MacOS"),
"/Applications/Firefox Developer Edition.app/Contents/MacOS",
os.path.expanduser("~/Applications/Firefox Developer Edition.app/Contents/MacOS"),
"/Applications/Firefox.app/Contents/MacOS",
os.path.expanduser("~/Applications/Firefox.app/Contents/MacOS")]
return find_executable("firefox", os.pathsep.join(macpaths))
if binary is None:
return find_executable("firefox")
return binary
def find_certutil(self):
path = find_executable("certutil")
if path is None:
return None
if os.path.splitdrive(os.path.normcase(path))[1].split(os.path.sep) == ["", "windows", "system32", "certutil.exe"]:
return None
return path
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("geckodriver")
def get_version_and_channel(self, binary):
version_string = call(binary, "--version").strip()
m = re.match(r"Mozilla Firefox (\d+\.\d+(?:\.\d+)?)(a|b)?", version_string)
if not m:
return None, "nightly"
version, status = m.groups()
channel = {"a": "nightly", "b": "beta"}
return version, channel.get(status, "stable")
def get_profile_bundle_url(self, version, channel):
if channel == "stable":
repo = "https://hg.mozilla.org/releases/mozilla-release"
tag = "FIREFOX_%s_RELEASE" % version.replace(".", "_")
elif channel == "beta":
repo = "https://hg.mozilla.org/releases/mozilla-beta"
major_version = version.split(".", 1)[0]
# For beta we have a different format for betas that are now in stable releases
# vs those that are not
tags = get("https://hg.mozilla.org/releases/mozilla-beta/json-tags").json()["tags"]
tags = {item["tag"] for item in tags}
end_tag = "FIREFOX_BETA_%s_END" % major_version
if end_tag in tags:
tag = end_tag
else:
tag = "tip"
else:
repo = "https://hg.mozilla.org/mozilla-central"
# Always use tip as the tag for nightly; this isn't quite right
# but to do better we need the actual build revision, which we
# can get if we have an application.ini file
tag = "tip"
return "%s/archive/%s.zip/testing/profiles/" % (repo, tag)
def install_prefs(self, binary, dest=None, channel=None):
if binary:
version, channel_ = self.get_version_and_channel(binary)
if channel is not None and channel != channel_:
# Beta doesn't always seem to have the b in the version string, so allow the
# manually supplied value to override the one from the binary
self.logger.warning("Supplied channel doesn't match binary, using supplied channel")
elif channel is None:
channel = channel_
else:
version = None
if dest is None:
dest = os.curdir
dest = os.path.join(dest, "profiles", channel)
if version:
dest = os.path.join(dest, version)
have_cache = False
if os.path.exists(dest) and len(os.listdir(dest)) > 0:
if channel != "nightly":
have_cache = True
else:
now = datetime.now()
have_cache = (datetime.fromtimestamp(os.stat(dest).st_mtime) >
now - timedelta(days=1))
# If we don't have a recent download, grab and extract the latest one
if not have_cache:
if os.path.exists(dest):
rmtree(dest)
os.makedirs(dest)
url = self.get_profile_bundle_url(version, channel)
self.logger.info("Installing test prefs from %s" % url)
try:
extract_dir = tempfile.mkdtemp()
unzip(get(url).raw, dest=extract_dir)
profiles = os.path.join(extract_dir, os.listdir(extract_dir)[0], 'testing', 'profiles')
for name in os.listdir(profiles):
path = os.path.join(profiles, name)
shutil.move(path, dest)
finally:
rmtree(extract_dir)
else:
self.logger.info("Using cached test prefs from %s" % dest)
return dest
def _latest_geckodriver_version(self):
"""Get and return latest version number for geckodriver."""
# This is used rather than an API call to avoid rate limits
tags = call("git", "ls-remote", "--tags", "--refs",
"https://github.com/mozilla/geckodriver.git")
release_re = re.compile(r".*refs/tags/v(\d+)\.(\d+)\.(\d+)")
latest_release = (0, 0, 0)
for item in tags.split("\n"):
m = release_re.match(item)
if m:
version = tuple(int(item) for item in m.groups())
if version > latest_release:
latest_release = version
assert latest_release != (0, 0, 0)
return "v%s.%s.%s" % tuple(str(item) for item in latest_release)
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
"""Install latest Geckodriver."""
if dest is None:
dest = os.getcwd()
path = None
if channel == "nightly":
path = self.install_geckodriver_nightly(dest)
if path is None:
self.logger.warning("Nightly webdriver not found; falling back to release")
if path is None:
version = self._latest_geckodriver_version()
format = "zip" if uname[0] == "Windows" else "tar.gz"
self.logger.debug("Latest geckodriver release %s" % version)
url = ("https://github.com/mozilla/geckodriver/releases/download/%s/geckodriver-%s-%s.%s" %
(version, version, self.platform_string_geckodriver(), format))
if format == "zip":
unzip(get(url).raw, dest=dest)
else:
untar(get(url).raw, dest=dest)
path = find_executable(os.path.join(dest, "geckodriver"))
assert path is not None
self.logger.info("Installed %s" %
subprocess.check_output([path, "--version"]).splitlines()[0])
return path
def install_geckodriver_nightly(self, dest):
self.logger.info("Attempting to install webdriver from nightly")
platform_bits = ("64" if uname[4] == "x86_64" else
("32" if self.platform == "win" else ""))
tc_platform = "%s%s" % (self.platform, platform_bits)
archive_ext = ".zip" if uname[0] == "Windows" else ".tar.gz"
archive_name = "public/build/geckodriver%s" % archive_ext
try:
resp = get_taskcluster_artifact(
"gecko.v2.mozilla-central.latest.geckodriver.%s" % tc_platform,
archive_name)
except Exception:
self.logger.info("Geckodriver download failed")
return
if archive_ext == ".zip":
unzip(resp.raw, dest)
else:
untar(resp.raw, dest)
exe_ext = ".exe" if uname[0] == "Windows" else ""
path = os.path.join(dest, "geckodriver%s" % exe_ext)
self.logger.info("Extracted geckodriver to %s" % path)
return path
def version(self, binary=None, webdriver_binary=None):
"""Retrieve the release version of the installed browser."""
version_string = call(binary, "--version").strip()
m = re.match(r"Mozilla Firefox (.*)", version_string)
if not m:
return None
return m.group(1)
class FirefoxAndroid(Browser):
"""Android-specific Firefox interface."""
product = "firefox_android"
requirements = "requirements_firefox.txt"
def __init__(self, logger):
super(FirefoxAndroid, self).__init__(logger)
self.apk_path = None
def download(self, dest=None, channel=None, rename=None):
if dest is None:
dest = os.pwd
resp = get_taskcluster_artifact(
"gecko.v2.mozilla-central.latest.mobile.android-x86_64-opt",
"public/build/geckoview-androidTest.apk")
filename = "geckoview-androidTest.apk"
if rename:
filename = "%s%s" % (rename, get_ext(filename)[1])
self.apk_path = os.path.join(dest, filename)
with open(self.apk_path, "wb") as f:
f.write(resp.content)
return self.apk_path
def install(self, dest=None, channel=None):
return self.download(dest, channel)
def install_prefs(self, binary, dest=None, channel=None):
fx_browser = Firefox(self.logger)
return fx_browser.install_prefs(binary, dest, channel)
def find_binary(self, venv_path=None, channel=None):
return self.apk_path
def find_webdriver(self, venv_path=None, channel=None):
raise NotImplementedError
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
return None
class Chrome(Browser):
"""Chrome-specific interface.
Includes webdriver installation, and wptrunner setup methods.
"""
product = "chrome"
requirements = "requirements_chrome.txt"
platforms = {
"Linux": "Linux",
"Windows": "Win",
"Darwin": "Mac",
}
def __init__(self, logger):
super(Chrome, self).__init__(logger)
self._last_change = None
def download(self, dest=None, channel=None, rename=None):
if channel != "nightly":
raise NotImplementedError("We can only download Chrome Nightly (Chromium ToT) for you.")
if dest is None:
dest = self._get_dest(None, channel)
filename = self._chromium_package_name() + ".zip"
url = self._latest_chromium_snapshot_url() + filename
self.logger.info("Downloading Chrome from %s" % url)
resp = get(url)
installer_path = os.path.join(dest, filename)
with open(installer_path, "wb") as f:
f.write(resp.content)
return installer_path
def install(self, dest=None, channel=None):
if channel != "nightly":
raise NotImplementedError("We can only install Chrome Nightly (Chromium ToT) for you.")
dest = self._get_dest(dest, channel)
installer_path = self.download(dest, channel)
with open(installer_path, "rb") as f:
unzip(f, dest)
os.remove(installer_path)
return self.find_nightly_binary(dest)
def install_mojojs(self, dest, channel, browser_binary):
if channel == "nightly" or channel == "canary":
url = self._latest_chromium_snapshot_url() + "mojojs.zip"
else:
chrome_version = self.version(binary=browser_binary)
assert chrome_version, "Cannot determine the version of Chrome"
# Remove channel suffixes (e.g. " dev").
chrome_version = chrome_version.split(' ')[0]
url = "https://storage.googleapis.com/chrome-wpt-mojom/%s/linux64/mojojs.zip" % chrome_version
extracted = os.path.join(dest, "mojojs", "gen")
last_url_file = os.path.join(extracted, "DOWNLOADED_FROM")
if os.path.exists(last_url_file):
with open(last_url_file, "rt") as f:
last_url = f.read().strip()
if last_url == url:
self.logger.info("Mojo bindings already up to date")
return extracted
rmtree(extracted)
self.logger.info("Downloading Mojo bindings from %s" % url)
unzip(get(url).raw, dest)
with open(last_url_file, "wt") as f:
f.write(url)
return extracted
def _chromedriver_platform_string(self):
platform = self.platforms.get(uname[0])
if platform is None:
raise ValueError("Unable to construct a valid Chrome package name for current platform")
platform = platform.lower()
if platform == "linux":
bits = "64" if uname[4] == "x86_64" else "32"
elif platform == "mac":
bits = "64"
elif platform == "win":
bits = "32"
return "%s%s" % (platform, bits)
def _chromium_platform_string(self):
platform = self.platforms.get(uname[0])
if platform is None:
raise ValueError("Unable to construct a valid Chromium package name for current platform")
if (platform == "Linux" or platform == "Win") and uname[4] == "x86_64":
platform += "_x64"
return platform
def _chromium_package_name(self):
return "chrome-%s" % self.platforms.get(uname[0]).lower()
def _latest_chromium_snapshot_url(self):
# Make sure we use the same revision in an invocation.
architecture = self._chromium_platform_string()
if self._last_change is None:
revision_url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/LAST_CHANGE" % architecture
self._last_change = get(revision_url).text.strip()
return "https://storage.googleapis.com/chromium-browser-snapshots/%s/%s/" % (architecture, self._last_change)
def find_nightly_binary(self, dest):
if uname[0] == "Darwin":
return find_executable("Chromium",
os.path.join(dest, self._chromium_package_name(), "Chromium.app", "Contents", "MacOS"))
# find_executable will add .exe on Windows automatically.
return find_executable("chrome", os.path.join(dest, self._chromium_package_name()))
def find_binary(self, venv_path=None, channel=None):
if channel == "nightly":
return self.find_nightly_binary(self._get_dest(venv_path, channel))
if uname[0] == "Linux":
name = "google-chrome"
if channel == "stable":
name += "-stable"
elif channel == "beta":
name += "-beta"
elif channel == "dev":
name += "-unstable"
# No Canary on Linux.
return find_executable(name)
if uname[0] == "Darwin":
suffix = ""
if channel in ("beta", "dev", "canary"):
suffix = " " + channel.capitalize()
return "/Applications/Google Chrome%s.app/Contents/MacOS/Google Chrome%s" % (suffix, suffix)
if uname[0] == "Windows":
path = os.path.expandvars(r"$SYSTEMDRIVE\Program Files (x86)\Google\Chrome\Application\chrome.exe")
if not os.path.exists(path):
path = os.path.expandvars(r"$SYSTEMDRIVE\Program Files\Google\Chrome\Application\chrome.exe")
return path
self.logger.warning("Unable to find the browser binary.")
return None
def find_webdriver(self, venv_path=None, channel=None, browser_binary=None):
return find_executable("chromedriver")
def webdriver_supports_browser(self, webdriver_binary, browser_binary, browser_channel):
chromedriver_version = self.webdriver_version(webdriver_binary)
if not chromedriver_version:
self.logger.warning(
"Unable to get version for ChromeDriver %s, rejecting it" %
webdriver_binary)
return False
browser_version = self.version(browser_binary)
if not browser_version:
# If we can't get the browser version, we just have to assume the
# ChromeDriver is good.
return True
# Check that the ChromeDriver version matches the Chrome version.
chromedriver_major = int(chromedriver_version.split('.')[0])
browser_major = int(browser_version.split('.')[0])
if chromedriver_major != browser_major:
# There is no official ChromeDriver release for the dev channel -
# it switches between beta and tip-of-tree, so we accept version+1
# too for dev.
if browser_channel == "dev" and chromedriver_major == (browser_major + 1):
self.logger.debug(
"Accepting ChromeDriver %s for Chrome/Chromium Dev %s" %
(chromedriver_version, browser_version))
return True
self.logger.warning(
"ChromeDriver %s does not match Chrome/Chromium %s" %
(chromedriver_version, browser_version))
return False
return True
def _official_chromedriver_url(self, chrome_version):
# http://chromedriver.chromium.org/downloads/version-selection
parts = chrome_version.split(".")
assert len(parts) == 4
latest_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s.%s.%s" % tuple(parts[:-1])
try:
latest = get(latest_url).text.strip()
except requests.RequestException:
latest_url = "https://chromedriver.storage.googleapis.com/LATEST_RELEASE_%s" % parts[0]
try:
latest = get(latest_url).text.strip()
except requests.RequestException:
return None
return "https://chromedriver.storage.googleapis.com/%s/chromedriver_%s.zip" % (
latest, self._chromedriver_platform_string())
def _chromium_chromedriver_url(self, chrome_version):
if chrome_version:
try:
# Try to find the Chromium build with the same revision.
omaha = get("https://omahaproxy.appspot.com/deps.json?version=" + chrome_version).json()
revision = omaha['chromium_base_position']
url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/%s/chromedriver_%s.zip" % (
self._chromium_platform_string(), revision, self._chromedriver_platform_string())
# Check the status without downloading the content (this is a streaming request).
get(url)
return url
except requests.RequestException:
pass
# Fall back to the tip-of-tree Chromium build.
return "%schromedriver_%s.zip" % (self._latest_chromium_snapshot_url(), self._chromedriver_platform_string())
def _latest_chromedriver_url(self, chrome_version):
# Remove channel suffixes (e.g. " dev").
chrome_version = chrome_version.split(' ')[0]
return (self._official_chromedriver_url(chrome_version) or
self._chromium_chromedriver_url(chrome_version))
def install_webdriver_by_version(self, version, dest=None):
if dest is None:
dest = os.pwd
# There may be an existing chromedriver binary from a previous install.
# To provide a clean install experience, remove the old binary - this
# avoids tricky issues like unzipping over a read-only file.
existing_binary_path = find_executable("chromedriver", dest)
if existing_binary_path:
self.logger.info("Removing existing ChromeDriver binary: %s" %
existing_binary_path)
os.chmod(existing_binary_path, stat.S_IWUSR)
os.remove(existing_binary_path)
url = self._latest_chromedriver_url(version) if version \
else self._chromium_chromedriver_url(None)
self.logger.info("Downloading ChromeDriver from %s" % url)
unzip(get(url).raw, dest)
# The two sources of ChromeDriver have different zip structures:
# * Chromium archives the binary inside a chromedriver_* directory;
# * Chrome archives the binary directly.
# We want to make sure the binary always ends up directly in bin/.
chromedriver_dir = os.path.join(
dest, 'chromedriver_%s' % self._chromedriver_platform_string())
binary_path = find_executable("chromedriver", chromedriver_dir)
if binary_path is not None:
shutil.move(binary_path, dest)
rmtree(chromedriver_dir)
binary_path = find_executable("chromedriver", dest)
assert binary_path is not None
return binary_path
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
if channel == "nightly":
# The "nightly" channel is not an official channel, so we simply download ToT.
return self.install_webdriver_by_version(None, dest)
if browser_binary is None:
browser_binary = self.find_binary(channel)
return self.install_webdriver_by_version(
self.version(browser_binary), dest)
def version(self, binary=None, webdriver_binary=None):
if not binary:
self.logger.warning("No browser binary provided.")
return None
if uname[0] == "Windows":
return _get_fileversion(binary, self.logger)
try:
version_string = call(binary, "--version").strip()
except (subprocess.CalledProcessError, OSError) as e:
self.logger.warning("Failed to call %s: %s" % (binary, e))
return None
m = re.match(r"(?:Google Chrome|Chromium) (.*)", version_string)
if not m:
self.logger.warning("Failed to extract version from: %s" % version_string)
return None
return m.group(1)
def webdriver_version(self, webdriver_binary):
if uname[0] == "Windows":
return _get_fileversion(webdriver_binary, self.logger)
try:
version_string = call(webdriver_binary, "--version").strip()
except (subprocess.CalledProcessError, OSError) as e:
self.logger.warning("Failed to call %s: %s" % (webdriver_binary, e))
return None
m = re.match(r"ChromeDriver ([0-9][0-9.]*)", version_string)
if not m:
self.logger.warning("Failed to extract version from: %s" % version_string)
return None
return m.group(1)
class ChromeAndroidBase(Browser):
"""A base class for ChromeAndroid and AndroidWebView.
On Android, WebView is based on Chromium open source project, and on some
versions of Android we share the library with Chrome. Therefore, we have
a very similar WPT runner implementation.
Includes webdriver installation.
"""
__metaclass__ = ABCMeta # This is an abstract class.
def __init__(self, logger):
super(ChromeAndroidBase, self).__init__(logger)
self.device_serial = None
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
@abstractmethod
def find_binary(self, venv_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("chromedriver")
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
if browser_binary is None:
browser_binary = self.find_binary(channel)
chrome = Chrome(self.logger)
return chrome.install_webdriver_by_version(
self.version(browser_binary), dest)
def version(self, binary=None, webdriver_binary=None):
if not binary:
self.logger.warning("No package name provided.")
return None
command = ['adb']
if self.device_serial:
command.extend(['-s', self.device_serial])
command.extend(['shell', 'dumpsys', 'package', binary])
try:
output = call(*command)
except (subprocess.CalledProcessError, OSError):
self.logger.warning("Failed to call %s" % " ".join(command))
return None
match = re.search(r'versionName=(.*)', output)
if not match:
self.logger.warning("Failed to find versionName")
return None
return match.group(1)
class ChromeAndroid(ChromeAndroidBase):
"""Chrome-specific interface for Android.
"""
product = "chrome_android"
requirements = "requirements_chrome_android.txt"
def find_binary(self, venv_path=None, channel=None):
if channel in ("beta", "dev", "canary"):
return "com.chrome." + channel
return "com.android.chrome"
# TODO(aluo): This is largely copied from the AndroidWebView implementation.
# Tests are not running for weblayer yet (crbug/1019521), this initial
# implementation will help to reproduce and debug any issues.
class AndroidWeblayer(ChromeAndroidBase):
"""Weblayer-specific interface for Android."""
product = "android_weblayer"
# TODO(aluo): replace this with weblayer version after tests are working.
requirements = "requirements_android_webview.txt"
def find_binary(self, venv_path=None, channel=None):
return "org.chromium.weblayer.shell"
class AndroidWebview(ChromeAndroidBase):
"""Webview-specific interface for Android.
Design doc:
https://docs.google.com/document/d/19cGz31lzCBdpbtSC92svXlhlhn68hrsVwSB7cfZt54o/view
"""
product = "android_webview"
requirements = "requirements_android_webview.txt"
def find_binary(self, venv_path=None, channel=None):
# Just get the current package name of the WebView provider.
# For WebView, it is not trivial to change the WebView provider, so
# we will just grab whatever is available.
# https://chromium.googlesource.com/chromium/src/+/HEAD/android_webview/docs/channels.md
command = ['adb']
if self.device_serial:
command.extend(['-s', self.device_serial])
command.extend(['shell', 'dumpsys', 'webviewupdate'])
try:
output = call(*command)
except (subprocess.CalledProcessError, OSError):
self.logger.warning("Failed to call %s" % " ".join(command))
return None
m = re.search(r'^\s*Current WebView package \(name, version\): \((.*), ([0-9.]*)\)$',
output, re.M)
if m is None:
self.logger.warning("Unable to find current WebView package in dumpsys output")
return None
self.logger.warning("Final package name: " + m.group(1))
return m.group(1)
class ChromeiOS(Browser):
"""Chrome-specific interface for iOS.
"""
product = "chrome_ios"
requirements = "requirements_chrome_ios.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
raise NotImplementedError
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
return None
class Opera(Browser):
"""Opera-specific interface.
Includes webdriver installation, and wptrunner setup methods.
"""
product = "opera"
requirements = "requirements_opera.txt"
@property
def binary(self):
if uname[0] == "Linux":
return "/usr/bin/opera"
# TODO Windows, Mac?
self.logger.warning("Unable to find the browser binary.")
return None
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def platform_string(self):
platform = {
"Linux": "linux",
"Windows": "win",
"Darwin": "mac"
}.get(uname[0])
if platform is None:
raise ValueError("Unable to construct a valid Opera package name for current platform")
if platform == "linux":
bits = "64" if uname[4] == "x86_64" else "32"
elif platform == "mac":
bits = "64"
elif platform == "win":
bits = "32"
return "%s%s" % (platform, bits)
def find_binary(self, venv_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("operadriver")
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
if dest is None:
dest = os.pwd
latest = get("https://api.github.com/repos/operasoftware/operachromiumdriver/releases/latest").json()["tag_name"]
url = "https://github.com/operasoftware/operachromiumdriver/releases/download/%s/operadriver_%s.zip" % (latest,
self.platform_string())
unzip(get(url).raw, dest)
operadriver_dir = os.path.join(dest, "operadriver_%s" % self.platform_string())
shutil.move(os.path.join(operadriver_dir, "operadriver"), dest)
rmtree(operadriver_dir)
path = find_executable("operadriver")
st = os.stat(path)
os.chmod(path, st.st_mode | stat.S_IEXEC)
return path
def version(self, binary=None, webdriver_binary=None):
"""Retrieve the release version of the installed browser."""
binary = binary or self.binary
try:
output = call(binary, "--version")
except subprocess.CalledProcessError:
self.logger.warning("Failed to call %s" % binary)
return None
m = re.search(r"[0-9\.]+( [a-z]+)?$", output.strip())
if m:
return m.group(0)
class EdgeChromium(Browser):
"""MicrosoftEdge-specific interface."""
platform = {
"Linux": "linux",
"Windows": "win",
"Darwin": "macos"
}.get(uname[0])
product = "edgechromium"
edgedriver_name = "msedgedriver"
requirements = "requirements_edge_chromium.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
self.logger.info(f'Finding Edge binary for channel {channel}')
if self.platform == "linux":
name = "microsoft-edge"
if channel == "stable":
name += "-stable"
elif channel == "beta":
name += "-beta"
elif channel == "dev":
name += "-dev"
# No Canary on Linux.
return find_executable(name)
if self.platform == "macos":
suffix = ""
if channel in ("beta", "dev", "canary"):
suffix = " " + channel.capitalize()
return f"/Applications/Microsoft Edge{suffix}.app/Contents/MacOS/Microsoft Edge{suffix}"
if self.platform == "win":
binaryname = "msedge"
if channel == "beta":
winpaths = [os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge Beta\\Application"),
os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge Beta\\Application")]
return find_executable(binaryname, os.pathsep.join(winpaths))
elif channel == "dev":
winpaths = [os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge Dev\\Application"),
os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge Dev\\Application")]
return find_executable(binaryname, os.pathsep.join(winpaths))
elif channel == "canary":
winpaths = [os.path.expanduser("~\\AppData\\Local\\Microsoft\\Edge\\Application"),
os.path.expanduser("~\\AppData\\Local\\Microsoft\\Edge SxS\\Application")]
return find_executable(binaryname, os.pathsep.join(winpaths))
else:
winpaths = [os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge\\Application"),
os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge\\Application")]
return find_executable(binaryname, os.pathsep.join(winpaths))
self.logger.warning("Unable to find the browser binary.")
return None
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("msedgedriver")
def webdriver_supports_browser(self, webdriver_binary, browser_binary):
edgedriver_version = self.webdriver_version(webdriver_binary)
if not edgedriver_version:
self.logger.warning(
f"Unable to get version for EdgeDriver {webdriver_binary}, rejecting it")
return False
browser_version = self.version(browser_binary)
if not browser_version:
# If we can't get the browser version, we just have to assume the
# EdgeDriver is good.
return True
# Check that the EdgeDriver version matches the Edge version.
edgedriver_major = int(edgedriver_version.split('.')[0])
browser_major = int(browser_version.split('.')[0])
if edgedriver_major != browser_major:
self.logger.warning(
f"EdgeDriver {edgedriver_version} does not match Edge {browser_version}")
return False
return True
def install_webdriver_by_version(self, version, dest=None):
if dest is None:
dest = os.pwd
if self.platform == "linux":
bits = "linux64"
edgedriver_path = os.path.join(dest, self.edgedriver_name)
elif self.platform == "macos":
bits = "mac64"
edgedriver_path = os.path.join(dest, self.edgedriver_name)
else:
bits = "win64" if uname[4] == "x86_64" else "win32"
edgedriver_path = os.path.join(dest, f"{self.edgedriver_name}.exe")
url = f"https://msedgedriver.azureedge.net/{version}/edgedriver_{bits}.zip"
# cleanup existing Edge driver files to avoid access_denied errors when unzipping
if os.path.isfile(edgedriver_path):
# remove read-only attribute
os.chmod(edgedriver_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777
print(f"Delete {edgedriver_path} file")
os.remove(edgedriver_path)
driver_notes_path = os.path.join(dest, "Driver_notes")
if os.path.isdir(driver_notes_path):
print(f"Delete {driver_notes_path} folder")
rmtree(driver_notes_path)
self.logger.info(f"Downloading MSEdgeDriver from {url}")
unzip(get(url).raw, dest)
if os.path.isfile(edgedriver_path):
self.logger.info(f"Successfully downloaded MSEdgeDriver to {edgedriver_path}")
return find_executable(self.edgedriver_name, dest)
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
self.logger.info(f"Installing MSEdgeDriver for channel {channel}")
if browser_binary is None:
browser_binary = self.find_binary(channel=channel)
else:
self.logger.info(f"Installing matching MSEdgeDriver for Edge binary at {browser_binary}")
version = self.version(browser_binary)
# If an exact version can't be found, use a suitable fallback based on
# the browser channel, if available.
if version is None:
platforms = {
"linux": "LINUX",
"macos": "MACOS",
"win": "WINDOWS"
}
if channel is None:
channel = "dev"
platform = platforms[self.platform]
suffix = f"{channel.upper()}_{platform}"
version_url = f"https://msedgedriver.azureedge.net/LATEST_{suffix}"
version = get(version_url).text.strip()
return self.install_webdriver_by_version(version, dest)
def version(self, binary=None, webdriver_binary=None):
if not binary:
self.logger.warning("No browser binary provided.")
return None
if self.platform == "win":
return _get_fileversion(binary, self.logger)
try:
version_string = call(binary, "--version").strip()
except (subprocess.CalledProcessError, OSError) as e:
self.logger.warning(f"Failed to call {binary}: {e}")
return None
m = re.match(r"Microsoft Edge (.*) ", version_string)
if not m:
self.logger.warning(f"Failed to extract version from: {version_string}")
return None
return m.group(1)
def webdriver_version(self, webdriver_binary):
if self.platform == "win":
return _get_fileversion(webdriver_binary, self.logger)
try:
version_string = call(webdriver_binary, "--version").strip()
except (subprocess.CalledProcessError, OSError) as e:
self.logger.warning(f"Failed to call {webdriver_binary}: {e}")
return None
m = re.match(r"MSEdgeDriver ([0-9][0-9.]*)", version_string)
if not m:
self.logger.warning(f"Failed to extract version from: {version_string}")
return None
return m.group(1)
class Edge(Browser):
"""Edge-specific interface."""
product = "edge"
requirements = "requirements_edge.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("MicrosoftWebDriver")
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
command = "(Get-AppxPackage Microsoft.MicrosoftEdge).Version"
try:
return call("powershell.exe", command).strip()
except (subprocess.CalledProcessError, OSError):
self.logger.warning("Failed to call %s in PowerShell" % command)
return None
class EdgeWebDriver(Edge):
product = "edge_webdriver"
class InternetExplorer(Browser):
"""Internet Explorer-specific interface."""
product = "ie"
requirements = "requirements_ie.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("IEDriverServer.exe")
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
return None
class Safari(Browser):
"""Safari-specific interface.
Includes installation, webdriver installation, and wptrunner setup methods.
"""
product = "safari"
requirements = "requirements_safari.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
path = None
if channel == "preview":
path = "/Applications/Safari Technology Preview.app/Contents/MacOS"
return find_executable("safaridriver", path)
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
if webdriver_binary is None:
self.logger.warning("Cannot find Safari version without safaridriver")
return None
# Use `safaridriver --version` to get the version. Example output:
# "Included with Safari 12.1 (14607.1.11)"
# "Included with Safari Technology Preview (Release 67, 13607.1.9.0.1)"
# The `--version` flag was added in STP 67, so allow the call to fail.
try:
version_string = call(webdriver_binary, "--version").strip()
except subprocess.CalledProcessError:
self.logger.warning("Failed to call %s --version" % webdriver_binary)
return None
m = re.match(r"Included with Safari (.*)", version_string)
if not m:
self.logger.warning("Failed to extract version from: %s" % version_string)
return None
return m.group(1)
class Servo(Browser):
"""Servo-specific interface."""
product = "servo"
requirements = "requirements_servo.txt"
def platform_components(self):
platform = {
"Linux": "linux",
"Windows": "win",
"Darwin": "mac"
}.get(uname[0])
if platform is None:
raise ValueError("Unable to construct a valid Servo package for current platform")
if platform == "linux":
extension = ".tar.gz"
decompress = untar
elif platform == "win" or platform == "mac":
raise ValueError("Unable to construct a valid Servo package for current platform")
return (platform, extension, decompress)
def _get(self, channel="nightly"):
if channel != "nightly":
raise ValueError("Only nightly versions of Servo are available")
platform, extension, _ = self.platform_components()
url = "https://download.servo.org/nightly/%s/servo-latest%s" % (platform, extension)
return get(url)
def download(self, dest=None, channel="nightly", rename=None):
if dest is None:
dest = os.pwd
resp = self._get(dest, channel)
_, extension, _ = self.platform_components()
filename = rename if rename is not None else "servo-latest"
with open(os.path.join(dest, "%s%s" % (filename, extension,)), "w") as f:
f.write(resp.content)
def install(self, dest=None, channel="nightly"):
"""Install latest Browser Engine."""
if dest is None:
dest = os.pwd
_, _, decompress = self.platform_components()
resp = self._get(channel)
decompress(resp.raw, dest=dest)
path = find_executable("servo", os.path.join(dest, "servo"))
st = os.stat(path)
os.chmod(path, st.st_mode | stat.S_IEXEC)
return path
def find_binary(self, venv_path=None, channel=None):
path = find_executable("servo", os.path.join(venv_path, "servo"))
if path is None:
path = find_executable("servo")
return path
def find_webdriver(self, venv_path=None, channel=None):
return None
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
"""Retrieve the release version of the installed browser."""
output = call(binary, "--version")
m = re.search(r"Servo ([0-9\.]+-[a-f0-9]+)?(-dirty)?$", output.strip())
if m:
return m.group(0)
class ServoWebDriver(Servo):
product = "servodriver"
class Sauce(Browser):
"""Sauce-specific interface."""
product = "sauce"
requirements = "requirements_sauce.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venev_path=None, channel=None):
raise NotImplementedError
def find_webdriver(self, venv_path=None, channel=None):
raise NotImplementedError
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
return None
class WebKit(Browser):
"""WebKit-specific interface."""
product = "webkit"
requirements = "requirements_webkit.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
return None
def find_webdriver(self, venv_path=None, channel=None):
return None
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
return None
class WebKitGTKMiniBrowser(WebKit):
def _get_osidversion(self):
with open('/etc/os-release', 'r') as osrelease_handle:
for line in osrelease_handle.readlines():
if line.startswith('ID='):
os_id = line.split('=')[1].strip().strip('"')
if line.startswith('VERSION_ID='):
version_id = line.split('=')[1].strip().strip('"')
assert(os_id)
assert(version_id)
osidversion = os_id + '-' + version_id
assert(' ' not in osidversion)
assert(len(osidversion) > 3)
return osidversion.capitalize()
def download(self, dest=None, channel=None, rename=None):
base_dowload_uri = "https://webkitgtk.org/built-products/"
base_download_dir = base_dowload_uri + "x86_64/release/" + channel + "/" + self._get_osidversion() + "/MiniBrowser/"
try:
response = get(base_download_dir + "LAST-IS")
except requests.exceptions.HTTPError as e:
if e.response.status_code == 404:
raise RuntimeError("Can't find a WebKitGTK MiniBrowser %s bundle for %s at %s"
% (channel, self._get_osidversion(), base_dowload_uri))
raise
bundle_filename = response.text.strip()
bundle_url = base_download_dir + bundle_filename
if dest is None:
dest = self._get_dest(None, channel)
bundle_file_path = os.path.join(dest, bundle_filename)
self.logger.info("Downloading WebKitGTK MiniBrowser bundle from %s" % bundle_url)
with open(bundle_file_path, "w+b") as f:
get_download_to_descriptor(f, bundle_url)
bundle_filename_no_ext, _ = os.path.splitext(bundle_filename)
bundle_hash_url = base_download_dir + bundle_filename_no_ext + ".sha256sum"
bundle_expected_hash = get(bundle_hash_url).text.strip().split(" ")[0]
bundle_computed_hash = sha256sum(bundle_file_path)
if bundle_expected_hash != bundle_computed_hash:
self.logger.error("Calculated SHA256 hash is %s but was expecting %s" % (bundle_computed_hash,bundle_expected_hash))
raise RuntimeError("The WebKitGTK MiniBrowser bundle at %s has incorrect SHA256 hash." % bundle_file_path)
return bundle_file_path
def install(self, dest=None, channel=None, prompt=True):
dest = self._get_dest(dest, channel)
bundle_path = self.download(dest, channel)
bundle_uncompress_directory = os.path.join(dest, "webkitgtk_minibrowser")
# Clean it from previous runs
if os.path.exists(bundle_uncompress_directory):
rmtree(bundle_uncompress_directory)
os.mkdir(bundle_uncompress_directory)
with open(bundle_path, "rb") as f:
unzip(f, bundle_uncompress_directory)
install_dep_script = os.path.join(bundle_uncompress_directory, "install-dependencies.sh")
if os.path.isfile(install_dep_script):
self.logger.info("Executing install-dependencies.sh script from bundle.")
install_dep_cmd = [install_dep_script]
if not prompt:
install_dep_cmd.append("--autoinstall")
# use subprocess.check_call() directly to display unbuffered stdout/stderr in real-time.
subprocess.check_call(install_dep_cmd)
minibrowser_path = os.path.join(bundle_uncompress_directory, "MiniBrowser")
if not os.path.isfile(minibrowser_path):
raise RuntimeError("Can't find a MiniBrowser binary at %s" % minibrowser_path)
os.remove(bundle_path)
install_ok_file = os.path.join(bundle_uncompress_directory, ".installation-ok")
open(install_ok_file, "w").close() # touch
self.logger.info("WebKitGTK MiniBrowser bundle for channel %s installed." % channel)
return minibrowser_path
def _find_executable_in_channel_bundle(self, binary, venv_path=None, channel=None):
if venv_path:
venv_base_path = self._get_dest(venv_path, channel)
bundle_dir = os.path.join(venv_base_path, "webkitgtk_minibrowser")
install_ok_file = os.path.join(bundle_dir, ".installation-ok")
if os.path.isfile(install_ok_file):
return find_executable(binary, bundle_dir)
return None
def find_binary(self, venv_path=None, channel=None):
minibrowser_path = self._find_executable_in_channel_bundle("MiniBrowser", venv_path, channel)
if minibrowser_path:
return minibrowser_path
libexecpaths = ["/usr/libexec/webkit2gtk-4.0"] # Fedora path
triplet = "x86_64-linux-gnu"
# Try to use GCC to detect this machine triplet
gcc = find_executable("gcc")
if gcc:
try:
triplet = call(gcc, "-dumpmachine").strip()
except subprocess.CalledProcessError:
pass
# Add Debian/Ubuntu path
libexecpaths.append("/usr/lib/%s/webkit2gtk-4.0" % triplet)
return find_executable("MiniBrowser", os.pathsep.join(libexecpaths))
def find_webdriver(self, venv_path=None, channel=None):
webdriver_path = self._find_executable_in_channel_bundle("WebKitWebDriver", venv_path, channel)
if not webdriver_path:
webdriver_path = find_executable("WebKitWebDriver")
return webdriver_path
def version(self, binary=None, webdriver_binary=None):
if binary is None:
return None
try: # WebKitGTK MiniBrowser before 2.26.0 doesn't support --version
output = call(binary, "--version").strip()
except subprocess.CalledProcessError:
return None
# Example output: "WebKitGTK 2.26.1"
if output:
m = re.match(r"WebKitGTK (.+)", output)
if not m:
self.logger.warning("Failed to extract version from: %s" % output)
return None
return m.group(1)
return None
class Epiphany(Browser):
"""Epiphany-specific interface."""
product = "epiphany"
requirements = "requirements_epiphany.txt"
def download(self, dest=None, channel=None, rename=None):
raise NotImplementedError
def install(self, dest=None, channel=None):
raise NotImplementedError
def find_binary(self, venv_path=None, channel=None):
return find_executable("epiphany")
def find_webdriver(self, venv_path=None, channel=None):
return find_executable("WebKitWebDriver")
def install_webdriver(self, dest=None, channel=None, browser_binary=None):
raise NotImplementedError
def version(self, binary=None, webdriver_binary=None):
if binary is None:
return None
output = call(binary, "--version")
if output:
# Stable release output looks like: "Web 3.30.2"
# Tech Preview output looks like "Web 3.31.3-88-g97db4f40f"
return output.split()[1]
return None