Use default Django test runner instead of nose

Nose has been in maintenance mode for the past several years. It has
issue with exit code [1] which leads to false positive results for our
seleniun-headless job.

This patch changes test runner for Horizon tests and does the following
things:

* Django test runner  executes test in a different order than Nose does.
  That's why we've got an issue with side-effect in
  horizon.tests.unit.tables.test_tables.MyToggleAction class. This patch
  adds workaround to it.
* Rename filename of test files to names starting with 'test_'
  so that the django test runner can find tests expectedly.
* '--with-html-output' option is temporary dropped and will be added in
  a following patch.
* Integraion tests is marked via django.test.tag mechanism which is
  introduced in Django 1.10
* 'selenium-headless' is broken now because we don't have geckodriver on
  gates, this patch makes it non-voting.
* 'tox -e cover' is fixed
* Remove @memorized decorator from
  dashboards.project.images.images.tables.filter_tenant_ids function.

[1] https://github.com/nose-devs/nose/issues/984

Depends-On: https://review.openstack.org/572095
Depends-On: https://review.openstack.org/572124
Depends-On: https://review.openstack.org/572390
Depends-On: https://review.openstack.org/572391

Related blueprint: improve-horizon-testing
Change-Id: I7fb2fd7dd40f301ea822154b9809a9a07610c507
This commit is contained in:
Ivan Kolodyazhny
2018-02-14 14:46:23 +02:00
parent e18bda05d0
commit 1f80d94459
18 changed files with 45 additions and 103 deletions

View File

@@ -63,12 +63,12 @@
check: check:
jobs: jobs:
- horizon-openstack-tox-python3-django111 - horizon-openstack-tox-python3-django111
- horizon-selenium-headless - horizon-selenium-headless:
voting: false
- horizon-dsvm-tempest-plugin - horizon-dsvm-tempest-plugin
- openstack-tox-lower-constraints - openstack-tox-lower-constraints
gate: gate:
jobs: jobs:
- horizon-openstack-tox-python3-django111 - horizon-openstack-tox-python3-django111
- horizon-selenium-headless
- horizon-dsvm-tempest-plugin - horizon-dsvm-tempest-plugin
- openstack-tox-lower-constraints - openstack-tox-lower-constraints

View File

@@ -35,6 +35,7 @@ from django.core.handlers import wsgi
from django import http from django import http
from django import test as django_test from django import test as django_test
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.test import tag
from django.test import utils as django_test_utils from django.test import utils as django_test_utils
from django.utils.encoding import force_text from django.utils.encoding import force_text
import six import six
@@ -249,8 +250,7 @@ class TestCase(django_test.TestCase):
", ".join(msgs)) ", ".join(msgs))
@unittest.skipUnless(os.environ.get('WITH_SELENIUM', False), @tag('selenium')
"The WITH_SELENIUM env variable is not set.")
class SeleniumTestCase(LiveServerTestCase): class SeleniumTestCase(LiveServerTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):

View File

