Skip to content

Commit 86b9ad2

Browse files
committed
[ADD] Adding test cases for module tabnanny. Testing stratergy:
Whitebox.
1 parent ab4a198 commit 86b9ad2

File tree

2 files changed

+344
-1
lines changed

2 files changed

+344
-1
lines changed

Lib/test/test_sundry.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
class TestUntestedModules(unittest.TestCase):
88
def test_untested_modules_can_be_imported(self):
9-
untested = ('encodings', 'formatter', 'tabnanny')
9+
untested = ('encodings', 'formatter')
1010
with support.check_warnings(quiet=True):
1111
for name in untested:
1212
try:

Lib/test/test_tabnanny.py

+343
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,343 @@
1+
"""Testing `tabnanny` module.
2+
3+
Glossary:
4+
* errored : Whitespace related problems present in file.
5+
"""
6+
from unittest import TestCase, mock
7+
from unittest import mock
8+
import tabnanny
9+
import tokenize
10+
import tempfile
11+
import textwrap
12+
from test.support import (captured_stderr, captured_stdout, script_helper,
13+
findfile, unlink)
14+
15+
16+
SOURCE_CODES = {
17+
"incomplete_expression": (
18+
'fruits = [\n'
19+
' "Apple",\n'
20+
' "Orange",\n'
21+
' "Banana",\n'
22+
'\n'
23+
'print(fruits)\n'
24+
),
25+
"wrong_indented": (
26+
'if True:\n'
27+
' print("hello")\n'
28+
' print("world")\n'
29+
'else:\n'
30+
' print("else called")\n'
31+
),
32+
"nannynag_errored": (
33+
'if True:\n'
34+
' \tprint("hello")\n'
35+
'\tprint("world")\n'
36+
'else:\n'
37+
' print("else called")\n'
38+
),
39+
"error_free": (
40+
'if True:\n'
41+
' print("hello")\n'
42+
' print("world")\n'
43+
'else:\n'
44+
' print("else called")\n'
45+
),
46+
"tab_space_errored_1": (
47+
'def my_func():\n'
48+
'\t print("hello world")\n'
49+
'\t if True:\n'
50+
'\t\tprint("If called")'
51+
),
52+
"tab_space_errored_2": (
53+
'def my_func():\n'
54+
'\t\tprint("Hello world")\n'
55+
'\t\tif True:\n'
56+
'\t print("If called")'
57+
)
58+
}
59+
60+
61+
class TemporaryPyFile:
62+
"""Create a temporary python source code file."""
63+
64+
def __init__(self, source_code='', directory=None):
65+
self.source_code = source_code
66+
self.dir = directory
67+
68+
def __enter__(self):
69+
with tempfile.NamedTemporaryFile(
70+
mode='w', dir=self.dir, suffix=".py", delete=False
71+
) as f:
72+
f.write(self.source_code)
73+
self.file_path = f.name
74+
return self.file_path
75+
76+
def __exit__(self, exc_type, exc_value, exc_traceback):
77+
unlink(self.file_path)
78+
79+
80+
class TestFormatWitnesses(TestCase):
81+
"""Testing `tabnanny.format_witnesses()`."""
82+
83+
def test_format_witnesses(self):
84+
"""Asserting formatter result by giving various input samples."""
85+
tests = [
86+
('Test', 'at tab sizes T, e, s, t'),
87+
('', 'at tab size '),
88+
('t', 'at tab size t'),
89+
(' t ', 'at tab sizes , , t, , '),
90+
]
91+
92+
for words, expected in tests:
93+
with self.subTest(words=words, expected=expected):
94+
self.assertEqual(tabnanny.format_witnesses(words), expected)
95+
96+
97+
class TestErrPrint(TestCase):
98+
"""Testing `tabnanny.errprint()`."""
99+
100+
def test_errprint(self):
101+
"""Asserting result of `tabnanny.errprint()` by giving sample inputs."""
102+
tests = [
103+
(['first', 'second'], 'first second\n'),
104+
(['first'], 'first\n'),
105+
([1, 2, 3], '1 2 3\n'),
106+
([], '\n')
107+
]
108+
109+
for args, expected in tests:
110+
with self.subTest(arguments=args, expected=expected):
111+
with captured_stderr() as stderr:
112+
tabnanny.errprint(*args)
113+
self.assertEqual(stderr.getvalue() , expected)
114+
115+
116+
class TestNannyNag(TestCase):
117+
def test_all_methods(self):
118+
"""Asserting behaviour of `tabnanny.NannyNag` exception."""
119+
tests = [
120+
(
121+
tabnanny.NannyNag(0, "foo", "bar"),
122+
{'lineno': 0, 'msg': 'foo', 'line': 'bar'}
123+
),
124+
(
125+
tabnanny.NannyNag(5, "testmsg", "testline"),
126+
{'lineno': 5, 'msg': 'testmsg', 'line': 'testline'}
127+
)
128+
]
129+
for nanny, expected in tests:
130+
line_number = nanny.get_lineno()
131+
msg = nanny.get_msg()
132+
line = nanny.get_line()
133+
with self.subTest(
134+
line_number=line_number, expected=expected['lineno']
135+
):
136+
self.assertEqual(expected['lineno'], line_number)
137+
with self.subTest(msg=msg, expected=expected['msg']):
138+
self.assertEqual(expected['msg'], msg)
139+
with self.subTest(line=line, expected=expected['line']):
140+
self.assertEqual(expected['line'], line)
141+
142+
143+
class TestCheck(TestCase):
144+
"""Testing tabnanny.check()."""
145+
146+
def setUp(self):
147+
self.addCleanup(setattr, tabnanny, 'verbose', tabnanny.verbose)
148+
tabnanny.verbose = 0 # Forcefully deactivating verbose mode.
149+
150+
def verify_tabnanny_check(self, dir_or_file, out="", err=""):
151+
"""Common verification for tabnanny.check().
152+
153+
Use this method to assert expected values of `stdout` and `stderr` after
154+
running tabnanny.check() on given `dir` or `file` path. Because
155+
tabnanny.check() captures exceptions and writes to `stdout` and
156+
`stderr`, asserting standard outputs is the only way.
157+
"""
158+
with captured_stdout() as stdout, captured_stderr() as stderr:
159+
tabnanny.check(dir_or_file)
160+
self.assertEqual(stdout.getvalue(), out)
161+
self.assertEqual(stderr.getvalue(), err)
162+
163+
def test_correct_file(self):
164+
"""A python source code file without any errors."""
165+
with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
166+
self.verify_tabnanny_check(file_path)
167+
168+
def test_correct_directory_verbose(self):
169+
"""Directory containing few error free python source code files.
170+
171+
Because order of files returned by `os.lsdir()` is not fixed, verify the
172+
existence of each output lines at `stdout` using `in` operator.
173+
`verbose` mode of `tabnanny.verbose` asserts `stdout`.
174+
"""
175+
with tempfile.TemporaryDirectory() as tmp_dir:
176+
lines = [f"{tmp_dir!r}: listing directory\n",]
177+
file1 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
178+
file2 = TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir)
179+
with file1 as file1_path, file2 as file2_path:
180+
for file_path in (file1_path, file2_path):
181+
lines.append(f"{file_path!r}: Clean bill of health.\n")
182+
183+
tabnanny.verbose = 1
184+
with captured_stdout() as stdout, captured_stderr() as stderr:
185+
tabnanny.check(tmp_dir)
186+
stdout = stdout.getvalue()
187+
for line in lines:
188+
with self.subTest(line=line):
189+
self.assertIn(line, stdout)
190+
self.assertEqual(stderr.getvalue(), "")
191+
192+
def test_correct_directory(self):
193+
"""Directory which contains few error free python source code files."""
194+
with tempfile.TemporaryDirectory() as tmp_dir:
195+
with TemporaryPyFile(SOURCE_CODES["error_free"], directory=tmp_dir):
196+
self.verify_tabnanny_check(tmp_dir)
197+
198+
def test_when_wrong_indented(self):
199+
"""A python source code file eligible for raising `IndentationError`."""
200+
with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
201+
err = ('unindent does not match any outer indentation level'
202+
' (<tokenize>, line 3)\n')
203+
err = f"{file_path!r}: Indentation Error: {err}"
204+
self.verify_tabnanny_check(file_path, err=err)
205+
206+
def test_when_tokenize_tokenerror(self):
207+
"""A python source code file eligible for raising 'tokenize.TokenError'."""
208+
with TemporaryPyFile(SOURCE_CODES["incomplete_expression"]) as file_path:
209+
err = "('EOF in multi-line statement', (7, 0))\n"
210+
err = f"{file_path!r}: Token Error: {err}"
211+
self.verify_tabnanny_check(file_path, err=err)
212+
213+
def test_when_nannynag_error_verbose(self):
214+
"""A python source code file eligible for raising `tabnanny.NannyNag`.
215+
216+
Tests will assert `stdout` after activating `tabnanny.verbose` mode.
217+
"""
218+
with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
219+
out = f"{file_path!r}: *** Line 3: trouble in tab city! ***\n"
220+
out += "offending line: '\\tprint(\"world\")\\n'\n"
221+
out += "indent not equal e.g. at tab size 1\n"
222+
223+
tabnanny.verbose = 1
224+
self.verify_tabnanny_check(file_path, out=out)
225+
226+
def test_when_nannynag_error(self):
227+
"""A python source code file eligible for raising `tabnanny.NannyNag`."""
228+
with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
229+
out = f"{file_path} 3 '\\tprint(\"world\")\\n'\n"
230+
self.verify_tabnanny_check(file_path, out=out)
231+
232+
def test_when_no_file(self):
233+
"""A python file which does not exist actually in system."""
234+
path = 'no_file.py'
235+
err = f"{path!r}: I/O Error: [Errno 2] No such file or directory: {path!r}\n"
236+
self.verify_tabnanny_check(path, err=err)
237+
238+
def test_errored_directory(self):
239+
"""Directory containing wrongly indented python source code files."""
240+
with tempfile.TemporaryDirectory() as tmp_dir:
241+
error_file = TemporaryPyFile(
242+
SOURCE_CODES["wrong_indented"], directory=tmp_dir
243+
)
244+
code_file = TemporaryPyFile(
245+
SOURCE_CODES["error_free"], directory=tmp_dir
246+
)
247+
with error_file as e_file, code_file as c_file:
248+
err = ('unindent does not match any outer indentation level'
249+
' (<tokenize>, line 3)\n')
250+
err = f"{e_file!r}: Indentation Error: {err}"
251+
self.verify_tabnanny_check(tmp_dir, err=err)
252+
253+
254+
class TestProcessTokens(TestCase):
255+
"""Testing `tabnanny.process_tokens()`."""
256+
257+
@mock.patch('tabnanny.NannyNag')
258+
def test_with_correct_code(self, MockNannyNag):
259+
"""A python source code without any whitespace related problems."""
260+
261+
with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
262+
with open(file_path) as f:
263+
tabnanny.process_tokens(tokenize.generate_tokens(f.readline))
264+
self.assertFalse(MockNannyNag.called)
265+
266+
def test_with_errored_codes_samples(self):
267+
"""A python source code with whitespace related sampled problems."""
268+
269+
# "tab_space_errored_1": executes block under type == tokenize.INDENT
270+
# at `tabnanny.process_tokens()`.
271+
# "tab space_errored_2": executes block under
272+
# `check_equal and type not in JUNK` condition at
273+
# `tabnanny.process_tokens()`.
274+
275+
for key in ["tab_space_errored_1", "tab_space_errored_2"]:
276+
with self.subTest(key=key):
277+
with TemporaryPyFile(SOURCE_CODES[key]) as file_path:
278+
with open(file_path) as f:
279+
tokens = tokenize.generate_tokens(f.readline)
280+
with self.assertRaises(tabnanny.NannyNag):
281+
tabnanny.process_tokens(tokens)
282+
283+
284+
class TestCommandLine(TestCase):
285+
"""Tests command line interface of `tabnanny`."""
286+
287+
def validate_cmd(self, *args, stdout="", stderr="", partial=False):
288+
"""Common function to assert the behaviour of command line interface."""
289+
_, out, err = script_helper.assert_python_ok('-m', 'tabnanny', *args)
290+
# Note: The `splitlines()` will solve the problem of CRLF(\r) added
291+
# by OS Windows.
292+
out = out.decode('ascii')
293+
err = err.decode('ascii')
294+
if partial:
295+
for std, output in ((stdout, out), (stderr, err)):
296+
_output = output.splitlines()
297+
for _std in std.splitlines():
298+
with self.subTest(std=_std, output=_output):
299+
self.assertIn(_std, _output)
300+
else:
301+
self.assertListEqual(out.splitlines(), stdout.splitlines())
302+
self.assertListEqual(err.splitlines(), stderr.splitlines())
303+
304+
def test_with_errored_file(self):
305+
"""Should displays error when errored python file is given."""
306+
with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path:
307+
stderr = f"{file_path!r}: Indentation Error: "
308+
stderr += ('unindent does not match any outer indentation level'
309+
' (<tokenize>, line 3)')
310+
self.validate_cmd(file_path, stderr=stderr)
311+
312+
def test_with_error_free_file(self):
313+
"""Should not display anything if python file is correctly indented."""
314+
with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path:
315+
self.validate_cmd(file_path)
316+
317+
def test_command_usage(self):
318+
"""Should display usage on no arguments."""
319+
path = findfile('tabnanny.py')
320+
stderr = f"Usage: {path} [-v] file_or_directory ..."
321+
self.validate_cmd(stderr=stderr)
322+
323+
def test_quiet_flag(self):
324+
"""Should display less when quite mode is on."""
325+
with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path:
326+
stdout = f"{file_path}\n"
327+
self.validate_cmd("-q", file_path, stdout=stdout)
328+
329+
def test_verbose_mode(self):
330+
"""Should display more error information if verbose mode is on."""
331+
with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
332+
stdout = textwrap.dedent(
333+
"offending line: '\\tprint(\"world\")\\n'"
334+
).strip()
335+
self.validate_cmd("-v", path, stdout=stdout, partial=True)
336+
337+
def test_double_verbose_mode(self):
338+
"""Should display detailed error information if double verbose is on."""
339+
with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path:
340+
stdout = textwrap.dedent(
341+
"offending line: '\\tprint(\"world\")\\n'"
342+
).strip()
343+
self.validate_cmd("-vv", path, stdout=stdout, partial=True)

0 commit comments

Comments
 (0)