diff options
author | Jenkins <jenkins@review.openstack.org> | 2016-02-08 14:28:55 +0000 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2016-02-08 14:28:56 +0000 |
commit | 8f459658e6b3334cc33d1876e959919e8061eda5 (patch) | |
tree | f539c6310616821085c15d319699314372c332db /horizon/test | |
parent | 1a03ea2a6c7592f4e6adf74c283a2d7853892685 (diff) | |
parent | a17612ed9748d141381fcfff3d88f178cbebe39a (diff) | |
download | horizon-8f459658e6b3334cc33d1876e959919e8061eda5.tar.gz |
Merge "Merge the two webdrivers"
Diffstat (limited to 'horizon/test')
-rw-r--r-- | horizon/test/firefox_binary.py | 74 | ||||
-rw-r--r-- | horizon/test/helpers.py | 8 | ||||
-rw-r--r-- | horizon/test/webdriver.py | 224 |
3 files changed, 235 insertions, 71 deletions
diff --git a/horizon/test/firefox_binary.py b/horizon/test/firefox_binary.py new file mode 100644 index 000000000..2597789d3 --- /dev/null +++ b/horizon/test/firefox_binary.py @@ -0,0 +1,74 @@ +# Copyright 2015, Rackspace, US, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import platform +import shutil +import subprocess + +from selenium.common import exceptions as selenium_exceptions +from selenium.webdriver.common import desired_capabilities as dc +from selenium.webdriver import firefox + + +class FirefoxBinary(firefox.firefox_binary.FirefoxBinary): + """Workarounds selenium firefox issues. + + There is race condition in the way firefox is spawned. The exact + cause hasn't been properly diagnosed yet but it's around: + + - getting a free port from the OS with + selenium.webdriver.common.utils free_port(), + + - release the port immediately but record it in ff prefs so that ff + can listen on that port for the internal http server. + + It has been observed that this leads to hanging processes for + 'firefox -silent'. + """ + + def _start_from_profile_path(self, path): + self._firefox_env["XRE_PROFILE_PATH"] = path + + if platform.system().lower() == 'linux': + self._modify_link_library_path() + command = [self._start_cmd, "-silent"] + if self.command_line is not None: + for cli in self.command_line: + command.append(cli) + +# The following exists upstream and is known to create hanging +# firefoxes, leading to zombies. +# subprocess.Popen(command, stdout=self._log_file, +# stderr=subprocess.STDOUT, +# env=self._firefox_env).communicate() + command[1] = '-foreground' + self.process = subprocess.Popen( + command, stdout=self._log_file, stderr=subprocess.STDOUT, + env=self._firefox_env) + + +class WebDriver(firefox.webdriver.WebDriver): + """Workarounds selenium firefox issues.""" + def __init__(self, firefox_profile=None, firefox_binary=None, timeout=30, + desired_capabilities=dc.DesiredCapabilities.FIREFOX, + proxy=None): + try: + super(WebDriver, self).__init__( + firefox_profile, FirefoxBinary(), timeout, + desired_capabilities, proxy) + except selenium_exceptions.WebDriverException: + # If we can't start, cleanup profile + shutil.rmtree(self.profile.path) + if self.profile.tempfolder is not None: + shutil.rmtree(self.profile.tempfolder) + raise diff --git a/horizon/test/helpers.py b/horizon/test/helpers.py index 5ad7ebadc..cc69a703d 100644 --- a/horizon/test/helpers.py +++ b/horizon/test/helpers.py @@ -41,15 +41,17 @@ from django.contrib.staticfiles.testing \ LOG = logging.getLogger(__name__) +# NOTE: Several distributions can't ship Selenium, or the Firefox +# component of it, due to its non-free license. So they have to patch +# it out of test-requirements.txt Avoid import failure and force not +# running selenium tests if we attempt to run selenium tests using the +# Firefox driver and it is not available. try: from selenium.webdriver.support import ui as selenium_ui import xvfbwrapper # Only needed when running the Selenium tests headless from horizon.test.webdriver import WebDriver # noqa except ImportError as e: - # NOTE(saschpe): Several distribution can't ship selenium due to its - # non-free license. So they have to patch it out of test-requirements.txt - # Avoid import failure and force not running selenium tests. LOG.warning("{0}, force WITH_SELENIUM=False".format(str(e))) os.environ['WITH_SELENIUM'] = '' diff --git a/horizon/test/webdriver.py b/horizon/test/webdriver.py index 87e02602a..2a699fcd1 100644 --- a/horizon/test/webdriver.py +++ b/horizon/test/webdriver.py @@ -19,77 +19,165 @@ import logging import os -import platform -import shutil -import subprocess +import time LOG = logging.getLogger(__name__) + +class ElementNotReloadableException(Exception): + """Raised when reload is not possible.""" + pass + + +from selenium.common import exceptions +from selenium.webdriver.common import by +from selenium.webdriver.common import desired_capabilities as dc +from selenium.webdriver.remote import webelement + # Select the WebDriver to use based on the --selenium-phantomjs switch. -# NOTE: Several distributions can't ship Selenium, or the Firefox -# component of it, due to its non-free license. So they have to patch -# it out of test-requirements.txt Avoid import failure and force not -# running selenium tests if we attempt to run selenium tests using the -# Firefox driver and it is not available. -try: - if os.environ.get('SELENIUM_PHANTOMJS'): - from selenium.webdriver import PhantomJS as WebDriver - else: - from selenium.common import exceptions as selenium_exceptions - from selenium.webdriver import firefox - - class FirefoxBinary(firefox.firefox_binary.FirefoxBinary): - """Workarounds selenium firefox issues. - - There is race condition in the way firefox is spawned. The exact - cause hasn't been properly diagnosed yet but it's around: - - - getting a free port from the OS with - selenium.webdriver.common.utils free_port(), - - - release the port immediately but record it in ff prefs so that ff - can listen on that port for the internal http server. - - It has been observed that this leads to hanging processes for - 'firefox -silent'. - """ - - def _start_from_profile_path(self, path): - self._firefox_env["XRE_PROFILE_PATH"] = path - - if platform.system().lower() == 'linux': - self._modify_link_library_path() - command = [self._start_cmd, "-silent"] - if self.command_line is not None: - for cli in self.command_line: - command.append(cli) - - # The following exists upstream and is known to create hanging - # firefoxes, leading to zombies. - # subprocess.Popen(command, stdout=self._log_file, - # stderr=subprocess.STDOUT, - # env=self._firefox_env).communicate() - command[1] = '-foreground' - self.process = subprocess.Popen( - command, stdout=self._log_file, stderr=subprocess.STDOUT, - env=self._firefox_env) - - class WebDriver(firefox.webdriver.WebDriver): - """Workarounds selenium firefox issues.""" - - def __init__(self, firefox_profile=None, firefox_binary=None, - timeout=30, capabilities=None, proxy=None): - try: - super(WebDriver, self).__init__( - firefox_profile, FirefoxBinary(), timeout, - capabilities, proxy) - except selenium_exceptions.WebDriverException: - # If we can't start, cleanup profile - shutil.rmtree(self.profile.path) - if self.profile.tempfolder is not None: - shutil.rmtree(self.profile.tempfolder) +if os.environ.get('SELENIUM_PHANTOMJS'): + from selenium.webdriver import PhantomJS as WebDriver + desired_capabilities = dc.DesiredCapabilities.PHANTOMJS +else: + from horizon.test.firefox_binary import WebDriver + desired_capabilities = dc.DesiredCapabilities.FIREFOX + + +class WrapperFindOverride(object): + """Mixin for overriding find_element methods.""" + + def find_element(self, by=by.By.ID, value=None): + web_el = super(WrapperFindOverride, self).find_element(by, value) + return WebElementWrapper(web_el.parent, web_el.id, (by, value), + self) + + def find_elements(self, by=by.By.ID, value=None): + web_els = super(WrapperFindOverride, self).find_elements(by, value) + result = [] + for index, web_el in enumerate(web_els): + result.append(WebElementWrapper(web_el.parent, web_el.id, + (by, value), self, index)) + return result + + +class WebElementWrapper(WrapperFindOverride, webelement.WebElement): + """WebElement class wrapper. + + WebElement wrapper that catches the StaleElementReferenceException and + tries to reload the element by sending request to its source element + (element that created actual element) for reload, in case that source + element needs to be reloaded as well, it asks its parent till web + driver is reached. In case driver was reached and did not manage to + find the element it is probable that programmer made a mistake and + actualStaleElementReferenceException is raised. + """ + + STALE_ELEMENT_REFERENCE_WAIT = 0.5 + STALE_ELEMENT_REFERENCE_MAX_TRY = 10 + + def __init__(self, parent, id_, locator, src_element, index=None): + super(WebElementWrapper, self).__init__(parent, id_) + self.locator = locator + self.src_element = src_element + + # StaleElementReferenceException occurrence counter + self.stale_reference_occurrence = 0 + + # storing if web element reload succeed or not + # in case of fail StaleElementReferenceException is raised + self.reload_failed = False + + # in case element was looked up previously via find_elements + # we need his position in the returned list + self.index = index + + # if reloading of some other web element is in progress + # StaleElementReferenceException is not raised within current + # context + self.web_element_reload = False + + def reload_request(self, locator, index=None): + self.web_element_reload = True + try: + # element was found out via find_elements + if index is not None: + web_els = self.src_element.find_elements(*locator) + web_el = web_els[index] + else: + web_el = self.src_element.find_element(*locator) + except (exceptions.NoSuchElementException, IndexError): + return False + + self.web_element_reload = False + return web_el + + def _reload_element(self): + """Method for starting reload process on current instance.""" + web_el = self.src_element.reload_request(self.locator, self.index) + if not web_el: + return + self._parent = web_el.parent + self._id = web_el.id + + def _execute(self, command, params=None): + """Overriding in order to catch StaleElementReferenceException.""" + result = None + while True: + try: + result = super(WebElementWrapper, self)._execute(command, + params) + break + except exceptions.StaleElementReferenceException: + + # in case we reach the limit + # STALE_ELEMENT_REFERENCE_MAX_TRY + # it is very probable that it is programmer fault + if self.reload_failed or self.stale_reference_occurrence \ + > self.STALE_ELEMENT_REFERENCE_MAX_TRY: raise -except ImportError as e: - LOG.warning("{0}, force WITH_SELENIUM=False".format(str(e))) - os.environ['WITH_SELENIUM'] = '' + # this is either programmer fault (bad logic in accessing + # elements) or web page content is been loaded via ajax, + # let's go with the second one and wait + # STALE_ELEMENT_REFERENCE_WAIT till the assumed page + # content is loaded and try to execute the whole process + # STALE_ELEMENT_REFERENCE_MAX_TRY times in case of failures + time.sleep(self.STALE_ELEMENT_REFERENCE_WAIT) + + # try to reload the web element if result is false it + # means that request has gone to the driver and he did not + # find the element -> must be programmer fault, because it + # seems we are on entirely different page + try: + self._reload_element() + except ElementNotReloadableException: + + # In case this element was responsible only for loading + # some other element raise the exception further + if self.web_element_reload: + raise + else: + self.reload_failed = True + + # increment occurrences + self.stale_reference_occurrence += 1 + + # reset counter + self.stale_reference_occurrence = 0 + return result + + +class WebDriverWrapper(WrapperFindOverride, WebDriver): + """Wrapper for webdriver to return WebElementWrapper on find_element. + """ + def reload_request(self, locator, index): + try: + # element was found out via find_elements + if index is not None: + web_els = self.find_elements(*locator) + web_el = web_els[index] + else: + web_el = self.find_element(*locator) + return web_el + except (exceptions.NoSuchElementException, IndexError): + raise ElementNotReloadableException() |