Enable PhantomJS for running Selenium tests

This patch enables the PhantomJS webdriver for running
the Selenium test suite.

Use it with the --selenium-phantomjs command-line switch
when executing the selenium and integration suite.

Change-Id: I443e6f6d7d1911df500b360f7c22686b417fbeae
Blueprint: enable-phantomjs-selenium
This commit is contained in:
Richard Jones 2015-11-09 15:35:48 +11:00
parent ab7d5c3ec5
commit 861f9c2c4a
7 changed files with 89 additions and 54 deletions

1
.gitignore vendored
View File

@ -35,3 +35,4 @@ dist
AUTHORS AUTHORS
ChangeLog ChangeLog
tags tags
ghostdriver.log

View File

@ -45,6 +45,16 @@ for a Debian OS flavour, or for Fedora/Red Hat flavours:
$ sudo yum install xorg-x11-server-Xvfb $ sudo yum install xorg-x11-server-Xvfb
If you can't run a virtual display, or would prefer not to, you can use the
PhantomJS web driver instead:
$ ./run_tests.sh --with-selenium --selenium-phantomjs
If you need to install PhantomJS, you may do so with `npm` like this:
$ npm -g install phantomjs
Writing tests Writing tests
============= =============

View File

@ -25,71 +25,71 @@ import subprocess
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# 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: try:
# NOTE: Several distribution can't ship selenium due to its if os.environ.get('SELENIUM_PHANTOMJS'):
# non-free license. So they have to patch it out of test-requirements.txt from selenium.webdriver import PhantomJS as WebDriver
# Avoid import failure and force not running selenium tests. else:
# The entire file is encapsulated in the try block because the classes from selenium.common import exceptions as selenium_exceptions
# inherit from the firefox class contained in selenium.webdriver, and from selenium.webdriver import firefox
# python will throw a NameError if the import is skipped.
from selenium.common import exceptions as selenium_exceptions
from selenium.webdriver import firefox
class FirefoxBinary(firefox.firefox_binary.FirefoxBinary): class FirefoxBinary(firefox.firefox_binary.FirefoxBinary):
"""Workarounds selenium firefox issues. """Workarounds selenium firefox issues.
There is race condition in the way firefox is spawned. The exact cause There is race condition in the way firefox is spawned. The exact
hasn't been properly diagnosed yet but it's around: cause hasn't been properly diagnosed yet but it's around:
- getting a free port from the OS with selenium.webdriver.common.utils - getting a free port from the OS with
free_port(), selenium.webdriver.common.utils free_port(),
- release the port immediately but record it in ff prefs so that ff can - release the port immediately but record it in ff prefs so that ff
listen on that port for the internal http server. can listen on that port for the internal http server.
It has been observed that this leads to hanging processes for 'firefox It has been observed that this leads to hanging processes for
-silent'. 'firefox -silent'.
""" """
def _start_from_profile_path(self, path): def _start_from_profile_path(self, path):
self._firefox_env["XRE_PROFILE_PATH"] = path self._firefox_env["XRE_PROFILE_PATH"] = path
if platform.system().lower() == 'linux': if platform.system().lower() == 'linux':
self._modify_link_library_path() self._modify_link_library_path()
command = [self._start_cmd, "-silent"] command = [self._start_cmd, "-silent"]
if self.command_line is not None: if self.command_line is not None:
for cli in self.command_line: for cli in self.command_line:
command.append(cli) command.append(cli)
# The following exists upstream and is known to create hanging firefoxes, # The following exists upstream and is known to create hanging
# leading to zombies. # firefoxes, leading to zombies.
# subprocess.Popen(command, stdout=self._log_file, # subprocess.Popen(command, stdout=self._log_file,
# stderr=subprocess.STDOUT, # stderr=subprocess.STDOUT,
# env=self._firefox_env).communicate() # env=self._firefox_env).communicate()
command[1] = '-foreground' command[1] = '-foreground'
self.process = subprocess.Popen( self.process = subprocess.Popen(
command, stdout=self._log_file, stderr=subprocess.STDOUT, command, stdout=self._log_file, stderr=subprocess.STDOUT,
env=self._firefox_env) env=self._firefox_env)
class WebDriver(firefox.webdriver.WebDriver): class WebDriver(firefox.webdriver.WebDriver):
"""Workarounds selenium firefox issues.""" """Workarounds selenium firefox issues."""
def __init__(self, firefox_profile=None, firefox_binary=None, def __init__(self, firefox_profile=None, firefox_binary=None,
timeout=30, capabilities=None, proxy=None): timeout=30, capabilities=None, proxy=None):
try: try:
super(WebDriver, self).__init__( super(WebDriver, self).__init__(
firefox_profile, FirefoxBinary(), timeout, capabilities, firefox_profile, FirefoxBinary(), timeout,
proxy) capabilities, proxy)
except selenium_exceptions.WebDriverException: except selenium_exceptions.WebDriverException:
# If we can't start, cleanup profile # If we can't start, cleanup profile
shutil.rmtree(self.profile.path) shutil.rmtree(self.profile.path)
if self.profile.tempfolder is not None: if self.profile.tempfolder is not None:
shutil.rmtree(self.profile.tempfolder) shutil.rmtree(self.profile.tempfolder)
raise raise
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

