Merge lp:~sil/desktopcouch/dc-load-design-docs into lp:desktopcouch
- dc-load-design-docs
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Eric Casteleijn |
Approved revision: | 37 |
Merged at revision: | not available |
Proposed branch: | lp:~sil/desktopcouch/dc-load-design-docs |
Merge into: | lp:desktopcouch |
Diff against target: | None lines |
To merge this branch: | bzr merge lp:~sil/desktopcouch/dc-load-design-docs |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Elliot Murphy (community) | Approve | ||
Eric Casteleijn (community) | Approve | ||
dobey (community) | Approve | ||
John O'Brien (community) | Abstain | ||
Review via email: mp+9822@code.launchpad.net |
Commit message
Load design documents on startup. Documentation for how to place a design document on the filesystem.
Description of the change
Stuart Langridge (sil) wrote : | # |
dobey (dobey) wrote : | # |
You need to add the following to MANIFEST.in:
recursive-include contrib *.py
Stuart Langridge (sil) wrote : | # |
> You need to add the following to MANIFEST.in:
>
> recursive-include contrib *.py
I am told that I don't need to; the mocker is there for the test suite and doesn't need to be in the final package.
John O'Brien (jdobrien) wrote : | # |
I am abstaining from reviewing this as I can't run any tests on desktopcouch. As an FYI (and no relation to the code from this branch) the code is full of PEP 8 violations.
dobey (dobey) wrote : | # |
> > You need to add the following to MANIFEST.in:
> >
> > recursive-include contrib *.py
>
> I am told that I don't need to; the mocker is there for the test suite and
> doesn't need to be in the final package.
Who told you that? It's true that contrib/ doesn't need to be in the binary packages in debian/
Elliot Murphy (statik) wrote : | # |
I probably caused the confusion. Yes, the file needs to be in the tarball, but not in the final binary packages.
Elliot Murphy (statik) wrote : | # |
=======
[ERROR]: desktopcouch.
Traceback (most recent call last):
File "/usr/lib/
self.setUp()
File "/home/
self.database = CouchDatabase(
File "/home/
if database not in self._server:
File "/usr/lib/
self.
File "/usr/lib/
return self._request(
File "/usr/lib/
resp, data = _make_request()
File "/usr/lib/
body=body, headers=headers)
File "/usr/lib/
(response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
File "/usr/lib/
(response, content) = self._conn_
File "/usr/lib/
response = conn.getresponse()
File "/usr/lib/
response.
File "/usr/lib/
version, status, reason = self._read_status()
File "/usr/lib/
line = self.fp.readline()
File "/usr/lib/
data = recv(1)
socket.error: [Errno 4] Interrupted system call
=======
[ERROR]: desktopcouch.
Traceback (most recent call last):
File "/home/
mocker.verify()
File "/home/
raise AssertionError(
exceptions.
=> desktopcouch.
- Performed fewer times than expected.
=> desktopcouch.
- Performed fewer times than expected.
=> desktopcouch.
- Performed fewer times than expected.
=> desktopcouch.
- Performed fewer times than expected.
=> desktopcouch.
Stuart Langridge (sil) wrote : | # |
> Traceback (most recent call last):
> File "/home/
> sil/desktopcouc
> test_create_
> mocker.verify()
> File "/home/
> sil/desktopcouc
> raise AssertionError(
> exceptions.
>
> => desktopcouch.
> - Performed fewer times than expected.
Now fixed; I needed to reimport xdg.BaseDirectory to make it notice the os.environ change.
- 36. By Stuart Langridge
-
add mocker to manifest
Stuart Langridge (sil) wrote : | # |
> I probably caused the confusion. Yes, the file needs to be in the tarball, but
> not in the final binary packages.
Added to manifest.
Eric Casteleijn (thisfred) wrote : | # |
I'm getting this again (and seem to be the only one) but it's easily fixed by not using twistedtestcase, which I don't think we need in any case.
=======
[ERROR]: desktopcouch.
Traceback (most recent call last):
Failure: twisted.
DelayedCalls: (set twisted.
<DelayedCall 155909676 [29.9993708134s] called=0 cancelled=0 Client.
<DelayedCall 155910124 [19.5468599796s] called=0 cancelled=0 exit_on_timeout()>
<DelayedCall 157621132 [59.999724865s] called=0 cancelled=0 ThreadedResolve
=======
[ERROR]: desktopcouch.
Traceback (most recent call last):
File "/usr/lib/
result = f(*args, **kw)
File "/home/
reactor.stop()
File "/usr/lib/
return self._reactorMe
File "/usr/lib/
"Can't stop reactor that isn't running.")
twisted.
-------
dobey (dobey) : | # |
- 37. By Stuart Langridge
-
use testtools, not twisted
Eric Casteleijn (thisfred) wrote : | # |
Works now, great!
Preview Diff
1 | === modified file 'README' |
2 | --- README 2009-07-07 15:15:36 +0000 |
3 | +++ README 2009-08-07 12:43:11 +0000 |
4 | @@ -1,1 +1,35 @@ |
5 | -This is desktopcouch. |
6 | +This is Desktop Couch, an infrastructure to place a CouchDB on every desktop |
7 | +and provide APIs and management tools for applications to store data within |
8 | +it and share that data between computers and to the cloud. |
9 | + |
10 | += Technical notes = |
11 | + |
12 | +== Creating databases, design documents, and views == |
13 | + |
14 | +Desktop Couch will automatically create databases and design documents for you |
15 | +on startup by inspecting the filesystem, meaning that you do not need to check |
16 | +in your application whether your views exist. It is of course possible to create |
17 | +views directly through the Records API, but having them created via the |
18 | +filesystem allows managing of the view definitions more easily (they are |
19 | +in separately editable files, which helps with version control) and packaging |
20 | +(the files can be separately stored in a distribution package and installed |
21 | +into the system-level XDG_DATA_DIR rather than the user-level folder). |
22 | + |
23 | +=== To create a database === |
24 | + |
25 | +Create a file $XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/database.cfg |
26 | + |
27 | +This file can currently be empty (in future it may contain database setup and |
28 | +configuration information). |
29 | + |
30 | +=== To create a design document === |
31 | + |
32 | +Create a file |
33 | +$XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/_design/DESIGN_DOC_NAME/views/VIEW_NAME/map.js |
34 | +containing the map function from your view. |
35 | +If you also require a reduce function for your view, create a file |
36 | +$XDG_DATA_DIR/desktop-couch/databases/YOUR_DB_NAME/_design/DESIGN_DOC_NAME/views/VIEW_NAME/reduce.js. |
37 | + |
38 | +This is compatible with the filesystem view structure from CouchApp and |
39 | +CouchDBKit. |
40 | + |
41 | |
42 | === added directory 'contrib' |
43 | === added file 'contrib/mocker.py' |
44 | --- contrib/mocker.py 1970-01-01 00:00:00 +0000 |
45 | +++ contrib/mocker.py 2009-08-06 13:57:30 +0000 |
46 | @@ -0,0 +1,2068 @@ |
47 | +""" |
48 | +Copyright (c) 2007 Gustavo Niemeyer <gustavo@niemeyer.net> |
49 | + |
50 | +Graceful platform for test doubles in Python (mocks, stubs, fakes, and dummies). |
51 | +""" |
52 | +import __builtin__ |
53 | +import tempfile |
54 | +import unittest |
55 | +import inspect |
56 | +import shutil |
57 | +import types |
58 | +import sys |
59 | +import os |
60 | +import gc |
61 | + |
62 | + |
63 | +if sys.version_info < (2, 4): |
64 | + from sets import Set as set # pragma: nocover |
65 | + |
66 | + |
67 | +__all__ = ["Mocker", "expect", "IS", "CONTAINS", "IN", "MATCH", |
68 | + "ANY", "ARGS", "KWARGS"] |
69 | + |
70 | + |
71 | +__author__ = "Gustavo Niemeyer <gustavo@niemeyer.net>" |
72 | +__license__ = "PSF License" |
73 | +__version__ = "0.10.1" |
74 | + |
75 | + |
76 | +ERROR_PREFIX = "[Mocker] " |
77 | + |
78 | + |
79 | +# -------------------------------------------------------------------- |
80 | +# Exceptions |
81 | + |
82 | +class MatchError(AssertionError): |
83 | + """Raised when an unknown expression is seen in playback mode.""" |
84 | + |
85 | + |
86 | +# -------------------------------------------------------------------- |
87 | +# Helper for chained-style calling. |
88 | + |
89 | +class expect(object): |
90 | + """This is a simple helper that allows a different call-style. |
91 | + |
92 | + With this class one can comfortably do chaining of calls to the |
93 | + mocker object responsible by the object being handled. For instance:: |
94 | + |
95 | + expect(obj.attr).result(3).count(1, 2) |
96 | + |
97 | + Is the same as:: |
98 | + |
99 | + obj.attr |
100 | + mocker.result(3) |
101 | + mocker.count(1, 2) |
102 | + |
103 | + """ |
104 | + |
105 | + def __init__(self, mock, attr=None): |
106 | + self._mock = mock |
107 | + self._attr = attr |
108 | + |
109 | + def __getattr__(self, attr): |
110 | + return self.__class__(self._mock, attr) |
111 | + |
112 | + def __call__(self, *args, **kwargs): |
113 | + getattr(self._mock.__mocker__, self._attr)(*args, **kwargs) |
114 | + return self |
115 | + |
116 | + |
117 | +# -------------------------------------------------------------------- |
118 | +# Extensions to Python's unittest. |
119 | + |
120 | +class MockerTestCase(unittest.TestCase): |
121 | + """unittest.TestCase subclass with Mocker support. |
122 | + |
123 | + @ivar mocker: The mocker instance. |
124 | + |
125 | + This is a convenience only. Mocker may easily be used with the |
126 | + standard C{unittest.TestCase} class if wanted. |
127 | + |
128 | + Test methods have a Mocker instance available on C{self.mocker}. |
129 | + At the end of each test method, expectations of the mocker will |
130 | + be verified, and any requested changes made to the environment |
131 | + will be restored. |
132 | + |
133 | + In addition to the integration with Mocker, this class provides |
134 | + a few additional helper methods. |
135 | + """ |
136 | + |
137 | + expect = expect |
138 | + |
139 | + def __init__(self, methodName="runTest"): |
140 | + # So here is the trick: we take the real test method, wrap it on |
141 | + # a function that do the job we have to do, and insert it in the |
142 | + # *instance* dictionary, so that getattr() will return our |
143 | + # replacement rather than the class method. |
144 | + test_method = getattr(self, methodName, None) |
145 | + if test_method is not None: |
146 | + def test_method_wrapper(): |
147 | + try: |
148 | + result = test_method() |
149 | + except: |
150 | + raise |
151 | + else: |
152 | + if (self.mocker.is_recording() and |
153 | + self.mocker.get_events()): |
154 | + raise RuntimeError("Mocker must be put in replay " |
155 | + "mode with self.mocker.replay()") |
156 | + if (hasattr(result, "addCallback") and |
157 | + hasattr(result, "addErrback")): |
158 | + def verify(result): |
159 | + self.mocker.verify() |
160 | + return result |
161 | + result.addCallback(verify) |
162 | + else: |
163 | + self.mocker.verify() |
164 | + return result |
165 | + # Copy all attributes from the original method.. |
166 | + for attr in dir(test_method): |
167 | + # .. unless they're present in our wrapper already. |
168 | + if not hasattr(test_method_wrapper, attr) or attr == "__doc__": |
169 | + setattr(test_method_wrapper, attr, |
170 | + getattr(test_method, attr)) |
171 | + setattr(self, methodName, test_method_wrapper) |
172 | + |
173 | + # We could overload run() normally, but other well-known testing |
174 | + # frameworks do it as well, and some of them won't call the super, |
175 | + # which might mean that cleanup wouldn't happen. With that in mind, |
176 | + # we make integration easier by using the following trick. |
177 | + run_method = self.run |
178 | + def run_wrapper(*args, **kwargs): |
179 | + try: |
180 | + return run_method(*args, **kwargs) |
181 | + finally: |
182 | + self.__cleanup() |
183 | + self.run = run_wrapper |
184 | + |
185 | + self.mocker = Mocker() |
186 | + |
187 | + self.__cleanup_funcs = [] |
188 | + self.__cleanup_paths = [] |
189 | + |
190 | + super(MockerTestCase, self).__init__(methodName) |
191 | + |
192 | + def __cleanup(self): |
193 | + for path in self.__cleanup_paths: |
194 | + if os.path.isfile(path): |
195 | + os.unlink(path) |
196 | + elif os.path.isdir(path): |
197 | + shutil.rmtree(path) |
198 | + self.mocker.restore() |
199 | + for func, args, kwargs in self.__cleanup_funcs: |
200 | + func(*args, **kwargs) |
201 | + |
202 | + def addCleanup(self, func, *args, **kwargs): |
203 | + self.__cleanup_funcs.append((func, args, kwargs)) |
204 | + |
205 | + def makeFile(self, content=None, suffix="", prefix="tmp", basename=None, |
206 | + dirname=None, path=None): |
207 | + """Create a temporary file and return the path to it. |
208 | + |
209 | + @param content: Initial content for the file. |
210 | + @param suffix: Suffix to be given to the file's basename. |
211 | + @param prefix: Prefix to be given to the file's basename. |
212 | + @param basename: Full basename for the file. |
213 | + @param dirname: Put file inside this directory. |
214 | + |
215 | + The file is removed after the test runs. |
216 | + """ |
217 | + if path is not None: |
218 | + self.__cleanup_paths.append(path) |
219 | + elif basename is not None: |
220 | + if dirname is None: |
221 | + dirname = tempfile.mkdtemp() |
222 | + self.__cleanup_paths.append(dirname) |
223 | + path = os.path.join(dirname, basename) |
224 | + else: |
225 | + fd, path = tempfile.mkstemp(suffix, prefix, dirname) |
226 | + self.__cleanup_paths.append(path) |
227 | + os.close(fd) |
228 | + if content is None: |
229 | + os.unlink(path) |
230 | + if content is not None: |
231 | + file = open(path, "w") |
232 | + file.write(content) |
233 | + file.close() |
234 | + return path |
235 | + |
236 | + def makeDir(self, suffix="", prefix="tmp", dirname=None, path=None): |
237 | + """Create a temporary directory and return the path to it. |
238 | + |
239 | + @param suffix: Suffix to be given to the file's basename. |
240 | + @param prefix: Prefix to be given to the file's basename. |
241 | + @param dirname: Put directory inside this parent directory. |
242 | + |
243 | + The directory is removed after the test runs. |
244 | + """ |
245 | + if path is not None: |
246 | + os.makedirs(path) |
247 | + else: |
248 | + path = tempfile.mkdtemp(suffix, prefix, dirname) |
249 | + self.__cleanup_paths.append(path) |
250 | + return path |
251 | + |
252 | + def failUnlessIs(self, first, second, msg=None): |
253 | + """Assert that C{first} is the same object as C{second}.""" |
254 | + if first is not second: |
255 | + raise self.failureException(msg or "%r is not %r" % (first, second)) |
256 | + |
257 | + def failIfIs(self, first, second, msg=None): |
258 | + """Assert that C{first} is not the same object as C{second}.""" |
259 | + if first is second: |
260 | + raise self.failureException(msg or "%r is %r" % (first, second)) |
261 | + |
262 | + def failUnlessIn(self, first, second, msg=None): |
263 | + """Assert that C{first} is contained in C{second}.""" |
264 | + if first not in second: |
265 | + raise self.failureException(msg or "%r not in %r" % (first, second)) |
266 | + |
267 | + def failUnlessStartsWith(self, first, second, msg=None): |
268 | + """Assert that C{first} starts with C{second}.""" |
269 | + if first[:len(second)] != second: |
270 | + raise self.failureException(msg or "%r doesn't start with %r" % |
271 | + (first, second)) |
272 | + |
273 | + def failIfStartsWith(self, first, second, msg=None): |
274 | + """Assert that C{first} doesn't start with C{second}.""" |
275 | + if first[:len(second)] == second: |
276 | + raise self.failureException(msg or "%r starts with %r" % |
277 | + (first, second)) |
278 | + |
279 | + def failUnlessEndsWith(self, first, second, msg=None): |
280 | + """Assert that C{first} starts with C{second}.""" |
281 | + if first[len(first)-len(second):] != second: |
282 | + raise self.failureException(msg or "%r doesn't end with %r" % |
283 | + (first, second)) |
284 | + |
285 | + def failIfEndsWith(self, first, second, msg=None): |
286 | + """Assert that C{first} doesn't start with C{second}.""" |
287 | + if first[len(first)-len(second):] == second: |
288 | + raise self.failureException(msg or "%r ends with %r" % |
289 | + (first, second)) |
290 | + |
291 | + def failIfIn(self, first, second, msg=None): |
292 | + """Assert that C{first} is not contained in C{second}.""" |
293 | + if first in second: |
294 | + raise self.failureException(msg or "%r in %r" % (first, second)) |
295 | + |
296 | + def failUnlessApproximates(self, first, second, tolerance, msg=None): |
297 | + """Assert that C{first} is near C{second} by at most C{tolerance}.""" |
298 | + if abs(first - second) > tolerance: |
299 | + raise self.failureException(msg or "abs(%r - %r) > %r" % |
300 | + (first, second, tolerance)) |
301 | + |
302 | + def failIfApproximates(self, first, second, tolerance, msg=None): |
303 | + """Assert that C{first} is far from C{second} by at least C{tolerance}. |
304 | + """ |
305 | + if abs(first - second) <= tolerance: |
306 | + raise self.failureException(msg or "abs(%r - %r) <= %r" % |
307 | + (first, second, tolerance)) |
308 | + |
309 | + def failUnlessMethodsMatch(self, first, second): |
310 | + """Assert that public methods in C{first} are present in C{second}. |
311 | + |
312 | + This method asserts that all public methods found in C{first} are also |
313 | + present in C{second} and accept the same arguments. C{first} may |
314 | + have its own private methods, though, and may not have all methods |
315 | + found in C{second}. Note that if a private method in C{first} matches |
316 | + the name of one in C{second}, their specification is still compared. |
317 | + |
318 | + This is useful to verify if a fake or stub class have the same API as |
319 | + the real class being simulated. |
320 | + """ |
321 | + first_methods = dict(inspect.getmembers(first, inspect.ismethod)) |
322 | + second_methods = dict(inspect.getmembers(second, inspect.ismethod)) |
323 | + for name, first_method in first_methods.items(): |
324 | + first_argspec = inspect.getargspec(first_method) |
325 | + first_formatted = inspect.formatargspec(*first_argspec) |
326 | + |
327 | + second_method = second_methods.get(name) |
328 | + if second_method is None: |
329 | + if name[:1] == "_": |
330 | + continue # First may have its own private methods. |
331 | + raise self.failureException("%s.%s%s not present in %s" % |
332 | + (first.__name__, name, first_formatted, second.__name__)) |
333 | + |
334 | + second_argspec = inspect.getargspec(second_method) |
335 | + if first_argspec != second_argspec: |
336 | + second_formatted = inspect.formatargspec(*second_argspec) |
337 | + raise self.failureException("%s.%s%s != %s.%s%s" % |
338 | + (first.__name__, name, first_formatted, |
339 | + second.__name__, name, second_formatted)) |
340 | + |
341 | + |
342 | + assertIs = failUnlessIs |
343 | + assertIsNot = failIfIs |
344 | + assertIn = failUnlessIn |
345 | + assertNotIn = failIfIn |
346 | + assertStartsWith = failUnlessStartsWith |
347 | + assertNotStartsWith = failIfStartsWith |
348 | + assertEndsWith = failUnlessEndsWith |
349 | + assertNotEndsWith = failIfEndsWith |
350 | + assertApproximates = failUnlessApproximates |
351 | + assertNotApproximates = failIfApproximates |
352 | + assertMethodsMatch = failUnlessMethodsMatch |
353 | + |
354 | + # The following are missing in Python < 2.4. |
355 | + assertTrue = unittest.TestCase.failUnless |
356 | + assertFalse = unittest.TestCase.failIf |
357 | + |
358 | + # The following is provided for compatibility with Twisted's trial. |
359 | + assertIdentical = assertIs |
360 | + assertNotIdentical = assertIsNot |
361 | + failUnlessIdentical = failUnlessIs |
362 | + failIfIdentical = failIfIs |
363 | + |
364 | + |
365 | +# -------------------------------------------------------------------- |
366 | +# Mocker. |
367 | + |
368 | +class classinstancemethod(object): |
369 | + |
370 | + def __init__(self, method): |
371 | + self.method = method |
372 | + |
373 | + def __get__(self, obj, cls=None): |
374 | + def bound_method(*args, **kwargs): |
375 | + return self.method(cls, obj, *args, **kwargs) |
376 | + return bound_method |
377 | + |
378 | + |
379 | +class MockerBase(object): |
380 | + """Controller of mock objects. |
381 | + |
382 | + A mocker instance is used to command recording and replay of |
383 | + expectations on any number of mock objects. |
384 | + |
385 | + Expectations should be expressed for the mock object while in |
386 | + record mode (the initial one) by using the mock object itself, |
387 | + and using the mocker (and/or C{expect()} as a helper) to define |
388 | + additional behavior for each event. For instance:: |
389 | + |
390 | + mock = mocker.mock() |
391 | + mock.hello() |
392 | + mocker.result("Hi!") |
393 | + mocker.replay() |
394 | + assert mock.hello() == "Hi!" |
395 | + mock.restore() |
396 | + mock.verify() |
397 | + |
398 | + In this short excerpt a mock object is being created, then an |
399 | + expectation of a call to the C{hello()} method was recorded, and |
400 | + when called the method should return the value C{10}. Then, the |
401 | + mocker is put in replay mode, and the expectation is satisfied by |
402 | + calling the C{hello()} method, which indeed returns 10. Finally, |
403 | + a call to the L{restore()} method is performed to undo any needed |
404 | + changes made in the environment, and the L{verify()} method is |
405 | + called to ensure that all defined expectations were met. |
406 | + |
407 | + The same logic can be expressed more elegantly using the |
408 | + C{with mocker:} statement, as follows:: |
409 | + |
410 | + mock = mocker.mock() |
411 | + mock.hello() |
412 | + mocker.result("Hi!") |
413 | + with mocker: |
414 | + assert mock.hello() == "Hi!" |
415 | + |
416 | + Also, the MockerTestCase class, which integrates the mocker on |
417 | + a unittest.TestCase subclass, may be used to reduce the overhead |
418 | + of controlling the mocker. A test could be written as follows:: |
419 | + |
420 | + class SampleTest(MockerTestCase): |
421 | + |
422 | + def test_hello(self): |
423 | + mock = self.mocker.mock() |
424 | + mock.hello() |
425 | + self.mocker.result("Hi!") |
426 | + self.mocker.replay() |
427 | + self.assertEquals(mock.hello(), "Hi!") |
428 | + """ |
429 | + |
430 | + _recorders = [] |
431 | + |
432 | + # For convenience only. |
433 | + on = expect |
434 | + |
435 | + class __metaclass__(type): |
436 | + def __init__(self, name, bases, dict): |
437 | + # Make independent lists on each subclass, inheriting from parent. |
438 | + self._recorders = list(getattr(self, "_recorders", ())) |
439 | + |
440 | + def __init__(self): |
441 | + self._recorders = self._recorders[:] |
442 | + self._events = [] |
443 | + self._recording = True |
444 | + self._ordering = False |
445 | + self._last_orderer = None |
446 | + |
447 | + def is_recording(self): |
448 | + """Return True if in recording mode, False if in replay mode. |
449 | + |
450 | + Recording is the initial state. |
451 | + """ |
452 | + return self._recording |
453 | + |
454 | + def replay(self): |
455 | + """Change to replay mode, where recorded events are reproduced. |
456 | + |
457 | + If already in replay mode, the mocker will be restored, with all |
458 | + expectations reset, and then put again in replay mode. |
459 | + |
460 | + An alternative and more comfortable way to replay changes is |
461 | + using the 'with' statement, as follows:: |
462 | + |
463 | + mocker = Mocker() |
464 | + <record events> |
465 | + with mocker: |
466 | + <reproduce events> |
467 | + |
468 | + The 'with' statement will automatically put mocker in replay |
469 | + mode, and will also verify if all events were correctly reproduced |
470 | + at the end (using L{verify()}), and also restore any changes done |
471 | + in the environment (with L{restore()}). |
472 | + |
473 | + Also check the MockerTestCase class, which integrates the |
474 | + unittest.TestCase class with mocker. |
475 | + """ |
476 | + if not self._recording: |
477 | + for event in self._events: |
478 | + event.restore() |
479 | + else: |
480 | + self._recording = False |
481 | + for event in self._events: |
482 | + event.replay() |
483 | + |
484 | + def restore(self): |
485 | + """Restore changes in the environment, and return to recording mode. |
486 | + |
487 | + This should always be called after the test is complete (succeeding |
488 | + or not). There are ways to call this method automatically on |
489 | + completion (e.g. using a C{with mocker:} statement, or using the |
490 | + L{MockerTestCase} class. |
491 | + """ |
492 | + if not self._recording: |
493 | + self._recording = True |
494 | + for event in self._events: |
495 | + event.restore() |
496 | + |
497 | + def reset(self): |
498 | + """Reset the mocker state. |
499 | + |
500 | + This will restore environment changes, if currently in replay |
501 | + mode, and then remove all events previously recorded. |
502 | + """ |
503 | + if not self._recording: |
504 | + self.restore() |
505 | + self.unorder() |
506 | + del self._events[:] |
507 | + |
508 | + def get_events(self): |
509 | + """Return all recorded events.""" |
510 | + return self._events[:] |
511 | + |
512 | + def add_event(self, event): |
513 | + """Add an event. |
514 | + |
515 | + This method is used internally by the implementation, and |
516 | + shouldn't be needed on normal mocker usage. |
517 | + """ |
518 | + self._events.append(event) |
519 | + if self._ordering: |
520 | + orderer = event.add_task(Orderer(event.path)) |
521 | + if self._last_orderer: |
522 | + orderer.add_dependency(self._last_orderer) |
523 | + self._last_orderer = orderer |
524 | + return event |
525 | + |
526 | + def verify(self): |
527 | + """Check if all expectations were met, and raise AssertionError if not. |
528 | + |
529 | + The exception message will include a nice description of which |
530 | + expectations were not met, and why. |
531 | + """ |
532 | + errors = [] |
533 | + for event in self._events: |
534 | + try: |
535 | + event.verify() |
536 | + except AssertionError, e: |
537 | + error = str(e) |
538 | + if not error: |
539 | + raise RuntimeError("Empty error message from %r" |
540 | + % event) |
541 | + errors.append(error) |
542 | + if errors: |
543 | + message = [ERROR_PREFIX + "Unmet expectations:", ""] |
544 | + for error in errors: |
545 | + lines = error.splitlines() |
546 | + message.append("=> " + lines.pop(0)) |
547 | + message.extend([" " + line for line in lines]) |
548 | + message.append("") |
549 | + raise AssertionError(os.linesep.join(message)) |
550 | + |
551 | + def mock(self, spec_and_type=None, spec=None, type=None, |
552 | + name=None, count=True): |
553 | + """Return a new mock object. |
554 | + |
555 | + @param spec_and_type: Handy positional argument which sets both |
556 | + spec and type. |
557 | + @param spec: Method calls will be checked for correctness against |
558 | + the given class. |
559 | + @param type: If set, the Mock's __class__ attribute will return |
560 | + the given type. This will make C{isinstance()} calls |
561 | + on the object work. |
562 | + @param name: Name for the mock object, used in the representation of |
563 | + expressions. The name is rarely needed, as it's usually |
564 | + guessed correctly from the variable name used. |
565 | + @param count: If set to false, expressions may be executed any number |
566 | + of times, unless an expectation is explicitly set using |
567 | + the L{count()} method. By default, expressions are |
568 | + expected once. |
569 | + """ |
570 | + if spec_and_type is not None: |
571 | + spec = type = spec_and_type |
572 | + return Mock(self, spec=spec, type=type, name=name, count=count) |
573 | + |
574 | + def proxy(self, object, spec=True, type=True, name=None, count=True, |
575 | + passthrough=True): |
576 | + """Return a new mock object which proxies to the given object. |
577 | + |
578 | + Proxies are useful when only part of the behavior of an object |
579 | + is to be mocked. Unknown expressions may be passed through to |
580 | + the real implementation implicitly (if the C{passthrough} argument |
581 | + is True), or explicitly (using the L{passthrough()} method |
582 | + on the event). |
583 | + |
584 | + @param object: Real object to be proxied, and replaced by the mock |
585 | + on replay mode. It may also be an "import path", |
586 | + such as C{"time.time"}, in which case the object |
587 | + will be the C{time} function from the C{time} module. |
588 | + @param spec: Method calls will be checked for correctness against |
589 | + the given object, which may be a class or an instance |
590 | + where attributes will be looked up. Defaults to the |
591 | + the C{object} parameter. May be set to None explicitly, |
592 | + in which case spec checking is disabled. Checks may |
593 | + also be disabled explicitly on a per-event basis with |
594 | + the L{nospec()} method. |
595 | + @param type: If set, the Mock's __class__ attribute will return |
596 | + the given type. This will make C{isinstance()} calls |
597 | + on the object work. Defaults to the type of the |
598 | + C{object} parameter. May be set to None explicitly. |
599 | + @param name: Name for the mock object, used in the representation of |
600 | + expressions. The name is rarely needed, as it's usually |
601 | + guessed correctly from the variable name used. |
602 | + @param count: If set to false, expressions may be executed any number |
603 | + of times, unless an expectation is explicitly set using |
604 | + the L{count()} method. By default, expressions are |
605 | + expected once. |
606 | + @param passthrough: If set to False, passthrough of actions on the |
607 | + proxy to the real object will only happen when |
608 | + explicitly requested via the L{passthrough()} |
609 | + method. |
610 | + """ |
611 | + if isinstance(object, basestring): |
612 | + if name is None: |
613 | + name = object |
614 | + import_stack = object.split(".") |
615 | + attr_stack = [] |
616 | + while import_stack: |
617 | + module_path = ".".join(import_stack) |
618 | + try: |
619 | + object = __import__(module_path, {}, {}, [""]) |
620 | + except ImportError: |
621 | + attr_stack.insert(0, import_stack.pop()) |
622 | + if not import_stack: |
623 | + raise |
624 | + continue |
625 | + else: |
626 | + for attr in attr_stack: |
627 | + object = getattr(object, attr) |
628 | + break |
629 | + if spec is True: |
630 | + spec = object |
631 | + if type is True: |
632 | + type = __builtin__.type(object) |
633 | + return Mock(self, spec=spec, type=type, object=object, |
634 | + name=name, count=count, passthrough=passthrough) |
635 | + |
636 | + def replace(self, object, spec=True, type=True, name=None, count=True, |
637 | + passthrough=True): |
638 | + """Create a proxy, and replace the original object with the mock. |
639 | + |
640 | + On replay, the original object will be replaced by the returned |
641 | + proxy in all dictionaries found in the running interpreter via |
642 | + the garbage collecting system. This should cover module |
643 | + namespaces, class namespaces, instance namespaces, and so on. |
644 | + |
645 | + @param object: Real object to be proxied, and replaced by the mock |
646 | + on replay mode. It may also be an "import path", |
647 | + such as C{"time.time"}, in which case the object |
648 | + will be the C{time} function from the C{time} module. |
649 | + @param spec: Method calls will be checked for correctness against |
650 | + the given object, which may be a class or an instance |
651 | + where attributes will be looked up. Defaults to the |
652 | + the C{object} parameter. May be set to None explicitly, |
653 | + in which case spec checking is disabled. Checks may |
654 | + also be disabled explicitly on a per-event basis with |
655 | + the L{nospec()} method. |
656 | + @param type: If set, the Mock's __class__ attribute will return |
657 | + the given type. This will make C{isinstance()} calls |
658 | + on the object work. Defaults to the type of the |
659 | + C{object} parameter. May be set to None explicitly. |
660 | + @param name: Name for the mock object, used in the representation of |
661 | + expressions. The name is rarely needed, as it's usually |
662 | + guessed correctly from the variable name used. |
663 | + @param passthrough: If set to False, passthrough of actions on the |
664 | + proxy to the real object will only happen when |
665 | + explicitly requested via the L{passthrough()} |
666 | + method. |
667 | + """ |
668 | + mock = self.proxy(object, spec, type, name, count, passthrough) |
669 | + event = self._get_replay_restore_event() |
670 | + event.add_task(ProxyReplacer(mock)) |
671 | + return mock |
672 | + |
673 | + def patch(self, object, spec=True): |
674 | + """Patch an existing object to reproduce recorded events. |
675 | + |
676 | + @param object: Class or instance to be patched. |
677 | + @param spec: Method calls will be checked for correctness against |
678 | + the given object, which may be a class or an instance |
679 | + where attributes will be looked up. Defaults to the |
680 | + the C{object} parameter. May be set to None explicitly, |
681 | + in which case spec checking is disabled. Checks may |
682 | + also be disabled explicitly on a per-event basis with |
683 | + the L{nospec()} method. |
684 | + |
685 | + The result of this method is still a mock object, which can be |
686 | + used like any other mock object to record events. The difference |
687 | + is that when the mocker is put on replay mode, the *real* object |
688 | + will be modified to behave according to recorded expectations. |
689 | + |
690 | + Patching works in individual instances, and also in classes. |
691 | + When an instance is patched, recorded events will only be |
692 | + considered on this specific instance, and other instances should |
693 | + behave normally. When a class is patched, the reproduction of |
694 | + events will be considered on any instance of this class once |
695 | + created (collectively). |
696 | + |
697 | + Observe that, unlike with proxies which catch only events done |
698 | + through the mock object, *all* accesses to recorded expectations |
699 | + will be considered; even these coming from the object itself |
700 | + (e.g. C{self.hello()} is considered if this method was patched). |
701 | + While this is a very powerful feature, and many times the reason |
702 | + to use patches in the first place, it's important to keep this |
703 | + behavior in mind. |
704 | + |
705 | + Patching of the original object only takes place when the mocker |
706 | + is put on replay mode, and the patched object will be restored |
707 | + to its original state once the L{restore()} method is called |
708 | + (explicitly, or implicitly with alternative conventions, such as |
709 | + a C{with mocker:} block, or a MockerTestCase class). |
710 | + """ |
711 | + if spec is True: |
712 | + spec = object |
713 | + patcher = Patcher() |
714 | + event = self._get_replay_restore_event() |
715 | + event.add_task(patcher) |
716 | + mock = Mock(self, object=object, patcher=patcher, |
717 | + passthrough=True, spec=spec) |
718 | + object.__mocker_mock__ = mock |
719 | + return mock |
720 | + |
721 | + def act(self, path): |
722 | + """This is called by mock objects whenever something happens to them. |
723 | + |
724 | + This method is part of the implementation between the mocker |
725 | + and mock objects. |
726 | + """ |
727 | + if self._recording: |
728 | + event = self.add_event(Event(path)) |
729 | + for recorder in self._recorders: |
730 | + recorder(self, event) |
731 | + return Mock(self, path) |
732 | + else: |
733 | + # First run events that may run, then run unsatisfied events, then |
734 | + # ones not previously run. We put the index in the ordering tuple |
735 | + # instead of the actual event because we want a stable sort |
736 | + # (ordering between 2 events is undefined). |
737 | + events = self._events |
738 | + order = [(events[i].satisfied()*2 + events[i].has_run(), i) |
739 | + for i in range(len(events))] |
740 | + order.sort() |
741 | + postponed = None |
742 | + for weight, i in order: |
743 | + event = events[i] |
744 | + if event.matches(path): |
745 | + if event.may_run(path): |
746 | + return event.run(path) |
747 | + elif postponed is None: |
748 | + postponed = event |
749 | + if postponed is not None: |
750 | + return postponed.run(path) |
751 | + raise MatchError(ERROR_PREFIX + "Unexpected expression: %s" % path) |
752 | + |
753 | + def get_recorders(cls, self): |
754 | + """Return recorders associated with this mocker class or instance. |
755 | + |
756 | + This method may be called on mocker instances and also on mocker |
757 | + classes. See the L{add_recorder()} method for more information. |
758 | + """ |
759 | + return (self or cls)._recorders[:] |
760 | + get_recorders = classinstancemethod(get_recorders) |
761 | + |
762 | + def add_recorder(cls, self, recorder): |
763 | + """Add a recorder to this mocker class or instance. |
764 | + |
765 | + @param recorder: Callable accepting C{(mocker, event)} as parameters. |
766 | + |
767 | + This is part of the implementation of mocker. |
768 | + |
769 | + All registered recorders are called for translating events that |
770 | + happen during recording into expectations to be met once the state |
771 | + is switched to replay mode. |
772 | + |
773 | + This method may be called on mocker instances and also on mocker |
774 | + classes. When called on a class, the recorder will be used by |
775 | + all instances, and also inherited on subclassing. When called on |
776 | + instances, the recorder is added only to the given instance. |
777 | + """ |
778 | + (self or cls)._recorders.append(recorder) |
779 | + return recorder |
780 | + add_recorder = classinstancemethod(add_recorder) |
781 | + |
782 | + def remove_recorder(cls, self, recorder): |
783 | + """Remove the given recorder from this mocker class or instance. |
784 | + |
785 | + This method may be called on mocker classes and also on mocker |
786 | + instances. See the L{add_recorder()} method for more information. |
787 | + """ |
788 | + (self or cls)._recorders.remove(recorder) |
789 | + remove_recorder = classinstancemethod(remove_recorder) |
790 | + |
791 | + def result(self, value): |
792 | + """Make the last recorded event return the given value on replay. |
793 | + |
794 | + @param value: Object to be returned when the event is replayed. |
795 | + """ |
796 | + self.call(lambda *args, **kwargs: value) |
797 | + |
798 | + def generate(self, sequence): |
799 | + """Last recorded event will return a generator with the given sequence. |
800 | + |
801 | + @param sequence: Sequence of values to be generated. |
802 | + """ |
803 | + def generate(*args, **kwargs): |
804 | + for value in sequence: |
805 | + yield value |
806 | + self.call(generate) |
807 | + |
808 | + def throw(self, exception): |
809 | + """Make the last recorded event raise the given exception on replay. |
810 | + |
811 | + @param exception: Class or instance of exception to be raised. |
812 | + """ |
813 | + def raise_exception(*args, **kwargs): |
814 | + raise exception |
815 | + self.call(raise_exception) |
816 | + |
817 | + def call(self, func): |
818 | + """Make the last recorded event cause the given function to be called. |
819 | + |
820 | + @param func: Function to be called. |
821 | + |
822 | + The result of the function will be used as the event result. |
823 | + """ |
824 | + self._events[-1].add_task(FunctionRunner(func)) |
825 | + |
826 | + def count(self, min, max=False): |
827 | + """Last recorded event must be replayed between min and max times. |
828 | + |
829 | + @param min: Minimum number of times that the event must happen. |
830 | + @param max: Maximum number of times that the event must happen. If |
831 | + not given, it defaults to the same value of the C{min} |
832 | + parameter. If set to None, there is no upper limit, and |
833 | + the expectation is met as long as it happens at least |
834 | + C{min} times. |
835 | + """ |
836 | + event = self._events[-1] |
837 | + for task in event.get_tasks(): |
838 | + if isinstance(task, RunCounter): |
839 | + event.remove_task(task) |
840 | + event.add_task(RunCounter(min, max)) |
841 | + |
842 | + def is_ordering(self): |
843 | + """Return true if all events are being ordered. |
844 | + |
845 | + See the L{order()} method. |
846 | + """ |
847 | + return self._ordering |
848 | + |
849 | + def unorder(self): |
850 | + """Disable the ordered mode. |
851 | + |
852 | + See the L{order()} method for more information. |
853 | + """ |
854 | + self._ordering = False |
855 | + self._last_orderer = None |
856 | + |
857 | + def order(self, *path_holders): |
858 | + """Create an expectation of order between two or more events. |
859 | + |
860 | + @param path_holders: Objects returned as the result of recorded events. |
861 | + |
862 | + By default, mocker won't force events to happen precisely in |
863 | + the order they were recorded. Calling this method will change |
864 | + this behavior so that events will only match if reproduced in |
865 | + the correct order. |
866 | + |
867 | + There are two ways in which this method may be used. Which one |
868 | + is used in a given occasion depends only on convenience. |
869 | + |
870 | + If no arguments are passed, the mocker will be put in a mode where |
871 | + all the recorded events following the method call will only be met |
872 | + if they happen in order. When that's used, the mocker may be put |
873 | + back in unordered mode by calling the L{unorder()} method, or by |
874 | + using a 'with' block, like so:: |
875 | + |
876 | + with mocker.ordered(): |
877 | + <record events> |
878 | + |
879 | + In this case, only expressions in <record events> will be ordered, |
880 | + and the mocker will be back in unordered mode after the 'with' block. |
881 | + |
882 | + The second way to use it is by specifying precisely which events |
883 | + should be ordered. As an example:: |
884 | + |
885 | + mock = mocker.mock() |
886 | + expr1 = mock.hello() |
887 | + expr2 = mock.world |
888 | + expr3 = mock.x.y.z |
889 | + mocker.order(expr1, expr2, expr3) |
890 | + |
891 | + This method of ordering only works when the expression returns |
892 | + another object. |
893 | + |
894 | + Also check the L{after()} and L{before()} methods, which are |
895 | + alternative ways to perform this. |
896 | + """ |
897 | + if not path_holders: |
898 | + self._ordering = True |
899 | + return OrderedContext(self) |
900 | + |
901 | + last_orderer = None |
902 | + for path_holder in path_holders: |
903 | + if type(path_holder) is Path: |
904 | + path = path_holder |
905 | + else: |
906 | + path = path_holder.__mocker_path__ |
907 | + for event in self._events: |
908 | + if event.path is path: |
909 | + for task in event.get_tasks(): |
910 | + if isinstance(task, Orderer): |
911 | + orderer = task |
912 | + break |
913 | + else: |
914 | + orderer = Orderer(path) |
915 | + event.add_task(orderer) |
916 | + if last_orderer: |
917 | + orderer.add_dependency(last_orderer) |
918 | + last_orderer = orderer |
919 | + break |
920 | + |
921 | + def after(self, *path_holders): |
922 | + """Last recorded event must happen after events referred to. |
923 | + |
924 | + @param path_holders: Objects returned as the result of recorded events |
925 | + which should happen before the last recorded event |
926 | + |
927 | + As an example, the idiom:: |
928 | + |
929 | + expect(mock.x).after(mock.y, mock.z) |
930 | + |
931 | + is an alternative way to say:: |
932 | + |
933 | + expr_x = mock.x |
934 | + expr_y = mock.y |
935 | + expr_z = mock.z |
936 | + mocker.order(expr_y, expr_x) |
937 | + mocker.order(expr_z, expr_x) |
938 | + |
939 | + See L{order()} for more information. |
940 | + """ |
941 | + last_path = self._events[-1].path |
942 | + for path_holder in path_holders: |
943 | + self.order(path_holder, last_path) |
944 | + |
945 | + def before(self, *path_holders): |
946 | + """Last recorded event must happen before events referred to. |
947 | + |
948 | + @param path_holders: Objects returned as the result of recorded events |
949 | + which should happen after the last recorded event |
950 | + |
951 | + As an example, the idiom:: |
952 | + |
953 | + expect(mock.x).before(mock.y, mock.z) |
954 | + |
955 | + is an alternative way to say:: |
956 | + |
957 | + expr_x = mock.x |
958 | + expr_y = mock.y |
959 | + expr_z = mock.z |
960 | + mocker.order(expr_x, expr_y) |
961 | + mocker.order(expr_x, expr_z) |
962 | + |
963 | + See L{order()} for more information. |
964 | + """ |
965 | + last_path = self._events[-1].path |
966 | + for path_holder in path_holders: |
967 | + self.order(last_path, path_holder) |
968 | + |
969 | + def nospec(self): |
970 | + """Don't check method specification of real object on last event. |
971 | + |
972 | + By default, when using a mock created as the result of a call to |
973 | + L{proxy()}, L{replace()}, and C{patch()}, or when passing the spec |
974 | + attribute to the L{mock()} method, method calls on the given object |
975 | + are checked for correctness against the specification of the real |
976 | + object (or the explicitly provided spec). |
977 | + |
978 | + This method will disable that check specifically for the last |
979 | + recorded event. |
980 | + """ |
981 | + event = self._events[-1] |
982 | + for task in event.get_tasks(): |
983 | + if isinstance(task, SpecChecker): |
984 | + event.remove_task(task) |
985 | + |
986 | + def passthrough(self, result_callback=None): |
987 | + """Make the last recorded event run on the real object once seen. |
988 | + |
989 | + @param result_callback: If given, this function will be called with |
990 | + the result of the *real* method call as the only argument. |
991 | + |
992 | + This can only be used on proxies, as returned by the L{proxy()} |
993 | + and L{replace()} methods, or on mocks representing patched objects, |
994 | + as returned by the L{patch()} method. |
995 | + """ |
996 | + event = self._events[-1] |
997 | + if event.path.root_object is None: |
998 | + raise TypeError("Mock object isn't a proxy") |
999 | + event.add_task(PathExecuter(result_callback)) |
1000 | + |
1001 | + def __enter__(self): |
1002 | + """Enter in a 'with' context. This will run replay().""" |
1003 | + self.replay() |
1004 | + return self |
1005 | + |
1006 | + def __exit__(self, type, value, traceback): |
1007 | + """Exit from a 'with' context. |
1008 | + |
1009 | + This will run restore() at all times, but will only run verify() |
1010 | + if the 'with' block itself hasn't raised an exception. Exceptions |
1011 | + in that block are never swallowed. |
1012 | + """ |
1013 | + self.restore() |
1014 | + if type is None: |
1015 | + self.verify() |
1016 | + return False |
1017 | + |
1018 | + def _get_replay_restore_event(self): |
1019 | + """Return unique L{ReplayRestoreEvent}, creating if needed. |
1020 | + |
1021 | + Some tasks only want to replay/restore. When that's the case, |
1022 | + they shouldn't act on other events during replay. Also, they |
1023 | + can all be put in a single event when that's the case. Thus, |
1024 | + we add a single L{ReplayRestoreEvent} as the first element of |
1025 | + the list. |
1026 | + """ |
1027 | + if not self._events or type(self._events[0]) != ReplayRestoreEvent: |
1028 | + self._events.insert(0, ReplayRestoreEvent()) |
1029 | + return self._events[0] |
1030 | + |
1031 | + |
1032 | +class OrderedContext(object): |
1033 | + |
1034 | + def __init__(self, mocker): |
1035 | + self._mocker = mocker |
1036 | + |
1037 | + def __enter__(self): |
1038 | + return None |
1039 | + |
1040 | + def __exit__(self, type, value, traceback): |
1041 | + self._mocker.unorder() |
1042 | + |
1043 | + |
1044 | +class Mocker(MockerBase): |
1045 | + __doc__ = MockerBase.__doc__ |
1046 | + |
1047 | +# Decorator to add recorders on the standard Mocker class. |
1048 | +recorder = Mocker.add_recorder |
1049 | + |
1050 | + |
1051 | +# -------------------------------------------------------------------- |
1052 | +# Mock object. |
1053 | + |
1054 | +class Mock(object): |
1055 | + |
1056 | + def __init__(self, mocker, path=None, name=None, spec=None, type=None, |
1057 | + object=None, passthrough=False, patcher=None, count=True): |
1058 | + self.__mocker__ = mocker |
1059 | + self.__mocker_path__ = path or Path(self, object) |
1060 | + self.__mocker_name__ = name |
1061 | + self.__mocker_spec__ = spec |
1062 | + self.__mocker_object__ = object |
1063 | + self.__mocker_passthrough__ = passthrough |
1064 | + self.__mocker_patcher__ = patcher |
1065 | + self.__mocker_replace__ = False |
1066 | + self.__mocker_type__ = type |
1067 | + self.__mocker_count__ = count |
1068 | + |
1069 | + def __mocker_act__(self, kind, args=(), kwargs={}, object=None): |
1070 | + if self.__mocker_name__ is None: |
1071 | + self.__mocker_name__ = find_object_name(self, 2) |
1072 | + action = Action(kind, args, kwargs, self.__mocker_path__) |
1073 | + path = self.__mocker_path__ + action |
1074 | + if object is not None: |
1075 | + path.root_object = object |
1076 | + try: |
1077 | + return self.__mocker__.act(path) |
1078 | + except MatchError, exception: |
1079 | + root_mock = path.root_mock |
1080 | + if (path.root_object is not None and |
1081 | + root_mock.__mocker_passthrough__): |
1082 | + return path.execute(path.root_object) |
1083 | + # Reinstantiate to show raise statement on traceback, and |
1084 | + # also to make the traceback shown shorter. |
1085 | + raise MatchError(str(exception)) |
1086 | + except AssertionError, e: |
1087 | + lines = str(e).splitlines() |
1088 | + message = [ERROR_PREFIX + "Unmet expectation:", ""] |
1089 | + message.append("=> " + lines.pop(0)) |
1090 | + message.extend([" " + line for line in lines]) |
1091 | + message.append("") |
1092 | + raise AssertionError(os.linesep.join(message)) |
1093 | + |
1094 | + def __getattribute__(self, name): |
1095 | + if name.startswith("__mocker_"): |
1096 | + return super(Mock, self).__getattribute__(name) |
1097 | + if name == "__class__": |
1098 | + if self.__mocker__.is_recording() or self.__mocker_type__ is None: |
1099 | + return type(self) |
1100 | + return self.__mocker_type__ |
1101 | + return self.__mocker_act__("getattr", (name,)) |
1102 | + |
1103 | + def __setattr__(self, name, value): |
1104 | + if name.startswith("__mocker_"): |
1105 | + return super(Mock, self).__setattr__(name, value) |
1106 | + return self.__mocker_act__("setattr", (name, value)) |
1107 | + |
1108 | + def __delattr__(self, name): |
1109 | + return self.__mocker_act__("delattr", (name,)) |
1110 | + |
1111 | + def __call__(self, *args, **kwargs): |
1112 | + return self.__mocker_act__("call", args, kwargs) |
1113 | + |
1114 | + def __contains__(self, value): |
1115 | + return self.__mocker_act__("contains", (value,)) |
1116 | + |
1117 | + def __getitem__(self, key): |
1118 | + return self.__mocker_act__("getitem", (key,)) |
1119 | + |
1120 | + def __setitem__(self, key, value): |
1121 | + return self.__mocker_act__("setitem", (key, value)) |
1122 | + |
1123 | + def __delitem__(self, key): |
1124 | + return self.__mocker_act__("delitem", (key,)) |
1125 | + |
1126 | + def __len__(self): |
1127 | + # MatchError is turned on an AttributeError so that list() and |
1128 | + # friends act properly when trying to get length hints on |
1129 | + # something that doesn't offer them. |
1130 | + try: |
1131 | + result = self.__mocker_act__("len") |
1132 | + except MatchError, e: |
1133 | + raise AttributeError(str(e)) |
1134 | + if type(result) is Mock: |
1135 | + return 0 |
1136 | + return result |
1137 | + |
1138 | + def __nonzero__(self): |
1139 | + try: |
1140 | + return self.__mocker_act__("nonzero") |
1141 | + except MatchError, e: |
1142 | + return True |
1143 | + |
1144 | + def __iter__(self): |
1145 | + # XXX On py3k, when next() becomes __next__(), we'll be able |
1146 | + # to return the mock itself because it will be considered |
1147 | + # an iterator (we'll be mocking __next__ as well, which we |
1148 | + # can't now). |
1149 | + result = self.__mocker_act__("iter") |
1150 | + if type(result) is Mock: |
1151 | + return iter([]) |
1152 | + return result |
1153 | + |
1154 | + # When adding a new action kind here, also add support for it on |
1155 | + # Action.execute() and Path.__str__(). |
1156 | + |
1157 | + |
1158 | +def find_object_name(obj, depth=0): |
1159 | + """Try to detect how the object is named on a previous scope.""" |
1160 | + try: |
1161 | + frame = sys._getframe(depth+1) |
1162 | + except: |
1163 | + return None |
1164 | + for name, frame_obj in frame.f_locals.iteritems(): |
1165 | + if frame_obj is obj: |
1166 | + return name |
1167 | + self = frame.f_locals.get("self") |
1168 | + if self is not None: |
1169 | + try: |
1170 | + items = list(self.__dict__.iteritems()) |
1171 | + except: |
1172 | + pass |
1173 | + else: |
1174 | + for name, self_obj in items: |
1175 | + if self_obj is obj: |
1176 | + return name |
1177 | + return None |
1178 | + |
1179 | + |
1180 | +# -------------------------------------------------------------------- |
1181 | +# Action and path. |
1182 | + |
1183 | +class Action(object): |
1184 | + |
1185 | + def __init__(self, kind, args, kwargs, path=None): |
1186 | + self.kind = kind |
1187 | + self.args = args |
1188 | + self.kwargs = kwargs |
1189 | + self.path = path |
1190 | + self._execute_cache = {} |
1191 | + |
1192 | + def __repr__(self): |
1193 | + if self.path is None: |
1194 | + return "Action(%r, %r, %r)" % (self.kind, self.args, self.kwargs) |
1195 | + return "Action(%r, %r, %r, %r)" % \ |
1196 | + (self.kind, self.args, self.kwargs, self.path) |
1197 | + |
1198 | + def __eq__(self, other): |
1199 | + return (self.kind == other.kind and |
1200 | + self.args == other.args and |
1201 | + self.kwargs == other.kwargs) |
1202 | + |
1203 | + def __ne__(self, other): |
1204 | + return not self.__eq__(other) |
1205 | + |
1206 | + def matches(self, other): |
1207 | + return (self.kind == other.kind and |
1208 | + match_params(self.args, self.kwargs, other.args, other.kwargs)) |
1209 | + |
1210 | + def execute(self, object): |
1211 | + # This caching scheme may fail if the object gets deallocated before |
1212 | + # the action, as the id might get reused. It's somewhat easy to fix |
1213 | + # that with a weakref callback. For our uses, though, the object |
1214 | + # should never get deallocated before the action itself, so we'll |
1215 | + # just keep it simple. |
1216 | + if id(object) in self._execute_cache: |
1217 | + return self._execute_cache[id(object)] |
1218 | + execute = getattr(object, "__mocker_execute__", None) |
1219 | + if execute is not None: |
1220 | + result = execute(self, object) |
1221 | + else: |
1222 | + kind = self.kind |
1223 | + if kind == "getattr": |
1224 | + result = getattr(object, self.args[0]) |
1225 | + elif kind == "setattr": |
1226 | + result = setattr(object, self.args[0], self.args[1]) |
1227 | + elif kind == "delattr": |
1228 | + result = delattr(object, self.args[0]) |
1229 | + elif kind == "call": |
1230 | + result = object(*self.args, **self.kwargs) |
1231 | + elif kind == "contains": |
1232 | + result = self.args[0] in object |
1233 | + elif kind == "getitem": |
1234 | + result = object[self.args[0]] |
1235 | + elif kind == "setitem": |
1236 | + result = object[self.args[0]] = self.args[1] |
1237 | + elif kind == "delitem": |
1238 | + del object[self.args[0]] |
1239 | + result = None |
1240 | + elif kind == "len": |
1241 | + result = len(object) |
1242 | + elif kind == "nonzero": |
1243 | + result = bool(object) |
1244 | + elif kind == "iter": |
1245 | + result = iter(object) |
1246 | + else: |
1247 | + raise RuntimeError("Don't know how to execute %r kind." % kind) |
1248 | + self._execute_cache[id(object)] = result |
1249 | + return result |
1250 | + |
1251 | + |
1252 | +class Path(object): |
1253 | + |
1254 | + def __init__(self, root_mock, root_object=None, actions=()): |
1255 | + self.root_mock = root_mock |
1256 | + self.root_object = root_object |
1257 | + self.actions = tuple(actions) |
1258 | + self.__mocker_replace__ = False |
1259 | + |
1260 | + def parent_path(self): |
1261 | + if not self.actions: |
1262 | + return None |
1263 | + return self.actions[-1].path |
1264 | + parent_path = property(parent_path) |
1265 | + |
1266 | + def __add__(self, action): |
1267 | + """Return a new path which includes the given action at the end.""" |
1268 | + return self.__class__(self.root_mock, self.root_object, |
1269 | + self.actions + (action,)) |
1270 | + |
1271 | + def __eq__(self, other): |
1272 | + """Verify if the two paths are equal. |
1273 | + |
1274 | + Two paths are equal if they refer to the same mock object, and |
1275 | + have the actions with equal kind, args and kwargs. |
1276 | + """ |
1277 | + if (self.root_mock is not other.root_mock or |
1278 | + self.root_object is not other.root_object or |
1279 | + len(self.actions) != len(other.actions)): |
1280 | + return False |
1281 | + for action, other_action in zip(self.actions, other.actions): |
1282 | + if action != other_action: |
1283 | + return False |
1284 | + return True |
1285 | + |
1286 | + def matches(self, other): |
1287 | + """Verify if the two paths are equivalent. |
1288 | + |
1289 | + Two paths are equal if they refer to the same mock object, and |
1290 | + have the same actions performed on them. |
1291 | + """ |
1292 | + if (self.root_mock is not other.root_mock or |
1293 | + len(self.actions) != len(other.actions)): |
1294 | + return False |
1295 | + for action, other_action in zip(self.actions, other.actions): |
1296 | + if not action.matches(other_action): |
1297 | + return False |
1298 | + return True |
1299 | + |
1300 | + def execute(self, object): |
1301 | + """Execute all actions sequentially on object, and return result. |
1302 | + """ |
1303 | + for action in self.actions: |
1304 | + object = action.execute(object) |
1305 | + return object |
1306 | + |
1307 | + def __str__(self): |
1308 | + """Transform the path into a nice string such as obj.x.y('z').""" |
1309 | + result = self.root_mock.__mocker_name__ or "<mock>" |
1310 | + for action in self.actions: |
1311 | + if action.kind == "getattr": |
1312 | + result = "%s.%s" % (result, action.args[0]) |
1313 | + elif action.kind == "setattr": |
1314 | + result = "%s.%s = %r" % (result, action.args[0], action.args[1]) |
1315 | + elif action.kind == "delattr": |
1316 | + result = "del %s.%s" % (result, action.args[0]) |
1317 | + elif action.kind == "call": |
1318 | + args = [repr(x) for x in action.args] |
1319 | + items = list(action.kwargs.iteritems()) |
1320 | + items.sort() |
1321 | + for pair in items: |
1322 | + args.append("%s=%r" % pair) |
1323 | + result = "%s(%s)" % (result, ", ".join(args)) |
1324 | + elif action.kind == "contains": |
1325 | + result = "%r in %s" % (action.args[0], result) |
1326 | + elif action.kind == "getitem": |
1327 | + result = "%s[%r]" % (result, action.args[0]) |
1328 | + elif action.kind == "setitem": |
1329 | + result = "%s[%r] = %r" % (result, action.args[0], |
1330 | + action.args[1]) |
1331 | + elif action.kind == "delitem": |
1332 | + result = "del %s[%r]" % (result, action.args[0]) |
1333 | + elif action.kind == "len": |
1334 | + result = "len(%s)" % result |
1335 | + elif action.kind == "nonzero": |
1336 | + result = "bool(%s)" % result |
1337 | + elif action.kind == "iter": |
1338 | + result = "iter(%s)" % result |
1339 | + else: |
1340 | + raise RuntimeError("Don't know how to format kind %r" % |
1341 | + action.kind) |
1342 | + return result |
1343 | + |
1344 | + |
1345 | +class SpecialArgument(object): |
1346 | + """Base for special arguments for matching parameters.""" |
1347 | + |
1348 | + def __init__(self, object=None): |
1349 | + self.object = object |
1350 | + |
1351 | + def __repr__(self): |
1352 | + if self.object is None: |
1353 | + return self.__class__.__name__ |
1354 | + else: |
1355 | + return "%s(%r)" % (self.__class__.__name__, self.object) |
1356 | + |
1357 | + def matches(self, other): |
1358 | + return True |
1359 | + |
1360 | + def __eq__(self, other): |
1361 | + return type(other) == type(self) and self.object == other.object |
1362 | + |
1363 | + |
1364 | +class ANY(SpecialArgument): |
1365 | + """Matches any single argument.""" |
1366 | + |
1367 | +ANY = ANY() |
1368 | + |
1369 | + |
1370 | +class ARGS(SpecialArgument): |
1371 | + """Matches zero or more positional arguments.""" |
1372 | + |
1373 | +ARGS = ARGS() |
1374 | + |
1375 | + |
1376 | +class KWARGS(SpecialArgument): |
1377 | + """Matches zero or more keyword arguments.""" |
1378 | + |
1379 | +KWARGS = KWARGS() |
1380 | + |
1381 | + |
1382 | +class IS(SpecialArgument): |
1383 | + |
1384 | + def matches(self, other): |
1385 | + return self.object is other |
1386 | + |
1387 | + def __eq__(self, other): |
1388 | + return type(other) == type(self) and self.object is other.object |
1389 | + |
1390 | + |
1391 | +class CONTAINS(SpecialArgument): |
1392 | + |
1393 | + def matches(self, other): |
1394 | + try: |
1395 | + other.__contains__ |
1396 | + except AttributeError: |
1397 | + try: |
1398 | + iter(other) |
1399 | + except TypeError: |
1400 | + # If an object can't be iterated, and has no __contains__ |
1401 | + # hook, it'd blow up on the test below. We test this in |
1402 | + # advance to prevent catching more errors than we really |
1403 | + # want. |
1404 | + return False |
1405 | + return self.object in other |
1406 | + |
1407 | + |
1408 | +class IN(SpecialArgument): |
1409 | + |
1410 | + def matches(self, other): |
1411 | + return other in self.object |
1412 | + |
1413 | + |
1414 | +class MATCH(SpecialArgument): |
1415 | + |
1416 | + def matches(self, other): |
1417 | + return bool(self.object(other)) |
1418 | + |
1419 | + def __eq__(self, other): |
1420 | + return type(other) == type(self) and self.object is other.object |
1421 | + |
1422 | + |
1423 | +def match_params(args1, kwargs1, args2, kwargs2): |
1424 | + """Match the two sets of parameters, considering special parameters.""" |
1425 | + |
1426 | + has_args = ARGS in args1 |
1427 | + has_kwargs = KWARGS in args1 |
1428 | + |
1429 | + if has_kwargs: |
1430 | + args1 = [arg1 for arg1 in args1 if arg1 is not KWARGS] |
1431 | + elif len(kwargs1) != len(kwargs2): |
1432 | + return False |
1433 | + |
1434 | + if not has_args and len(args1) != len(args2): |
1435 | + return False |
1436 | + |
1437 | + # Either we have the same number of kwargs, or unknown keywords are |
1438 | + # accepted (KWARGS was used), so check just the ones in kwargs1. |
1439 | + for key, arg1 in kwargs1.iteritems(): |
1440 | + if key not in kwargs2: |
1441 | + return False |
1442 | + arg2 = kwargs2[key] |
1443 | + if isinstance(arg1, SpecialArgument): |
1444 | + if not arg1.matches(arg2): |
1445 | + return False |
1446 | + elif arg1 != arg2: |
1447 | + return False |
1448 | + |
1449 | + # Keywords match. Now either we have the same number of |
1450 | + # arguments, or ARGS was used. If ARGS wasn't used, arguments |
1451 | + # must match one-on-one necessarily. |
1452 | + if not has_args: |
1453 | + for arg1, arg2 in zip(args1, args2): |
1454 | + if isinstance(arg1, SpecialArgument): |
1455 | + if not arg1.matches(arg2): |
1456 | + return False |
1457 | + elif arg1 != arg2: |
1458 | + return False |
1459 | + return True |
1460 | + |
1461 | + # Easy choice. Keywords are matching, and anything on args is accepted. |
1462 | + if (ARGS,) == args1: |
1463 | + return True |
1464 | + |
1465 | + # We have something different there. If we don't have positional |
1466 | + # arguments on the original call, it can't match. |
1467 | + if not args2: |
1468 | + # Unless we have just several ARGS (which is bizarre, but..). |
1469 | + for arg1 in args1: |
1470 | + if arg1 is not ARGS: |
1471 | + return False |
1472 | + return True |
1473 | + |
1474 | + # Ok, all bets are lost. We have to actually do the more expensive |
1475 | + # matching. This is an algorithm based on the idea of the Levenshtein |
1476 | + # Distance between two strings, but heavily hacked for this purpose. |
1477 | + args2l = len(args2) |
1478 | + if args1[0] is ARGS: |
1479 | + args1 = args1[1:] |
1480 | + array = [0]*args2l |
1481 | + else: |
1482 | + array = [1]*args2l |
1483 | + for i in range(len(args1)): |
1484 | + last = array[0] |
1485 | + if args1[i] is ARGS: |
1486 | + for j in range(1, args2l): |
1487 | + last, array[j] = array[j], min(array[j-1], array[j], last) |
1488 | + else: |
1489 | + array[0] = i or int(args1[i] != args2[0]) |
1490 | + for j in range(1, args2l): |
1491 | + last, array[j] = array[j], last or int(args1[i] != args2[j]) |
1492 | + if 0 not in array: |
1493 | + return False |
1494 | + if array[-1] != 0: |
1495 | + return False |
1496 | + return True |
1497 | + |
1498 | + |
1499 | +# -------------------------------------------------------------------- |
1500 | +# Event and task base. |
1501 | + |
1502 | +class Event(object): |
1503 | + """Aggregation of tasks that keep track of a recorded action. |
1504 | + |
1505 | + An event represents something that may or may not happen while the |
1506 | + mocked environment is running, such as an attribute access, or a |
1507 | + method call. The event is composed of several tasks that are |
1508 | + orchestrated together to create a composed meaning for the event, |
1509 | + including for which actions it should be run, what happens when it |
1510 | + runs, and what's the expectations about the actions run. |
1511 | + """ |
1512 | + |
1513 | + def __init__(self, path=None): |
1514 | + self.path = path |
1515 | + self._tasks = [] |
1516 | + self._has_run = False |
1517 | + |
1518 | + def add_task(self, task): |
1519 | + """Add a new task to this taks.""" |
1520 | + self._tasks.append(task) |
1521 | + return task |
1522 | + |
1523 | + def remove_task(self, task): |
1524 | + self._tasks.remove(task) |
1525 | + |
1526 | + def get_tasks(self): |
1527 | + return self._tasks[:] |
1528 | + |
1529 | + def matches(self, path): |
1530 | + """Return true if *all* tasks match the given path.""" |
1531 | + for task in self._tasks: |
1532 | + if not task.matches(path): |
1533 | + return False |
1534 | + return bool(self._tasks) |
1535 | + |
1536 | + def has_run(self): |
1537 | + return self._has_run |
1538 | + |
1539 | + def may_run(self, path): |
1540 | + """Verify if any task would certainly raise an error if run. |
1541 | + |
1542 | + This will call the C{may_run()} method on each task and return |
1543 | + false if any of them returns false. |
1544 | + """ |
1545 | + for task in self._tasks: |
1546 | + if not task.may_run(path): |
1547 | + return False |
1548 | + return True |
1549 | + |
1550 | + def run(self, path): |
1551 | + """Run all tasks with the given action. |
1552 | + |
1553 | + @param path: The path of the expression run. |
1554 | + |
1555 | + Running an event means running all of its tasks individually and in |
1556 | + order. An event should only ever be run if all of its tasks claim to |
1557 | + match the given action. |
1558 | + |
1559 | + The result of this method will be the last result of a task |
1560 | + which isn't None, or None if they're all None. |
1561 | + """ |
1562 | + self._has_run = True |
1563 | + result = None |
1564 | + errors = [] |
1565 | + for task in self._tasks: |
1566 | + try: |
1567 | + task_result = task.run(path) |
1568 | + except AssertionError, e: |
1569 | + error = str(e) |
1570 | + if not error: |
1571 | + raise RuntimeError("Empty error message from %r" % task) |
1572 | + errors.append(error) |
1573 | + else: |
1574 | + if task_result is not None: |
1575 | + result = task_result |
1576 | + if errors: |
1577 | + message = [str(self.path)] |
1578 | + if str(path) != message[0]: |
1579 | + message.append("- Run: %s" % path) |
1580 | + for error in errors: |
1581 | + lines = error.splitlines() |
1582 | + message.append("- " + lines.pop(0)) |
1583 | + message.extend([" " + line for line in lines]) |
1584 | + raise AssertionError(os.linesep.join(message)) |
1585 | + return result |
1586 | + |
1587 | + def satisfied(self): |
1588 | + """Return true if all tasks are satisfied. |
1589 | + |
1590 | + Being satisfied means that there are no unmet expectations. |
1591 | + """ |
1592 | + for task in self._tasks: |
1593 | + try: |
1594 | + task.verify() |
1595 | + except AssertionError: |
1596 | + return False |
1597 | + return True |
1598 | + |
1599 | + def verify(self): |
1600 | + """Run verify on all tasks. |
1601 | + |
1602 | + The verify method is supposed to raise an AssertionError if the |
1603 | + task has unmet expectations, with a one-line explanation about |
1604 | + why this item is unmet. This method should be safe to be called |
1605 | + multiple times without side effects. |
1606 | + """ |
1607 | + errors = [] |
1608 | + for task in self._tasks: |
1609 | + try: |
1610 | + task.verify() |
1611 | + except AssertionError, e: |
1612 | + error = str(e) |
1613 | + if not error: |
1614 | + raise RuntimeError("Empty error message from %r" % task) |
1615 | + errors.append(error) |
1616 | + if errors: |
1617 | + message = [str(self.path)] |
1618 | + for error in errors: |
1619 | + lines = error.splitlines() |
1620 | + message.append("- " + lines.pop(0)) |
1621 | + message.extend([" " + line for line in lines]) |
1622 | + raise AssertionError(os.linesep.join(message)) |
1623 | + |
1624 | + def replay(self): |
1625 | + """Put all tasks in replay mode.""" |
1626 | + self._has_run = False |
1627 | + for task in self._tasks: |
1628 | + task.replay() |
1629 | + |
1630 | + def restore(self): |
1631 | + """Restore the state of all tasks.""" |
1632 | + for task in self._tasks: |
1633 | + task.restore() |
1634 | + |
1635 | + |
1636 | +class ReplayRestoreEvent(Event): |
1637 | + """Helper event for tasks which need replay/restore but shouldn't match.""" |
1638 | + |
1639 | + def matches(self, path): |
1640 | + return False |
1641 | + |
1642 | + |
1643 | +class Task(object): |
1644 | + """Element used to track one specific aspect on an event. |
1645 | + |
1646 | + A task is responsible for adding any kind of logic to an event. |
1647 | + Examples of that are counting the number of times the event was |
1648 | + made, verifying parameters if any, and so on. |
1649 | + """ |
1650 | + |
1651 | + def matches(self, path): |
1652 | + """Return true if the task is supposed to be run for the given path. |
1653 | + """ |
1654 | + return True |
1655 | + |
1656 | + def may_run(self, path): |
1657 | + """Return false if running this task would certainly raise an error.""" |
1658 | + return True |
1659 | + |
1660 | + def run(self, path): |
1661 | + """Perform the task item, considering that the given action happened. |
1662 | + """ |
1663 | + |
1664 | + def verify(self): |
1665 | + """Raise AssertionError if expectations for this item are unmet. |
1666 | + |
1667 | + The verify method is supposed to raise an AssertionError if the |
1668 | + task has unmet expectations, with a one-line explanation about |
1669 | + why this item is unmet. This method should be safe to be called |
1670 | + multiple times without side effects. |
1671 | + """ |
1672 | + |
1673 | + def replay(self): |
1674 | + """Put the task in replay mode. |
1675 | + |
1676 | + Any expectations of the task should be reset. |
1677 | + """ |
1678 | + |
1679 | + def restore(self): |
1680 | + """Restore any environmental changes made by the task. |
1681 | + |
1682 | + Verify should continue to work after this is called. |
1683 | + """ |
1684 | + |
1685 | + |
1686 | +# -------------------------------------------------------------------- |
1687 | +# Task implementations. |
1688 | + |
1689 | +class OnRestoreCaller(Task): |
1690 | + """Call a given callback when restoring.""" |
1691 | + |
1692 | + def __init__(self, callback): |
1693 | + self._callback = callback |
1694 | + |
1695 | + def restore(self): |
1696 | + self._callback() |
1697 | + |
1698 | + |
1699 | +class PathMatcher(Task): |
1700 | + """Match the action path against a given path.""" |
1701 | + |
1702 | + def __init__(self, path): |
1703 | + self.path = path |
1704 | + |
1705 | + def matches(self, path): |
1706 | + return self.path.matches(path) |
1707 | + |
1708 | +def path_matcher_recorder(mocker, event): |
1709 | + event.add_task(PathMatcher(event.path)) |
1710 | + |
1711 | +Mocker.add_recorder(path_matcher_recorder) |
1712 | + |
1713 | + |
1714 | +class RunCounter(Task): |
1715 | + """Task which verifies if the number of runs are within given boundaries. |
1716 | + """ |
1717 | + |
1718 | + def __init__(self, min, max=False): |
1719 | + self.min = min |
1720 | + if max is None: |
1721 | + self.max = sys.maxint |
1722 | + elif max is False: |
1723 | + self.max = min |
1724 | + else: |
1725 | + self.max = max |
1726 | + self._runs = 0 |
1727 | + |
1728 | + def replay(self): |
1729 | + self._runs = 0 |
1730 | + |
1731 | + def may_run(self, path): |
1732 | + return self._runs < self.max |
1733 | + |
1734 | + def run(self, path): |
1735 | + self._runs += 1 |
1736 | + if self._runs > self.max: |
1737 | + self.verify() |
1738 | + |
1739 | + def verify(self): |
1740 | + if not self.min <= self._runs <= self.max: |
1741 | + if self._runs < self.min: |
1742 | + raise AssertionError("Performed fewer times than expected.") |
1743 | + raise AssertionError("Performed more times than expected.") |
1744 | + |
1745 | + |
1746 | +class ImplicitRunCounter(RunCounter): |
1747 | + """RunCounter inserted by default on any event. |
1748 | + |
1749 | + This is a way to differentiate explicitly added counters and |
1750 | + implicit ones. |
1751 | + """ |
1752 | + |
1753 | +def run_counter_recorder(mocker, event): |
1754 | + """Any event may be repeated once, unless disabled by default.""" |
1755 | + if event.path.root_mock.__mocker_count__: |
1756 | + event.add_task(ImplicitRunCounter(1)) |
1757 | + |
1758 | +Mocker.add_recorder(run_counter_recorder) |
1759 | + |
1760 | +def run_counter_removal_recorder(mocker, event): |
1761 | + """ |
1762 | + Events created by getattr actions which lead to other events |
1763 | + may be repeated any number of times. For that, we remove implicit |
1764 | + run counters of any getattr actions leading to the current one. |
1765 | + """ |
1766 | + parent_path = event.path.parent_path |
1767 | + for event in mocker.get_events()[::-1]: |
1768 | + if (event.path is parent_path and |
1769 | + event.path.actions[-1].kind == "getattr"): |
1770 | + for task in event.get_tasks(): |
1771 | + if type(task) is ImplicitRunCounter: |
1772 | + event.remove_task(task) |
1773 | + |
1774 | +Mocker.add_recorder(run_counter_removal_recorder) |
1775 | + |
1776 | + |
1777 | +class MockReturner(Task): |
1778 | + """Return a mock based on the action path.""" |
1779 | + |
1780 | + def __init__(self, mocker): |
1781 | + self.mocker = mocker |
1782 | + |
1783 | + def run(self, path): |
1784 | + return Mock(self.mocker, path) |
1785 | + |
1786 | +def mock_returner_recorder(mocker, event): |
1787 | + """Events that lead to other events must return mock objects.""" |
1788 | + parent_path = event.path.parent_path |
1789 | + for event in mocker.get_events(): |
1790 | + if event.path is parent_path: |
1791 | + for task in event.get_tasks(): |
1792 | + if isinstance(task, MockReturner): |
1793 | + break |
1794 | + else: |
1795 | + event.add_task(MockReturner(mocker)) |
1796 | + break |
1797 | + |
1798 | +Mocker.add_recorder(mock_returner_recorder) |
1799 | + |
1800 | + |
1801 | +class FunctionRunner(Task): |
1802 | + """Task that runs a function everything it's run. |
1803 | + |
1804 | + Arguments of the last action in the path are passed to the function, |
1805 | + and the function result is also returned. |
1806 | + """ |
1807 | + |
1808 | + def __init__(self, func): |
1809 | + self._func = func |
1810 | + |
1811 | + def run(self, path): |
1812 | + action = path.actions[-1] |
1813 | + return self._func(*action.args, **action.kwargs) |
1814 | + |
1815 | + |
1816 | +class PathExecuter(Task): |
1817 | + """Task that executes a path in the real object, and returns the result.""" |
1818 | + |
1819 | + def __init__(self, result_callback=None): |
1820 | + self._result_callback = result_callback |
1821 | + |
1822 | + def get_result_callback(self): |
1823 | + return self._result_callback |
1824 | + |
1825 | + def run(self, path): |
1826 | + result = path.execute(path.root_object) |
1827 | + if self._result_callback is not None: |
1828 | + self._result_callback(result) |
1829 | + return result |
1830 | + |
1831 | + |
1832 | +class Orderer(Task): |
1833 | + """Task to establish an order relation between two events. |
1834 | + |
1835 | + An orderer task will only match once all its dependencies have |
1836 | + been run. |
1837 | + """ |
1838 | + |
1839 | + def __init__(self, path): |
1840 | + self.path = path |
1841 | + self._run = False |
1842 | + self._dependencies = [] |
1843 | + |
1844 | + def replay(self): |
1845 | + self._run = False |
1846 | + |
1847 | + def has_run(self): |
1848 | + return self._run |
1849 | + |
1850 | + def may_run(self, path): |
1851 | + for dependency in self._dependencies: |
1852 | + if not dependency.has_run(): |
1853 | + return False |
1854 | + return True |
1855 | + |
1856 | + def run(self, path): |
1857 | + for dependency in self._dependencies: |
1858 | + if not dependency.has_run(): |
1859 | + raise AssertionError("Should be after: %s" % dependency.path) |
1860 | + self._run = True |
1861 | + |
1862 | + def add_dependency(self, orderer): |
1863 | + self._dependencies.append(orderer) |
1864 | + |
1865 | + def get_dependencies(self): |
1866 | + return self._dependencies |
1867 | + |
1868 | + |
1869 | +class SpecChecker(Task): |
1870 | + """Task to check if arguments of the last action conform to a real method. |
1871 | + """ |
1872 | + |
1873 | + def __init__(self, method): |
1874 | + self._method = method |
1875 | + self._unsupported = False |
1876 | + |
1877 | + if method: |
1878 | + try: |
1879 | + self._args, self._varargs, self._varkwargs, self._defaults = \ |
1880 | + inspect.getargspec(method) |
1881 | + except TypeError: |
1882 | + self._unsupported = True |
1883 | + else: |
1884 | + if self._defaults is None: |
1885 | + self._defaults = () |
1886 | + if type(method) is type(self.run): |
1887 | + self._args = self._args[1:] |
1888 | + |
1889 | + def get_method(self): |
1890 | + return self._method |
1891 | + |
1892 | + def _raise(self, message): |
1893 | + spec = inspect.formatargspec(self._args, self._varargs, |
1894 | + self._varkwargs, self._defaults) |
1895 | + raise AssertionError("Specification is %s%s: %s" % |
1896 | + (self._method.__name__, spec, message)) |
1897 | + |
1898 | + def verify(self): |
1899 | + if not self._method: |
1900 | + raise AssertionError("Method not found in real specification") |
1901 | + |
1902 | + def may_run(self, path): |
1903 | + try: |
1904 | + self.run(path) |
1905 | + except AssertionError: |
1906 | + return False |
1907 | + return True |
1908 | + |
1909 | + def run(self, path): |
1910 | + if not self._method: |
1911 | + raise AssertionError("Method not found in real specification") |
1912 | + if self._unsupported: |
1913 | + return # Can't check it. Happens with builtin functions. :-( |
1914 | + action = path.actions[-1] |
1915 | + obtained_len = len(action.args) |
1916 | + obtained_kwargs = action.kwargs.copy() |
1917 | + nodefaults_len = len(self._args) - len(self._defaults) |
1918 | + for i, name in enumerate(self._args): |
1919 | + if i < obtained_len and name in action.kwargs: |
1920 | + self._raise("%r provided twice" % name) |
1921 | + if (i >= obtained_len and i < nodefaults_len and |
1922 | + name not in action.kwargs): |
1923 | + self._raise("%r not provided" % name) |
1924 | + obtained_kwargs.pop(name, None) |
1925 | + if obtained_len > len(self._args) and not self._varargs: |
1926 | + self._raise("too many args provided") |
1927 | + if obtained_kwargs and not self._varkwargs: |
1928 | + self._raise("unknown kwargs: %s" % ", ".join(obtained_kwargs)) |
1929 | + |
1930 | +def spec_checker_recorder(mocker, event): |
1931 | + spec = event.path.root_mock.__mocker_spec__ |
1932 | + if spec: |
1933 | + actions = event.path.actions |
1934 | + if len(actions) == 1: |
1935 | + if actions[0].kind == "call": |
1936 | + method = getattr(spec, "__call__", None) |
1937 | + event.add_task(SpecChecker(method)) |
1938 | + elif len(actions) == 2: |
1939 | + if actions[0].kind == "getattr" and actions[1].kind == "call": |
1940 | + method = getattr(spec, actions[0].args[0], None) |
1941 | + event.add_task(SpecChecker(method)) |
1942 | + |
1943 | +Mocker.add_recorder(spec_checker_recorder) |
1944 | + |
1945 | + |
1946 | +class ProxyReplacer(Task): |
1947 | + """Task which installs and deinstalls proxy mocks. |
1948 | + |
1949 | + This task will replace a real object by a mock in all dictionaries |
1950 | + found in the running interpreter via the garbage collecting system. |
1951 | + """ |
1952 | + |
1953 | + def __init__(self, mock): |
1954 | + self.mock = mock |
1955 | + self.__mocker_replace__ = False |
1956 | + |
1957 | + def replay(self): |
1958 | + global_replace(self.mock.__mocker_object__, self.mock) |
1959 | + |
1960 | + def restore(self): |
1961 | + global_replace(self.mock, self.mock.__mocker_object__) |
1962 | + |
1963 | + |
1964 | +def global_replace(remove, install): |
1965 | + """Replace object 'remove' with object 'install' on all dictionaries.""" |
1966 | + for referrer in gc.get_referrers(remove): |
1967 | + if (type(referrer) is dict and |
1968 | + referrer.get("__mocker_replace__", True)): |
1969 | + for key, value in referrer.items(): |
1970 | + if value is remove: |
1971 | + referrer[key] = install |
1972 | + |
1973 | + |
1974 | +class Undefined(object): |
1975 | + |
1976 | + def __repr__(self): |
1977 | + return "Undefined" |
1978 | + |
1979 | +Undefined = Undefined() |
1980 | + |
1981 | + |
1982 | +class Patcher(Task): |
1983 | + |
1984 | + def __init__(self): |
1985 | + super(Patcher, self).__init__() |
1986 | + self._monitored = {} # {kind: {id(object): object}} |
1987 | + self._patched = {} |
1988 | + |
1989 | + def is_monitoring(self, obj, kind): |
1990 | + monitored = self._monitored.get(kind) |
1991 | + if monitored: |
1992 | + if id(obj) in monitored: |
1993 | + return True |
1994 | + cls = type(obj) |
1995 | + if issubclass(cls, type): |
1996 | + cls = obj |
1997 | + bases = set([id(base) for base in cls.__mro__]) |
1998 | + bases.intersection_update(monitored) |
1999 | + return bool(bases) |
2000 | + return False |
2001 | + |
2002 | + def monitor(self, obj, kind): |
2003 | + if kind not in self._monitored: |
2004 | + self._monitored[kind] = {} |
2005 | + self._monitored[kind][id(obj)] = obj |
2006 | + |
2007 | + def patch_attr(self, obj, attr, value): |
2008 | + original = obj.__dict__.get(attr, Undefined) |
2009 | + self._patched[id(obj), attr] = obj, attr, original |
2010 | + setattr(obj, attr, value) |
2011 | + |
2012 | + def get_unpatched_attr(self, obj, attr): |
2013 | + cls = type(obj) |
2014 | + if issubclass(cls, type): |
2015 | + cls = obj |
2016 | + result = Undefined |
2017 | + for mro_cls in cls.__mro__: |
2018 | + key = (id(mro_cls), attr) |
2019 | + if key in self._patched: |
2020 | + result = self._patched[key][2] |
2021 | + if result is not Undefined: |
2022 | + break |
2023 | + elif attr in mro_cls.__dict__: |
2024 | + result = mro_cls.__dict__.get(attr, Undefined) |
2025 | + break |
2026 | + if isinstance(result, object) and hasattr(type(result), "__get__"): |
2027 | + if cls is obj: |
2028 | + obj = None |
2029 | + return result.__get__(obj, cls) |
2030 | + return result |
2031 | + |
2032 | + def _get_kind_attr(self, kind): |
2033 | + if kind == "getattr": |
2034 | + return "__getattribute__" |
2035 | + return "__%s__" % kind |
2036 | + |
2037 | + def replay(self): |
2038 | + for kind in self._monitored: |
2039 | + attr = self._get_kind_attr(kind) |
2040 | + seen = set() |
2041 | + for obj in self._monitored[kind].itervalues(): |
2042 | + cls = type(obj) |
2043 | + if issubclass(cls, type): |
2044 | + cls = obj |
2045 | + if cls not in seen: |
2046 | + seen.add(cls) |
2047 | + unpatched = getattr(cls, attr, Undefined) |
2048 | + self.patch_attr(cls, attr, |
2049 | + PatchedMethod(kind, unpatched, |
2050 | + self.is_monitoring)) |
2051 | + self.patch_attr(cls, "__mocker_execute__", |
2052 | + self.execute) |
2053 | + |
2054 | + def restore(self): |
2055 | + for obj, attr, original in self._patched.itervalues(): |
2056 | + if original is Undefined: |
2057 | + delattr(obj, attr) |
2058 | + else: |
2059 | + setattr(obj, attr, original) |
2060 | + self._patched.clear() |
2061 | + |
2062 | + def execute(self, action, object): |
2063 | + attr = self._get_kind_attr(action.kind) |
2064 | + unpatched = self.get_unpatched_attr(object, attr) |
2065 | + try: |
2066 | + return unpatched(*action.args, **action.kwargs) |
2067 | + except AttributeError: |
2068 | + if action.kind == "getattr": |
2069 | + # The normal behavior of Python is to try __getattribute__, |
2070 | + # and if it raises AttributeError, try __getattr__. We've |
2071 | + # tried the unpatched __getattribute__ above, and we'll now |
2072 | + # try __getattr__. |
2073 | + try: |
2074 | + __getattr__ = unpatched("__getattr__") |
2075 | + except AttributeError: |
2076 | + pass |
2077 | + else: |
2078 | + return __getattr__(*action.args, **action.kwargs) |
2079 | + raise |
2080 | + |
2081 | + |
2082 | +class PatchedMethod(object): |
2083 | + |
2084 | + def __init__(self, kind, unpatched, is_monitoring): |
2085 | + self._kind = kind |
2086 | + self._unpatched = unpatched |
2087 | + self._is_monitoring = is_monitoring |
2088 | + |
2089 | + def __get__(self, obj, cls=None): |
2090 | + object = obj or cls |
2091 | + if not self._is_monitoring(object, self._kind): |
2092 | + return self._unpatched.__get__(obj, cls) |
2093 | + def method(*args, **kwargs): |
2094 | + if self._kind == "getattr" and args[0].startswith("__mocker_"): |
2095 | + return self._unpatched.__get__(obj, cls)(args[0]) |
2096 | + mock = object.__mocker_mock__ |
2097 | + return mock.__mocker_act__(self._kind, args, kwargs, object) |
2098 | + return method |
2099 | + |
2100 | + def __call__(self, obj, *args, **kwargs): |
2101 | + # At least with __getattribute__, Python seems to use *both* the |
2102 | + # descriptor API and also call the class attribute directly. It |
2103 | + # looks like an interpreter bug, or at least an undocumented |
2104 | + # inconsistency. |
2105 | + return self.__get__(obj)(*args, **kwargs) |
2106 | + |
2107 | + |
2108 | +def patcher_recorder(mocker, event): |
2109 | + mock = event.path.root_mock |
2110 | + if mock.__mocker_patcher__ and len(event.path.actions) == 1: |
2111 | + patcher = mock.__mocker_patcher__ |
2112 | + patcher.monitor(mock.__mocker_object__, event.path.actions[0].kind) |
2113 | + |
2114 | +Mocker.add_recorder(patcher_recorder) |
2115 | |
2116 | === modified file 'desktopcouch/__init__.py' |
2117 | --- desktopcouch/__init__.py 2009-08-04 11:15:52 +0000 |
2118 | +++ desktopcouch/__init__.py 2009-08-07 12:31:27 +0000 |
2119 | @@ -16,14 +16,12 @@ |
2120 | "Desktop Couch helper files" |
2121 | |
2122 | from __future__ import with_statement |
2123 | -import os |
2124 | -import re |
2125 | -import errno |
2126 | -import time |
2127 | +import os, re, errno, time, fcntl |
2128 | + |
2129 | |
2130 | def find_pid(): |
2131 | - # Work out whether CouchDB is running by looking at its pid file |
2132 | from desktopcouch import local_files |
2133 | + |
2134 | pid = '' |
2135 | try: |
2136 | fp = open(local_files.FILE_PID) |
2137 | @@ -39,6 +37,8 @@ |
2138 | from desktopcouch import start_local_couchdb |
2139 | start_local_couchdb.start_couchdb() |
2140 | time.sleep(2) # give the process a chance to start |
2141 | + # now load the design documents, because it's started |
2142 | + start_local_couchdb.update_design_documents() |
2143 | |
2144 | # get the pid |
2145 | try: |
2146 | |
2147 | === modified file 'desktopcouch/start_local_couchdb.py' |
2148 | --- desktopcouch/start_local_couchdb.py 2009-07-27 18:16:19 +0000 |
2149 | +++ desktopcouch/start_local_couchdb.py 2009-08-07 12:33:43 +0000 |
2150 | @@ -32,11 +32,12 @@ |
2151 | """ |
2152 | |
2153 | from __future__ import with_statement |
2154 | -import os, subprocess, sys |
2155 | +import os, subprocess, sys, glob |
2156 | import desktopcouch |
2157 | from desktopcouch import local_files |
2158 | import xdg.BaseDirectory |
2159 | import time |
2160 | +from desktopcouch.records.server import CouchDatabase |
2161 | |
2162 | def dump_ini(data, filename): |
2163 | """Dump INI data with sorted sections and keywords""" |
2164 | @@ -99,8 +100,43 @@ |
2165 | exit(1) |
2166 | |
2167 | def update_design_documents(): |
2168 | - """Check system design documents and update any that need updating""" |
2169 | - pass |
2170 | + """Check system design documents and update any that need updating |
2171 | + |
2172 | + A database should be created if |
2173 | + $XDG_DATA_DIRs/desktop-couch/databases/dbname/database.cfg exists |
2174 | + Design docs are defined by the existence of |
2175 | + $XDG_DATA_DIRs/desktop-couch/databases/dbname/_design/designdocname/views/viewname/map.js |
2176 | + reduce.js may also exist in the same folder. |
2177 | + """ |
2178 | + for base in xdg.BaseDirectory.xdg_data_dirs: |
2179 | + db_spec = os.path.join(base, "desktop-couch", "databases", "*", "database.cfg") |
2180 | + for database_path in glob.glob(db_spec): |
2181 | + database_root = os.path.split(database_path)[0] |
2182 | + database_name = os.path.split(database_root)[1] |
2183 | + # Just the presence of database.cfg is enough to create the database |
2184 | + db = CouchDatabase(database_name, create=True) |
2185 | + # look for design documents |
2186 | + dd_spec = os.path.join(database_root, "_design", "*", "views", "*", "map.js") |
2187 | + for dd_path in glob.glob(dd_spec): |
2188 | + view_root = os.path.split(dd_path)[0] |
2189 | + view_name = os.path.split(view_root)[1] |
2190 | + dd_root = os.path.split(os.path.split(view_root)[0])[0] |
2191 | + dd_name = os.path.split(dd_root)[1] |
2192 | + |
2193 | + def load_js_file(filename_no_extension): |
2194 | + fn = os.path.join(view_root, "%s.js" % (filename_no_extension)) |
2195 | + if not os.path.isfile(fn): return None |
2196 | + fp = open(fn) |
2197 | + data = fp.read() |
2198 | + fp.close() |
2199 | + return data |
2200 | + |
2201 | + mapjs = load_js_file("map") |
2202 | + reducejs = load_js_file("reduce") |
2203 | + |
2204 | + # XXX check whether this already exists or not, rather |
2205 | + # than inefficiently just overwriting it regardless |
2206 | + db.add_view(view_name, mapjs, reducejs, dd_name) |
2207 | |
2208 | def write_bookmark_file(): |
2209 | """Write out an HTML document that the user can bookmark to find their DB""" |
2210 | @@ -132,7 +168,11 @@ |
2211 | """Execute each step to start a desktop CouchDB""" |
2212 | create_ini_file() |
2213 | run_couchdb() |
2214 | - update_design_documents() |
2215 | + # Note that we do not call update_design_documents here. This is because |
2216 | + # Couch won't actually have started yet, so when update_design_documents |
2217 | + # calls the Records API, that will call back into get_pid and we end up |
2218 | + # starting Couch again. Instead, get_pid calls update_design_documents |
2219 | + # *after* Couch startup has occurred. |
2220 | write_bookmark_file() |
2221 | |
2222 | if __name__ == "__main__": |
2223 | |
2224 | === added file 'desktopcouch/tests/test_start_local_couchdb.py' |
2225 | --- desktopcouch/tests/test_start_local_couchdb.py 1970-01-01 00:00:00 +0000 |
2226 | +++ desktopcouch/tests/test_start_local_couchdb.py 2009-08-06 13:57:30 +0000 |
2227 | @@ -0,0 +1,134 @@ |
2228 | +"""testing desktopcouch/local_files.py module""" |
2229 | + |
2230 | +from twisted.trial.unittest import TestCase as TwistedTestCase |
2231 | +import os, tempfile, sys |
2232 | +import desktopcouch |
2233 | +sys.path.append(os.path.join(os.path.split(desktopcouch.__file__)[0], "..", "contrib")) |
2234 | +from mocker import Mocker |
2235 | + |
2236 | +class TestUpdateDesignDocuments(TwistedTestCase): |
2237 | + """Testing that the database/designdoc filesystem loader works""" |
2238 | + |
2239 | + def setUp(self): |
2240 | + # create temp folder with databases and design documents in |
2241 | + dc_root = os.path.split(desktopcouch.__file__)[0] |
2242 | + branch_root = os.path.split(dc_root)[0] |
2243 | + self.tmpdir = tempfile.mkdtemp(dir=branch_root) |
2244 | + self.basedir = os.path.join(self.tmpdir, "desktop-couch") |
2245 | + os.mkdir(self.basedir) |
2246 | + DIRS = [ |
2247 | + "databases", |
2248 | + "databases/nocfg", |
2249 | + "databases/cfg", |
2250 | + "databases/cfg_and_empty_design", |
2251 | + "databases/cfg_and_empty_design/_design", |
2252 | + "databases/cfg_and_design_no_views", |
2253 | + "databases/cfg_and_design_no_views/_design", |
2254 | + "databases/cfg_and_design_no_views/_design/doc1", |
2255 | + "databases/cfg_and_design_no_views/_design/doc1/views", |
2256 | + "databases/cfg_and_design_one_view_no_map", |
2257 | + "databases/cfg_and_design_one_view_no_map/_design", |
2258 | + "databases/cfg_and_design_one_view_no_map/_design/doc1", |
2259 | + "databases/cfg_and_design_one_view_no_map/_design/doc1/views", |
2260 | + "databases/cfg_and_design_one_view_no_map/_design/doc1/views/view1", |
2261 | + "databases/cfg_and_design_one_view_map_no_reduce", |
2262 | + "databases/cfg_and_design_one_view_map_no_reduce/_design", |
2263 | + "databases/cfg_and_design_one_view_map_no_reduce/_design/doc1", |
2264 | + "databases/cfg_and_design_one_view_map_no_reduce/_design/doc1/views", |
2265 | + "databases/cfg_and_design_one_view_map_no_reduce/_design/doc1/views/view1", |
2266 | + "databases/cfg_and_design_one_view_map_reduce", |
2267 | + "databases/cfg_and_design_one_view_map_reduce/_design", |
2268 | + "databases/cfg_and_design_one_view_map_reduce/_design/doc1", |
2269 | + "databases/cfg_and_design_one_view_map_reduce/_design/doc1/views", |
2270 | + "databases/cfg_and_design_one_view_map_reduce/_design/doc1/views/view1", |
2271 | + "databases/cfg_and_design_two_views_map_reduce", |
2272 | + "databases/cfg_and_design_two_views_map_reduce/_design", |
2273 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1", |
2274 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views", |
2275 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views/view1", |
2276 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views/view2", |
2277 | + ] |
2278 | + FILES = { |
2279 | + "databases/cfg/database.cfg": "", |
2280 | + "databases/cfg_and_empty_design/database.cfg": "", |
2281 | + "databases/cfg_and_design_no_views/database.cfg": "", |
2282 | + "databases/cfg_and_design_one_view_no_map/database.cfg": "", |
2283 | + "databases/cfg_and_design_one_view_map_no_reduce/database.cfg": "", |
2284 | + "databases/cfg_and_design_one_view_map_no_reduce/_design/doc1/views/view1/map.js": |
2285 | + "cfg_and_design_one_view_map_no_reduce:map", |
2286 | + "databases/cfg_and_design_one_view_map_reduce/database.cfg": "", |
2287 | + "databases/cfg_and_design_one_view_map_reduce/_design/doc1/views/view1/map.js": |
2288 | + "cfg_and_design_one_view_map_reduce:map", |
2289 | + "databases/cfg_and_design_one_view_map_reduce/_design/doc1/views/view1/reduce.js": |
2290 | + "cfg_and_design_one_view_map_reduce:reduce", |
2291 | + |
2292 | + "databases/cfg_and_design_two_views_map_reduce/database.cfg": "", |
2293 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views/view1/map.js": |
2294 | + "cfg_and_design_two_views_map_reduce:map1", |
2295 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views/view1/reduce.js": |
2296 | + "cfg_and_design_two_views_map_reduce:reduce1", |
2297 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views/view2/map.js": |
2298 | + "cfg_and_design_two_views_map_reduce:map2", |
2299 | + "databases/cfg_and_design_two_views_map_reduce/_design/doc1/views/view2/reduce.js": |
2300 | + "cfg_and_design_two_views_map_reduce:reduce2", |
2301 | + } |
2302 | + for d in DIRS: |
2303 | + os.mkdir(os.path.join(self.basedir, d)) |
2304 | + for f, data in FILES.items(): |
2305 | + fp = open(os.path.join(self.basedir, f), "w") |
2306 | + fp.write(data) |
2307 | + fp.close() |
2308 | + |
2309 | + def tearDown(self): |
2310 | + # delete temp folder |
2311 | + for root, dirs, files in os.walk(self.tmpdir, topdown=False): |
2312 | + for name in files: |
2313 | + os.remove(os.path.join(root, name)) |
2314 | + for name in dirs: |
2315 | + os.rmdir(os.path.join(root, name)) |
2316 | + os.rmdir(self.tmpdir) |
2317 | + |
2318 | + def test_create_databases_and_design_docs(self): |
2319 | + """Are databases and design documents correctly |
2320 | + created from the filesystem?""" |
2321 | + # poke local_files so that it returns our temp folder |
2322 | + os.environ['XDG_DATA_HOME'] = self.tmpdir |
2323 | + os.environ['XDG_DATA_DIRS'] = '' |
2324 | + |
2325 | + # Mock CouchDatabase |
2326 | + mocker = Mocker() |
2327 | + couchdb = mocker.replace("desktopcouch.records.server.CouchDatabase") |
2328 | + |
2329 | + # databases that should be created |
2330 | + couchdb("cfg", create=True) |
2331 | + couchdb("cfg_and_empty_design", create=True) |
2332 | + couchdb("cfg_and_design_no_views", create=True) |
2333 | + couchdb("cfg_and_design_one_view_no_map", create=True) |
2334 | + couchdb("cfg_and_design_one_view_map_no_reduce", create=True) |
2335 | + dbmock1 = mocker.mock() |
2336 | + mocker.result(dbmock1) |
2337 | + dbmock1.add_view("view1", "cfg_and_design_one_view_map_no_reduce:map", |
2338 | + None, "doc1") |
2339 | + couchdb("cfg_and_design_one_view_map_reduce", create=True) |
2340 | + dbmock2 = mocker.mock() |
2341 | + mocker.result(dbmock2) |
2342 | + dbmock2.add_view("view1", "cfg_and_design_one_view_map_reduce:map", |
2343 | + "cfg_and_design_one_view_map_reduce:reduce", "doc1") |
2344 | + couchdb("cfg_and_design_two_views_map_reduce", create=True) |
2345 | + dbmock3 = mocker.mock() |
2346 | + mocker.result(dbmock3) |
2347 | + dbmock3.add_view("view1", "cfg_and_design_two_views_map_reduce:map1", |
2348 | + "cfg_and_design_two_views_map_reduce:reduce1", "doc1") |
2349 | + dbmock3.add_view("view2", "cfg_and_design_two_views_map_reduce:map2", |
2350 | + "cfg_and_design_two_views_map_reduce:reduce2", "doc1") |
2351 | + |
2352 | + # actually call update_design_documents to confirm that it creates |
2353 | + # all the right things |
2354 | + mocker.replay() |
2355 | + from desktopcouch.start_local_couchdb import update_design_documents |
2356 | + update_design_documents() |
2357 | + |
2358 | + mocker.restore() |
2359 | + mocker.verify() |
2360 | + |
2361 | + |
Load design documents on startup. Documentation for how to place a design document on the filesystem.