comparison tests/memfs.py @ 411:e7b865f8f335

bake: Enable multiprocess baking. Baking is now done by running a worker per CPU, and sending jobs to them. This changes several things across the codebase: * Ability to not cache things related to pages other than the 'main' page (i.e. the page at the bottom of the execution stack). * Decouple the baking process from the bake records, so only the main process keeps track (and modifies) the bake record. * Remove the need for 'batch page getters' and loading a page directly from the page factories. There are various smaller changes too included here, including support for scope performance timers that are saved with the bake record and can be printed out to the console. Yes I got carried away. For testing, the in-memory 'mock' file-system doesn't work anymore, since we're spawning processes, so this is replaced by a 'tmpfs' file-system which is saved in temporary files on disk and deleted after tests have run.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 12 Jun 2015 17:09:19 -0700
parents
children f987b29d6fab
comparison
equal deleted inserted replaced
410:d1a472464e57 411:e7b865f8f335
1 import os.path
2 import io
3 import time
4 import errno
5 import random
6 import codecs
7 import shutil
8 import mock
9 from piecrust import RESOURCES_DIR
10 from .basefs import TestFileSystemBase
11
12
13 class _MockFsEntry(object):
14 def __init__(self, contents):
15 self.contents = contents
16 self.metadata = {'mtime': time.time()}
17
18
19 class _MockFsEntryWriter(object):
20 def __init__(self, entry, mode='rt'):
21 self._entry = entry
22 self._mode = mode
23
24 if 'b' in mode:
25 data = entry.contents
26 if isinstance(data, str):
27 data = data.encode('utf8')
28 self._stream = io.BytesIO(data)
29 else:
30 self._stream = io.StringIO(entry.contents)
31
32 def __getattr__(self, name):
33 return getattr(self._stream, name)
34
35 def __enter__(self):
36 return self
37
38 def __exit__(self, exc_type, exc_value, exc_tb):
39 if 'w' in self._mode:
40 if 'a' in self._mode:
41 self._entry.contents += self._stream.getvalue()
42 else:
43 self._entry.contents = self._stream.getvalue()
44 self._entry.metadata['mtime'] = time.time()
45 self._stream.close()
46
47
48 class MemoryFileSystem(TestFileSystemBase):
49 def __init__(self, default_spec=True):
50 self._root = 'root_%d' % random.randrange(1000)
51 self._fs = {self._root: {}}
52 if default_spec:
53 self._initDefaultSpec()
54
55 def path(self, p):
56 p = p.replace('\\', '/')
57 if p in ['/', '', None]:
58 return '/%s' % self._root
59 return '/%s/%s' % (self._root, p.lstrip('/'))
60
61 def getStructure(self, path=None):
62 root = self._fs[self._root]
63 if path:
64 root = self._getEntry(self.path(path))
65 if root is None:
66 raise Exception("No such path: %s" % path)
67 if not isinstance(root, dict):
68 raise Exception("Path is not a directory: %s" % path)
69
70 res = {}
71 for k, v in root.items():
72 self._getStructureRecursive(v, res, k)
73 return res
74
75 def getFileEntry(self, path):
76 entry = self._getEntry(self.path(path))
77 if entry is None:
78 raise Exception("No such file: %s" % path)
79 if not isinstance(entry, _MockFsEntry):
80 raise Exception("Path is not a file: %s" % path)
81 return entry.contents
82
83 def _getStructureRecursive(self, src, target, name):
84 if isinstance(src, _MockFsEntry):
85 target[name] = src.contents
86 return
87
88 e = {}
89 for k, v in src.items():
90 self._getStructureRecursive(v, e, k)
91 target[name] = e
92
93 def _getEntry(self, path):
94 cur = self._fs
95 path = path.replace('\\', '/').lstrip('/')
96 bits = path.split('/')
97 for p in bits:
98 try:
99 cur = cur[p]
100 except KeyError:
101 return None
102 return cur
103
104 def _createDir(self, path):
105 cur = self._fs
106 path = path.replace('\\', '/').strip('/')
107 bits = path.split('/')
108 for b in bits:
109 if b not in cur:
110 cur[b] = {}
111 cur = cur[b]
112 return self
113
114 def _createFile(self, path, contents):
115 cur = self._fs
116 path = path.replace('\\', '/').lstrip('/')
117 bits = path.split('/')
118 for b in bits[:-1]:
119 if b not in cur:
120 cur[b] = {}
121 cur = cur[b]
122 cur[bits[-1]] = _MockFsEntry(contents)
123 return self
124
125 def _deleteEntry(self, path):
126 parent = self._getEntry(os.path.dirname(path))
127 assert parent is not None
128 name = os.path.basename(path)
129 assert name in parent
130 del parent[name]
131
132
133 class MemoryScope(object):
134 def __init__(self, fs, open_patches=None):
135 self.open_patches = open_patches or []
136 self._fs = fs
137 self._patchers = []
138 self._originals = {}
139
140 @property
141 def root(self):
142 return self._fs._root
143
144 def __enter__(self):
145 self._startMock()
146 return self
147
148 def __exit__(self, type, value, traceback):
149 self._endMock()
150
151 def _startMock(self):
152 # TODO: sadly, there seems to be no way to replace `open` everywhere?
153 modules = self.open_patches + [
154 '__main__',
155 'piecrust.records',
156 'jinja2.utils']
157 for m in modules:
158 self._createMock('%s.open' % m, open, self._open, create=True)
159
160 self._createMock('codecs.open', codecs.open, self._codecsOpen)
161 self._createMock('os.listdir', os.listdir, self._listdir)
162 self._createMock('os.makedirs', os.makedirs, self._makedirs)
163 self._createMock('os.remove', os.remove, self._remove)
164 self._createMock('os.rename', os.rename, self._rename)
165 self._createMock('os.path.exists', os.path.exists, self._exists)
166 self._createMock('os.path.isdir', os.path.isdir, self._isdir)
167 self._createMock('os.path.isfile', os.path.isfile, self._isfile)
168 self._createMock('os.path.islink', os.path.islink, self._islink)
169 self._createMock('os.path.getmtime', os.path.getmtime, self._getmtime)
170 self._createMock('shutil.copyfile', shutil.copyfile, self._copyfile)
171 self._createMock('shutil.rmtree', shutil.rmtree, self._rmtree)
172 for p in self._patchers:
173 p.start()
174
175 def _endMock(self):
176 for p in self._patchers:
177 p.stop()
178
179 def _createMock(self, name, orig, func, **kwargs):
180 self._originals[name] = orig
181 self._patchers.append(mock.patch(name, func, **kwargs))
182
183 def _doOpen(self, orig_name, path, mode, *args, **kwargs):
184 path = os.path.normpath(path)
185 if path.startswith(RESOURCES_DIR):
186 return self._originals[orig_name](path, mode, *args, **kwargs)
187
188 if 'r' in mode:
189 e = self._getFsEntry(path)
190 elif 'w' in mode or 'x' in mode or 'a' in mode:
191 e = self._getFsEntry(path)
192 if e is None:
193 contents = ''
194 if 'b' in mode:
195 contents = bytes()
196 self._fs._createFile(path, contents)
197 e = self._getFsEntry(path)
198 assert e is not None
199 elif 'x' in mode:
200 err = IOError("File '%s' already exists" % path)
201 err.errno = errno.EEXIST
202 raise err
203 else:
204 err = IOError("Unsupported open mode: %s" % mode)
205 err.errno = errno.EINVAL
206 raise err
207
208 if e is None:
209 err = IOError("No such file: %s" % path)
210 err.errno = errno.ENOENT
211 raise err
212 if not isinstance(e, _MockFsEntry):
213 err = IOError("'%s' is not a file %s" % (path, e))
214 err.errno = errno.EISDIR
215 raise err
216
217 return _MockFsEntryWriter(e, mode)
218
219 def _open(self, path, mode, *args, **kwargs):
220 return self._doOpen('__main__.open', path, mode, *args, **kwargs)
221
222 def _codecsOpen(self, path, mode, *args, **kwargs):
223 return self._doOpen('codecs.open', path, mode, *args, **kwargs)
224
225 def _listdir(self, path):
226 path = os.path.normpath(path)
227 if path.startswith(RESOURCES_DIR):
228 return self._originals['os.listdir'](path)
229
230 e = self._getFsEntry(path)
231 if e is None:
232 raise OSError("No such directory: %s" % path)
233 if not isinstance(e, dict):
234 raise OSError("'%s' is not a directory." % path)
235 return list(e.keys())
236
237 def _makedirs(self, path, mode=0o777):
238 if not path.replace('\\', '/').startswith('/' + self.root):
239 raise Exception("Shouldn't create directory: %s" % path)
240 self._fs._createDir(path)
241
242 def _remove(self, path):
243 path = os.path.normpath(path)
244 self._fs._deleteEntry(path)
245
246 def _exists(self, path):
247 path = os.path.normpath(path)
248 if path.startswith(RESOURCES_DIR):
249 return self._originals['os.path.isdir'](path)
250 e = self._getFsEntry(path)
251 return e is not None
252
253 def _isdir(self, path):
254 path = os.path.normpath(path)
255 if path.startswith(RESOURCES_DIR):
256 return self._originals['os.path.isdir'](path)
257 e = self._getFsEntry(path)
258 return e is not None and isinstance(e, dict)
259
260 def _isfile(self, path):
261 path = os.path.normpath(path)
262 if path.startswith(RESOURCES_DIR):
263 return self._originals['os.path.isfile'](path)
264 e = self._getFsEntry(path)
265 return e is not None and isinstance(e, _MockFsEntry)
266
267 def _islink(self, path):
268 path = os.path.normpath(path)
269 if path.startswith(RESOURCES_DIR):
270 return self._originals['os.path.islink'](path)
271 return False
272
273 def _getmtime(self, path):
274 path = os.path.normpath(path)
275 if path.startswith(RESOURCES_DIR):
276 return self._originals['os.path.getmtime'](path)
277 e = self._getFsEntry(path)
278 if e is None:
279 raise OSError("No such file: %s" % path)
280 return e.metadata['mtime']
281
282 def _copyfile(self, src, dst):
283 src = os.path.normpath(src)
284 if src.startswith(RESOURCES_DIR):
285 with self._originals['__main__.open'](src, 'r') as fp:
286 src_text = fp.read()
287 else:
288 e = self._getFsEntry(src)
289 src_text = e.contents
290 if not dst.replace('\\', '/').startswith('/' + self.root):
291 raise Exception("Shouldn't copy to: %s" % dst)
292 self._fs._createFile(dst, src_text)
293
294 def _rename(self, src, dst):
295 src = os.path.normpath(src)
296 if src.startswith(RESOURCES_DIR) or dst.startswith(RESOURCES_DIR):
297 raise Exception("Shouldn't rename files in the resources path.")
298 self._copyfile(src, dst)
299 self._remove(src)
300
301 def _rmtree(self, path):
302 if not path.replace('\\', '/').startswith('/' + self.root):
303 raise Exception("Shouldn't delete trees from: %s" % path)
304 e = self._fs._getEntry(os.path.dirname(path))
305 del e[os.path.basename(path)]
306
307 def _getFsEntry(self, path):
308 return self._fs._getEntry(path)
309