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