1"""Testing `tabnanny` module. 2 3Glossary: 4 * errored : Whitespace related problems present in file. 5""" 6from unittest import TestCase, mock 7from unittest import mock 8import errno 9import os 10import tabnanny 11import tokenize 12import tempfile 13import textwrap 14from test.support import (captured_stderr, captured_stdout, script_helper, 15 findfile) 16from test.support.os_helper import 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' 238 err = (f"{path!r}: I/O Error: [Errno {errno.ENOENT}] " 239 f"{os.strerror(errno.ENOENT)}: {path!r}\n") 240 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) 348