You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
13 KiB
347 lines
13 KiB
4 months ago
|
"""Testing `tabnanny` module.
|
||
|
|
||
|
Glossary:
|
||
|
* errored : Whitespace related problems present in file.
|
||
|
"""
|
||
|
from unittest import TestCase, mock
|
||
|
from unittest import mock
|
||
|
import errno
|
||
|
import os
|
||
|
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 {errno.ENOENT}] "
|
||
|
f"{os.strerror(errno.ENOENT)}: {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)
|