blob: ec887361730b96e3874781bf838c5094e61a41bd [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
8import tabnanny
9import tokenize
10import tempfile
11import textwrap
12from test.support import (captured_stderr, captured_stdout, script_helper,
13 findfile, unlink)
14
15
16SOURCE_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
61class 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
80class 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
97class 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
116class 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
143class 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
254class 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
284class 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)