1# Copyright 2009 Google Inc. All Rights Reserved. 2# Copyright 2015-2017 John McGehee 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 16"""Common helper classes used in tests, or as test class base.""" 17import os 18import platform 19import shutil 20import stat 21import sys 22import tempfile 23import unittest 24from contextlib import contextmanager 25from unittest import mock 26 27from pyfakefs import fake_filesystem 28from pyfakefs.helpers import is_byte_string, to_string 29 30 31class DummyTime: 32 """Mock replacement for time.time. Increases returned time on access.""" 33 34 def __init__(self, curr_time, increment): 35 self.current_time = curr_time 36 self.increment = increment 37 38 def __call__(self, *args, **kwargs): 39 current_time = self.current_time 40 self.current_time += self.increment 41 return current_time 42 43 44class DummyMock: 45 def start(self): 46 pass 47 48 def stop(self): 49 pass 50 51 def __enter__(self): 52 return self 53 54 def __exit__(self, exc_type, exc_val, exc_tb): 55 pass 56 57 58def time_mock(start=200, step=20): 59 return mock.patch('pyfakefs.fake_filesystem.now', 60 DummyTime(start, step)) 61 62 63class TestCase(unittest.TestCase): 64 """Test base class with some convenience methods and attributes""" 65 is_windows = sys.platform == 'win32' 66 is_cygwin = sys.platform == 'cygwin' 67 is_macos = sys.platform == 'darwin' 68 symlinks_can_be_tested = None 69 70 def assert_mode_equal(self, expected, actual): 71 return self.assertEqual(stat.S_IMODE(expected), stat.S_IMODE(actual)) 72 73 @contextmanager 74 def raises_os_error(self, subtype): 75 try: 76 yield 77 self.fail('No exception was raised, OSError expected') 78 except OSError as exc: 79 if isinstance(subtype, list): 80 self.assertIn(exc.errno, subtype) 81 else: 82 self.assertEqual(subtype, exc.errno) 83 84 def assert_raises_os_error(self, subtype, expression, *args, **kwargs): 85 """Asserts that a specific subtype of OSError is raised.""" 86 try: 87 expression(*args, **kwargs) 88 self.fail('No exception was raised, OSError expected') 89 except OSError as exc: 90 if isinstance(subtype, list): 91 self.assertIn(exc.errno, subtype) 92 else: 93 self.assertEqual(subtype, exc.errno) 94 95 96class RealFsTestMixin: 97 """Test mixin to allow tests to run both in the fake filesystem and in the 98 real filesystem. 99 To run tests in the real filesystem, a new test class can be derived from 100 the test class testing the fake filesystem which overwrites 101 `use_real_fs()` to return `True`. 102 All tests in the real file system operate inside the local temp path. 103 104 In order to make a test able to run in the real FS, it must not use the 105 fake filesystem functions directly. For access to `os` and `open`, 106 the respective attributes must be used, which point either to the native 107 or to the fake modules. A few convenience methods allow to compose 108 paths, create files and directories. 109 """ 110 111 def __init__(self): 112 self.filesystem = None 113 self.open = open 114 self.os = os 115 self.base_path = None 116 117 def setUp(self): 118 if not os.environ.get('TEST_REAL_FS'): 119 self.skip_real_fs() 120 if self.use_real_fs(): 121 self.base_path = tempfile.mkdtemp() 122 123 def tearDown(self): 124 if self.use_real_fs(): 125 self.os.chdir(os.path.dirname(self.base_path)) 126 shutil.rmtree(self.base_path, ignore_errors=True) 127 os.chdir(self.cwd) 128 129 @property 130 def is_windows_fs(self): 131 return TestCase.is_windows 132 133 def set_windows_fs(self, value): 134 if self.filesystem is not None: 135 self.filesystem.is_windows_fs = value 136 if value: 137 self.filesystem.is_macos = False 138 self.create_basepath() 139 140 @property 141 def is_macos(self): 142 return TestCase.is_macos 143 144 @property 145 def is_pypy(self): 146 return platform.python_implementation() == 'PyPy' 147 148 def use_real_fs(self): 149 """Return True if the real file system shall be tested.""" 150 return False 151 152 def path_separator(self): 153 """Can be overwritten to use a specific separator in the 154 fake filesystem.""" 155 if self.use_real_fs(): 156 return os.path.sep 157 return '/' 158 159 def check_windows_only(self): 160 """If called at test start, the real FS test is executed only under 161 Windows, and the fake filesystem test emulates a Windows system. 162 """ 163 if self.use_real_fs(): 164 if not TestCase.is_windows: 165 raise unittest.SkipTest( 166 'Testing Windows specific functionality') 167 else: 168 self.set_windows_fs(True) 169 170 def check_linux_only(self): 171 """If called at test start, the real FS test is executed only under 172 Linux, and the fake filesystem test emulates a Linux system. 173 """ 174 if self.use_real_fs(): 175 if TestCase.is_macos or TestCase.is_windows: 176 raise unittest.SkipTest( 177 'Testing Linux specific functionality') 178 else: 179 self.set_windows_fs(False) 180 self.filesystem.is_macos = False 181 182 def check_macos_only(self): 183 """If called at test start, the real FS test is executed only under 184 MacOS, and the fake filesystem test emulates a MacOS system. 185 """ 186 if self.use_real_fs(): 187 if not TestCase.is_macos: 188 raise unittest.SkipTest( 189 'Testing MacOS specific functionality') 190 else: 191 self.set_windows_fs(False) 192 self.filesystem.is_macos = True 193 194 def check_linux_and_windows(self): 195 """If called at test start, the real FS test is executed only under 196 Linux and Windows, and the fake filesystem test emulates a Linux 197 system under MacOS. 198 """ 199 if self.use_real_fs(): 200 if TestCase.is_macos: 201 raise unittest.SkipTest( 202 'Testing non-MacOs functionality') 203 else: 204 self.filesystem.is_macos = False 205 206 def check_case_insensitive_fs(self): 207 """If called at test start, the real FS test is executed only in a 208 case-insensitive FS (e.g. Windows or MacOS), and the fake filesystem 209 test emulates a case-insensitive FS under the running OS. 210 """ 211 if self.use_real_fs(): 212 if not TestCase.is_macos and not TestCase.is_windows: 213 raise unittest.SkipTest( 214 'Testing case insensitive specific functionality') 215 else: 216 self.filesystem.is_case_sensitive = False 217 218 def check_case_sensitive_fs(self): 219 """If called at test start, the real FS test is executed only in a 220 case-sensitive FS (e.g. under Linux), and the fake file system test 221 emulates a case-sensitive FS under the running OS. 222 """ 223 if self.use_real_fs(): 224 if TestCase.is_macos or TestCase.is_windows: 225 raise unittest.SkipTest( 226 'Testing case sensitive specific functionality') 227 else: 228 self.filesystem.is_case_sensitive = True 229 230 def check_posix_only(self): 231 """If called at test start, the real FS test is executed only under 232 Linux and MacOS, and the fake filesystem test emulates a Linux 233 system under Windows. 234 """ 235 if self.use_real_fs(): 236 if TestCase.is_windows: 237 raise unittest.SkipTest( 238 'Testing Posix specific functionality') 239 else: 240 self.set_windows_fs(False) 241 242 def skip_real_fs(self): 243 """If called at test start, no real FS test is executed.""" 244 if self.use_real_fs(): 245 raise unittest.SkipTest('Only tests fake FS') 246 247 def skip_real_fs_failure(self, skip_windows=True, skip_posix=True, 248 skip_macos=True, skip_linux=True): 249 """If called at test start, no real FS test is executed for the given 250 conditions. This is used to mark tests that do not pass correctly under 251 certain systems and shall eventually be fixed. 252 """ 253 if True: 254 if (self.use_real_fs() and 255 (TestCase.is_windows and skip_windows or 256 not TestCase.is_windows 257 and skip_macos and skip_linux or 258 TestCase.is_macos and skip_macos or 259 not TestCase.is_windows and 260 not TestCase.is_macos and skip_linux or 261 not TestCase.is_windows and skip_posix)): 262 raise unittest.SkipTest( 263 'Skipping because FakeFS does not match real FS') 264 265 def symlink_can_be_tested(self, force_real_fs=False): 266 """Used to check if symlinks and hard links can be tested under 267 Windows. All tests are skipped under Windows for Python versions 268 not supporting links, and real tests are skipped if running without 269 administrator rights. 270 """ 271 if (not TestCase.is_windows or 272 (not force_real_fs and not self.use_real_fs())): 273 return True 274 if TestCase.symlinks_can_be_tested is None: 275 if force_real_fs: 276 self.base_path = tempfile.mkdtemp() 277 link_path = self.make_path('link') 278 try: 279 self.os.symlink(self.base_path, link_path) 280 TestCase.symlinks_can_be_tested = True 281 self.os.remove(link_path) 282 except (OSError, NotImplementedError): 283 TestCase.symlinks_can_be_tested = False 284 if force_real_fs: 285 self.base_path = None 286 return TestCase.symlinks_can_be_tested 287 288 def skip_if_symlink_not_supported(self, force_real_fs=False): 289 """If called at test start, tests are skipped if symlinks are not 290 supported.""" 291 if not self.symlink_can_be_tested(force_real_fs): 292 raise unittest.SkipTest( 293 'Symlinks under Windows need admin privileges') 294 295 def make_path(self, *args): 296 """Create a path with the given component(s). A base path is prepended 297 to the path which represents a temporary directory in the real FS, 298 and a fixed path in the fake filesystem. 299 Always use to compose absolute paths for tests also running in the 300 real FS. 301 """ 302 if isinstance(args[0], (list, tuple)): 303 path = self.base_path 304 for arg in args[0]: 305 path = self.os.path.join(path, to_string(arg)) 306 return path 307 args = [to_string(arg) for arg in args] 308 return self.os.path.join(self.base_path, *args) 309 310 def create_dir(self, dir_path): 311 """Create the directory at `dir_path`, including subdirectories. 312 `dir_path` shall be composed using `make_path()`. 313 """ 314 existing_path = dir_path 315 components = [] 316 while existing_path and not self.os.path.exists(existing_path): 317 existing_path, component = self.os.path.split(existing_path) 318 if not component and existing_path: 319 # existing path is a drive or UNC root 320 if not self.os.path.exists(existing_path): 321 self.filesystem.add_mount_point(existing_path) 322 break 323 components.insert(0, component) 324 for component in components: 325 existing_path = self.os.path.join(existing_path, component) 326 self.os.mkdir(existing_path) 327 self.os.chmod(existing_path, 0o777) 328 329 def create_file(self, file_path, contents=None, encoding=None, perm=0o666): 330 """Create the given file at `file_path` with optional contents, 331 including subdirectories. `file_path` shall be composed using 332 `make_path()`. 333 """ 334 self.create_dir(self.os.path.dirname(file_path)) 335 mode = ('wb' if encoding is not None or is_byte_string(contents) 336 else 'w') 337 338 if encoding is not None and contents is not None: 339 contents = contents.encode(encoding) 340 with self.open(file_path, mode) as f: 341 if contents is not None: 342 f.write(contents) 343 self.os.chmod(file_path, perm) 344 345 def create_symlink(self, link_path, target_path): 346 """Create the path at `link_path`, and a symlink to this path at 347 `target_path`. `link_path` shall be composed using `make_path()`. 348 """ 349 self.create_dir(self.os.path.dirname(link_path)) 350 self.os.symlink(target_path, link_path) 351 352 def check_contents(self, file_path, contents): 353 """Compare `contents` with the contents of the file at `file_path`. 354 Asserts equality. 355 """ 356 mode = 'rb' if is_byte_string(contents) else 'r' 357 with self.open(file_path, mode) as f: 358 self.assertEqual(contents, f.read()) 359 360 def create_basepath(self): 361 """Create the path used as base path in `make_path`.""" 362 if self.filesystem is not None: 363 old_base_path = self.base_path 364 self.base_path = self.filesystem.path_separator + 'basepath' 365 if self.is_windows_fs: 366 self.base_path = 'C:' + self.base_path 367 if old_base_path != self.base_path: 368 if old_base_path is not None: 369 self.filesystem.reset() 370 if not self.filesystem.exists(self.base_path): 371 self.filesystem.create_dir(self.base_path) 372 if old_base_path is not None: 373 self.setUpFileSystem() 374 375 def assert_equal_paths(self, actual, expected): 376 if self.is_windows: 377 actual = str(actual).replace('\\\\?\\', '') 378 expected = str(expected).replace('\\\\?\\', '') 379 if os.name == 'nt' and self.use_real_fs(): 380 # work around a problem that the user name, but not the full 381 # path is shown as the short name 382 self.assertEqual(self.path_with_short_username(actual), 383 self.path_with_short_username(expected)) 384 else: 385 self.assertEqual(actual, expected) 386 elif self.is_macos: 387 self.assertEqual(str(actual).replace('/private/var/', '/var/'), 388 str(expected).replace('/private/var/', '/var/')) 389 else: 390 self.assertEqual(actual, expected) 391 392 @staticmethod 393 def path_with_short_username(path): 394 components = path.split(os.sep) 395 if len(components) >= 3: 396 components[2] = components[2][:6].upper() + '~1' 397 return os.sep.join(components) 398 399 def mock_time(self, start=200, step=20): 400 if not self.use_real_fs(): 401 return mock.patch('pyfakefs.fake_filesystem.now', 402 DummyTime(start, step)) 403 return DummyMock() 404 405 406class RealFsTestCase(TestCase, RealFsTestMixin): 407 """Can be used as base class for tests also running in the real 408 file system.""" 409 410 def __init__(self, methodName='runTest'): 411 TestCase.__init__(self, methodName) 412 RealFsTestMixin.__init__(self) 413 414 def setUp(self): 415 RealFsTestMixin.setUp(self) 416 self.cwd = os.getcwd() 417 if not self.use_real_fs(): 418 self.filesystem = fake_filesystem.FakeFilesystem( 419 path_separator=self.path_separator()) 420 self.open = fake_filesystem.FakeFileOpen(self.filesystem) 421 self.os = fake_filesystem.FakeOsModule(self.filesystem) 422 self.create_basepath() 423 424 self.setUpFileSystem() 425 426 def tearDown(self): 427 RealFsTestMixin.tearDown(self) 428 429 def setUpFileSystem(self): 430 pass 431 432 @property 433 def is_windows_fs(self): 434 if self.use_real_fs(): 435 return self.is_windows 436 return self.filesystem.is_windows_fs 437 438 @property 439 def is_macos(self): 440 if self.use_real_fs(): 441 return TestCase.is_macos 442 return self.filesystem.is_macos 443