1# Copyright 2014 Altera Corporation. All Rights Reserved.
2# Copyright 2015 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"""A base class for unit tests using the :py:class:`pyfakefs` module.
17
18This class searches `sys.modules` for modules that import the `os`, `glob`,
19`shutil`, and `tempfile` modules.
20
21The `setUp()` method binds these modules to the corresponding fake
22modules from `pyfakefs`.  Further, the built in functions `file()` and
23`open()` are bound to fake functions.
24
25The `tearDownPyfakefs()` method returns the module bindings to their original
26state.
27
28It is expected that `setUp()` be invoked at the beginning of the derived
29class' `setUp()` method, and `tearDownPyfakefs()` be invoked at the end of the
30derived class' `tearDown()` method.
31
32During the test, everything uses the fake file system and modules.  This means
33that even in your test, you can use familiar functions like `open()` and
34`os.makedirs()` to manipulate the fake file system.
35
36This also means existing unit tests that use the real file system can be
37retrofitted to use `pyfakefs` by simply changing their base class from
38`:py:class`unittest.TestCase` to
39`:py:class`pyfakefs.fake_filesystem_unittest.TestCase`.
40"""
41
42import sys
43if sys.version_info < (2, 7):
44    import unittest2 as unittest
45else:
46    import unittest
47import doctest
48import inspect
49from pyfakefs import fake_filesystem
50from pyfakefs import fake_filesystem_glob
51from pyfakefs import fake_filesystem_shutil
52from pyfakefs import fake_tempfile
53if sys.version_info < (3,):
54    import __builtin__ as builtins
55else:
56    import builtins
57
58import mox3.stubout
59
60def load_doctests(loader, tests, ignore, module):
61    '''Load the doctest tests for the specified module into unittest.'''
62    _patcher = Patcher()
63    globs = _patcher.replaceGlobs(vars(module))
64    tests.addTests(doctest.DocTestSuite(module,
65                                        globs=globs,
66                                        setUp=_patcher.setUp,
67                                        tearDown=_patcher.tearDown))
68    return tests
69
70
71class TestCase(unittest.TestCase):
72    def __init__(self, methodName='runTest'):
73        super(TestCase, self).__init__(methodName)
74        self._stubber = Patcher()
75
76    @property
77    def fs(self):
78        return self._stubber.fs
79
80    @property
81    def patches(self):
82        return self._stubber.patches
83
84    def setUpPyfakefs(self):
85        '''Bind the file-related modules to the :py:class:`pyfakefs` fake file
86        system instead of the real file system.  Also bind the fake `file()` and
87        `open()` functions.
88
89        Invoke this at the beginning of the `setUp()` method in your unit test
90        class.
91        '''
92        self._stubber.setUp()
93        self.addCleanup(self._stubber.tearDown)
94
95
96    def tearDownPyfakefs(self):
97        ''':meth:`pyfakefs.fake_filesystem_unittest.setUpPyfakefs` registers the
98        tear down procedure using :py:meth:`unittest.TestCase.addCleanup`.  Thus this
99        method is deprecated, and remains just for backward compatibility.
100        '''
101        pass
102
103class Patcher(object):
104    '''
105    Instantiate a stub creator to bind and un-bind the file-related modules to
106    the :py:mod:`pyfakefs` fake modules.
107    '''
108    SKIPMODULES = set([None, fake_filesystem, fake_filesystem_glob,
109                      fake_filesystem_shutil, fake_tempfile, sys])
110    '''Stub nothing that is imported within these modules.
111    `sys` is included to prevent `sys.path` from being stubbed with the fake
112    `os.path`.
113    '''
114    assert None in SKIPMODULES, "sys.modules contains 'None' values; must skip them."
115
116    # To add py.test support per issue https://github.com/jmcgeheeiv/pyfakefs/issues/43,
117    # it appears that adding  'py', 'pytest', '_pytest' to SKIPNAMES will help
118    SKIPNAMES = set(['os', 'glob', 'path', 'shutil', 'tempfile'])
119
120    def __init__(self):
121        # Attributes set by _findModules()
122        self._osModules = None
123        self._globModules = None
124        self._pathModules = None
125        self._shutilModules = None
126        self._tempfileModules = None
127        self._findModules()
128        assert None not in vars(self).values(), \
129                "_findModules() missed the initialization of an instance variable"
130
131        # Attributes set by _refresh()
132        self._stubs = None
133        self.fs = None
134        self.fake_os = None
135        self.fake_glob = None
136        self.fake_path = None
137        self.fake_shutil = None
138        self.fake_tempfile_ = None
139        self.fake_open = None
140        # _isStale is set by tearDown(), reset by _refresh()
141        self._isStale = True
142        self._refresh()
143        assert None not in vars(self).values(), \
144                "_refresh() missed the initialization of an instance variable"
145        assert self._isStale == False, "_refresh() did not reset _isStale"
146
147    def _findModules(self):
148        '''Find and cache all modules that import file system modules.
149        Later, `setUp()` will stub these with the fake file system
150        modules.
151        '''
152        self._osModules = set()
153        self._globModules = set()
154        self._pathModules = set()
155        self._shutilModules = set()
156        self._tempfileModules = set()
157        for name, module in set(sys.modules.items()):
158            if (module in self.SKIPMODULES or
159                (not inspect.ismodule(module)) or
160                name.split('.')[0] in self.SKIPNAMES):
161                continue
162            if 'os' in module.__dict__:
163                self._osModules.add(module)
164            if 'glob' in module.__dict__:
165                self._globModules.add(module)
166            if 'path' in module.__dict__:
167                self._pathModules.add(module)
168            if 'shutil' in module.__dict__:
169                self._shutilModules.add(module)
170            if 'tempfile' in module.__dict__:
171                self._tempfileModules.add(module)
172
173    def _refresh(self):
174        '''Renew the fake file system and set the _isStale flag to `False`.'''
175        if self._stubs is not None:
176            self._stubs.SmartUnsetAll()
177        self._stubs = mox3.stubout.StubOutForTesting()
178
179        self.fs = fake_filesystem.FakeFilesystem()
180        self.fake_os = fake_filesystem.FakeOsModule(self.fs)
181        self.fake_glob = fake_filesystem_glob.FakeGlobModule(self.fs)
182        self.fake_path = self.fake_os.path
183        self.fake_shutil = fake_filesystem_shutil.FakeShutilModule(self.fs)
184        self.fake_tempfile_ = fake_tempfile.FakeTempfileModule(self.fs)
185        self.fake_open = fake_filesystem.FakeFileOpen(self.fs)
186
187        self._isStale = False
188
189    def setUp(self, doctester=None):
190        '''Bind the file-related modules to the :py:mod:`pyfakefs` fake
191        modules real ones.  Also bind the fake `file()` and `open()` functions.
192        '''
193        if self._isStale:
194            self._refresh()
195
196        if doctester is not None:
197            doctester.globs = self.replaceGlobs(doctester.globs)
198
199        if sys.version_info < (3,):
200            # file() was eliminated in Python3
201            self._stubs.SmartSet(builtins, 'file', self.fake_open)
202        self._stubs.SmartSet(builtins, 'open', self.fake_open)
203
204        for module in self._osModules:
205            self._stubs.SmartSet(module,  'os', self.fake_os)
206        for module in self._globModules:
207            self._stubs.SmartSet(module,  'glob', self.fake_glob)
208        for module in self._pathModules:
209            self._stubs.SmartSet(module,  'path', self.fake_path)
210        for module in self._shutilModules:
211            self._stubs.SmartSet(module,  'shutil', self.fake_shutil)
212        for module in self._tempfileModules:
213            self._stubs.SmartSet(module,  'tempfile', self.fake_tempfile_)
214
215    def replaceGlobs(self, globs_):
216        globs = globs_.copy()
217        if self._isStale:
218            self._refresh()
219        if 'os' in globs:
220            globs['os'] = fake_filesystem.FakeOsModule(self.fs)
221        if 'glob' in globs:
222            globs['glob'] = fake_filesystem_glob.FakeGlobModule(self.fs)
223        if 'path' in globs:
224            fake_os = globs['os'] if 'os' in globs \
225                else fake_filesystem.FakeOsModule(self.fs)
226            globs['path'] = fake_os.path
227        if 'shutil' in globs:
228            globs['shutil'] = fake_filesystem_shutil.FakeShutilModule(self.fs)
229        if 'tempfile' in globs:
230            globs['tempfile'] = fake_tempfile.FakeTempfileModule(self.fs)
231        return globs
232
233    def tearDown(self, doctester=None):
234        '''Clear the fake filesystem bindings created by `setUp()`.'''
235        self._isStale = True
236        self._stubs.SmartUnsetAll()
237