| import unittest |
| import sys |
| try: |
| from StringIO import StringIO |
| except ImportError: |
| from io import StringIO |
| |
| import pytest |
| |
| import mccabe |
| from mccabe import get_code_complexity |
| |
| |
| # Snippets are put outside of testcases because of spacing issue that would |
| # otherwise occur with triple quoted strings. |
| trivial = 'def f(): pass' |
| |
| |
| expr_as_statement = '''\ |
| def f(): |
| 0xF00D |
| ''' |
| |
| |
| sequential = """\ |
| def f(n): |
| k = n + 4 |
| s = k + n |
| return s |
| """ |
| |
| |
| sequential_unencapsulated = """\ |
| k = 2 + 4 |
| s = k + 3 |
| """ |
| |
| |
| if_elif_else_dead_path = """\ |
| def f(n): |
| if n > 3: |
| return "bigger than three" |
| elif n > 4: |
| return "is never executed" |
| else: |
| return "smaller than or equal to three" |
| """ |
| |
| |
| for_loop = """\ |
| def f(): |
| for i in range(10): |
| print(i) |
| """ |
| |
| |
| for_else = """\ |
| def f(mylist): |
| for i in mylist: |
| print(i) |
| else: |
| print(None) |
| """ |
| |
| |
| recursive = """\ |
| def f(n): |
| if n > 4: |
| return f(n - 1) |
| else: |
| return n |
| """ |
| |
| |
| nested_functions = """\ |
| def a(): |
| def b(): |
| def c(): |
| pass |
| c() |
| b() |
| """ |
| |
| try_else = """\ |
| try: |
| print(1) |
| except TypeA: |
| print(2) |
| except TypeB: |
| print(3) |
| else: |
| print(4) |
| """ |
| |
| async_keywords = """\ |
| async def foobar(a, b, c): |
| await whatever(a, b, c) |
| if await b: |
| pass |
| |
| async with c: |
| pass |
| |
| async for x in a: |
| pass |
| """ |
| |
| annotated_assign = """\ |
| def f(): |
| x: Any = None |
| """ |
| |
| |
| def get_complexity_number(snippet, strio, max=0): |
| """Get the complexity number from the printed string.""" |
| # Report from the lowest complexity number. |
| get_code_complexity(snippet, max) |
| strio_val = strio.getvalue() |
| if strio_val: |
| return int(strio_val.split()[-1].strip("()")) |
| else: |
| return None |
| |
| |
| class McCabeTestCase(unittest.TestCase): |
| def setUp(self): |
| # If not assigned to sys.stdout then getvalue() won't capture anything. |
| self._orig_stdout = sys.stdout |
| sys.stdout = self.strio = StringIO() |
| |
| def tearDown(self): |
| # https://mail.python.org/pipermail/tutor/2012-January/088031.html |
| self.strio.close() |
| sys.stdout = self._orig_stdout |
| |
| def assert_complexity(self, snippet, max): |
| complexity = get_complexity_number(snippet, self.strio) |
| self.assertEqual(complexity, max) |
| |
| # should have the same complexity when inside a function as well. |
| infunc = 'def f():\n ' + snippet.replace('\n', '\n ') |
| complexity = get_complexity_number(infunc, self.strio) |
| self.assertEqual(complexity, max) |
| |
| def test_print_message(self): |
| get_code_complexity(sequential, 0) |
| printed_message = self.strio.getvalue() |
| self.assertEqual(printed_message, |
| "stdin:1:1: C901 'f' is too complex (1)\n") |
| |
| def test_sequential_snippet(self): |
| complexity = get_complexity_number(sequential, self.strio) |
| self.assertEqual(complexity, 1) |
| |
| def test_sequential_unencapsulated_snippet(self): |
| complexity = get_complexity_number(sequential_unencapsulated, |
| self.strio) |
| self.assertEqual(complexity, None) |
| |
| def test_if_elif_else_dead_path_snippet(self): |
| complexity = get_complexity_number(if_elif_else_dead_path, self.strio) |
| # Paths that will never be executed are counted! |
| self.assertEqual(complexity, 3) |
| |
| def test_for_loop_snippet(self): |
| complexity = get_complexity_number(for_loop, self.strio) |
| # The for loop adds an execution path; sometimes it won't be run. |
| self.assertEqual(complexity, 2) |
| |
| def test_for_else_snippet(self): |
| complexity = get_complexity_number(for_else, self.strio) |
| # The for loop doesn't add an execution path, but its `else` does |
| self.assertEqual(complexity, 2) |
| |
| def test_recursive_snippet(self): |
| complexity = get_complexity_number(recursive, self.strio) |
| self.assertEqual(complexity, 2) |
| |
| def test_nested_functions_snippet(self): |
| complexity = get_complexity_number(nested_functions, self.strio) |
| self.assertEqual(complexity, 3) |
| |
| def test_trivial(self): |
| """The most-trivial program should pass a max-complexity=1 test""" |
| complexity = get_complexity_number(trivial, self.strio, max=1) |
| self.assertEqual(complexity, None) |
| printed_message = self.strio.getvalue() |
| self.assertEqual(printed_message, "") |
| |
| def test_expr_as_statement(self): |
| complexity = get_complexity_number(expr_as_statement, self.strio) |
| self.assertEqual(complexity, 1) |
| |
| def test_try_else(self): |
| self.assert_complexity(try_else, 4) |
| |
| @pytest.mark.skipif(sys.version_info < (3, 5), |
| reason="Async keywords are only valid on Python 3.5+") |
| def test_async_keywords(self): |
| """Validate that we properly process async keyword usage.""" |
| complexity = get_complexity_number(async_keywords, self.strio) |
| self.assertEqual(complexity, 3) |
| |
| @pytest.mark.skipif( |
| sys.version_info < (3, 6), |
| reason="Annotated assignments are only valid on Python 3.6+", |
| ) |
| def test_annotated_assignment(self): |
| complexity = get_complexity_number(annotated_assign, self.strio) |
| self.assertEqual(complexity, 1) |
| |
| |
| class RegressionTests(unittest.TestCase): |
| def setUp(self): |
| self.original_complexity = mccabe.McCabeChecker.max_complexity |
| |
| def tearDown(self): |
| mccabe.McCabeChecker.max_complexity = self.original_complexity |
| |
| def test_max_complexity_is_always_an_int(self): |
| """Ensure bug #32 does not regress.""" |
| class _options(object): |
| max_complexity = None |
| |
| options = _options() |
| options.max_complexity = '16' |
| |
| self.assertEqual(0, mccabe.McCabeChecker.max_complexity) |
| mccabe.McCabeChecker.parse_options(options) |
| self.assertEqual(16, mccabe.McCabeChecker.max_complexity) |
| |
| def test_get_module_complexity(self): |
| self.assertEqual(0, mccabe.get_module_complexity("mccabe.py")) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |