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