diff --git a/.gitignore b/.gitignore index f5705bfd4..1817d1b9a 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,4 @@ dist AUTHORS ChangeLog tags +ghostdriver.log diff --git a/doc/source/testing.rst b/doc/source/testing.rst index 117e160bb..deb581728 100644 --- a/doc/source/testing.rst +++ b/doc/source/testing.rst @@ -45,6 +45,16 @@ for a Debian OS flavour, or for Fedora/Red Hat flavours: $ 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 ============= diff --git a/horizon/test/webdriver.py b/horizon/test/webdriver.py index 8750c9dcf..87e02602a 100644 --- a/horizon/test/webdriver.py +++ b/horizon/test/webdriver.py @@ -25,71 +25,71 @@ import subprocess 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: - # NOTE: 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. - # The entire file is encapsulated in the try block because the classes - # inherit from the firefox class contained in selenium.webdriver, and - # python will throw a NameError if the import is skipped. - 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 + else: + from selenium.common import exceptions as selenium_exceptions + from selenium.webdriver import firefox - class FirefoxBinary(firefox.firefox_binary.FirefoxBinary): - """Workarounds selenium firefox issues. + 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: + 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(), + - 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. + - 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'. - """ + 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 + 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) + 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) + # 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.""" + 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) - raise + 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) + raise 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/openstack_dashboard/dashboards/project/images/tests.py b/openstack_dashboard/dashboards/project/images/tests.py index a1878eafa..e7d989e3a 100644 --- a/openstack_dashboard/dashboards/project/images/tests.py +++ b/openstack_dashboard/dashboards/project/images/tests.py @@ -17,7 +17,9 @@ # License for the specific language governing permissions and limitations # under the License. +import os from socket import timeout as socket_timeout # noqa +import unittest from django.core.urlresolvers import reverse from django import http @@ -411,6 +413,8 @@ class SeleniumTests(test.SeleniumTestCase): self.assertTrue("ISO" in body.text, "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',)}) def test_modal_create_image_from_file(self): driver = self.selenium @@ -471,6 +475,8 @@ class SeleniumTests(test.SeleniumTestCase): self.assertTrue("ISO" in body.text, "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',)}) def test_create_image_from_file(self): driver = self.selenium diff --git a/openstack_dashboard/test/integration_tests/webdriver.py b/openstack_dashboard/test/integration_tests/webdriver.py index 84fb78ac6..34e89ee68 100644 --- a/openstack_dashboard/test/integration_tests/webdriver.py +++ b/openstack_dashboard/test/integration_tests/webdriver.py @@ -9,6 +9,7 @@ # 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 @@ -145,7 +146,14 @@ class WebElementWrapper(WrapperFindOverride, webelement.WebElement): 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.""" def __init__(self, logging_prefs=None, capabilities=None, **kwargs): if capabilities is None: diff --git a/releasenotes/notes/enable-phantomjs-selenium-cce59f25cb327ca2.yaml b/releasenotes/notes/enable-phantomjs-selenium-cce59f25cb327ca2.yaml new file mode 100644 index 000000000..79a8b660c --- /dev/null +++ b/releasenotes/notes/enable-phantomjs-selenium-cce59f25cb327ca2.yaml @@ -0,0 +1,3 @@ +--- +features: + - Selenium tests may now be exercised using the headless PhantomJS driver. diff --git a/run_tests.sh b/run_tests.sh index f56a6ff5a..cfea8c49d 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -33,6 +33,7 @@ function usage { echo " --only-selenium Run only the Selenium unit tests" echo " --with-selenium Run unit tests including Selenium tests" 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 " OpenStack environment)" echo " --runserver Run the Django development server for" @@ -79,6 +80,7 @@ runserver=0 only_selenium=0 with_selenium=0 selenium_headless=0 +selenium_phantomjs=0 integration=0 testopts="" testargs="" @@ -121,6 +123,7 @@ function process_option { --only-selenium) only_selenium=1;; --with-selenium) with_selenium=1;; --selenium-headless) selenium_headless=1;; + --selenium-phantomjs) selenium_phantomjs=1;; --integration) integration=1;; --docs) just_docs=1;; --runserver) runserver=1;; @@ -347,6 +350,10 @@ function run_tests { export SELENIUM_HEADLESS=1 fi + if [ $selenium_phantomjs -eq 1 ]; then + export SELENIUM_PHANTOMJS=1 + fi + if [ -z "$testargs" ]; then run_tests_all else