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