]> git.apps.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: clean-up tox and unit tests
authorErnesto Puerta <epuertat@redhat.com>
Fri, 19 Jul 2019 16:10:49 +0000 (18:10 +0200)
committerErnesto Puerta <epuertat@redhat.com>
Fri, 16 Aug 2019 17:17:27 +0000 (19:17 +0200)
Refactor CMake add_tox_test to automatically add py27 and/or py3 to
provided toxenvs.

Refactor tox.ini:
- Remove requirements-{py27,py3}.txt, as python release dependant
packages can be handled with PEP 508 syntax.
- Remove develepment dependencies from requirements.
- Move pycodestyle settings to separate section.
- Add flake8 check and other checkers (rst, naming, etc). Some of them
are commented out for future clean-ups (Ceph trackers have been opened)
- Pycodestyle removed, as flake8 is a wrapper for pycodestyle.
- Add instafail plugin to report failures immediately
- Add timeout plugin to limit max run time (sometimes test_tasks hangs)
- Remove unused dependencies (lru_cache, pluggy)

Test and code linting fixes:
- Unused imports
- Fixes to HACKING.rst

Doc:
- Update HACKING.rst

Add conftest.py to mock imported modules (rados, rbd, cephfs), and mock
also rados Error and OSError Exceptions.

Fixes: https://tracker.ceph.com/issues/40487
Fixes: https://tracker.ceph.com/issues/41152
Signed-off-by: Ernesto Puerta <epuertat@redhat.com>
41 files changed:
cmake/modules/AddCephTest.cmake
install-deps.sh
src/pybind/mgr/dashboard/.pylintrc
src/pybind/mgr/dashboard/CMakeLists.txt
src/pybind/mgr/dashboard/HACKING.rst
src/pybind/mgr/dashboard/__init__.py
src/pybind/mgr/dashboard/conftest.py [new file with mode: 0644]
src/pybind/mgr/dashboard/constraints.txt [new file with mode: 0644]
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/osd.py
src/pybind/mgr/dashboard/controllers/pool.py
src/pybind/mgr/dashboard/controllers/rbd_mirroring.py
src/pybind/mgr/dashboard/controllers/saml2.py
src/pybind/mgr/dashboard/plugins/__init__.py
src/pybind/mgr/dashboard/plugins/feature_toggles.py
src/pybind/mgr/dashboard/requirements-lint.txt [new file with mode: 0644]
src/pybind/mgr/dashboard/requirements-py27.txt [deleted file]
src/pybind/mgr/dashboard/requirements-py3.txt [deleted file]
src/pybind/mgr/dashboard/requirements-test.txt [new file with mode: 0644]
src/pybind/mgr/dashboard/requirements.txt
src/pybind/mgr/dashboard/rest_client.py
src/pybind/mgr/dashboard/services/exception.py
src/pybind/mgr/dashboard/services/iscsi_client.py
src/pybind/mgr/dashboard/services/sso.py
src/pybind/mgr/dashboard/tests/__init__.py
src/pybind/mgr/dashboard/tests/test_api_auditing.py
src/pybind/mgr/dashboard/tests/test_feature_toggles.py
src/pybind/mgr/dashboard/tests/test_ganesha.py
src/pybind/mgr/dashboard/tests/test_iscsi.py
src/pybind/mgr/dashboard/tests/test_osd.py
src/pybind/mgr/dashboard/tests/test_pool.py
src/pybind/mgr/dashboard/tests/test_prometheus.py
src/pybind/mgr/dashboard/tests/test_rbd_mirroring.py
src/pybind/mgr/dashboard/tests/test_rest_client.py
src/pybind/mgr/dashboard/tests/test_rest_tasks.py
src/pybind/mgr/dashboard/tests/test_rgw.py
src/pybind/mgr/dashboard/tests/test_rgw_client.py
src/pybind/mgr/dashboard/tests/test_tools.py
src/pybind/mgr/dashboard/tools.py
src/pybind/mgr/dashboard/tox.ini
src/tools/setup-virtualenv.sh

index bb5936cf8ba61c9328e8453fc7f37a03cf32cf99..b21ae660166ce76dde9c6655d290ff4b663453df 100644 (file)
@@ -69,7 +69,7 @@ function(add_tox_test name)
     list(APPEND tox_envs py3)
   endif()
   if(DEFINED TOXTEST_TOX_ENVS)
