Add a hacking rule for non-existent assertions

Add a hacking rule for non-existent mock assertion methods and
attributes.

[N364] Non existent mock assertion method or attribute (<name>) is used.
       Check a typo or whether the assertion method should begin with
       'assert_'.

Change-Id: Ic6860e373120086a1a2ae9953f09a7bbaa032a7b
This commit is contained in:
Takashi NATSUME 2019-08-21 11:45:22 +09:00
parent 5ccdbc7189
commit d4ed9ed93f
3 changed files with 124 additions and 0 deletions

View File

@ -74,6 +74,7 @@ Nova Specific Commandments
not "from nova.privsep import path". This ensures callers know that the method they're not "from nova.privsep import path". This ensures callers know that the method they're
calling is using priviledge escalation. calling is using priviledge escalation.
- [N363] Disallow ``(not_a_tuple)`` because you meant ``(a_tuple_of_one,)``. - [N363] Disallow ``(not_a_tuple)`` because you meant ``(a_tuple_of_one,)``.
- [N364] Check non-existent mock assertion methods and attributes.
Creating Unit Tests Creating Unit Tests
------------------- -------------------

View File

@ -115,6 +115,17 @@ privsep_import_re = re.compile(
# Close paren # Close paren
disguised_as_tuple_re = re.compile(r''' in \((['"]?)[a-zA-Z0-9_.]+\1\)''') disguised_as_tuple_re = re.compile(r''' in \((['"]?)[a-zA-Z0-9_.]+\1\)''')
# NOTE(takashin): The patterns of nox-existent mock assertion methods and
# attributes do not cover all cases. If you find a new pattern,
# add the pattern in the following regex patterns.
mock_assert_method_re = re.compile(
r"\.((called_once(_with)*|has_calls)|"
r"mock_assert_(called(_(once|with|once_with))?"
r"|any_call|has_calls|not_called)|"
r"(asser|asset|asssert|assset)_(called(_(once|with|once_with))?"
r"|any_call|has_calls|not_called))\(")
mock_attribute_re = re.compile(r"[\.\(](retrun_value)[,=\s]")
class BaseASTChecker(ast.NodeVisitor): class BaseASTChecker(ast.NodeVisitor):
"""Provides a simple framework for writing AST-based checks. """Provides a simple framework for writing AST-based checks.
@ -898,6 +909,39 @@ def did_you_mean_tuple(logical_line):
"certainly meant ``in (a_tuple_of_one,)``.") "certainly meant ``in (a_tuple_of_one,)``.")
def nonexistent_assertion_methods_and_attributes(logical_line, filename):
"""Check non-existent mock assertion methods and attributes.
The following assertion methods do not exist.
- called_once()
- called_once_with()
- has_calls()
- mock_assert_*()
The following typos were found in the past cases.
- asser_*
- asset_*
- assset_*
- asssert_*
- retrun_value
N364
"""
msg = ("N364: Non existent mock assertion method or attribute (%s) is "
"used. Check a typo or whether the assertion method should begin "
"with 'assert_'.")
if 'nova/tests/' in filename:
match = mock_assert_method_re.search(logical_line)
if match:
yield (0, msg % match.group(1))
match = mock_attribute_re.search(logical_line)
if match:
yield (0, msg % match.group(1))
def factory(register): def factory(register):
register(import_no_db_in_virt) register(import_no_db_in_virt)
register(no_db_session_in_public_api) register(no_db_session_in_public_api)
@ -944,3 +988,4 @@ def factory(register):
register(assert_regexpmatches) register(assert_regexpmatches)
register(privsep_imports_not_aliased) register(privsep_imports_not_aliased)
register(did_you_mean_tuple) register(did_you_mean_tuple)
register(nonexistent_assertion_methods_and_attributes)

View File

@ -913,3 +913,81 @@ class HackingTestCase(test.NoDBTestCase):
or foo in (method_call_should_this_work()): or foo in (method_call_should_this_work()):
""" """
self._assert_has_no_errors(code, checks.did_you_mean_tuple) self._assert_has_no_errors(code, checks.did_you_mean_tuple)
def test_nonexistent_assertion_methods_and_attributes(self):
code = """
mock_sample.called_once()
mock_sample.called_once_with(a, "TEST")
mock_sample.has_calls([mock.call(x), mock.call(y)])
mock_sample.mock_assert_called()
mock_sample.mock_assert_called_once()
mock_sample.mock_assert_called_with(a, "xxxx")
mock_sample.mock_assert_called_once_with(a, b)
mock_sample.mock_assert_any_call(1, 2, instance=instance)
sample.mock_assert_has_calls([mock.call(x), mock.call(y)])
mock_sample.mock_assert_not_called()
mock_sample.asser_called()
mock_sample.asser_called_once()
mock_sample.asser_called_with(a, "xxxx")
mock_sample.asser_called_once_with(a, b)
mock_sample.asser_any_call(1, 2, instance=instance)
mock_sample.asser_has_calls([mock.call(x), mock.call(y)])
mock_sample.asser_not_called()
mock_sample.asset_called()
mock_sample.asset_called_once()
mock_sample.asset_called_with(a, "xxxx")
mock_sample.asset_called_once_with(a, b)
mock_sample.asset_any_call(1, 2, instance=instance)
mock_sample.asset_has_calls([mock.call(x), mock.call(y)])
mock_sample.asset_not_called()
mock_sample.asssert_called()
mock_sample.asssert_called_once()
mock_sample.asssert_called_with(a, "xxxx")
mock_sample.asssert_called_once_with(a, b)
mock_sample.asssert_any_call(1, 2, instance=instance)
mock_sample.asssert_has_calls([mock.call(x), mock.call(y)])
mock_sample.asssert_not_called()
mock_sample.assset_called()
mock_sample.assset_called_once()
mock_sample.assset_called_with(a, "xxxx")
mock_sample.assset_called_once_with(a, b)
mock_sample.assset_any_call(1, 2, instance=instance)
mock_sample.assset_has_calls([mock.call(x), mock.call(y)])
mock_sample.assset_not_called()
mock_sample.retrun_value = 100
sample.call_method(mock_sample.retrun_value, 100)
mock.Mock(retrun_value=100)
"""
errors = [(x + 1, 0, 'N364') for x in range(41)]
# Check errors in 'nova/tests' directory.
self._assert_has_errors(
code, checks.nonexistent_assertion_methods_and_attributes,
expected_errors=errors, filename="nova/tests/unit/test_context.py")
# Check no errors in other than 'nova/tests' directory.
self._assert_has_no_errors(
code, checks.nonexistent_assertion_methods_and_attributes,
filename="nova/compute/api.py")
code = """
mock_sample.assert_called()
mock_sample.assert_called_once()
mock_sample.assert_called_with(a, "xxxx")
mock_sample.assert_called_once_with(a, b)
mock_sample.assert_any_call(1, 2, instance=instance)
mock_sample.assert_has_calls([mock.call(x), mock.call(y)])
mock_sample.assert_not_called()
mock_sample.return_value = 100
sample.call_method(mock_sample.return_value, 100)
mock.Mock(return_value=100)
sample.has_other_calls([1, 2, 3, 4])
sample.get_called_once("TEST")
sample.check_called_once_with("TEST")
mock_assert_method.assert_called()
test.asset_has_all(test)
test_retrun_value = 99
test.retrun_values = 100
"""
self._assert_has_no_errors(
code, checks.nonexistent_assertion_methods_and_attributes,
filename="nova/tests/unit/test_context.py")