]> granicus.if.org Git - python/commitdiff
bpo-19382: Adding test cases for module tabnanny (GH-851)
authorJaysinh Shukla <jaysinhp@gmail.com>
Thu, 14 Jun 2018 07:05:35 +0000 (12:35 +0530)
committerVictor Stinner <vstinner@redhat.com>
Thu, 14 Jun 2018 07:05:35 +0000 (09:05 +0200)
Testing strategy: whitebox.

Lib/test/test_sundry.py
Lib/test/test_tabnanny.py [new file with mode: 0644]

index 4025c2354a6cdbe3f6db7fd74f6e1e6594165103..6e36a6123daa0e85efa18ac5260298b77eef9f49 100644 (file)
@@ -6,7 +6,7 @@ import unittest
 
 class TestUntestedModules(unittest.TestCase):
     def test_untested_modules_can_be_imported(self):
-        untested = ('encodings', 'formatter', 'tabnanny')
+        untested = ('encodings', 'formatter')
         with support.check_warnings(quiet=True):
             for name in untested:
                 try:
diff --git a/Lib/test/test_tabnanny.py b/Lib/test/test_tabnanny.py
new file mode 100644 (file)
index 0000000..ec88736
--- /dev/null
@@ -0,0 +1,343 @@
+"""Testing `tabnanny` module.
+
+Glossary:
+    * errored    : Whitespace related problems present in file.
+"""
+from unittest import TestCase, mock
+from unittest import mock
+import tabnanny
+import tokenize
+import tempfile
+import textwrap
+from test.support import (captured_stderr, captured_stdout, script_helper,
+                          findfile, unlink)
+
+
+SOURCE_CODES = {
+    "incomplete_expression": (
+        'fruits = [\n'
+        '    "Apple",\n'
+        '    "Orange",\n'
+        '    "Banana",\n'
+        '\n'
+        'print(fruits)\n'
+    ),
+    "wrong_indented": (
+        'if True:\n'
+        '    print("hello")\n'
+        '  print("world")\n'
+        'else:\n'
+        '    print("else called")\n'
+    ),
+    "nannynag_errored": (
+        'if True:\n'
+        ' \tprint("hello")\n'
+        '\tprint("world")\n'
+        'else:\n'
+        '    print("else called")\n'
+    ),
+    "error_free": (
+        'if True:\n'
+        '    print("hello")\n'
+        '    print("world")\n'
+        'else:\n'
+        '    print("else called")\n'
+    ),
+    "tab_space_errored_1": (
+        'def my_func():\n'
+        '\t  print("hello world")\n'
+        '\t  if True:\n'
+        '\t\tprint("If called")'
+    ),
+    "tab_space_errored_2": (
+        'def my_func():\n'
+        '\t\tprint("Hello world")\n'
+        '\t\tif True:\n'
+        '\t        print("If called")'
+    )
+}
+
+
+class TemporaryPyFile:
+    """Create a temporary python source code file."""
+
+    def __init__(self, source_code='', directory=None):
+        self.source_code = source_code
+        self.dir = directory
+
+    def __enter__(self):
+        with tempfile.NamedTemporaryFile(
+            mode='w', dir=self.dir, suffix=".py", delete=False
+        ) as f:
+            f.write(self.source_code)
+        self.file_path = f.name
+        return self.file_path
+
+    def __exit__(self, exc_type, exc_value, exc_traceback):
+        unlink(self.file_path)
+
+
+class TestFormatWitnesses(TestCase):
+    """Testing `tabnanny.format_witnesses()`."""
+
+    def test_format_witnesses(self):
+        """Asserting formatter result by giving various input samples."""
+        tests = [
+            ('Test', 'at tab sizes T, e, s, t'),
+            ('', 'at tab size '),
+            ('t', 'at tab size t'),
+            ('  t  ', 'at tab sizes  ,  , t,  ,  '),
+        ]
+
+        for words, expected in tests:
+            with self.subTest(words=words, expected=expected):
+                self.assertEqual(tabnanny.format_witnesses(words), expected)
+
+
+class TestErrPrint(TestCase):
+    """Testing `tabnanny.errprint()`."""
+
+    def test_errprint(self):
+        """Asserting result of `tabnanny.errprint()` by giving sample inputs."""
+        tests = [
+            (['first', 'second'], 'first second\n'),
+            (['first'], 'first\n'),
+            ([1, 2, 3], '1 2 3\n'),
+            ([], '\n')
+        ]
+
+        for args, expected in tests:
+            with self.subTest(arguments=args, expected=expected):
+                with captured_stderr() as stderr:
+                    tabnanny.errprint(*args)
+                self.assertEqual(stderr.getvalue() , expected)
+
+
+class TestNannyNag(TestCase):
+    def test_all_methods(self):
+        """Asserting behaviour of `tabnanny.NannyNag` exception."""
+        tests = [
+            (
+                tabnanny.NannyNag(0, "foo", "bar"),
+                {'lineno': 0, 'msg': 'foo', 'line': 'bar'}
+            ),
+            (
+                tabnanny.NannyNag(5, "testmsg", "testline"),
+                {'lineno': 5, 'msg': 'testmsg', 'line': 'testline'}
+            )
+        ]
+        for nanny, expected in tests:
+            line_number = nanny.get_lineno()
+            msg = nanny.get_msg()
+            line = nanny.get_line()
+            with self.subTest(
+                line_number=line_number, expected=expected['lineno']
+            ):
+                self.assertEqual(expected['lineno'], line_number)
+            with self.subTest(msg=msg, expected=expected['msg']):
+                self.assertEqual(expected['msg'], msg)
+            with self.subTest(line=line, expected=expected['line']):
+                self.assertEqual(expected['line'], line)
+
+
+class TestCheck(TestCase):
+    """Testing tabnanny.check()."""
+
+    def setUp(self):
+        self.addCleanup(setattr, tabnanny, 'verbose', tabnanny.verbose)
+        tabnanny.verbose = 0  # Forcefully deactivating verbose mode.
+
+    def verify_tabnanny_check(self, dir_or_file, out="", err=""):
+        """Common verification for tabnanny.check().
+
+        Use this method to assert expected values of `stdout` and `stderr` after
+        running tabnanny.check() on given `dir` or `file` path. Because
+        tabnanny.check() captures exceptions and writes to `stdout` and
+        `stderr`, asserting standard outputs is the only way.
+        """
+        with captured_stdout() as stdout, captured_stderr() as stderr:
+            tabnanny.check(dir_or_file)
+        self.assertEqual(stdout.getvalue(), out)
+        self.assertEqual(stderr.getvalue(), err)
+
+    def test_correct_file(self):
+        """A python source code file without any errors."""
+        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
+            self.verify_tabnanny_check(file_path)
+
+    def test_correct_directory_verbose(self):
+        """Directory containing few error free python source code files.
+
+        Because order of files returned by `os.lsdir()` is not fixed, verify the
+        existence of each output lines at `stdout` using `in` operator.
+        `verbose` mode of `tabnanny.verbose` asserts `stdout`.
+        """
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            lines = [f"{tmp_dir!r}: listing directory\n",]
+            file1 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
+            file2 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
+            with file1 as file1_path, file2 as file2_path:
+                for file_path in (file1_path, file2_path):
+                    lines.append(f"{file_path!r}: Clean bill of health.\n")
+
+                tabnanny.verbose = 1
+                with captured_stdout() as stdout, captured_stderr() as stderr:
+                    tabnanny.check(tmp_dir)
+                stdout = stdout.getvalue()
+                for line in lines:
+                    with self.subTest(line=line):
+                        self.assertIn(line, stdout)
+                self.assertEqual(stderr.getvalue(), "")
+
+    def test_correct_directory(self):
+        """Directory which contains few error free python source code files."""
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            with TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir):
+                self.verify_tabnanny_check(tmp_dir)
+
+    def test_when_wrong_indented(self):
+        """A python source code file eligible for raising `IndentationError`."""
+        with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
+            err = ('unindent does not match any outer indentation level'
+                ' (<tokenize>, line 3)\n')
+            err = f"{file_path!r}: Indentation Error: {err}"
+            self.verify_tabnanny_check(file_path, err=err)
+
+    def test_when_tokenize_tokenerror(self):
+        """A python source code file eligible for raising 'tokenize.TokenError'."""
+        with TemporaryPyFile(SOURCE_CODES["incomplete_expression"]) as file_path:
+            err = "('EOF in multi-line statement', (7, 0))\n"
+            err = f"{file_path!r}: Token Error: {err}"
+            self.verify_tabnanny_check(file_path, err=err)
+
+    def test_when_nannynag_error_verbose(self):
+        """A python source code file eligible for raising `tabnanny.NannyNag`.
+
+        Tests will assert `stdout` after activating `tabnanny.verbose` mode.
+        """
+        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
+            out = f"{file_path!r}: *** Line 3: trouble in tab city! ***\n"
+            out += "offending line: '\\tprint(\"world\")\\n'\n"
+            out += "indent not equal e.g. at tab size 1\n"
+
+            tabnanny.verbose = 1
+            self.verify_tabnanny_check(file_path, out=out)
+
+    def test_when_nannynag_error(self):
+        """A python source code file eligible for raising `tabnanny.NannyNag`."""
+        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
+            out = f"{file_path} 3 '\\tprint(\"world\")\\n'\n"
+            self.verify_tabnanny_check(file_path, out=out)
+
+    def test_when_no_file(self):
+        """A python file which does not exist actually in system."""
+        path = 'no_file.py'
+        err = f"{path!r}: I/O Error: [Errno 2] No such file or directory: {path!r}\n"
+        self.verify_tabnanny_check(path, err=err)
+
+    def test_errored_directory(self):
+        """Directory containing wrongly indented python source code files."""
+        with tempfile.TemporaryDirectory() as tmp_dir:
+            error_file = TemporaryPyFile(
+                SOURCE_CODES["wrong_indented"], directory=tmp_dir
+            )
+            code_file = TemporaryPyFile(
+                SOURCE_CODES["error_free"], directory=tmp_dir
+            )
+            with error_file as e_file, code_file as c_file:
+                err = ('unindent does not match any outer indentation level'
+                            ' (<tokenize>, line 3)\n')
+                err = f"{e_file!r}: Indentation Error: {err}"
+                self.verify_tabnanny_check(tmp_dir, err=err)
+
+
+class TestProcessTokens(TestCase):
+    """Testing `tabnanny.process_tokens()`."""
+
+    @mock.patch('tabnanny.NannyNag')
+    def test_with_correct_code(self, MockNannyNag):
+        """A python source code without any whitespace related problems."""
+
+        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
+            with open(file_path) as f:
+                tabnanny.process_tokens(tokenize.generate_tokens(f.readline))
+            self.assertFalse(MockNannyNag.called)
+
+    def test_with_errored_codes_samples(self):
+        """A python source code with whitespace related sampled problems."""
+
+        # "tab_space_errored_1": executes block under type == tokenize.INDENT
+        #                        at `tabnanny.process_tokens()`.
+        # "tab space_errored_2": executes block under
+        #                        `check_equal and type not in JUNK` condition at
+        #                        `tabnanny.process_tokens()`.
+
+        for key in ["tab_space_errored_1", "tab_space_errored_2"]:
+            with self.subTest(key=key):
+                with TemporaryPyFile(SOURCE_CODES[key]) as file_path:
+                    with open(file_path) as f:
+                        tokens = tokenize.generate_tokens(f.readline)
+                        with self.assertRaises(tabnanny.NannyNag):
+                            tabnanny.process_tokens(tokens)
+
+
+class TestCommandLine(TestCase):
+    """Tests command line interface of `tabnanny`."""
+
+    def validate_cmd(self, *args, stdout="", stderr="", partial=False):
+        """Common function to assert the behaviour of command line interface."""
+        _, out, err = script_helper.assert_python_ok('-m', 'tabnanny', *args)
+        # Note: The `splitlines()` will solve the problem of CRLF(\r) added
+        # by OS Windows.
+        out = out.decode('ascii')
+        err = err.decode('ascii')
+        if partial:
+            for std, output in ((stdout, out), (stderr, err)):
+                _output = output.splitlines()
+                for _std in std.splitlines():
+                    with self.subTest(std=_std, output=_output):
+                        self.assertIn(_std, _output)
+        else:
+            self.assertListEqual(out.splitlines(), stdout.splitlines())
+            self.assertListEqual(err.splitlines(), stderr.splitlines())
+
+    def test_with_errored_file(self):
+        """Should displays error when errored python file is given."""
+        with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
+            stderr  = f"{file_path!r}: Indentation Error: "
+            stderr += ('unindent does not match any outer indentation level'
+                    ' (<tokenize>, line 3)')
+            self.validate_cmd(file_path, stderr=stderr)
+
+    def test_with_error_free_file(self):
+        """Should not display anything if python file is correctly indented."""
+        with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
+            self.validate_cmd(file_path)
+
+    def test_command_usage(self):
+        """Should display usage on no arguments."""
+        path = findfile('tabnanny.py')
+        stderr = f"Usage: {path} [-v] file_or_directory ..."
+        self.validate_cmd(stderr=stderr)
+
+    def test_quiet_flag(self):
+        """Should display less when quite mode is on."""
+        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
+            stdout = f"{file_path}\n"
+            self.validate_cmd("-q", file_path, stdout=stdout)
+
+    def test_verbose_mode(self):
+        """Should display more error information if verbose mode is on."""
+        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
+            stdout = textwrap.dedent(
+                "offending line: '\\tprint(\"world\")\\n'"
+            ).strip()
+            self.validate_cmd("-v", path, stdout=stdout, partial=True)
+
+    def test_double_verbose_mode(self):
+        """Should display detailed error information if double verbose is on."""
+        with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
+            stdout = textwrap.dedent(
+                "offending line: '\\tprint(\"world\")\\n'"
+            ).strip()
+            self.validate_cmd("-vv", path, stdout=stdout, partial=True)