• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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