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