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