-    set(tox_envs ${TOXTEST_TOX_ENVS})
+    list(APPEND tox_envs ${TOXTEST_TOX_ENVS})
   endif()
   string(REPLACE ";" "," tox_envs "${tox_envs}")
   add_custom_command(
index 177d89229b0177043a62851f6dbf72828824f6aa..f116c213b64c9e8ddda1e601dcc2ecfe64faaf21 100755 (executable)
@@ -439,9 +439,11 @@ function populate_wheelhouse() {
 
     # although pip comes with virtualenv, having a recent version
     # of pip matters when it comes to using wheel packages
-    pip --timeout 300 $install 'setuptools >= 0.8' 'pip >= 7.0' 'wheel >= 0.24' || return 1
+    PIP_OPTS="--timeout 300 --exists-action i"
+    pip $PIP_OPTS $install \
+      'setuptools >= 0.8' 'pip >= 7.0' 'wheel >= 0.24' 'tox >= 2.9.1' || return 1
     if test $# != 0 ; then
-        pip --timeout 300 $install $@ || return 1
+        pip $PIP_OPTS $install $@ || return 1
     fi
 }
 
@@ -485,10 +487,13 @@ wip_wheelhouse=wheelhouse-wip
 find . -name tox.ini | while read ini ; do
     (
         cd $(dirname $ini)
-        require=$(ls *requirements.txt 2>/dev/null | sed -e 's/^/-r /')
+        require_files=$(ls *requirements*.txt 2>/dev/null) || true
+        constraint_files=$(ls *constraints*.txt 2>/dev/null) || true
+        require=$(echo -n "$require_files" | sed -e 's/^/-r /')
+        constraint=$(echo -n "$constraint_files" | sed -e 's/^/-c /')
         md5=wheelhouse/md5
         if test "$require"; then
-            if ! test -f $md5 || ! md5sum -c $md5 ; then
+            if ! test -f $md5 || ! md5sum -c $md5 > /dev/null; then
                 rm -rf wheelhouse
             fi
         fi
@@ -496,10 +501,10 @@ find . -name tox.ini | while read ini ; do
             for interpreter in python2.7 python3 ; do
                 type $interpreter > /dev/null 2>&1 || continue
                 activate_virtualenv $top_srcdir $interpreter || exit 1
-                populate_wheelhouse "wheel -w $wip_wheelhouse" $require || exit 1
+                populate_wheelhouse "download -d $wip_wheelhouse" $require $constraint || exit 1
             done
             mv $wip_wheelhouse wheelhouse
-            md5sum *requirements.txt > $md5
+            md5sum $require_files $constraint_files > $md5
         fi
     )
 done
index caa510fef7f26841048019979a663dd2e06f8e44..8efd540f49e530f2a4f596e89dfa8aa81715dda1 100644 (file)
@@ -393,7 +393,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local
 # (useful for modules/projects where namespaces are manipulated during runtime
 # and thus existing member attributes cannot be deduced by static analysis. It
 # supports qualified module names, as well as Unix pattern matching.
-ignored-modules=cherrypy,distutils
+ignored-modules=cherrypy,distutils,rados,rbd,cephfs
 
 # Show a hint with possible names when a member name was not found. The aspect
 # of finding the hint is based on edit distance.
@@ -432,7 +432,7 @@ ignore-comments=yes
 ignore-docstrings=yes
 
 # Ignore imports when computing similarities.
-ignore-imports=no
+ignore-imports=yes
 
 # Minimum lines number of a similarity.
 min-similarity-lines=4
index 946e730eb02edccf2058011d7c88371ddcf36954..03c4b4b57148093b9b2d8a341938ebce69c700fd 100644 (file)
@@ -98,12 +98,5 @@ endif(WITH_MGR_DASHBOARD_FRONTEND AND NOT CMAKE_SYSTEM_PROCESSOR MATCHES "aarch6
 
 if(WITH_TESTS)
   include(AddCephTest)
-  if(WITH_PYTHON2)
-    list(APPEND dashboard_tox_envs py27-cov py27-lint py27-check)
-  endif()
-  if(WITH_PYTHON3)
-    list(APPEND dashboard_tox_envs py3-cov py3-lint py3-check)
-  endif()
-  add_tox_test(mgr-dashboard
-    TOX_ENVS ${dashboard_tox_envs})
+  add_tox_test(mgr-dashboard TOX_ENVS lint check)
 endif()
index 639434eba00761c9c4781405fd52f8e18125273c..d21dccb41c0eb4c7bc1c89cb53b43d3f1f5c11c4 100644 (file)
@@ -129,7 +129,7 @@ Run ``npm run test`` to execute the unit tests via `Jest
 <https://facebook.github.io/jest/>`_.
 
 If you get errors on all tests, it could be because `Jest
-<https://facebook.github.io/jest/>`_ or something else was updated.
+<https://facebook.github.io/jest/>`__ or something else was updated.
 There are a few ways how you can try to resolve this:
 
 - Remove all modules with ``rm -rf dist node_modules`` and run ``npm install``
@@ -314,7 +314,7 @@ This components are declared on the components module:
 `src/pybind/mgr/dashboard/frontend/src/app/shared/components`.
 
 Helper
-......
+~~~~~~
 
 This component should be used to provide additional information to the user.
 
@@ -453,9 +453,9 @@ To do that, check the settings in the i18n config file
 ``src/pybind/mgr/dashboard/frontend/i18n.config.json``:: and make sure that the
 organization is *ceph*, the project is *ceph-dashboard* and the resource is
 the one you want to pull from and push to e.g. *Master:master*. To find a list
-of avaiable resources visit ``https://www.transifex.com/ceph/ceph-dashboard/content/``::
+of avaiable resources visit `<https://www.transifex.com/ceph/ceph-dashboard/content/>`_.
 
-After you checked the config go to the directory ``src/pybind/mgr/dashboard/frontend``:: and run
+After you checked the config go to the directory ``src/pybind/mgr/dashboard/frontend`` and run::
 
   $ npm run i18n
 
@@ -467,7 +467,7 @@ The tool will ask you for an api token, unless you added it by running:
 
   $ npm run i18n:token
 
-To create a transifex api token visit ``https://www.transifex.com/user/settings/api/``::
+To create a transifex api token visit `<https://www.transifex.com/user/settings/api/>`_.
 
 After the command ran successfully, build the UI and check if everything is
 working as expected. You also might want to run the frontend tests.
@@ -477,7 +477,7 @@ Suggestions
 
 Strings need to start and end in the same line as the element:
 
-.. code-block:: xml
+.. code-block:: html
 
   <!-- avoid -->
   <span i18n>
@@ -500,7 +500,7 @@ Strings need to start and end in the same line as the element:
 
 Isolated interpolations should not be translated:
 
-.. code-block:: xml
+.. code-block:: html
 
   <!-- avoid -->
   <span i18n>{{ foo }}</span>
@@ -510,14 +510,14 @@ Isolated interpolations should not be translated:
 
 Interpolations used in a sentence should be kept in the translation:
 
-.. code-block:: xml
+.. code-block:: html
 
   <!-- recommended -->
   <span i18n>There are {{ x }} OSDs.</span>
 
 Remove elements that are outside the context of the translation:
 
-.. code-block:: xml
+.. code-block:: html
 
   <!-- avoid -->
   <label i18n>
@@ -533,7 +533,7 @@ Remove elements that are outside the context of the translation:
 
 Keep elements that affect the sentence:
 
-.. code-block:: xml
+.. code-block:: html
 
   <!-- recommended -->
   <span i18n>Profile <b>foo</b> will be removed.</span>
@@ -575,25 +575,25 @@ Alternatively, you can use Python's native package installation method::
   $ pip install tox
   $ pip install coverage
 
-To run the tests, run ``run-tox.sh`` in the dashboard directory (where
+To run the tests, run ``run_tox.sh`` in the dashboard directory (where
 ``tox.ini`` is located)::
 
   ## Run Python 2+3 tests+lint commands:
-  $ ./run-tox.sh
+  $ ../../../script/run_tox.sh --tox-env py27,py3,lint,check
 
   ## Run Python 3 tests+lint commands:
-  $ WITH_PYTHON2=OFF ./run-tox.sh
+  $ ../../../script/run_tox.sh --tox-env py3,lint,check
 
   ## Run Python 3 arbitrary command (e.g. 1 single test):
-  $ WITH_PYTHON2=OFF ./run-tox.sh pytest tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
+  $ WITH_PYTHON2=OFF ../../../script/run_tox.sh --tox-env py3 "" tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
 
-You can also run tox instead of ``run-tox.sh``::
+You can also run tox instead of ``run_tox.sh``::
 
   ## Run Python 3 tests command:
-  $ CEPH_BUILD_DIR=.tox tox -e py3-cov
+  $ tox -e py3
 
   ## Run Python 3 arbitrary command (e.g. 1 single test):
-  $ CEPH_BUILD_DIR=.tox tox -e py3-run pytest tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
+  $ tox -e py3 tests/test_rgw_client.py::RgwClientTest::test_ssl_verify
 
 We also collect coverage information from the backend code when you run tests. You can check the
 coverage information provided by the tox output, or by running the following
@@ -779,17 +779,17 @@ endpoint:
     # URL: /ping/{key}?opt1=...&opt2=...
     @Endpoint(path="/", query_params=['opt1'])
     def index(self, key, opt1, opt2=None):
-      # ...
+      """..."""
 
     # URL: /ping/{key}?opt1=...&opt2=...
     @Endpoint(query_params=['opt1'])
     def __call__(self, key, opt1, opt2=None):
-      # ...
+      """..."""
 
     # URL: /ping/post/{key1}/{key2}
     @Endpoint('POST', path_params=['key1', 'key2'])
     def post(self, key1, key2, data1, data2=None):
-      # ...
+      """..."""
 
 
 In the above example we see how the ``path`` option can be used to override the
@@ -826,7 +826,7 @@ Consider the following example:
     # URL: /ping/{node}/stats/{date}/latency?unit=...
     @Endpoint(path="/{date}/latency")
     def latency(self, node, date, unit="ms"):
-      # ...
+      """ ..."""
 
 In this example we explicitly declare a path parameter ``{node}`` in the
 controller URL path, and a path parameter ``{date}`` in the ``latency``
@@ -864,9 +864,10 @@ Example:
 
     @Proxy()
     def proxy(self, path, **params):
-      # if requested URL is "/foo/proxy/access/service?opt=1"
-      # then path is "access/service" and params is {'opt': '1'}
-      # ...
+      """
+      if requested URL is "/foo/proxy/access/service?opt=1"
+      then path is "access/service" and params is {'opt': '1'}
+      """
 
 
 How does the RESTController work?
@@ -1587,6 +1588,7 @@ type and not as a string. Allowed values are ``str``, ``int``, ``bool``, ``float
 .. code-block:: python
 
  @EndpointDoc(parameters={'my_string': (str, 'Description of my_string')})
+ def method(my_string): pass
 
 For body parameters, more complex cases are possible. If the parameter is a
 dictionary, the type should be replaced with a ``dict`` containing its nested
@@ -1605,6 +1607,7 @@ for nested parameters).
       'item2': (str, 'Description of item2', True),  # item2 is optional
       'item3': (str, 'Description of item3', True, 'foo'),  # item3 is optional with 'foo' as default value
   }, 'Description of my_dictionary')})
