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