• 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
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