+  def method(my_dictionary): pass
 
 If the parameter is a ``list`` of primitive types, the type should be
 surrounded with square brackets.
@@ -1612,6 +1615,7 @@ surrounded with square brackets.
 .. code-block:: python
 
   @EndpointDoc(parameters={'my_list': ([int], 'Description of my_list')})
+  def method(my_list): pass
 
 If the parameter is a ``list`` with nested parameters, the nested parameters
 should be placed in a dictionary and surrounded with square brackets.
@@ -1623,6 +1627,7 @@ should be placed in a dictionary and surrounded with square brackets.
       'list_item': (str, 'Description of list_item'),
       'list_item2': (str, 'Description of list_item2')
   }], 'Description of my_list')})
+  def method(my_list): pass
 
 
 ``responses``: A dict used for describing responses. Rules for describing
@@ -1633,7 +1638,8 @@ example below:
 .. code-block:: python
 
   @EndpointDoc(responses={
-    '400':{'my_response': (str, 'Description of my_response')}
+    '400':{'my_response': (str, 'Description of my_response')}})
+  def method(): pass
 
 
 Error Handling in Python
@@ -1743,7 +1749,7 @@ The available interfaces are the following:
 - ``CanLog``: provides the plug-in with access to the Ceph Dashboard logger under ``self.log``.
 - ``Setupable``: requires overriding ``setup()`` hook. This method is run in the Ceph Dashboard ``serve()`` method, right after CherryPy has been configured, but before it is started. It's a placeholder for the plug-in initialization logic.
 - ``HasOptions``: requires overriding ``get_options()`` hook by returning a list of ``Options()``. The options returned here are added to the ``MODULE_OPTIONS``.
