1# -*- coding: utf-8 -*- 2# Copyright 2015 Google Inc. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15"""Tests for yapf.file_resources.""" 16 17import codecs 18import contextlib 19import os 20import shutil 21import tempfile 22import unittest 23from io import BytesIO 24 25from yapf.yapflib import errors 26from yapf.yapflib import file_resources 27 28from yapftests import utils 29from yapftests import yapf_test_helper 30 31 32@contextlib.contextmanager 33def _restore_working_dir(): 34 curdir = os.getcwd() 35 try: 36 yield 37 finally: 38 os.chdir(curdir) 39 40 41@contextlib.contextmanager 42def _exists_mocked_in_module(module, mock_implementation): 43 unmocked_exists = getattr(module, 'exists') 44 setattr(module, 'exists', mock_implementation) 45 try: 46 yield 47 finally: 48 setattr(module, 'exists', unmocked_exists) 49 50 51class GetExcludePatternsForDir(yapf_test_helper.YAPFTest): 52 53 def setUp(self): # pylint: disable=g-missing-super-call 54 self.test_tmpdir = tempfile.mkdtemp() 55 56 def tearDown(self): # pylint: disable=g-missing-super-call 57 shutil.rmtree(self.test_tmpdir) 58 59 def test_get_exclude_file_patterns_from_yapfignore(self): 60 local_ignore_file = os.path.join(self.test_tmpdir, '.yapfignore') 61 ignore_patterns = ['temp/**/*.py', 'temp2/*.py'] 62 with open(local_ignore_file, 'w') as f: 63 f.writelines('\n'.join(ignore_patterns)) 64 65 self.assertEqual( 66 sorted(file_resources.GetExcludePatternsForDir(self.test_tmpdir)), 67 sorted(ignore_patterns)) 68 69 def test_get_exclude_file_patterns_from_yapfignore_with_wrong_syntax(self): 70 local_ignore_file = os.path.join(self.test_tmpdir, '.yapfignore') 71 ignore_patterns = ['temp/**/*.py', './wrong/syntax/*.py'] 72 with open(local_ignore_file, 'w') as f: 73 f.writelines('\n'.join(ignore_patterns)) 74 75 with self.assertRaises(errors.YapfError): 76 file_resources.GetExcludePatternsForDir(self.test_tmpdir) 77 78 def test_get_exclude_file_patterns_from_pyproject(self): 79 local_ignore_file = os.path.join(self.test_tmpdir, 'pyproject.toml') 80 ignore_patterns = ['temp/**/*.py', 'temp2/*.py'] 81 with open(local_ignore_file, 'w') as f: 82 f.write('[tool.yapfignore]\n') 83 f.write('ignore_patterns=[') 84 f.writelines('\n,'.join(['"{}"'.format(p) for p in ignore_patterns])) 85 f.write(']') 86 87 self.assertEqual( 88 sorted(file_resources.GetExcludePatternsForDir(self.test_tmpdir)), 89 sorted(ignore_patterns)) 90 91 def test_get_exclude_file_patterns_from_pyproject_no_ignore_section(self): 92 local_ignore_file = os.path.join(self.test_tmpdir, 'pyproject.toml') 93 ignore_patterns = [] 94 open(local_ignore_file, 'w').close() 95 96 self.assertEqual( 97 sorted(file_resources.GetExcludePatternsForDir(self.test_tmpdir)), 98 sorted(ignore_patterns)) 99 100 def test_get_exclude_file_patterns_from_pyproject_ignore_section_empty(self): 101 local_ignore_file = os.path.join(self.test_tmpdir, 'pyproject.toml') 102 ignore_patterns = [] 103 with open(local_ignore_file, 'w') as f: 104 f.write('[tool.yapfignore]\n') 105 106 self.assertEqual( 107 sorted(file_resources.GetExcludePatternsForDir(self.test_tmpdir)), 108 sorted(ignore_patterns)) 109 110 def test_get_exclude_file_patterns_with_no_config_files(self): 111 ignore_patterns = [] 112 113 self.assertEqual( 114 sorted(file_resources.GetExcludePatternsForDir(self.test_tmpdir)), 115 sorted(ignore_patterns)) 116 117 118class GetDefaultStyleForDirTest(yapf_test_helper.YAPFTest): 119 120 def setUp(self): # pylint: disable=g-missing-super-call 121 self.test_tmpdir = tempfile.mkdtemp() 122 123 def tearDown(self): # pylint: disable=g-missing-super-call 124 shutil.rmtree(self.test_tmpdir) 125 126 def test_no_local_style(self): 127 test_file = os.path.join(self.test_tmpdir, 'file.py') 128 style_name = file_resources.GetDefaultStyleForDir(test_file) 129 self.assertEqual(style_name, 'pep8') 130 131 def test_no_local_style_custom_default(self): 132 test_file = os.path.join(self.test_tmpdir, 'file.py') 133 style_name = file_resources.GetDefaultStyleForDir( 134 test_file, default_style='custom-default') 135 self.assertEqual(style_name, 'custom-default') 136 137 def test_with_local_style(self): 138 # Create an empty .style.yapf file in test_tmpdir 139 style_file = os.path.join(self.test_tmpdir, '.style.yapf') 140 open(style_file, 'w').close() 141 142 test_filename = os.path.join(self.test_tmpdir, 'file.py') 143 self.assertEqual(style_file, 144 file_resources.GetDefaultStyleForDir(test_filename)) 145 146 test_filename = os.path.join(self.test_tmpdir, 'dir1', 'file.py') 147 self.assertEqual(style_file, 148 file_resources.GetDefaultStyleForDir(test_filename)) 149 150 def test_setup_config(self): 151 # An empty setup.cfg file should not be used 152 setup_config = os.path.join(self.test_tmpdir, 'setup.cfg') 153 open(setup_config, 'w').close() 154 155 test_dir = os.path.join(self.test_tmpdir, 'dir1') 156 style_name = file_resources.GetDefaultStyleForDir(test_dir) 157 self.assertEqual(style_name, 'pep8') 158 159 # One with a '[yapf]' section should be used 160 with open(setup_config, 'w') as f: 161 f.write('[yapf]\n') 162 self.assertEqual(setup_config, 163 file_resources.GetDefaultStyleForDir(test_dir)) 164 165 def test_pyproject_toml(self): 166 pyproject_toml = os.path.join(self.test_tmpdir, 'pyproject.toml') 167 open(pyproject_toml, 'w').close() 168 169 test_dir = os.path.join(self.test_tmpdir, 'dir1') 170 style_name = file_resources.GetDefaultStyleForDir(test_dir) 171 self.assertEqual(style_name, 'pep8') 172 173 # One with a '[tool.yapf]' section should be used 174 with open(pyproject_toml, 'w') as f: 175 f.write('[tool.yapf]\n') 176 self.assertEqual(pyproject_toml, 177 file_resources.GetDefaultStyleForDir(test_dir)) 178 179 def test_local_style_at_root(self): 180 # Test behavior of files located on the root, and under root. 181 rootdir = os.path.abspath(os.path.sep) 182 test_dir_at_root = os.path.join(rootdir, 'dir1') 183 test_dir_under_root = os.path.join(rootdir, 'dir1', 'dir2') 184 185 # Fake placing only a style file at the root by mocking `os.path.exists`. 186 style_file = os.path.join(rootdir, '.style.yapf') 187 188 def mock_exists_implementation(path): 189 return path == style_file 190 191 with _exists_mocked_in_module(file_resources.os.path, 192 mock_exists_implementation): 193 # Both files should find the style file at the root. 194 default_style_at_root = file_resources.GetDefaultStyleForDir( 195 test_dir_at_root) 196 self.assertEqual(style_file, default_style_at_root) 197 default_style_under_root = file_resources.GetDefaultStyleForDir( 198 test_dir_under_root) 199 self.assertEqual(style_file, default_style_under_root) 200 201 202def _touch_files(filenames): 203 for name in filenames: 204 open(name, 'a').close() 205 206 207class GetCommandLineFilesTest(yapf_test_helper.YAPFTest): 208 209 def setUp(self): # pylint: disable=g-missing-super-call 210 self.test_tmpdir = tempfile.mkdtemp() 211 self.old_dir = os.getcwd() 212 213 def tearDown(self): # pylint: disable=g-missing-super-call 214 os.chdir(self.old_dir) 215 shutil.rmtree(self.test_tmpdir) 216 217 def _make_test_dir(self, name): 218 fullpath = os.path.normpath(os.path.join(self.test_tmpdir, name)) 219 os.makedirs(fullpath) 220 return fullpath 221 222 def test_find_files_not_dirs(self): 223 tdir1 = self._make_test_dir('test1') 224 tdir2 = self._make_test_dir('test2') 225 file1 = os.path.join(tdir1, 'testfile1.py') 226 file2 = os.path.join(tdir2, 'testfile2.py') 227 _touch_files([file1, file2]) 228 229 self.assertEqual( 230 file_resources.GetCommandLineFiles([file1, file2], 231 recursive=False, 232 exclude=None), [file1, file2]) 233 self.assertEqual( 234 file_resources.GetCommandLineFiles([file1, file2], 235 recursive=True, 236 exclude=None), [file1, file2]) 237 238 def test_nonrecursive_find_in_dir(self): 239 tdir1 = self._make_test_dir('test1') 240 tdir2 = self._make_test_dir('test1/foo') 241 file1 = os.path.join(tdir1, 'testfile1.py') 242 file2 = os.path.join(tdir2, 'testfile2.py') 243 _touch_files([file1, file2]) 244 245 self.assertRaises( 246 errors.YapfError, 247 file_resources.GetCommandLineFiles, 248 command_line_file_list=[tdir1], 249 recursive=False, 250 exclude=None) 251 252 def test_recursive_find_in_dir(self): 253 tdir1 = self._make_test_dir('test1') 254 tdir2 = self._make_test_dir('test2/testinner/') 255 tdir3 = self._make_test_dir('test3/foo/bar/bas/xxx') 256 files = [ 257 os.path.join(tdir1, 'testfile1.py'), 258 os.path.join(tdir2, 'testfile2.py'), 259 os.path.join(tdir3, 'testfile3.py'), 260 ] 261 _touch_files(files) 262 263 self.assertEqual( 264 sorted( 265 file_resources.GetCommandLineFiles([self.test_tmpdir], 266 recursive=True, 267 exclude=None)), sorted(files)) 268 269 def test_recursive_find_in_dir_with_exclude(self): 270 tdir1 = self._make_test_dir('test1') 271 tdir2 = self._make_test_dir('test2/testinner/') 272 tdir3 = self._make_test_dir('test3/foo/bar/bas/xxx') 273 files = [ 274 os.path.join(tdir1, 'testfile1.py'), 275 os.path.join(tdir2, 'testfile2.py'), 276 os.path.join(tdir3, 'testfile3.py'), 277 ] 278 _touch_files(files) 279 280 self.assertEqual( 281 sorted( 282 file_resources.GetCommandLineFiles([self.test_tmpdir], 283 recursive=True, 284 exclude=['*test*3.py'])), 285 sorted([ 286 os.path.join(tdir1, 'testfile1.py'), 287 os.path.join(tdir2, 'testfile2.py'), 288 ])) 289 290 def test_find_with_excluded_hidden_dirs(self): 291 tdir1 = self._make_test_dir('.test1') 292 tdir2 = self._make_test_dir('test_2') 293 tdir3 = self._make_test_dir('test.3') 294 files = [ 295 os.path.join(tdir1, 'testfile1.py'), 296 os.path.join(tdir2, 'testfile2.py'), 297 os.path.join(tdir3, 'testfile3.py'), 298 ] 299 _touch_files(files) 300 301 actual = file_resources.GetCommandLineFiles([self.test_tmpdir], 302 recursive=True, 303 exclude=['*.test1*']) 304 305 self.assertEqual( 306 sorted(actual), 307 sorted([ 308 os.path.join(tdir2, 'testfile2.py'), 309 os.path.join(tdir3, 'testfile3.py'), 310 ])) 311 312 def test_find_with_excluded_hidden_dirs_relative(self): 313 """Test find with excluded hidden dirs. 314 315 A regression test against a specific case where a hidden directory (one 316 beginning with a period) is being excluded, but it is also an immediate 317 child of the current directory which has been specified in a relative 318 manner. 319 320 At its core, the bug has to do with overzealous stripping of "./foo" so that 321 it removes too much from "./.foo" . 322 """ 323 tdir1 = self._make_test_dir('.test1') 324 tdir2 = self._make_test_dir('test_2') 325 tdir3 = self._make_test_dir('test.3') 326 files = [ 327 os.path.join(tdir1, 'testfile1.py'), 328 os.path.join(tdir2, 'testfile2.py'), 329 os.path.join(tdir3, 'testfile3.py'), 330 ] 331 _touch_files(files) 332 333 # We must temporarily change the current directory, so that we test against 334 # patterns like ./.test1/file instead of /tmp/foo/.test1/file 335 with _restore_working_dir(): 336 337 os.chdir(self.test_tmpdir) 338 actual = file_resources.GetCommandLineFiles( 339 [os.path.relpath(self.test_tmpdir)], 340 recursive=True, 341 exclude=['*.test1*']) 342 343 self.assertEqual( 344 sorted(actual), 345 sorted([ 346 os.path.join( 347 os.path.relpath(self.test_tmpdir), os.path.basename(tdir2), 348 'testfile2.py'), 349 os.path.join( 350 os.path.relpath(self.test_tmpdir), os.path.basename(tdir3), 351 'testfile3.py'), 352 ])) 353 354 def test_find_with_excluded_dirs(self): 355 tdir1 = self._make_test_dir('test1') 356 tdir2 = self._make_test_dir('test2/testinner/') 357 tdir3 = self._make_test_dir('test3/foo/bar/bas/xxx') 358 files = [ 359 os.path.join(tdir1, 'testfile1.py'), 360 os.path.join(tdir2, 'testfile2.py'), 361 os.path.join(tdir3, 'testfile3.py'), 362 ] 363 _touch_files(files) 364 365 os.chdir(self.test_tmpdir) 366 367 found = sorted( 368 file_resources.GetCommandLineFiles(['test1', 'test2', 'test3'], 369 recursive=True, 370 exclude=[ 371 'test1', 372 'test2/testinner/', 373 ])) 374 375 self.assertEqual( 376 found, ['test3/foo/bar/bas/xxx/testfile3.py'.replace('/', os.path.sep)]) 377 378 found = sorted( 379 file_resources.GetCommandLineFiles(['.'], 380 recursive=True, 381 exclude=[ 382 'test1', 383 'test3', 384 ])) 385 386 self.assertEqual( 387 found, ['./test2/testinner/testfile2.py'.replace('/', os.path.sep)]) 388 389 def test_find_with_excluded_current_dir(self): 390 with self.assertRaises(errors.YapfError): 391 file_resources.GetCommandLineFiles([], False, exclude=['./z']) 392 393 394class IsPythonFileTest(yapf_test_helper.YAPFTest): 395 396 def setUp(self): # pylint: disable=g-missing-super-call 397 self.test_tmpdir = tempfile.mkdtemp() 398 399 def tearDown(self): # pylint: disable=g-missing-super-call 400 shutil.rmtree(self.test_tmpdir) 401 402 def test_with_py_extension(self): 403 file1 = os.path.join(self.test_tmpdir, 'testfile1.py') 404 self.assertTrue(file_resources.IsPythonFile(file1)) 405 406 def test_empty_without_py_extension(self): 407 file1 = os.path.join(self.test_tmpdir, 'testfile1') 408 self.assertFalse(file_resources.IsPythonFile(file1)) 409 file2 = os.path.join(self.test_tmpdir, 'testfile1.rb') 410 self.assertFalse(file_resources.IsPythonFile(file2)) 411 412 def test_python_shebang(self): 413 file1 = os.path.join(self.test_tmpdir, 'testfile1') 414 with open(file1, 'w') as f: 415 f.write('#!/usr/bin/python\n') 416 self.assertTrue(file_resources.IsPythonFile(file1)) 417 418 file2 = os.path.join(self.test_tmpdir, 'testfile2.run') 419 with open(file2, 'w') as f: 420 f.write('#! /bin/python2\n') 421 self.assertTrue(file_resources.IsPythonFile(file1)) 422 423 def test_with_latin_encoding(self): 424 file1 = os.path.join(self.test_tmpdir, 'testfile1') 425 with codecs.open(file1, mode='w', encoding='latin-1') as f: 426 f.write('#! /bin/python2\n') 427 self.assertTrue(file_resources.IsPythonFile(file1)) 428 429 def test_with_invalid_encoding(self): 430 file1 = os.path.join(self.test_tmpdir, 'testfile1') 431 with open(file1, 'w') as f: 432 f.write('#! /bin/python2\n') 433 f.write('# -*- coding: iso-3-14159 -*-\n') 434 self.assertFalse(file_resources.IsPythonFile(file1)) 435 436 437class IsIgnoredTest(yapf_test_helper.YAPFTest): 438 439 def test_root_path(self): 440 self.assertTrue(file_resources.IsIgnored('media', ['media'])) 441 self.assertFalse(file_resources.IsIgnored('media', ['media/*'])) 442 443 def test_sub_path(self): 444 self.assertTrue(file_resources.IsIgnored('media/a', ['*/a'])) 445 self.assertTrue(file_resources.IsIgnored('media/b', ['media/*'])) 446 self.assertTrue(file_resources.IsIgnored('media/b/c', ['*/*/c'])) 447 448 def test_trailing_slash(self): 449 self.assertTrue(file_resources.IsIgnored('z', ['z'])) 450 self.assertTrue(file_resources.IsIgnored('z', ['z' + os.path.sep])) 451 452 453class BufferedByteStream(object): 454 455 def __init__(self): 456 self.stream = BytesIO() 457 458 def getvalue(self): # pylint: disable=invalid-name 459 return self.stream.getvalue().decode('utf-8') 460 461 @property 462 def buffer(self): 463 return self.stream 464 465 466class WriteReformattedCodeTest(yapf_test_helper.YAPFTest): 467 468 @classmethod 469 def setUpClass(cls): # pylint: disable=g-missing-super-call 470 cls.test_tmpdir = tempfile.mkdtemp() 471 472 @classmethod 473 def tearDownClass(cls): # pylint: disable=g-missing-super-call 474 shutil.rmtree(cls.test_tmpdir) 475 476 def test_write_to_file(self): 477 s = 'foobar\n' 478 with utils.NamedTempFile(dirname=self.test_tmpdir) as (f, fname): 479 file_resources.WriteReformattedCode( 480 fname, s, in_place=True, encoding='utf-8') 481 f.flush() 482 483 with open(fname) as f2: 484 self.assertEqual(f2.read(), s) 485 486 def test_write_to_stdout(self): 487 s = 'foobar' 488 stream = BufferedByteStream() 489 with utils.stdout_redirector(stream): 490 file_resources.WriteReformattedCode( 491 None, s, in_place=False, encoding='utf-8') 492 self.assertEqual(stream.getvalue(), s) 493 494 def test_write_encoded_to_stdout(self): 495 s = '\ufeff# -*- coding: utf-8 -*-\nresult = "passed"\n' # pylint: disable=anomalous-unicode-escape-in-string # noqa 496 stream = BufferedByteStream() 497 with utils.stdout_redirector(stream): 498 file_resources.WriteReformattedCode( 499 None, s, in_place=False, encoding='utf-8') 500 self.assertEqual(stream.getvalue(), s) 501 502 503class LineEndingTest(yapf_test_helper.YAPFTest): 504 505 def test_line_ending_linefeed(self): 506 lines = ['spam\n', 'spam\n'] 507 actual = file_resources.LineEnding(lines) 508 self.assertEqual(actual, '\n') 509 510 def test_line_ending_carriage_return(self): 511 lines = ['spam\r', 'spam\r'] 512 actual = file_resources.LineEnding(lines) 513 self.assertEqual(actual, '\r') 514 515 def test_line_ending_combo(self): 516 lines = ['spam\r\n', 'spam\r\n'] 517 actual = file_resources.LineEnding(lines) 518 self.assertEqual(actual, '\r\n') 519 520 def test_line_ending_weighted(self): 521 lines = [ 522 'spam\n', 523 'spam\n', 524 'spam\r', 525 'spam\r\n', 526 ] 527 actual = file_resources.LineEnding(lines) 528 self.assertEqual(actual, '\n') 529 530 def test_line_ending_empty(self): 531 lines = [] 532 actual = file_resources.LineEnding(lines) 533 self.assertEqual(actual, '\n') 534 535 def test_line_ending_no_newline(self): 536 lines = ['spam'] 537 actual = file_resources.LineEnding(lines) 538 self.assertEqual(actual, '\n') 539 540 def test_line_ending_tie(self): 541 lines = [ 542 'spam\n', 543 'spam\n', 544 'spam\r\n', 545 'spam\r\n', 546 ] 547 actual = file_resources.LineEnding(lines) 548 self.assertEqual(actual, '\n') 549 550 551if __name__ == '__main__': 552 unittest.main() 553