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
This commit is contained in:
Richard Jones 2015-11-11 15:37:12 +11:00 committed by Timur Sufiev
parent abad2d3af4
commit a17612ed97
6 changed files with 234 additions and 240 deletions

View File

@ -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

View File

@ -41,15 +41,17 @@ from django.contrib.staticfiles.testing \
LOG = logging.getLogger(__name__) 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: try:
from selenium.webdriver.support import ui as selenium_ui from selenium.webdriver.support import ui as selenium_ui
import xvfbwrapper # Only needed when running the Selenium tests headless import xvfbwrapper # Only needed when running the Selenium tests headless
from horizon.test.webdriver import WebDriver # noqa from horizon.test.webdriver import WebDriver # noqa
except ImportError as e: 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))) LOG.warning("{0}, force WITH_SELENIUM=False".format(str(e)))
os.environ['WITH_SELENIUM'] = '' os.environ['WITH_SELENIUM'] = ''

View File

@ -19,77 +19,165 @@
import logging import logging
import os import os
import platform import time
import shutil
import subprocess
LOG = logging.getLogger(__name__) 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. # Select the WebDriver to use based on the --selenium-phantomjs switch.
# NOTE: Several distributions can't ship Selenium, or the Firefox if os.environ.get('SELENIUM_PHANTOMJS'):
# 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 from selenium.webdriver import PhantomJS as WebDriver
else: desired_capabilities = dc.DesiredCapabilities.PHANTOMJS
from selenium.common import exceptions as selenium_exceptions else:
from selenium.webdriver import firefox 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 class WrapperFindOverride(object):
cause hasn't been properly diagnosed yet but it's around: """Mixin for overriding find_element methods."""
- getting a free port from the OS with def find_element(self, by=by.By.ID, value=None):
selenium.webdriver.common.utils free_port(), 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 def find_elements(self, by=by.By.ID, value=None):
can listen on that port for the internal http server. 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'. 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.
""" """
def _start_from_profile_path(self, path): STALE_ELEMENT_REFERENCE_WAIT = 0.5
self._firefox_env["XRE_PROFILE_PATH"] = path STALE_ELEMENT_REFERENCE_MAX_TRY = 10
if platform.system().lower() == 'linux': def __init__(self, parent, id_, locator, src_element, index=None):
self._modify_link_library_path() super(WebElementWrapper, self).__init__(parent, id_)
command = [self._start_cmd, "-silent"] self.locator = locator
if self.command_line is not None: self.src_element = src_element
for cli in self.command_line:
command.append(cli)
# The following exists upstream and is known to create hanging # StaleElementReferenceException occurrence counter
# firefoxes, leading to zombies. self.stale_reference_occurrence = 0
# 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): # storing if web element reload succeed or not
"""Workarounds selenium firefox issues.""" # in case of fail StaleElementReferenceException is raised
self.reload_failed = False
def __init__(self, firefox_profile=None, firefox_binary=None, # in case element was looked up previously via find_elements
timeout=30, capabilities=None, proxy=None): # 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: try:
super(WebDriver, self).__init__( # element was found out via find_elements
firefox_profile, FirefoxBinary(), timeout, if index is not None:
capabilities, proxy) web_els = self.src_element.find_elements(*locator)
except selenium_exceptions.WebDriverException: web_el = web_els[index]
# If we can't start, cleanup profile else:
shutil.rmtree(self.profile.path) web_el = self.src_element.find_element(*locator)
if self.profile.tempfolder is not None: except (exceptions.NoSuchElementException, IndexError):
shutil.rmtree(self.profile.tempfolder) 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 raise
except ImportError as e: # this is either programmer fault (bad logic in accessing
LOG.warning("{0}, force WITH_SELENIUM=False".format(str(e))) # elements) or web page content is been loaded via ajax,
os.environ['WITH_SELENIUM'] = '' # 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()

View File

@ -20,9 +20,9 @@ import uuid
import testtools import testtools
import xvfbwrapper import xvfbwrapper
from horizon.test import webdriver
from openstack_dashboard.test.integration_tests import config from openstack_dashboard.test.integration_tests import config
from openstack_dashboard.test.integration_tests.pages import loginpage 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__)) ROOT_PATH = os.path.dirname(os.path.abspath(config.__file__))
@ -67,8 +67,11 @@ class BaseTestCase(testtools.TestCase):
# and the webdriver. # and the webdriver.
socket.setdefaulttimeout(60) socket.setdefaulttimeout(60)
# Start the Selenium webdriver and setup configuration. # Start the Selenium webdriver and setup configuration.
desired_capabilities = dict(webdriver.desired_capabilities)
desired_capabilities['loggingPrefs'] = {'browser': 'ALL'}
self.driver = webdriver.WebDriverWrapper( self.driver = webdriver.WebDriverWrapper(
logging_prefs={'browser': 'ALL'}) desired_capabilities=desired_capabilities
)
self.driver.maximize_window() self.driver.maximize_window()
self.driver.implicitly_wait(self.CONFIG.selenium.implicit_wait) self.driver.implicitly_wait(self.CONFIG.selenium.implicit_wait)
self.driver.set_page_load_timeout( self.driver.set_page_load_timeout(

View File

@ -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()

View File

@ -420,6 +420,10 @@ function run_integration_tests {
export SELENIUM_HEADLESS=1 export SELENIUM_HEADLESS=1
fi fi
if [ $selenium_phantomjs -eq 1 ]; then
export SELENIUM_PHANTOMJS=1
fi
echo "Running Horizon integration tests..." echo "Running Horizon integration tests..."
if [ -z "$testargs" ]; then if [ -z "$testargs" ]; then
${command_wrapper} nosetests openstack_dashboard/test/integration_tests/tests ${command_wrapper} nosetests openstack_dashboard/test/integration_tests/tests