-- ``HasCommands``: requires overriding ``register_commands()`` hook by defining the commands the plug-in can handle and decorating them with ``@CLICommand`. The commands can be optionally returned, so that they can be invoked externally (which makes unit testing easier).
+- ``HasCommands``: requires overriding ``register_commands()`` hook by defining the commands the plug-in can handle and decorating them with ``@CLICommand``. The commands can be optionally returned, so that they can be invoked externally (which makes unit testing easier).
 - ``HasControllers``: requires overriding ``get_controllers()`` hook by defining and returning the controllers as usual.
 - ``FilterRequest.BeforeHandler``: requires overriding ``filter_request_before_handler()`` hook. This method receives a ``cherrypy.request`` object for processing. A usual implementation of this method will allow some requests to pass or will raise a ``cherrypy.HTTPError` based on the ``request`` metadata and other conditions.
 
index 527073db5e8e758419457e5aed5eb77c603a387e..3c0050e8ccff76143a5edc9f8b07ead5edc2a6aa 100644 (file)
@@ -35,7 +35,8 @@ if 'UNITTEST' not in os.environ:
     mgr = _ModuleProxy()
     logger = _LoggerProxy()
 
-    from .module import Module, StandbyModule
+    # DO NOT REMOVE: required for ceph-mgr to load a module
+    from .module import Module, StandbyModule  # noqa: F401
 else:
     import logging
     logging.basicConfig(level=logging.DEBUG)
@@ -47,7 +48,11 @@ else:
     # Mock ceph module otherwise every module that is involved in a testcase and imports it will
     # raise an ImportError
     import sys
-    import mock
+    try:
+        import mock
+    except ImportError:
+        import unittest.mock as mock
+
     sys.modules['ceph_module'] = mock.Mock()
 
     mgr = mock.Mock()
diff --git a/src/pybind/mgr/dashboard/conftest.py b/src/pybind/mgr/dashboard/conftest.py
new file mode 100644 (file)
index 0000000..b7fa2d1
--- /dev/null
@@ -0,0 +1,26 @@
+import sys
+
+try:
+    from mock import Mock
+except ImportError:
+    from unittest.mock import Mock
+
+
+class MockRadosError(Exception):
+    def __init__(self, message, errno=None):
+        super(MockRadosError, self).__init__(message)
+        self.errno = errno
+
+    def __str__(self):
+        msg = super(MockRadosError, self).__str__()
+        if self.errno is None:
+            return msg
+        return '[errno {0}] {1}'.format(self.errno, msg)
+
+
+def pytest_configure(config):
+    sys.modules.update({
+        'rados': Mock(Error=MockRadosError, OSError=MockRadosError),
+        'rbd': Mock(),
+        'cephfs': Mock(),
+    })
diff --git a/src/pybind/mgr/dashboard/constraints.txt b/src/pybind/mgr/dashboard/constraints.txt
new file mode 100644 (file)
index 0000000..dfb1f13
--- /dev/null
@@ -0,0 +1,10 @@
+CherryPy==13.1.0
+enum34==1.1.6
+more-itertools==4.1.0
+PyJWT==1.6.4
+pyopenssl==17.5.0
+bcrypt==3.1.4
+python3-saml==1.4.1
+requests==2.20.0
+Routes==2.4.1
+six==1.11.0
index 5affb0d7e075427b2009a0fd993b9b667508fe2e..c0720e686c6be32fe1892aaf014694de1089a0d5 100644 (file)
@@ -10,10 +10,8 @@ import os
 import pkgutil
 import sys
 
-if sys.version_info >= (3, 0):
-    from urllib.parse import unquote  # pylint: disable=no-name-in-module,import-error
-else:
-    from urllib import unquote  # pylint: disable=no-name-in-module
+import six
+from six.moves.urllib.parse import unquote
 
 # pylint: disable=wrong-import-position
 import cherrypy
@@ -637,9 +635,7 @@ class BaseController(object):
         @wraps(func)
         def inner(*args, **kwargs):
             for key, value in kwargs.items():
-                # pylint: disable=undefined-variable
-                if (sys.version_info < (3, 0) and isinstance(value, unicode)) \
-                        or isinstance(value, str):
+                if isinstance(value, six.text_type):
                     kwargs[key] = unquote(value)
 
             # Process method arguments.
