From a17612ed9748d141381fcfff3d88f178cbebe39a Mon Sep 17 00:00:00 2001 From: Richard Jones Date: Wed, 11 Nov 2015 15:37:12 +1100 Subject: [PATCH] Merge the two webdrivers Horizon currently has two webdrivers for selenium-based testing; this patch merges them into one so all tests (selenium functional tests and integration tests) benefit from improvements we make to the webdriver. Change-Id: Iac471c2c7e40c799711746ef9e3e4ef87aecf098 --- horizon/test/firefox_binary.py | 74 +++++++ horizon/test/helpers.py | 8 +- horizon/test/webdriver.py | 204 +++++++++++++----- .../test/integration_tests/helpers.py | 7 +- .../test/integration_tests/webdriver.py | 177 --------------- run_tests.sh | 4 + 6 files changed, 234 insertions(+), 240 deletions(-) create mode 100644 horizon/test/firefox_binary.py delete mode 100644 openstack_dashboard/test/integration_tests/webdriver.py 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 +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 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: +class WrapperFindOverride(object): + """Mixin for overriding find_element methods.""" - - getting a free port from the OS with - selenium.webdriver.common.utils free_port(), + 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) - - release the port immediately but record it in ff prefs so that ff - can listen on that port for the internal http server. + 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 - 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 +class WebElementWrapper(WrapperFindOverride, webelement.WebElement): + """WebElement class wrapper. - 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) + 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. + """ - # 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) + STALE_ELEMENT_REFERENCE_WAIT = 0.5 + STALE_ELEMENT_REFERENCE_MAX_TRY = 10 - class WebDriver(firefox.webdriver.WebDriver): - """Workarounds selenium firefox issues.""" + def __init__(self, parent, id_, locator, src_element, index=None): + super(WebElementWrapper, self).__init__(parent, id_) + self.locator = locator + self.src_element = src_element - 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) + # 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() diff --git a/openstack_dashboard/test/integration_tests/helpers.py b/openstack_dashboard/test/integration_tests/helpers.py index 6f0026147..f9b1e571c 100644 --- a/openstack_dashboard/test/integration_tests/helpers.py +++ b/openstack_dashboard/test/integration_tests/helpers.py @@ -20,9 +20,9 @@ import uuid import testtools import xvfbwrapper +from horizon.test import webdriver from openstack_dashboard.test.integration_tests import config from openstack_dashboard.test.integration_tests.pages import loginpage -from openstack_dashboard.test.integration_tests import webdriver ROOT_PATH = os.path.dirname(os.path.abspath(config.__file__)) @@ -67,8 +67,11 @@ class BaseTestCase(testtools.TestCase): # and the webdriver. socket.setdefaulttimeout(60) # Start the Selenium webdriver and setup configuration. + desired_capabilities = dict(webdriver.desired_capabilities) + desired_capabilities['loggingPrefs'] = {'browser': 'ALL'} self.driver = webdriver.WebDriverWrapper( - logging_prefs={'browser': 'ALL'}) + desired_capabilities=desired_capabilities + ) self.driver.maximize_window() self.driver.implicitly_wait(self.CONFIG.selenium.implicit_wait) self.driver.set_page_load_timeout( diff --git a/openstack_dashboard/test/integration_tests/webdriver.py b/openstack_dashboard/test/integration_tests/webdriver.py deleted file mode 100644 index 34e89ee68..000000000 --- a/openstack_dashboard/test/integration_tests/webdriver.py +++ /dev/null @@ -1,177 +0,0 @@ -# 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 os -import time - -from selenium.common import exceptions -from selenium import webdriver -from selenium.webdriver.common import by -from selenium.webdriver.common import desired_capabilities as dc -from selenium.webdriver.remote import webelement - - -class ElementNotReloadableException(Exception): - """Raised when reload is not possible.""" - pass - - -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 actual - StaleElementReferenceException 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 - - # 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 - - -# select the active webdriver based on whether we --selenium-phantomjs or not -if os.environ.get('SELENIUM_PHANTOMJS'): - WebDriver = webdriver.PhantomJS -else: - WebDriver = webdriver.Firefox - - -class WebDriverWrapper(WrapperFindOverride, WebDriver): - """Wrapper for webdriver to return WebElementWrapper on find_element.""" - def __init__(self, logging_prefs=None, capabilities=None, **kwargs): - if capabilities is None: - capabilities = dc.DesiredCapabilities.FIREFOX - if logging_prefs is None: - logging_prefs = {'browser': 'ALL'} - capabilities['loggingPrefs'] = logging_prefs - super(WebDriverWrapper, self).__init__(capabilities=capabilities, - **kwargs) - - 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() diff --git a/run_tests.sh b/run_tests.sh index cfea8c49d..d144785f5 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -420,6 +420,10 @@ function run_integration_tests { export SELENIUM_HEADLESS=1 fi + if [ $selenium_phantomjs -eq 1 ]; then + export SELENIUM_PHANTOMJS=1 + fi + echo "Running Horizon integration tests..." if [ -z "$testargs" ]; then ${command_wrapper} nosetests openstack_dashboard/test/integration_tests/tests