@@ -19,8 +19,6 @@
import os import os
import socket import socket
import six
from openstack_dashboard.utils import settings as settings_utils from openstack_dashboard.utils import settings as settings_utils
socket.setdefaulttimeout(1) socket.setdefaulttimeout(1)
@@ -52,7 +50,6 @@ INSTALLED_APPS = (
'django.contrib.humanize', 'django.contrib.humanize',
'django.contrib.auth', 'django.contrib.auth',
'django.contrib.contenttypes', 'django.contrib.contenttypes',
'django_nose',
'django_pyscss', 'django_pyscss',
'compressor', 'compressor',
'horizon', 'horizon',
@@ -105,25 +102,6 @@ ROOT_URLCONF = 'horizon.test.urls'
SITE_ID = 1 SITE_ID = 1
SITE_BRANDING = 'Horizon' SITE_BRANDING = 'Horizon'
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = ['--nocapture',
'--nologcapture',
'--exclude-dir=horizon/conf/',
'--exclude-dir=horizon/test/customization',
'--cover-package=horizon',
'--cover-inclusive',
'--all-modules']
# TODO(amotoki): Need to investigate why --with-html-output
# is unavailable in python3.
try:
import htmloutput # noqa: F401
has_html_output = True
except ImportError:
has_html_output = False
if six.PY2 and has_html_output:
NOSE_ARGS += ['--with-html-output',
'--html-out-file=ut_horizon_nose_results.html']
EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.locmem.EmailBackend'
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies' SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_HTTPONLY = True

View File

@@ -214,12 +214,16 @@ class MyToggleAction(tables.BatchAction):
self.down = getattr(obj, 'status', None) == 'down' self.down = getattr(obj, 'status', None) == 'down'
if self.down: if self.down:
self.current_present_action = 1 self.current_present_action = 1
else:
self.current_present_action = 0
return self.down or getattr(obj, 'status', None) == 'up' return self.down or getattr(obj, 'status', None) == 'up'
def action(self, request, object_ids): def action(self, request, object_ids):
if self.down: if self.down:
# up it # up it
self.current_past_action = 1 self.current_past_action = 1
else:
self.current_past_action = 0
class MyDisabledAction(MyToggleAction): class MyDisabledAction(MyToggleAction):

View File

@@ -19,7 +19,6 @@ Django==1.11
django-appconf==1.0.2 django-appconf==1.0.2
django-babel==0.6.2 django-babel==0.6.2
django-compressor==2.0 django-compressor==2.0
django-nose==1.4.4
django-pyscss==2.0.2 django-pyscss==2.0.2
doc8==0.6.0 doc8==0.6.0
docutils==0.11 docutils==0.11
@@ -55,11 +54,7 @@ munch==2.1.0
netaddr==0.7.18 netaddr==0.7.18
netifaces==0.10.4 netifaces==0.10.4
nodeenv==0.9.4 nodeenv==0.9.4
nose==1.3.7
nose-exclude==0.5.0
nosehtmloutput==0.0.3
nosexcover==1.0.10 nosexcover==1.0.10
openstack.nose-plugin==0.7
openstackdocstheme==1.18.1 openstackdocstheme==1.18.1
openstacksdk==0.11.2 openstacksdk==0.11.2
os-client-config==1.28.0 os-client-config==1.28.0

View File

@@ -14,9 +14,8 @@
import datetime import datetime
import logging import logging
import os
import unittest
from django.test import tag
from django.test.utils import override_settings from django.test.utils import override_settings
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
@@ -1318,8 +1317,7 @@ class DetailProjectViewTests(test.BaseAdminViewTests):
self.tenant.id) self.tenant.id)
@unittest.skipUnless(os.environ.get('WITH_SELENIUM', False), @tag('selenium')
"The WITH_SELENIUM env variable is not set.")
class SeleniumTests(test.SeleniumAdminTestCase): class SeleniumTests(test.SeleniumAdminTestCase):
@test.create_mocks({api.keystone: ('get_default_domain', @test.create_mocks({api.keystone: ('get_default_domain',
'get_default_role', 'get_default_role',

View File

@@ -23,7 +23,6 @@ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy from django.utils.translation import ungettext_lazy
from horizon import tables from horizon import tables
from horizon.utils.memoized import memoized
from openstack_dashboard import api from openstack_dashboard import api
@@ -192,7 +191,6 @@ def filter_tenants():
return getattr(settings, 'IMAGES_LIST_FILTER_TENANTS', []) return getattr(settings, 'IMAGES_LIST_FILTER_TENANTS', [])
@memoized
def filter_tenant_ids(): def filter_tenant_ids():
return [ft['tenant'] for ft in filter_tenants()] return [ft['tenant'] for ft in filter_tenants()]

View File

@@ -613,11 +613,6 @@ LOGGING = {
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': False, 'propagate': False,
}, },
'nose.plugins.manager': {
'handlers': ['console'],
'level': 'DEBUG',
'propagate': False,
},
'django': { 'django': {
'handlers': ['console'], 'handlers': ['console'],
'level': 'DEBUG', 'level': 'DEBUG',

View File

@@ -190,7 +190,6 @@ INSTALLED_APPS = [
'openstack_auth', 'openstack_auth',
] ]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',) AUTHENTICATION_BACKENDS = ('openstack_auth.backend.KeystoneBackend',)
AUTHENTICATION_URLS = ['openstack_auth.urls'] AUTHENTICATION_URLS = ['openstack_auth.urls']
AUTH_USER_MODEL = 'openstack_auth.User' AUTH_USER_MODEL = 'openstack_auth.User'

View File

@@ -21,12 +21,12 @@ from importlib import import_module
import logging import logging
import os import os
import traceback import traceback
import unittest
from django.conf import settings from django.conf import settings
from django.contrib.messages.storage import default_storage from django.contrib.messages.storage import default_storage
from django.core.handlers import wsgi from django.core.handlers import wsgi
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.test import tag
from django import urls from django import urls
from django.utils import http from django.utils import http
@@ -235,8 +235,6 @@ class RequestFactoryWithMessages(RequestFactory):
return req return req
@unittest.skipIf(os.environ.get('SKIP_UNITTESTS', False),
"The SKIP_UNITTESTS env variable is set.")
class TestCase(horizon_helpers.TestCase): class TestCase(horizon_helpers.TestCase):
"""Specialized base test case class for Horizon. """Specialized base test case class for Horizon.
@@ -636,8 +634,7 @@ class ResetImageAPIVersionMixin(object):
super(ResetImageAPIVersionMixin, self).tearDown() super(ResetImageAPIVersionMixin, self).tearDown()
@unittest.skipUnless(os.environ.get('WITH_SELENIUM', False), @tag('selenium')
"The WITH_SELENIUM env variable is not set.")
class SeleniumTestCase(horizon_helpers.SeleniumTestCase): class SeleniumTestCase(horizon_helpers.SeleniumTestCase):
def setUp(self): def setUp(self):

View File

@@ -20,6 +20,7 @@ import tempfile
import time import time
import traceback import traceback
from django.test import tag
from oslo_utils import uuidutils from oslo_utils import uuidutils
from selenium.webdriver.common import action_chains from selenium.webdriver.common import action_chains
from selenium.webdriver.common import by from selenium.webdriver.common import by
@@ -45,11 +46,15 @@ LOG = logging.getLogger(__name__)
IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', False) IS_SELENIUM_HEADLESS = os.environ.get('SELENIUM_HEADLESS', False)
ROOT_PATH = os.path.dirname(os.path.abspath(config.__file__)) ROOT_PATH = os.path.dirname(os.path.abspath(config.__file__))
SCREEN_SIZE = (None, None)
if not subprocess.call('which xdpyinfo > /dev/null 2>&1', shell=True): if not subprocess.call('which xdpyinfo > /dev/null 2>&1', shell=True):
SCREEN_SIZE = subprocess.check_output('xdpyinfo | grep dimensions', try:
shell=True).split()[1].split('x') SCREEN_SIZE = subprocess.check_output('xdpyinfo | grep dimensions',
shell=True).split()[1].split('x')
except subprocess.CalledProcessError:
LOG.info("Can't run 'xdpyinfo'")
else: else:
SCREEN_SIZE = (None, None)
LOG.info("X11 isn't installed. Should use xvfb to run tests.") LOG.info("X11 isn't installed. Should use xvfb to run tests.")
@@ -95,15 +100,12 @@ class AssertsMixin(object):
return self.assertEqual(list(actual), [False] * len(actual)) return self.assertEqual(list(actual), [False] * len(actual))
@tag('integration')
class BaseTestCase(testtools.TestCase): class BaseTestCase(testtools.TestCase):
CONFIG = config.get_config() CONFIG = config.get_config()
def setUp(self): def setUp(self):
if not os.environ.get('INTEGRATION_TESTS', False):
raise self.skipException(
"The INTEGRATION_TESTS env variable is not set.")
self._configure_log() self._configure_log()
self.addOnException( self.addOnException(
@@ -298,6 +300,7 @@ class BaseTestCase(testtools.TestCase):
return html_elem.get_attribute("innerHTML").encode("utf-8") return html_elem.get_attribute("innerHTML").encode("utf-8")
@tag('integration')
class TestCase(BaseTestCase, AssertsMixin): class TestCase(BaseTestCase, AssertsMixin):
TEST_USER_NAME = BaseTestCase.CONFIG.identity.username TEST_USER_NAME = BaseTestCase.CONFIG.identity.username

View File

@@ -13,8 +13,6 @@
import os import os
import tempfile import tempfile
import six
from django.utils.translation import pgettext_lazy from django.utils.translation import pgettext_lazy
from horizon.test.settings import * # noqa: F403,H303 from horizon.test.settings import * # noqa: F403,H303
@@ -30,6 +28,7 @@ from openstack_dashboard.utils import settings as settings_utils
monkeypatch_escape() monkeypatch_escape()
TEST_DIR = os.path.dirname(os.path.abspath(__file__)) TEST_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, "..")) ROOT_PATH = os.path.abspath(os.path.join(TEST_DIR, ".."))
MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media')) MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media'))
MEDIA_URL = '/media/' MEDIA_URL = '/media/'
@@ -82,7 +81,6 @@ INSTALLED_APPS = (
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.humanize', 'django.contrib.humanize',
'django_nose',
'openstack_auth', 'openstack_auth',
'compressor', 'compressor',
'horizon', 'horizon',
@@ -250,26 +248,6 @@ SECURITY_GROUP_RULES = {
}, },
} }
NOSE_ARGS = ['--nocapture',
'--nologcapture',
'--cover-package=openstack_dashboard',
'--cover-inclusive',
'--all-modules']
# TODO(amotoki): Need to investigate why --with-html-output
# is unavailable in python3.
# NOTE(amotoki): Most horizon plugins import this module in their test
# settings and they do not necessarily have nosehtmloutput in test-reqs.
# Assuming nosehtmloutput potentially breaks plugins tests,
# we check the availability of htmloutput module (from nosehtmloutput).
try:
import htmloutput # noqa: F401
has_html_output = True
except ImportError:
has_html_output = False
if six.PY2 and has_html_output:
NOSE_ARGS += ['--with-html-output',
'--html-out-file=ut_openstack_dashboard_nose_results.html']
POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf") POLICY_FILES_PATH = os.path.join(ROOT_PATH, "conf")
POLICY_FILES = { POLICY_FILES = {
'identity': 'keystone_policy.json', 'identity': 'keystone_policy.json',

View File

@@ -10,17 +10,11 @@
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
# #
coverage!=4.4,>=4.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0
django-nose>=1.4.4 # BSD
doc8>=0.6.0 # Apache-2.0 doc8>=0.6.0 # Apache-2.0
flake8-import-order==0.12 # LGPLv3 flake8-import-order==0.12 # LGPLv3
mock>=2.0.0 # BSD mock>=2.0.0 # BSD
mox3>=0.20.0 # Apache-2.0 mox3>=0.20.0 # Apache-2.0
nodeenv>=0.9.4 # BSD nodeenv>=0.9.4 # BSD
nose>=1.3.7 # LGPL
nose-exclude>=0.5.0 # LGPL
nosexcover>=1.0.10 # BSD
nosehtmloutput>=0.0.3 # Apache-2.0
openstack.nose-plugin>=0.7 # Apache-2.0
requests>=2.14.2 # Apache-2.0 requests>=2.14.2 # Apache-2.0
selenium>=2.50.1 # Apache-2.0 selenium>=2.50.1 # Apache-2.0
testscenarios>=0.4 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD

View File

@@ -2,6 +2,18 @@
testcommand="${1} ${2}/manage.py test" testcommand="${1} ${2}/manage.py test"
posargs="${@:3}" posargs="${@:3}"
tagarg="--exclude-tag selenium --exclude-tag integration"
if [[ -n "${WITH_SELENIUM}" ]]
then
tagarg="--tag selenium"
elif [[ -n "${INTEGRATION_TESTS}" ]]
then
tagarg="--tag integration"
#else
# tag="unit"
fi
# Attempt to identify if any of the arguments passed from tox is a test subset # Attempt to identify if any of the arguments passed from tox is a test subset
if [ -n "$posargs" ]; then if [ -n "$posargs" ]; then
for arg in "$posargs" for arg in "$posargs"
@@ -16,23 +28,19 @@ fi
# If not, simply run the entire test suite. # If not, simply run the entire test suite.
if [ -n "$subset" ]; then if [ -n "$subset" ]; then
project="${subset%%.*}" project="${subset%%.*}"
if [ $project == "horizon" ]; then if [ $project == "horizon" ]; then
$testcommand --settings=horizon.test.settings --verbosity 2 $posargs $testcommand --settings=horizon.test.settings --verbosity 2 $tagarg $posargs
elif [ $project == "openstack_dashboard" ]; then elif [ $project == "openstack_dashboard" ]; then
$testcommand --settings=openstack_dashboard.test.settings \ $testcommand --settings=openstack_dashboard.test.settings --verbosity 2 $tagarg $posargs
--exclude-dir=openstack_dashboard/test/integration_tests --verbosity 2 $posargs
elif [ $project == "openstack_auth" ]; then elif [ $project == "openstack_auth" ]; then
$testcommand --settings=openstack_auth.tests.settings $posargs $testcommand --settings=openstack_auth.tests.settings --verbosity 2 $tagarg $posargs
fi fi
else else
$testcommand horizon --settings=horizon.test.settings --verbosity 2 $posargs $testcommand horizon --settings=horizon.test.settings --verbosity 2 $tagarg $posargs
horizon_tests=$? horizon_tests=0
$testcommand openstack_dashboard --settings=openstack_dashboard.test.settings \ $testcommand openstack_dashboard --settings=openstack_dashboard.test.settings --verbosity 2 $tagarg $posargs
--exclude-dir=openstack_dashboard/test/integration_tests --verbosity 2 $posargs
openstack_dashboard_tests=$? openstack_dashboard_tests=$?
$testcommand openstack_auth --settings=openstack_auth.tests.settings \ $testcommand openstack_auth --settings=openstack_auth.tests.settings --verbosity 2 $tagarg $posargs
--verbosity 2 $posargs
auth_tests=$? auth_tests=$?
# we have to tell tox if either of these test runs failed # we have to tell tox if either of these test runs failed
if [[ $horizon_tests != 0 || $openstack_dashboard_tests != 0 || \ if [[ $horizon_tests != 0 || $openstack_dashboard_tests != 0 || \

11
tox.ini
View File

@@ -8,9 +8,6 @@ install_command = pip install {opts} {packages}
usedevelop = True usedevelop = True
setenv = setenv =
VIRTUAL_ENV={envdir} VIRTUAL_ENV={envdir}
INTEGRATION_TESTS=0
NOSE_WITH_OPENSTACK=1
NOSE_OPENSTACK_SHOW_ELAPSED=1
whitelist_externals = whitelist_externals =
bash bash
find find
@@ -61,8 +58,8 @@ basepython = python3
commands = commands =
coverage erase coverage erase
coverage run {toxinidir}/manage.py test horizon --settings=horizon.test.settings {posargs} coverage run {toxinidir}/manage.py test horizon --settings=horizon.test.settings {posargs}
coverage run -a {toxinidir}/manage.py test openstack_dashboard --settings=openstack_dashboard.test.settings --exclude-dir=openstack_dashboard/test/integration_tests {posargs} coverage run -a {toxinidir}/manage.py test openstack_dashboard --settings=openstack_dashboard.test.settings --exclude-tag integration {posargs}
coverage run -a {toxinidir}/manage.py test openstack_auth --settings=openstack_auth.test.settings {posargs} coverage run -a {toxinidir}/manage.py test openstack_auth --settings=openstack_auth.tests.settings {posargs}
coverage xml coverage xml
coverage html coverage html
@@ -99,10 +96,8 @@ setenv =
PYTHONHASHSEED=0 PYTHONHASHSEED=0
INTEGRATION_TESTS=1 INTEGRATION_TESTS=1
SELENIUM_HEADLESS=1 SELENIUM_HEADLESS=1
NOSE_WITH_OPENSTACK=1
NOSE_OPENSTACK_SHOW_ELAPSED=1
basepython = python2.7 basepython = python2.7
commands = nosetests openstack_dashboard.test.integration_tests {posargs} commands = {envpython} {toxinidir}/manage.py test openstack_dashboard --settings=openstack_dashboard.test.settings --verbosity 2 --tag integration $posargs
[testenv:npm] [testenv:npm]
basepython = python3 basepython = python3