@ -17,7 +17,9 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
from socket import timeout as socket_timeout # noqa from socket import timeout as socket_timeout # noqa
import unittest
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django import http from django import http
@ -411,6 +413,8 @@ class SeleniumTests(test.SeleniumTestCase):
self.assertTrue("ISO" in body.text, self.assertTrue("ISO" in body.text,
"ISO should be selected when the extension is *.iso") "ISO should be selected when the extension is *.iso")
@unittest.skipIf(os.environ.get('SELENIUM_PHANTOMJS'),
"PhantomJS cannot test file upload widgets.")
@test.create_stubs({api.glance: ('image_list_detailed',)}) @test.create_stubs({api.glance: ('image_list_detailed',)})
def test_modal_create_image_from_file(self): def test_modal_create_image_from_file(self):
driver = self.selenium driver = self.selenium
@ -471,6 +475,8 @@ class SeleniumTests(test.SeleniumTestCase):
self.assertTrue("ISO" in body.text, self.assertTrue("ISO" in body.text,
"ISO should be selected when the extension is *.iso") "ISO should be selected when the extension is *.iso")
@unittest.skipIf(os.environ.get('SELENIUM_PHANTOMJS'),
"PhantomJS cannot test file upload widgets.")
@test.create_stubs({api.glance: ('image_list_detailed',)}) @test.create_stubs({api.glance: ('image_list_detailed',)})
def test_create_image_from_file(self): def test_create_image_from_file(self):
driver = self.selenium driver = self.selenium

View File

@ -9,6 +9,7 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
import time import time
from selenium.common import exceptions from selenium.common import exceptions
@ -145,7 +146,14 @@ class WebElementWrapper(WrapperFindOverride, webelement.WebElement):
return result return result
class WebDriverWrapper(WrapperFindOverride, webdriver.Firefox): # 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.""" """Wrapper for webdriver to return WebElementWrapper on find_element."""
def __init__(self, logging_prefs=None, capabilities=None, **kwargs): def __init__(self, logging_prefs=None, capabilities=None, **kwargs):
if capabilities is None: if capabilities is None:

View File

@ -0,0 +1,3 @@
---
features:
- Selenium tests may now be exercised using the headless PhantomJS driver.

View File

@ -33,6 +33,7 @@ function usage {
echo " --only-selenium Run only the Selenium unit tests" echo " --only-selenium Run only the Selenium unit tests"
echo " --with-selenium Run unit tests including Selenium tests" echo " --with-selenium Run unit tests including Selenium tests"
echo " --selenium-headless Run Selenium tests headless" echo " --selenium-headless Run Selenium tests headless"
echo " --selenium-phantomjs Run Selenium tests using phantomjs (headless)"
echo " --integration Run the integration tests (requires a running " echo " --integration Run the integration tests (requires a running "
echo " OpenStack environment)" echo " OpenStack environment)"
echo " --runserver Run the Django development server for" echo " --runserver Run the Django development server for"
@ -79,6 +80,7 @@ runserver=0
only_selenium=0 only_selenium=0
with_selenium=0 with_selenium=0
selenium_headless=0 selenium_headless=0
selenium_phantomjs=0
integration=0 integration=0
testopts="" testopts=""
testargs="" testargs=""
@ -121,6 +123,7 @@ function process_option {
--only-selenium) only_selenium=1;; --only-selenium) only_selenium=1;;
--with-selenium) with_selenium=1;; --with-selenium) with_selenium=1;;
--selenium-headless) selenium_headless=1;; --selenium-headless) selenium_headless=1;;
--selenium-phantomjs) selenium_phantomjs=1;;
--integration) integration=1;; --integration) integration=1;;
--docs) just_docs=1;; --docs) just_docs=1;;
--runserver) runserver=1;; --runserver) runserver=1;;
@ -347,6 +350,10 @@ function run_tests {
export SELENIUM_HEADLESS=1 export SELENIUM_HEADLESS=1
fi fi
if [ $selenium_phantomjs -eq 1 ]; then
export SELENIUM_PHANTOMJS=1
fi
if [ -z "$testargs" ]; then if [ -z "$testargs" ]; then
run_tests_all run_tests_all
else else