index 409a0bb93004d0ce7894d3251ce3a1b33e18095a..0968f8b833dd25064d96088faba9c692b8436c7e 100644 (file)
@@ -7,7 +7,7 @@ from ..services.ceph_service import CephService, SendCommandError
 from ..services.exception import handle_send_command_error
 from ..tools import str_to_bool
 try:
-    from typing import Dict, List, Any, Union  # pylint: disable=unused-import
+    from typing import Dict, List, Any, Union  # noqa: F401 pylint: disable=unused-import
 except ImportError:
     pass  # For typing only
 
index 98c50bd9fe12581a47d1d0233349231c7b98d97a..9868219de1d46cf4655b66298a3818f0247e1664 100644 (file)
@@ -63,7 +63,7 @@ class Pool(RESTController):
     def _get(cls, pool_name, attrs=None, stats=False):
         # type: (str, str, bool) -> dict
         pools = cls._pool_list(attrs, stats)
-        pool = [pool for pool in pools if pool['pool_name'] == pool_name]
+        pool = [p for p in pools if p['pool_name'] == pool_name]
         if not pool:
             raise cherrypy.NotFound('No such pool')
         return pool[0]
index f32ddbc699de1dc0fd535db49367ae66ef2e7fb2..6b7932c660ac250ce97e9e6d83d1f5076793a9b1 100644 (file)
@@ -51,7 +51,7 @@ def get_daemons_and_pools():  # pylint: disable=R0915
 
                 try:
                     status = json.loads(status['json'])
-                except (ValueError, KeyError) as _:
+                except (ValueError, KeyError):
                     status = {}
 
                 instance_id = metadata['instance_id']
index 7d897e3979322fa44f1486159113029d49e9e9ae..a0ad345b1c99ef173096a7bde48104f6cd92e33e 100644 (file)
@@ -1,7 +1,6 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
-import sys
 import cherrypy
 
 try:
@@ -37,9 +36,7 @@ class Saml2(BaseController):
     @staticmethod
     def _check_python_saml():
         if not python_saml_imported:
-            python_saml_name = 'python3-saml' if sys.version_info >= (3, 0) else 'python-saml'
-            raise cherrypy.HTTPError(400,
-                                     'Required library not found: `{}`'.format(python_saml_name))
+            raise cherrypy.HTTPError(400, 'Required library not found: `python3-saml`')
         try:
             OneLogin_Saml2_Settings(mgr.SSO_DB.saml2.onelogin_settings)
         except OneLogin_Saml2_Error:
index 33027eff625b414a057cb41ee191c8a6db7cf009..2a4ded2b6d1c3729efc92e87235411016d5ee6ab 100644 (file)
@@ -59,4 +59,4 @@ class DashboardPluginManager(object):
 PLUGIN_MANAGER = DashboardPluginManager("ceph-mgr.dashboard")
 
 # Load all interfaces and their hooks
-from . import interfaces  # pylint: disable=wrong-import-position,cyclic-import
+from . import interfaces  # noqa: F401 pylint: disable=wrong-import-position,cyclic-import
index ac8ebcfd2890ef2e93b7ac0555addda98a435fa6..e1b7edff28852fea89bbda8dc5775d64757f5be0 100644 (file)
@@ -6,7 +6,7 @@ import cherrypy
 from mgr_module import CLICommand, Option
 
 from . import PLUGIN_MANAGER as PM
-from . import interfaces as I
+from . import interfaces as I  # noqa: E741
 from .ttl_cache import ttl_cache
 
 from ..controllers.rbd import Rbd, RbdSnapshot, RbdTrash
diff --git a/src/pybind/mgr/dashboard/requirements-lint.txt b/src/pybind/mgr/dashboard/requirements-lint.txt
new file mode 100644 (file)
index 0000000..ecc1d86
--- /dev/null
@@ -0,0 +1,9 @@
+pylint
+flake8
+flake8-colors
+#TODO: Fix docstring issues: https://tracker.ceph.com/issues/41224
+#flake8-docstrings
+#flake8-import-order
+#flake8-typing-imports; python_version >= '3'
+#pep8-naming
+rstcheck
diff --git a/src/pybind/mgr/dashboard/requirements-py27.txt b/src/pybind/mgr/dashboard/requirements-py27.txt
deleted file mode 100644 (file)
index d8fb6f6..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-astroid==1.6.1
-pylint==1.8.2
-python-saml==2.4.2
diff --git a/src/pybind/mgr/dashboard/requirements-py3.txt b/src/pybind/mgr/dashboard/requirements-py3.txt
deleted file mode 100644 (file)
index 364ce28..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-astroid==2.1.0
-pylint==2.2.2
-python3-saml==1.4.1
diff --git a/src/pybind/mgr/dashboard/requirements-test.txt b/src/pybind/mgr/dashboard/requirements-test.txt
new file mode 100644 (file)
index 0000000..dbfe3c7
--- /dev/null
@@ -0,0 +1,4 @@
+mock; python_version <= '3.3'
+pytest<4
+pytest-cov
+pytest-instafail
index caa1db80e96fbef58fadb3b13036011464a3df9d..8661c59998f66b44da128b89f15a89d4acfabed4 100644 (file)
@@ -1,35 +1,10 @@
-attrs==17.4.0
-backports.functools-lru-cache==1.4
-bcrypt==3.1.4
-cheroot==6.0.0
-CherryPy==13.1.0
-configparser==3.5.0
-coverage==4.5.2
-enum34==1.1.6
-funcsigs==1.0.2
-isort==4.2.15
-lazy-object-proxy==1.3.1
-mccabe==0.6.1
-mock==2.0.0
-more-itertools==4.1.0
-pbr==3.1.1
-pluggy==0.6.0
-portend==2.2
-py==1.5.2
-pycodestyle==2.4.0
-pycparser==2.18
-PyJWT==1.6.4
-pyopenssl==17.5.0
-pytest==3.3.2
-pytest-cov==2.5.1
-pytest-faulthandler==1.0.1
-pytz==2017.3
-requests==2.20.0
-Routes==2.4.1
-singledispatch==3.4.0.3
-six==1.11.0
-tempora==1.10
-tox==2.9.1
-virtualenv==15.1.0
-werkzeug==0.14.1
-wrapt==1.10.11
+bcrypt
+CherryPy
+enum34; python_version<'3.4'
+more-itertools
+PyJWT
+pyopenssl
+python3-saml
+requests
+Routes
+six
index b287086f9863096dad968ae7401d400d4e4a07a3..e3f5dafc8209a164e87e581bdea74a471d85fd5c 100644 (file)
@@ -421,7 +421,6 @@ class RestClient(object):
                 logger.error(
                     "%s REST API failed %s req status: %s", self.client_name,
                     method.upper(), resp.status_code)
