1"""Testing `tabnanny` module. 2 3Glossary: 4 * errored : Whitespace related problems present in file. 5""" 6from unittest import TestCase, mock 7import errno 8import os 9import tabnanny 10import tokenize 11import tempfile 12import textwrap 13from test.support import (captured_stderr, captured_stdout, script_helper, 14 findfile) 15from test.support.os_helper import 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 self.assertRaises(SystemExit): 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 with self.assertRaises(SystemExit): 208 self.verify_tabnanny_check(file_path, err=err) 209 210 def test_when_tokenize_tokenerror(self): 211 """A python source code file eligible for raising 'tokenize.TokenError'.""" 212 with TemporaryPyFile(SOURCE_CODES["incomplete_expression"]) as file_path: 213 err = "('EOF in multi-line statement', (7, 0))\n" 214 err = f"{file_path!r}: Token Error: {err}" 215 with self.assertRaises(SystemExit): 216 self.verify_tabnanny_check(file_path, err=err) 217 218 def test_when_nannynag_error_verbose(self): 219 """A python source code file eligible for raising `tabnanny.NannyNag`. 220 221 Tests will assert `stdout` after activating `tabnanny.verbose` mode. 222 """ 223 with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path: 224 out = f"{file_path!r}: *** Line 3: trouble in tab city! ***\n" 225 out += "offending line: '\\tprint(\"world\")'\n" 226 out += "inconsistent use of tabs and spaces in indentation\n" 227 228 tabnanny.verbose = 1 229 self.verify_tabnanny_check(file_path, out=out) 230 231 def test_when_nannynag_error(self): 232 """A python source code file eligible for raising `tabnanny.NannyNag`.""" 233 with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path: 234 out = f"{file_path} 3 '\\tprint(\"world\")'\n" 235 self.verify_tabnanny_check(file_path, out=out) 236 237 def test_when_no_file(self): 238 """A python file which does not exist actually in system.""" 239 path = 'no_file.py' 240 err = (f"{path!r}: I/O Error: [Errno {errno.ENOENT}] " 241 f"{os.strerror(errno.ENOENT)}: {path!r}\n") 242 with self.assertRaises(SystemExit): 243 self.verify_tabnanny_check(path, err=err) 244 245 def test_errored_directory(self): 246 """Directory containing wrongly indented python source code files.""" 247 with tempfile.TemporaryDirectory() as tmp_dir: 248 error_file = TemporaryPyFile( 249 SOURCE_CODES["wrong_indented"], directory=tmp_dir 250 ) 251 code_file = TemporaryPyFile( 252 SOURCE_CODES["error_free"], directory=tmp_dir 253 ) 254 with error_file as e_file, code_file as c_file: 255 err = ('unindent does not match any outer indentation level' 256 ' (<tokenize>, line 3)\n') 257 err = f"{e_file!r}: Indentation Error: {err}" 258 with self.assertRaises(SystemExit): 259 self.verify_tabnanny_check(tmp_dir, err=err) 260 261 262class TestProcessTokens(TestCase): 263 """Testing `tabnanny.process_tokens()`.""" 264 265 @mock.patch('tabnanny.NannyNag') 266 def test_with_correct_code(self, MockNannyNag): 267 """A python source code without any whitespace related problems.""" 268 269 with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path: 270 with open(file_path) as f: 271 tabnanny.process_tokens(tokenize.generate_tokens(f.readline)) 272 self.assertFalse(MockNannyNag.called) 273 274 def test_with_errored_codes_samples(self): 275 """A python source code with whitespace related sampled problems.""" 276 277 # "tab_space_errored_1": executes block under type == tokenize.INDENT 278 # at `tabnanny.process_tokens()`. 279 # "tab space_errored_2": executes block under 280 # `check_equal and type not in JUNK` condition at 281 # `tabnanny.process_tokens()`. 282 283 for key in ["tab_space_errored_1", "tab_space_errored_2"]: 284 with self.subTest(key=key): 285 with TemporaryPyFile(SOURCE_CODES[key]) as file_path: 286 with open(file_path) as f: 287 tokens = tokenize.generate_tokens(f.readline) 288 with self.assertRaises(tabnanny.NannyNag): 289 tabnanny.process_tokens(tokens) 290 291 292class TestCommandLine(TestCase): 293 """Tests command line interface of `tabnanny`.""" 294 295 def validate_cmd(self, *args, stdout="", stderr="", partial=False, expect_failure=False): 296 """Common function to assert the behaviour of command line interface.""" 297 if expect_failure: 298 _, out, err = script_helper.assert_python_failure('-m', 'tabnanny', *args) 299 else: 300 _, out, err = script_helper.assert_python_ok('-m', 'tabnanny', *args) 301 # Note: The `splitlines()` will solve the problem of CRLF(\r) added 302 # by OS Windows. 303 out = os.fsdecode(out) 304 err = os.fsdecode(err) 305 if partial: 306 for std, output in ((stdout, out), (stderr, err)): 307 _output = output.splitlines() 308 for _std in std.splitlines(): 309 with self.subTest(std=_std, output=_output): 310 self.assertIn(_std, _output) 311 else: 312 self.assertListEqual(out.splitlines(), stdout.splitlines()) 313 self.assertListEqual(err.splitlines(), stderr.splitlines()) 314 315 def test_with_errored_file(self): 316 """Should displays error when errored python file is given.""" 317 with TemporaryPyFile(SOURCE_CODES["wrong_indented"]) as file_path: 318 stderr = f"{file_path!r}: Indentation Error: " 319 stderr += ('unindent does not match any outer indentation level' 320 ' (<string>, line 3)') 321 self.validate_cmd(file_path, stderr=stderr, expect_failure=True) 322 323 def test_with_error_free_file(self): 324 """Should not display anything if python file is correctly indented.""" 325 with TemporaryPyFile(SOURCE_CODES["error_free"]) as file_path: 326 self.validate_cmd(file_path) 327 328 def test_command_usage(self): 329 """Should display usage on no arguments.""" 330 path = findfile('tabnanny.py') 331 stderr = f"Usage: {path} [-v] file_or_directory ..." 332 self.validate_cmd(stderr=stderr, expect_failure=True) 333 334 def test_quiet_flag(self): 335 """Should display less when quite mode is on.""" 336 with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as file_path: 337 stdout = f"{file_path}\n" 338 self.validate_cmd("-q", file_path, stdout=stdout) 339 340 def test_verbose_mode(self): 341 """Should display more error information if verbose mode is on.""" 342 with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path: 343 stdout = textwrap.dedent( 344 "offending line: '\\tprint(\"world\")'" 345 ).strip() 346 self.validate_cmd("-v", path, stdout=stdout, partial=True) 347 348 def test_double_verbose_mode(self): 349 """Should display detailed error information if double verbose is on.""" 350 with TemporaryPyFile(SOURCE_CODES["nannynag_errored"]) as path: 351 stdout = textwrap.dedent( 352 "offending line: '\\tprint(\"world\")'" 353 ).strip() 354 self.validate_cmd("-vv", path, stdout=stdout, partial=True) 355