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, 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' 237 err = (f"{path!r}: I/O Error: [Errno {errno.ENOENT}] " 238 f"{os.strerror(errno.ENOENT)}: {path!r}\n") 239 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) 347