-                from pprint import pprint as pp
                 from pprint import pformat as pf
 
                 raise RequestException(
index b5c0bd58a17f45a2c4875113e26e658337082bb3..8db88bd1f61782e8aaee3e850fad2efca028965e 100644 (file)
@@ -2,8 +2,8 @@
 from __future__ import absolute_import
 
 import json
-import sys
 from contextlib import contextmanager
+import six
 
 import cherrypy
 
@@ -15,7 +15,7 @@ from ..services.ceph_service import SendCommandError
 from ..exceptions import ViewCacheNoDataException, DashboardException
 from ..tools import wraps
 
-if sys.version_info < (3, 0):
+if six.PY2:
     # Monkey-patch a __call__ method into @contextmanager to make
     # it compatible to Python 3
 
@@ -50,7 +50,7 @@ if sys.version_info < (3, 0):
     GeneratorContextManager.__call__ = call
 
     # pylint: disable=function-redefined
-    def contextmanager(func):
+    def contextmanager(func):  # noqa: F811
 
         @wraps(func)
         def helper(*args, **kwds):
index be2ca8a34d2bc09ece6617d5e02cc20c4e8a3213..773b4cef87b9193a6f80f6186f9d8b5a7434312c 100644 (file)
@@ -11,7 +11,7 @@ try:
 except ImportError:
     from urllib.parse import urlparse
 
-from .iscsi_config import IscsiGatewaysConfig
+from .iscsi_config import IscsiGatewaysConfig  # pylint: disable=cyclic-import
 from .. import logger
 from ..settings import Settings
 from ..rest_client import RestClient
index 492a6e596344d685ab2be002c9da54e582fa209b..03c73c4f6a96fe28997ef58e586fe31347415524 100644 (file)
@@ -4,9 +4,12 @@ from __future__ import absolute_import
 
 import errno
 import json
-import sys
 import threading
 
+import six
+if six.PY2:
+    FileNotFoundError = IOError
+
 try:
     from onelogin.saml2.settings import OneLogin_Saml2_Settings
     from onelogin.saml2.errors import OneLogin_Saml2_Error
@@ -140,8 +143,7 @@ def handle_sso_command(cmd):
         return -errno.ENOSYS, '', ''
 
     if not python_saml_imported:
-        python_saml_name = 'python3-saml' if sys.version_info >= (3, 0) else 'python-saml'
-        return -errno.EPERM, '', 'Required library not found: `{}`'.format(python_saml_name)
+        return -errno.EPERM, '', 'Required library not found: `python3-saml`'
 
     if cmd['prefix'] == 'dashboard sso enable saml2':
         try:
@@ -179,12 +181,6 @@ def handle_sso_command(cmd):
         if not sp_x_509_cert and sp_private_key:
             return -errno.EINVAL, '', 'Missing parameter `sp_x_509_cert`.'
         has_sp_cert = sp_x_509_cert != "" and sp_private_key != ""
-        try:
-            # pylint: disable=undefined-variable
-            FileNotFoundError
-        except NameError:
-            # pylint: disable=redefined-builtin
-            FileNotFoundError = IOError
         try:
             f = open(sp_x_509_cert, 'r')
             sp_x_509_cert = f.read()
index 49adbb55c80e2eaeae7e4099113ed5f4a0c0cd85..b4517b02d97b71679f8b35ad7e022e61cc870295 100644 (file)
@@ -10,7 +10,7 @@ import cherrypy
 from cherrypy._cptools import HandlerWrapperTool
 from cherrypy.test import helper
 
-from mgr_module import CLICommand, MgrModule
+from mgr_module import CLICommand
 
 from .. import logger, mgr
 from ..controllers import json_error_page, generate_controller_routes
@@ -207,7 +207,6 @@ class ControllerTestCase(helper.CPWebCase):
         else:
             self.status = 500
         self.body = json.dumps(thread.res_task['exception'])
-        return
 
     def _task_post(self, url, data=None, timeout=60):
         self._task_request('POST', url, data, timeout)
index ae95e3409067b35ff370312aa80db3388d1a275e..0416c0363857968be7ad87fe8199126e16042b62 100644 (file)
@@ -4,7 +4,10 @@ from __future__ import absolute_import
 import re
 import json
 import cherrypy
-import mock
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
 
 from . import ControllerTestCase, KVStoreMockMixin
 from ..controllers import RESTController, Controller
index 5c70c88a1843db4d28c75cf61007fbf2de57bf2f..031d0ef8394448fe178b6c7c06b0096a797f6b58 100644 (file)
@@ -2,7 +2,10 @@
 from __future__ import absolute_import
 
 import unittest
-from mock import Mock, patch
+try:
+    from mock import Mock, patch
+except ImportError:
+    from unittest.mock import Mock, patch
 
 from . import KVStoreMockMixin
 from ..plugins.feature_toggles import FeatureToggles, Features
index 5dced126424b6fa3e2e07554d4c354848f5add2f..03f1f5b5513e8863d447d109012b180c1df1d389 100644 (file)
@@ -3,7 +3,10 @@ from __future__ import absolute_import
 
 import unittest
 
-from mock import MagicMock, Mock
+try:
+    from mock import MagicMock, Mock
+except ImportError:
+    from unittest.mock import MagicMock, Mock
 
 import orchestrator
 from . import KVStoreMockMixin
index 9905f33b49e9fed9c7aeab899c01fc20e892473c..d4e23c0f575fc0bdf28595b47b361744d3748def 100644 (file)
@@ -3,7 +3,11 @@
 import copy
 import errno
 import json
-import mock
+
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
 
 from . import CmdException, ControllerTestCase, CLICommandTestMixin
 from .. import mgr
index 0f24d25e7e2d59508562f8c0dd9b528dcd9f9417..eabee3fc58eb6b045abebbac1841c25a638de911 100644 (file)
@@ -4,7 +4,10 @@ from __future__ import absolute_import
 import uuid
 from contextlib import contextmanager
 
-from mock import patch
+try:
+    from mock import patch
+except ImportError:
+    from unittest.mock import patch
 
 from . import ControllerTestCase
 from ..controllers.osd import Osd
index 157f2720f9e802b4e576b38747f560bf497aa274..e33e4365405378b91dcac431640daa1c242b6b06 100644 (file)
@@ -1,7 +1,10 @@
 # -*- coding: utf-8 -*-
 # pylint: disable=protected-access
 import time
-import mock
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
 
 from . import ControllerTestCase
 from ..controllers.pool import Pool
index 73dedbab843e1cd5bcf8c40e65d3411fa7f23b50..13831cd3eda8525ef321c99bf8f67faed1874bc1 100644 (file)
@@ -1,6 +1,9 @@
 # -*- coding: utf-8 -*-
 # pylint: disable=protected-access
-from mock import patch
+try:
+    from mock import patch
+except ImportError:
+    from unittest.mock import patch
 
 from . import ControllerTestCase
 from .. import mgr
index 41c1a3852077c9a6707723972c2a4fb0d87cb446..7c05df2c6f6a20fbd98eb05ca9281e945c9eed3d 100644 (file)
@@ -1,7 +1,10 @@
 from __future__ import absolute_import
 
 import json
-import mock
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
 
 from . import ControllerTestCase
 from .. import mgr
index 36ecd51a334b4612f553da146a72164bbeb94156..fc48f0eb0369bfd656f26065aa9705ab0522bcf2 100644 (file)
@@ -2,7 +2,11 @@
 import unittest
 import requests.exceptions
 
-from mock import patch
+try:
+    from mock import patch
+except ImportError:
+    from unittest.mock import patch
+
 from urllib3.exceptions import MaxRetryError, ProtocolError
 from .. import mgr
 from ..rest_client import RequestException, RestClient
index 44bce050a741eed03c937f3697b9d587345c717e..e9d7907f0524bdc246bd8392908fe761398652df 100644 (file)
@@ -3,7 +3,10 @@
 
 import time
 
-import mock
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
 
 from . import ControllerTestCase
 from ..controllers import Controller, RESTController, Task
index 38c3d0af87b0dc0c913fa0dda91bb83e6c0be80d..9ce5161a3fe7648c30351fe99fc011d3901c3a7d 100644 (file)
@@ -1,4 +1,7 @@
-import mock
+try:
+    import mock
+except ImportError:
+    import unittest.mock as mock
 
 from . import ControllerTestCase
 from ..controllers.rgw import RgwUser
index 7d476f72f98f95b0da2b30efefb1287b0ecb7860..c9a7ed1f098091e28f7815b9a73bbb49c907211f 100644 (file)
@@ -1,6 +1,10 @@
 # -*- coding: utf-8 -*-
 import unittest
-from mock import patch
+
+try:
+    from mock import patch
+except ImportError:
+    from unittest.mock import patch
 
 from .. import mgr
 from ..services.rgw_client import RgwClient
index 3506e176cd58402af6fa184e1d930e976dd295f6..5eafb015a00c8ef9129dc1c00c1159d35869cb2f 100644 (file)
@@ -5,7 +5,10 @@ import unittest
 
 import cherrypy
 from cherrypy.lib.sessions import RamSession
-from mock import patch
+try:
+    from mock import patch
+except ImportError:
+    from unittest.mock import patch
 
 from . import ControllerTestCase
 from ..services.exception import handle_rados_error
index 9d01154d871acd8bdfd7b35997ea1126fd272a55..8b4045c78d59a41713d6544ed6d6b8109008f5c3 100644 (file)
@@ -28,7 +28,7 @@ from .settings import Settings
 from .services.auth import JwtManager
 
 try:
-    from typing import Any, AnyStr, Dict, List  # pylint: disable=unused-import
+    from typing import Any, AnyStr, Dict, List  # noqa pylint: disable=unused-import
 except ImportError:
     pass  # For typing only
 
index 499aee83eaebb6ba525d2081195907c55c01f2b5..9897a677c03f18fa7882d72fc7da808165237772 100644 (file)
@@ -1,30 +1,74 @@
 [tox]
-envlist = py27-{cov,lint,run,check},py3-{cov,lint,run,check}
+envlist =
+    py{27,3},
+    lint,
+    check,
+    run,
 skipsdist = true
-toxworkdir = {env:CEPH_BUILD_DIR}/dashboard
-minversion = 2.8.1
+minversion = 2.9.1
+
+[pytest]
+addopts =
+    --cov --cov-append --cov-report=term
+    --doctest-modules
+    --ignore=frontend/ --ignore=module.py
+    --instafail
 
 [testenv]
-setenv=
+deps =
+    -rrequirements.txt
+    -cconstraints.txt
+    -rrequirements-test.txt
+setenv =
     CFLAGS = -DXMLSEC_NO_SIZE_T
+    PYTHONUNBUFFERED=1
+    PYTHONDONTWRITEBYTECODE=1
     UNITTEST = true
     WEBTEST_INTERACTIVE = false
-    LD_LIBRARY_PATH = {toxinidir}/../../../../build/lib
-    PATH = {toxinidir}/../../../../build/bin:$PATH
-    py27: PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.2
-    py3:  PYTHONPATH = {toxinidir}/../../../../build/lib/cython_modules/lib.3
-    cov:  UNITTEST = true
-    cov:  COVERAGE_FILE = .coverage.{envname}
-commands=
-    pip install -r {toxinidir}/requirements.txt
-    py27: pip install -r {toxinidir}/requirements-py27.txt
-    py3: pip install -r {toxinidir}/requirements-py3.txt
-    cov: coverage erase
-    cov: {envbindir}/py.test --cov=. --cov-report= --junitxml=junit.{envname}.xml --doctest-modules controllers/rbd.py services/ tests/ tools.py
-    cov: coverage combine {toxinidir}/{env:COVERAGE_FILE}
-    cov: coverage report
-    cov: coverage xml
-    lint: pylint --rcfile=.pylintrc --jobs=5 . module.py tools.py controllers tests services exceptions.py grafana.py ci/check_grafana_uids.py
-    lint: pycodestyle --max-line-length=100 --exclude=.tox,venv,frontend,.vscode --ignore=E402,E121,E123,E126,E226,E24,E704,W503,E741 .
-    check: python ci/check_grafana_uids.py frontend/src/app ../../../../monitoring/grafana/dashboards
-    run: {posargs}
+commands =
+    pytest {posargs}
+
+[testenv:run]
+whitelist_externals = *
+commands = {posargs}
+
+[flake8]
+max-line-length = 100
+ignore = E123 E126 E226 E402 W503 E741 F812
+exclude = venv, frontend, .*
+statistics = True
+#TODO: Uncomment and refactor (https://tracker.ceph.com/issues/41221)
+#max-complexity = 10
+format = ${cyan}%(path)s${reset}:${yellow_bold}%(row)d${reset}:${green_bold}%(col)d${reset}: ${red_bold}%(code)s${reset} %(text)s
+
+[pylint]
+# Allow similarity/code duplication detection
+jobs = 1
+dirs = . controllers plugins services tests
+addopts =
+    -rn
+    --rcfile=.pylintrc
+    --jobs={[pylint]jobs}
+# Detect Python 2-3 migration issues
+    --py3k
+
+[rstlint]
+dirs =
+    README.rst
+    HACKING.rst
+
+[testenv:lint]
+basepython = python3
+deps =
+    -rrequirements-lint.txt
+commands =
+    rstcheck --report info --debug {posargs:{[rstlint]dirs}}
+    flake8 {posargs}
+    pylint {[pylint]addopts} {posargs:{[pylint]dirs}}
+
+
+[testenv:check]
+deps = 
+    six==1.11.0
+commands =
+    python ci/check_grafana_uids.py frontend/src/app ../../../../monitoring/grafana/dashboards
index 89e50b744ff9716181251753d469fe7fd61162ba..4515ab8f7044286400464fbfd079d2cde5051ffa 100755 (executable)
@@ -82,9 +82,16 @@ if test -d wheelhouse ; then
 fi
 
 pip $DISABLE_PIP_VERSION_CHECK --log $DIR/log.txt install $NO_INDEX --find-links=file://$(pwd)/wheelhouse 'tox >=2.9.1'
-if test -f requirements.txt ; then
-    if ! test -f wheelhouse/md5 || ! md5sum -c wheelhouse/md5 > /dev/null; then
+
+require_files=$(ls *requirements*.txt 2>/dev/null) || true
+constraint_files=$(ls *constraints*.txt 2>/dev/null) || true
+require=$(echo -n "$require_files" | sed -e 's/^/-r /')
+constraint=$(echo -n "$constraint_files" | sed -e 's/^/-c /')
+md5=wheelhouse/md5
+if test "$require"; then
+    if ! test -f $md5 || ! md5sum -c wheelhouse/md5 > /dev/null; then
         NO_INDEX=''
     fi
-    pip $DISABLE_PIP_VERSION_CHECK --log $DIR/log.txt install $NO_INDEX --find-links=file://$(pwd)/wheelhouse -r requirements.txt
+    pip --exists-action i $DISABLE_PIP_VERSION_CHECK --log $DIR/log.txt install $NO_INDEX \
+      --find-links=file://$(pwd)/wheelhouse $require $constraint 
 fi