Apply Octavia hacking checks to the tempest plugin
Currently we are not applying the custom hacking checks we use in
Octavia to the octavia-tempest-plugin. This leads to style issues
sneaking into the plugin.
This patch copies the same checks from Octavia (for simpler
management) into octavia-tempest-plugin and resolves some of the
existing style violations.
Change-Id: Ic103d8bededfec77ec0034d7c167ac1e49954a5c
diff --git a/octavia_tempest_plugin/hacking/__init__.py b/octavia_tempest_plugin/hacking/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/octavia_tempest_plugin/hacking/__init__.py
diff --git a/octavia_tempest_plugin/hacking/checks.py b/octavia_tempest_plugin/hacking/checks.py
new file mode 100644
index 0000000..eec7476
--- /dev/null
+++ b/octavia_tempest_plugin/hacking/checks.py
@@ -0,0 +1,277 @@
+# Copyright (c) 2014 OpenStack Foundation.
+#
+# 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.
+
+
+"""
+Guidelines for writing new hacking checks
+
+ - Use only for Octavia specific tests. OpenStack general tests
+ should be submitted to the common 'hacking' module.
+ - Pick numbers in the range O3xx. Find the current test with
+ the highest allocated number and then pick the next value.
+ - Keep the test method code in the source file ordered based
+ on the O3xx value.
+ - List the new rule in the top level HACKING.rst file
+ - Add test cases for each new rule to
+ octavia/tests/unit/test_hacking.py
+
+"""
+
+import re
+
+from hacking import core
+
+
+_all_log_levels = {'critical', 'error', 'exception', 'info', 'warning'}
+_all_hints = {'_LC', '_LE', '_LI', '_', '_LW'}
+
+_log_translation_hint = re.compile(
+ r".*LOG\.(%(levels)s)\(\s*(%(hints)s)\(" % {
+ 'levels': '|'.join(_all_log_levels),
+ 'hints': '|'.join(_all_hints),
+ })
+
+assert_trueinst_re = re.compile(
+ r"(.)*assertTrue\(isinstance\((\w|\.|\'|\"|\[|\])+, "
+ r"(\w|\.|\'|\"|\[|\])+\)\)")
+assert_equal_in_end_with_true_or_false_re = re.compile(
+ r"assertEqual\((\w|[][.'\"])+ in (\w|[][.'\", ])+, (True|False)\)")
+assert_equal_in_start_with_true_or_false_re = re.compile(
+ r"assertEqual\((True|False), (\w|[][.'\"])+ in (\w|[][.'\", ])+\)")
+assert_equal_with_true_re = re.compile(
+ r"assertEqual\(True,")
+assert_equal_with_false_re = re.compile(
+ r"assertEqual\(False,")
+mutable_default_args = re.compile(r"^\s*def .+\((.+=\{\}|.+=\[\])")
+assert_equal_end_with_none_re = re.compile(r"(.)*assertEqual\(.+, None\)")
+assert_equal_start_with_none_re = re.compile(r".*assertEqual\(None, .+\)")
+assert_not_equal_end_with_none_re = re.compile(
+ r"(.)*assertNotEqual\(.+, None\)")
+assert_not_equal_start_with_none_re = re.compile(
+ r"(.)*assertNotEqual\(None, .+\)")
+revert_must_have_kwargs_re = re.compile(
+ r'[ ]*def revert\(.+,[ ](?!\*\*kwargs)\w+\):')
+untranslated_exception_re = re.compile(r"raise (?:\w*)\((.*)\)")
+no_eventlet_re = re.compile(r'(import|from)\s+[(]?eventlet')
+no_line_continuation_backslash_re = re.compile(r'.*(\\)\n')
+no_logging_re = re.compile(r'(import|from)\s+[(]?logging')
+import_mock_re = re.compile(r"\bimport[\s]+mock\b")
+import_from_mock_re = re.compile(r"\bfrom[\s]+mock[\s]+import\b")
+
+
+def _translation_checks_not_enforced(filename):
+ # Do not do these validations on tests
+ return any(pat in filename for pat in ["/tests/", "rally-jobs/plugins/"])
+
+
+@core.flake8ext
+def assert_true_instance(logical_line):
+ """Check for assertTrue(isinstance(a, b)) sentences
+
+ O316
+ """
+ if assert_trueinst_re.match(logical_line):
+ yield (0, "O316: assertTrue(isinstance(a, b)) sentences not allowed. "
+ "Use assertIsInstance instead.")
+
+
+@core.flake8ext
+def assert_equal_or_not_none(logical_line):
+ """Check for assertEqual(A, None) or assertEqual(None, A) sentences,
+
+ assertNotEqual(A, None) or assertNotEqual(None, A) sentences
+
+ O318
+ """
+ msg = ("O318: assertEqual/assertNotEqual(A, None) or "
+ "assertEqual/assertNotEqual(None, A) sentences not allowed")
+ res = (assert_equal_start_with_none_re.match(logical_line) or
+ assert_equal_end_with_none_re.match(logical_line) or
+ assert_not_equal_start_with_none_re.match(logical_line) or
+ assert_not_equal_end_with_none_re.match(logical_line))
+ if res:
+ yield (0, msg)
+
+
+@core.flake8ext
+def assert_equal_true_or_false(logical_line):
+ """Check for assertEqual(True, A) or assertEqual(False, A) sentences
+
+ O323
+ """
+ res = (assert_equal_with_true_re.search(logical_line) or
+ assert_equal_with_false_re.search(logical_line))
+ if res:
+ yield (0, "O323: assertEqual(True, A) or assertEqual(False, A) "
+ "sentences not allowed")
+
+
+@core.flake8ext
+def no_mutable_default_args(logical_line):
+ msg = "O324: Method's default argument shouldn't be mutable!"
+ if mutable_default_args.match(logical_line):
+ yield (0, msg)
+
+
+@core.flake8ext
+def assert_equal_in(logical_line):
+ """Check for assertEqual(A in B, True), assertEqual(True, A in B),
+
+ assertEqual(A in B, False) or assertEqual(False, A in B) sentences
+
+ O338
+ """
+ res = (assert_equal_in_start_with_true_or_false_re.search(logical_line) or
+ assert_equal_in_end_with_true_or_false_re.search(logical_line))
+ if res:
+ yield (0, "O338: Use assertIn/NotIn(A, B) rather than "
+ "assertEqual(A in B, True/False) when checking collection "
+ "contents.")
+
+
+@core.flake8ext
+def no_log_warn(logical_line):
+ """Disallow 'LOG.warn('
+
+ O339
+ """
+ if logical_line.startswith('LOG.warn('):
+ yield(0, "O339:Use LOG.warning() rather than LOG.warn()")
+
+
+@core.flake8ext
+def no_translate_logs(logical_line, filename):
+ """O341 - Don't translate logs.
+
+ Check for 'LOG.*(_(' and 'LOG.*(_Lx('
+
+ Translators don't provide translations for log messages, and operators
+ asked not to translate them.
+
+ * This check assumes that 'LOG' is a logger.
+
+ :param logical_line: The logical line to check.
+ :param filename: The file name where the logical line exists.
+ :returns: None if the logical line passes the check, otherwise a tuple
+ is yielded that contains the offending index in logical line
+ and a message describe the check validation failure.
+ """
+ if _translation_checks_not_enforced(filename):
+ return
+
+ msg = "O341: Log messages should not be translated!"
+ match = _log_translation_hint.match(logical_line)
+ if match:
+ yield (logical_line.index(match.group()), msg)
+
+
+@core.flake8ext
+def check_raised_localized_exceptions(logical_line, filename):
+ """O342 - Untranslated exception message.
+
+ :param logical_line: The logical line to check.
+ :param filename: The file name where the logical line exists.
+ :returns: None if the logical line passes the check, otherwise a tuple
+ is yielded that contains the offending index in logical line
+ and a message describe the check validation failure.
+ """
+ if _translation_checks_not_enforced(filename):
+ return
+
+ logical_line = logical_line.strip()
+ raised_search = untranslated_exception_re.match(logical_line)
+ if raised_search:
+ exception_msg = raised_search.groups()[0]
+ if exception_msg.startswith("\"") or exception_msg.startswith("\'"):
+ msg = "O342: Untranslated exception message."
+ yield (logical_line.index(exception_msg), msg)
+
+
+@core.flake8ext
+def check_no_eventlet_imports(logical_line):
+ """O345 - Usage of Python eventlet module not allowed.
+
+ :param logical_line: The logical line to check.
+ :returns: None if the logical line passes the check, otherwise a tuple
+ is yielded that contains the offending index in logical line
+ and a message describe the check validation failure.
+ """
+ if no_eventlet_re.match(logical_line):
+ msg = 'O345 Usage of Python eventlet module not allowed'
+ yield logical_line.index('eventlet'), msg
+
+
+@core.flake8ext
+def check_line_continuation_no_backslash(logical_line, tokens):
+ """O346 - Don't use backslashes for line continuation.
+
+ :param logical_line: The logical line to check. Not actually used.
+ :param tokens: List of tokens to check.
+ :returns: None if the tokens don't contain any issues, otherwise a tuple
+ is yielded that contains the offending index in the logical
+ line and a message describe the check validation failure.
+ """
+ backslash = None
+ for token_type, text, start, end, orig_line in tokens:
+ m = no_line_continuation_backslash_re.match(orig_line)
+ if m:
+ backslash = (start[0], m.start(1))
+ break
+
+ if backslash is not None:
+ msg = 'O346 Backslash line continuations not allowed'
+ yield backslash, msg
+
+
+@core.flake8ext
+def revert_must_have_kwargs(logical_line):
+ """O347 - Taskflow revert methods must have \\*\\*kwargs.
+
+ :param logical_line: The logical line to check.
+ :returns: None if the logical line passes the check, otherwise a tuple
+ is yielded that contains the offending index in logical line
+ and a message describe the check validation failure.
+ """
+ if revert_must_have_kwargs_re.match(logical_line):
+ msg = 'O347 Taskflow revert methods must have **kwargs'
+ yield 0, msg
+
+
+@core.flake8ext
+def check_no_logging_imports(logical_line):
+ """O348 - Usage of Python logging module not allowed.
+
+ :param logical_line: The logical line to check.
+ :returns: None if the logical line passes the check, otherwise a tuple
+ is yielded that contains the offending index in logical line
+ and a message describe the check validation failure.
+ """
+ if no_logging_re.match(logical_line):
+ msg = 'O348 Usage of Python logging module not allowed, use oslo_log'
+ yield logical_line.index('logging'), msg
+
+
+@core.flake8ext
+def check_no_import_mock(logical_line):
+ """O349 - Test code must not import mock library.
+
+ :param logical_line: The logical line to check.
+ :returns: None if the logical line passes the check, otherwise a tuple
+ is yielded that contains the offending index in logical line
+ and a message describe the check validation failure.
+ """
+ if (import_mock_re.match(logical_line) or
+ import_from_mock_re.match(logical_line)):
+ msg = 'O349 Test code must not import mock library, use unittest.mock'
+ yield 0, msg
diff --git a/octavia_tempest_plugin/services/load_balancer/v2/__init__.py b/octavia_tempest_plugin/services/load_balancer/v2/__init__.py
index d31d6cf..04cb473 100644
--- a/octavia_tempest_plugin/services/load_balancer/v2/__init__.py
+++ b/octavia_tempest_plugin/services/load_balancer/v2/__init__.py
@@ -12,7 +12,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from octavia_tempest_plugin.services.load_balancer.v2.loadbalancer_client \
- import LoadbalancerClient
+from .loadbalancer_client import LoadbalancerClient
__all__ = ['LoadbalancerClient']
diff --git a/octavia_tempest_plugin/tests/api/v2/test_l7rule.py b/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
index aaad8a0..9a73034 100644
--- a/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
+++ b/octavia_tempest_plugin/tests/api/v2/test_l7rule.py
@@ -573,7 +573,7 @@
l7rule_check = self.mem_l7rule_client.show_l7rule(
l7rule[const.ID], l7policy_id=self.l7policy_id)
self.assertEqual(const.ACTIVE, l7rule_check[const.PROVISIONING_STATUS])
- self.assertEqual(False, l7rule_check[const.ADMIN_STATE_UP])
+ self.assertFalse(l7rule_check[const.ADMIN_STATE_UP])
# Test that a user, without the load balancer member role, cannot
# update this l7rule
@@ -588,7 +588,7 @@
l7rule_check = self.mem_l7rule_client.show_l7rule(
l7rule[const.ID], l7policy_id=self.l7policy_id)
self.assertEqual(const.ACTIVE, l7rule_check[const.PROVISIONING_STATUS])
- self.assertEqual(False, l7rule_check[const.ADMIN_STATE_UP])
+ self.assertFalse(l7rule_check[const.ADMIN_STATE_UP])
l7rule_update_kwargs = {
const.L7POLICY_ID: self.l7policy_id,
diff --git a/octavia_tempest_plugin/tests/scenario/v2/test_pool.py b/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
index 5e0622c..720e80a 100644
--- a/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
+++ b/octavia_tempest_plugin/tests/scenario/v2/test_pool.py
@@ -167,8 +167,8 @@
}
if self.lb_feature_enabled.pool_algorithms_enabled:
- pool_update_kwargs[const.LB_ALGORITHM] = \
- const.LB_ALGORITHM_LEAST_CONNECTIONS
+ pool_update_kwargs[const.LB_ALGORITHM] = (
+ const.LB_ALGORITHM_LEAST_CONNECTIONS)
if self.protocol == const.HTTP and (
self.lb_feature_enabled.session_persistence_enabled):
diff --git a/tox.ini b/tox.ini
index 4a61222..a419c62 100644
--- a/tox.ini
+++ b/tox.ini
@@ -43,6 +43,7 @@
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
-r{toxinidir}/requirements.txt
+ -r{toxinidir}/test-requirements.txt
-r{toxinidir}/doc/requirements.txt
whitelist_externals = rm
commands =
@@ -85,6 +86,24 @@
# [H904]: Delay string interpolations at logging calls
enable-extensions=H106,H203,H204,H205,H904
+[flake8:local-plugins]
+extension =
+ O316 = checks:assert_true_instance
+ O318 = checks:assert_equal_or_not_none
+ O323 = checks:assert_equal_true_or_false
+ O324 = checks:no_mutable_default_args
+ O338 = checks:assert_equal_in
+ O339 = checks:no_log_warn
+ O341 = checks:no_translate_logs
+ O342 = checks:check_raised_localized_exceptions
+ O345 = checks:check_no_eventlet_imports
+ O346 = checks:check_line_continuation_no_backslash
+ O347 = checks:revert_must_have_kwargs
+ O348 = checks:check_no_logging_imports
+ O349 = checks:check_no_import_mock
+paths =
+ ./octavia_tempest_plugin/hacking
+
[testenv:genconfig]
basepython = python3
whitelist_externals = mkdir