| import os |
| import platform |
| import re |
| import shutil |
| import stat |
| import errno |
| import subprocess |
| import tempfile |
| import urlparse |
| from abc import ABCMeta, abstractmethod |
| from datetime import datetime, timedelta |
| from distutils.spawn import find_executable |
| |
| import requests |
| |
| from utils import call, get, untar, unzip |
| |
| uname = platform.uname() |
| |
| |
| 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 handle_remove_readonly(func, path, exc): |
| excvalue = exc[1] |
| if func in (os.rmdir, os.remove) and excvalue.errno == errno.EACCES: |
| os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0777 |
| func(path) |
| else: |
| raise |
| |
| class Browser(object): |
| __metaclass__ = ABCMeta |
| |
| def __init__(self, logger): |
| self.logger = logger |
| |
| @abstractmethod |
| def install(self, dest=None): |
| """Install the browser.""" |
| return NotImplemented |
| |
| @abstractmethod |
| def install_webdriver(self, dest=None, channel=None, browser_binary=None): |
| """Install the WebDriver implementation for this browser.""" |
| 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, 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" |
| platform_ini = "browsers/firefox/platform.ini" |
| 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 install(self, dest=None, channel="nightly"): |
| """Install Firefox.""" |
| |
| import mozinstall |
| |
| 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 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) |
| |
| 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) |
| |
| 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 = requests.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 = urlparse.urlsplit(resp.url).path.rsplit("/", 1)[1] |
| |
| if not filename: |
| filename = "firefox.tar.bz2" |
| |
| installer_path = os.path.join(dest, filename) |
| |
| with open(installer_path, "wb") as f: |
| f.write(resp.content) |
| |
| 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, 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): |
| 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_ |
| if dest is None: |
| dest = os.pwd |
| |
| 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): |
| shutil.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: |
| shutil.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 |
| for item in tags.split("\n"): |
| m = release_re.match(item) |
| if m: |
| version = [int(item) for item in m.groups()] |
| if version > latest_release: |
| latest_release = version |
| assert latest_release != 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() |
| |
| if channel == "nightly": |
| path = self.install_geckodriver_nightly(dest) |
| if path is not None: |
| return path |
| else: |
| self.logger.warning("Nightly webdriver not found; falling back to release") |
| |
| 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) |
| return find_executable(os.path.join(dest, "geckodriver")) |
| |
| def install_geckodriver_nightly(self, dest): |
| import tarfile |
| import mozdownload |
| self.logger.info("Attempting to install webdriver from nightly") |
| try: |
| s = mozdownload.DailyScraper(branch="mozilla-central", |
| extension="common.tests.tar.gz", |
| destination=dest) |
| package_path = s.download() |
| except mozdownload.errors.NotFoundError: |
| return |
| |
| try: |
| exe_suffix = ".exe" if uname[0] == "Windows" else "" |
| with tarfile.open(package_path, "r") as f: |
| try: |
| member = f.getmember("bin%sgeckodriver%s" % (os.path.sep, |
| exe_suffix)) |
| except KeyError: |
| return |
| # Remove bin/ from the path. |
| member.name = os.path.basename(member.name) |
| f.extractall(members=[member], path=dest) |
| path = os.path.join(dest, member.name) |
| self.logger.info("Extracted geckodriver to %s" % path) |
| finally: |
| os.unlink(package_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 Fennec(Browser): |
| """Fennec-specific interface.""" |
| |
| product = "fennec" |
| requirements = "requirements_firefox.txt" |
| |
| def install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, 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" |
| |
| @property |
| def binary(self): |
| if uname[0] == "Linux": |
| return "/usr/bin/google-chrome" |
| if uname[0] == "Darwin": |
| return "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" |
| if uname[0] == "Windows": |
| return os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe") |
| self.logger.warning("Unable to find the browser binary.") |
| return None |
| |
| 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 Chrome 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 chromium_platform_string(self): |
| platform = { |
| "Linux": "Linux", |
| "Windows": "Win", |
| "Darwin": "Mac" |
| }.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 find_binary(self, venv_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, channel=None): |
| return find_executable("chromedriver") |
| |
| 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.platform_string()) |
| |
| def _chromium_chromedriver_url(self, 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.platform_string()) |
| # Check the status without downloading the content (this is a streaming request). |
| get(url) |
| except requests.RequestException: |
| # Fall back to the tip-of-tree Chromium build. |
| revision_url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/LAST_CHANGE" % ( |
| self.chromium_platform_string()) |
| revision = get(revision_url).text.strip() |
| url = "https://storage.googleapis.com/chromium-browser-snapshots/%s/%s/chromedriver_%s.zip" % ( |
| self.chromium_platform_string(), revision, self.platform_string()) |
| return url |
| |
| def _latest_chromedriver_url(self, browser_binary=None): |
| chrome_version = self.version(browser_binary) |
| assert chrome_version, "Cannot detect the version of Chrome" |
| # 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(self, dest=None, channel=None, browser_binary=None): |
| if dest is None: |
| dest = os.pwd |
| url = self._latest_chromedriver_url(browser_binary) |
| self.logger.info("Downloading ChromeDriver from %s" % url) |
| unzip(get(url).raw, dest) |
| chromedriver_dir = os.path.join(dest, 'chromedriver_%s' % self.platform_string()) |
| if os.path.isfile(os.path.join(chromedriver_dir, "chromedriver")): |
| shutil.move(os.path.join(chromedriver_dir, "chromedriver"), dest) |
| shutil.rmtree(chromedriver_dir) |
| return find_executable("chromedriver", dest) |
| |
| def version(self, binary=None, webdriver_binary=None): |
| binary = binary or self.binary |
| if uname[0] == "Windows": |
| return _get_fileversion(binary, self.logger) |
| |
| try: |
| version_string = call(binary, "--version").strip() |
| except subprocess.CalledProcessError: |
| self.logger.warning("Failed to call %s" % binary) |
| 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) |
| |
| |
| class ChromeAndroid(Browser): |
| """Chrome-specific interface for Android. |
| |
| Includes webdriver installation. |
| """ |
| |
| product = "chrome_android" |
| requirements = "requirements_chrome_android.txt" |
| |
| def install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, channel=None): |
| return find_executable("chromedriver") |
| |
| def install_webdriver(self, dest=None, channel=None, browser_binary=None): |
| chrome = Chrome() |
| return chrome.install_webdriver(dest, channel) |
| |
| 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 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, 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) |
| shutil.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 install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| binary = None |
| if self.platform == "win": |
| binaryname = "msedge" |
| binary = find_executable(binaryname) |
| if not binary: |
| # Use paths from different Edge channels starting with Release\Beta\Dev\Canary |
| winpaths = [os.path.expanduser("~\\AppData\\Local\\Microsoft\\Edge\\Application"), |
| os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge Beta\\Application"), |
| os.path.expandvars("$SYSTEMDRIVE\\Program Files\\Microsoft\\Edge Dev\\Application"), |
| os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge Beta\\Application"), |
| os.path.expandvars("$SYSTEMDRIVE\\Program Files (x86)\\Microsoft\\Edge Dev\\Application"), |
| os.path.expanduser("~\\AppData\Local\\Microsoft\\Edge SxS\\Application"),] |
| return find_executable(binaryname, os.pathsep.join(winpaths)) |
| if self.platform == "macos": |
| binaryname = "Microsoft Edge Canary" |
| binary = find_executable(binaryname) |
| if not binary: |
| macpaths = ["/Applications/Microsoft Edge.app/Contents/MacOS", |
| os.path.expanduser("~/Applications/Microsoft Edge.app/Contents/MacOS"), |
| "/Applications/Microsoft Edge Canary.app/Contents/MacOS", |
| os.path.expanduser("~/Applications/Microsoft Edge Canary.app/Contents/MacOS")] |
| return find_executable("Microsoft Edge Canary", os.pathsep.join(macpaths)) |
| return binary |
| |
| def find_webdriver(self, channel=None): |
| return find_executable("msedgedriver") |
| |
| def install_webdriver(self, dest=None, channel=None, browser_binary=None): |
| if self.platform != "win" and self.platform != "macos": |
| raise ValueError("Only Windows and Mac platforms are currently supported") |
| |
| if dest is None: |
| dest = os.pwd |
| |
| if channel is None: |
| version_url = "https://msedgedriver.azureedge.net/LATEST_DEV" |
| else: |
| version_url = "https://msedgedriver.azureedge.net/LATEST_%s" % channel.upper() |
| version = get(version_url).text.strip() |
| |
| if 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, "%s.exe" % self.edgedriver_name) |
| url = "https://msedgedriver.azureedge.net/%s/edgedriver_%s.zip" % (version, bits) |
| |
| # 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 |
| os.remove(edgedriver_path) |
| driver_notes_path = os.path.join(dest, "Driver_notes") |
| if os.path.isdir(driver_notes_path): |
| shutil.rmtree(driver_notes_path, ignore_errors=False, onerror=handle_remove_readonly) |
| |
| self.logger.info("Downloading MSEdgeDriver from %s" % url) |
| unzip(get(url).raw, dest) |
| if os.path.isfile(edgedriver_path): |
| self.logger.info("Successfully downloaded MSEdgeDriver to %s" % edgedriver_path) |
| return find_executable(self.edgedriver_name, dest) |
| |
| def version(self, binary=None, webdriver_binary=None): |
| if binary is None: |
| binary = self.find_binary() |
| if self.platform != "win": |
| try: |
| version_string = call(binary, "--version").strip() |
| except subprocess.CalledProcessError: |
| self.logger.warning("Failed to call %s" % binary) |
| return None |
| m = re.match(r"Microsoft Edge (.*) ", version_string) |
| if not m: |
| self.logger.warning("Failed to extract version from: %s" % version_string) |
| return None |
| return m.group(1) |
| else: |
| if binary is not None: |
| return _get_fileversion(binary, self.logger) |
| self.logger.warning("Failed to find Edge binary.") |
| return None |
| |
| class Edge(Browser): |
| """Edge-specific interface.""" |
| |
| product = "edge" |
| requirements = "requirements_edge.txt" |
| |
| def install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, 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 install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, 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 install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, 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 install(self, dest=None, channel="nightly"): |
| """Install latest Browser Engine.""" |
| if channel != "nightly": |
| raise ValueError("Only nightly versions of Servo are available") |
| if dest is None: |
| dest = os.pwd |
| |
| platform, extension, decompress = self.platform_components() |
| url = "https://download.servo.org/nightly/%s/servo-latest%s" % (platform, extension) |
| |
| decompress(get(url).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, 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 install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venev_path=None, channel=None): |
| raise NotImplementedError |
| |
| def find_webdriver(self, 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 install(self, dest=None, channel=None): |
| raise NotImplementedError |
| |
| def find_binary(self, venv_path=None, channel=None): |
| return None |
| |
| def find_webdriver(self, 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 Epiphany(Browser): |
| """Epiphany-specific interface.""" |
| |
| product = "epiphany" |
| requirements = "requirements_epiphany.txt" |
| |
| 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, 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 |