Merge lp:~spiv/loggerhead/jsonify into lp:loggerhead
- jsonify
- Merge into trunk-rich
Status: | Merged |
---|---|
Merged at revision: | 451 |
Proposed branch: | lp:~spiv/loggerhead/jsonify |
Merge into: | lp:loggerhead |
Diff against target: |
701 lines (+291/-129) 9 files modified
loggerhead/apps/branch.py (+3/-0) loggerhead/controllers/__init__.py (+14/-4) loggerhead/controllers/filediff_ui.py (+1/-1) loggerhead/controllers/inventory_ui.py (+32/-19) loggerhead/controllers/revision_ui.py (+63/-44) loggerhead/controllers/revlog_ui.py (+1/-4) loggerhead/tests/test_controllers.py (+136/-57) loggerhead/tests/test_simple.py (+31/-0) loggerhead/util.py (+10/-0) |
To merge this branch: | bzr merge lp:~spiv/loggerhead/jsonify |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vincent Ladeuil (community) | Approve | ||
Review via email: mp+66177@code.launchpad.net |
Commit message
Expose /+json URLs for getting machine readable content for various pages.
Description of the change
This adds raw JSON output from special requests to Loggerhead. For controllers that support it, you can insert /+json into the URL to get the raw form (eg, /revision/head: is the HTML page and /+json/
To get there:
1) Refactor TemplatedBranchView children to make it clearer what values are "data" and what values are there just to support template expansion (turning paths into URLs, etc.) In general we don't return any URLs in the JSON, we return the rev-ids and file-ids and paths, and clients can build up URLs. This was the way to avoid issues with stuff like URL prefixes.
2) Lots of Loggerhead code likes to return "Container" objects (which are basically wrappers around dicts so you can say foo.attrib rather than foo['attrib'].) This needed JSON encoding, which is a pretty trivial wrapper.
3) Add a flag so that you can't ask for /+json for URLs that we haven't fixed yet. This is things like /changes, because we don't yet have a great answer for what data we actually want to return there. And for ones like /download where it is already just returning the raw content bytes, no need to wrap them.
4) Add decent smoke test coverage. To determine that JSON apis don't explode, and that get_values has content that seems reasonable.
Preview Diff
1 | === modified file 'loggerhead/apps/branch.py' |
2 | --- loggerhead/apps/branch.py 2011-06-28 13:13:05 +0000 |
3 | +++ loggerhead/apps/branch.py 2011-06-28 16:21:27 +0000 |
4 | @@ -164,6 +164,9 @@ |
5 | self.absolute_url('/changes')) |
6 | if path == 'static': |
7 | return static_app |
8 | + elif path == '+json': |
9 | + environ['loggerhead.as_json'] = True |
10 | + path = request.path_info_pop(environ) |
11 | cls = self.controllers_dict.get(path) |
12 | if cls is None: |
13 | raise httpexceptions.HTTPNotFound() |
14 | |
15 | === modified file 'loggerhead/controllers/__init__.py' |
16 | --- loggerhead/controllers/__init__.py 2011-06-28 13:13:05 +0000 |
17 | +++ loggerhead/controllers/__init__.py 2011-06-28 16:21:27 +0000 |
18 | @@ -18,6 +18,7 @@ |
19 | # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
20 | |
21 | import bzrlib.errors |
22 | +import simplejson |
23 | import time |
24 | |
25 | from paste.httpexceptions import HTTPNotFound |
26 | @@ -53,6 +54,7 @@ |
27 | class TemplatedBranchView(object): |
28 | |
29 | template_path = None |
30 | + supports_json = False |
31 | |
32 | def __init__(self, branch, history_callable): |
33 | self._branch = branch |
34 | @@ -95,13 +97,17 @@ |
35 | |
36 | def __call__(self, environ, start_response): |
37 | z = time.time() |
38 | + if environ.get('loggerhead.as_json') and not self.supports_json: |
39 | + raise HTTPNotFound |
40 | path = self.parse_args(environ) |
41 | headers = {} |
42 | values = self.get_values(path, self.kwargs, headers) |
43 | |
44 | self.log.info('Getting information for %s: %.3f secs' % ( |
45 | self.__class__.__name__, time.time() - z)) |
46 | - if 'Content-Type' not in headers: |
47 | + if environ.get('loggerhead.as_json'): |
48 | + headers['Content-Type'] = 'application/json' |
49 | + elif 'Content-Type' not in headers: |
50 | headers['Content-Type'] = 'text/html' |
51 | writer = start_response("200 OK", headers.items()) |
52 | if environ.get('REQUEST_METHOD') == 'HEAD': |
53 | @@ -109,9 +115,13 @@ |
54 | return [] |
55 | z = time.time() |
56 | w = BufferingWriter(writer, 8192) |
57 | - self.add_template_values(values) |
58 | - template = load_template(self.template_path) |
59 | - template.expand_into(w, **values) |
60 | + if environ.get('loggerhead.as_json'): |
61 | + w.write(simplejson.dumps(values, |
62 | + default=util.convert_to_json_ready)) |
63 | + else: |
64 | + self.add_template_values(values) |
65 | + template = load_template(self.template_path) |
66 | + template.expand_into(w, **values) |
67 | w.flush() |
68 | self.log.info( |
69 | 'Rendering %s: %.3f secs, %s bytes' % ( |
70 | |
71 | === modified file 'loggerhead/controllers/filediff_ui.py' |
72 | --- loggerhead/controllers/filediff_ui.py 2009-07-07 23:52:24 +0000 |
73 | +++ loggerhead/controllers/filediff_ui.py 2011-06-28 16:21:27 +0000 |
74 | @@ -77,6 +77,7 @@ |
75 | class FileDiffUI(TemplatedBranchView): |
76 | |
77 | template_path = 'loggerhead.templates.filediff' |
78 | + supports_json = True |
79 | |
80 | def get_values(self, path, kwargs, headers): |
81 | revid = urllib.unquote(self.args[0]) |
82 | @@ -87,6 +88,5 @@ |
83 | self._history._branch.repository, file_id, compare_revid, revid) |
84 | |
85 | return { |
86 | - 'util': util, |
87 | 'chunks': chunks, |
88 | } |
89 | |
90 | === modified file 'loggerhead/controllers/inventory_ui.py' |
91 | --- loggerhead/controllers/inventory_ui.py 2010-07-12 15:12:04 +0000 |
92 | +++ loggerhead/controllers/inventory_ui.py 2011-06-28 16:21:27 +0000 |
93 | @@ -42,6 +42,7 @@ |
94 | class InventoryUI(TemplatedBranchView): |
95 | |
96 | template_path = 'loggerhead.templates.inventory' |
97 | + supports_json = True |
98 | |
99 | def get_filelist(self, inv, path, sort_type, revno_url): |
100 | """ |
101 | @@ -78,6 +79,11 @@ |
102 | absolutepath = path + '/' + pathname |
103 | revid = entry.revision |
104 | |
105 | + # TODO: For the JSON rendering, this inlines the "change" aka |
106 | + # revision information attached to each file. Consider either |
107 | + # pulling this out as a separate changes dict, or possibly just |
108 | + # including the revision id and having a separate request to get |
109 | + # back the revision info. |
110 | file = util.Container( |
111 | filename=filename, executable=entry.executable, |
112 | kind=entry.kind, absolutepath=absolutepath, |
113 | @@ -110,9 +116,6 @@ |
114 | start_revid = kwargs.get('start_revid', None) |
115 | sort_type = kwargs.get('sort', 'filename') |
116 | |
117 | - # no navbar for revisions |
118 | - navigation = util.Container() |
119 | - |
120 | if path is not None: |
121 | path = path.rstrip('/') |
122 | file_id = rev_tree.path2id(path) |
123 | @@ -133,14 +136,7 @@ |
124 | else: |
125 | updir = dirname(path) |
126 | |
127 | - # Directory Breadcrumbs |
128 | - directory_breadcrumbs = util.directory_breadcrumbs( |
129 | - self._branch.friendly_name, |
130 | - self._branch.is_root, |
131 | - 'files') |
132 | - |
133 | if not is_null_rev(revid): |
134 | - |
135 | change = history.get_changes([ revid ])[0] |
136 | # If we're looking at the tip, use head: in the URL instead |
137 | if revid == branch.last_revision(): |
138 | @@ -148,32 +144,49 @@ |
139 | else: |
140 | revno_url = history.get_revno(revid) |
141 | history.add_branch_nicks(change) |
142 | - |
143 | - # Create breadcrumb trail for the path within the branch |
144 | - branch_breadcrumbs = util.branch_breadcrumbs(path, rev_tree, 'files') |
145 | filelist = self.get_filelist(rev_tree.inventory, path, sort_type, revno_url) |
146 | + |
147 | else: |
148 | start_revid = None |
149 | change = None |
150 | path = "/" |
151 | updir = None |
152 | revno_url = 'head:' |
153 | - branch_breadcrumbs = [] |
154 | filelist = [] |
155 | |
156 | return { |
157 | - 'branch': self._branch, |
158 | - 'util': util, |
159 | 'revid': revid, |
160 | 'revno_url': revno_url, |
161 | 'change': change, |
162 | 'path': path, |
163 | 'updir': updir, |
164 | 'filelist': filelist, |
165 | - 'navigation': navigation, |
166 | - 'url': self._branch.context_url, |
167 | 'start_revid': start_revid, |
168 | + } |
169 | + |
170 | + def add_template_values(self, values): |
171 | + super(InventoryUI, self).add_template_values(values) |
172 | + # Directory Breadcrumbs |
173 | + directory_breadcrumbs = util.directory_breadcrumbs( |
174 | + self._branch.friendly_name, |
175 | + self._branch.is_root, |
176 | + 'files') |
177 | + |
178 | + path = values['path'] |
179 | + revid = values['revid'] |
180 | + # no navbar for revisions |
181 | + navigation = util.Container() |
182 | + |
183 | + if is_null_rev(revid): |
184 | + branch_breadcrumbs = [] |
185 | + else: |
186 | + # Create breadcrumb trail for the path within the branch |
187 | + branch = self._history._branch |
188 | + rev_tree = branch.repository.revision_tree(revid) |
189 | + branch_breadcrumbs = util.branch_breadcrumbs(path, rev_tree, 'files') |
190 | + values.update({ |
191 | 'fileview_active': True, |
192 | 'directory_breadcrumbs': directory_breadcrumbs, |
193 | 'branch_breadcrumbs': branch_breadcrumbs, |
194 | - } |
195 | + 'navigation': navigation, |
196 | + }) |
197 | |
198 | === modified file 'loggerhead/controllers/revision_ui.py' |
199 | --- loggerhead/controllers/revision_ui.py 2011-03-02 14:07:21 +0000 |
200 | +++ loggerhead/controllers/revision_ui.py 2011-06-28 16:21:27 +0000 |
201 | @@ -36,6 +36,7 @@ |
202 | class RevisionUI(TemplatedBranchView): |
203 | |
204 | template_path = 'loggerhead.templates.revision' |
205 | + supports_json = True |
206 | |
207 | def get_values(self, path, kwargs, headers): |
208 | h = self._history |
209 | @@ -44,9 +45,10 @@ |
210 | filter_file_id = kwargs.get('filter_file_id', None) |
211 | start_revid = h.fix_revid(kwargs.get('start_revid', None)) |
212 | query = kwargs.get('q', None) |
213 | - remember = h.fix_revid(kwargs.get('remember', None)) |
214 | compare_revid = h.fix_revid(kwargs.get('compare_revid', None)) |
215 | |
216 | + # TODO: This try/except looks to date before real exception handling |
217 | + # and should be removed |
218 | try: |
219 | revid, start_revid, revid_list = h.get_view(revid, |
220 | start_revid, |
221 | @@ -55,26 +57,64 @@ |
222 | except: |
223 | self.log.exception('Exception fetching changes') |
224 | raise HTTPServerError('Could not fetch changes') |
225 | - |
226 | + # XXX: Some concern about namespace collisions. These are only stored |
227 | + # here so they can be expanded into the template later. Should probably |
228 | + # be stored in a specific dict/etc. |
229 | + self.revid_list = revid_list |
230 | + self.compare_revid = compare_revid |
231 | + self.path = path |
232 | + kwargs['start_revid'] = start_revid |
233 | + |
234 | + change = h.get_changes([revid])[0] |
235 | + |
236 | + if compare_revid is None: |
237 | + file_changes = h.get_file_changes(change) |
238 | + else: |
239 | + file_changes = h.file_changes_for_revision_ids( |
240 | + compare_revid, change.revid) |
241 | + |
242 | + h.add_branch_nicks(change) |
243 | + |
244 | + if '.' in change.revno: |
245 | + # Walk "up" though the merge-sorted graph until we find a |
246 | + # revision with merge depth 0: this is the revision that merged |
247 | + # this one to mainline. |
248 | + ri = self._history._rev_info |
249 | + i = self._history._rev_indices[change.revid] |
250 | + while ri[i][0][2] > 0: |
251 | + i -= 1 |
252 | + merged_in = ri[i][0][3] |
253 | + else: |
254 | + merged_in = None |
255 | + |
256 | + return { |
257 | + 'revid': revid, |
258 | + 'change': change, |
259 | + 'file_changes': file_changes, |
260 | + 'merged_in': merged_in, |
261 | + } |
262 | + |
263 | + def add_template_values(self, values): |
264 | + super(RevisionUI, self).add_template_values(values) |
265 | + remember = self._history.fix_revid(self.kwargs.get('remember', None)) |
266 | + query = self.kwargs.get('q', None) |
267 | + filter_file_id = self.kwargs.get('filter_file_id', None) |
268 | + start_revid = self.kwargs['start_revid'] |
269 | navigation = util.Container( |
270 | - revid_list=revid_list, revid=revid, start_revid=start_revid, |
271 | + revid_list=self.revid_list, revid=values['revid'], |
272 | + start_revid=start_revid, |
273 | filter_file_id=filter_file_id, pagesize=1, |
274 | - scan_url='/revision', branch=self._branch, feed=True, history=h) |
275 | + scan_url='/revision', branch=self._branch, feed=True, |
276 | + history=self._history) |
277 | if query is not None: |
278 | navigation.query = query |
279 | util.fill_in_navigation(navigation) |
280 | - |
281 | - change = h.get_changes([revid])[0] |
282 | - |
283 | - if compare_revid is None: |
284 | - file_changes = h.get_file_changes(change) |
285 | - else: |
286 | - file_changes = h.file_changes_for_revision_ids( |
287 | - compare_revid, change.revid) |
288 | - |
289 | + path = self.path |
290 | if path in ('', '/'): |
291 | path = None |
292 | |
293 | + |
294 | + file_changes = values['file_changes'] |
295 | link_data = {} |
296 | path_to_id = {} |
297 | if path: |
298 | @@ -90,20 +130,6 @@ |
299 | dq(item.new_revision), dq(item.old_revision), dq(item.file_id)) |
300 | path_to_id[item.filename] = 'diff-' + str(i) |
301 | |
302 | - h.add_branch_nicks(change) |
303 | - |
304 | - if '.' in change.revno: |
305 | - # Walk "up" though the merge-sorted graph until we find a |
306 | - # revision with merge depth 0: this is the revision that merged |
307 | - # this one to mainline. |
308 | - ri = self._history._rev_info |
309 | - i = self._history._rev_indices[change.revid] |
310 | - while ri[i][0][2] > 0: |
311 | - i -= 1 |
312 | - merged_in = ri[i][0][3] |
313 | - else: |
314 | - merged_in = None |
315 | - |
316 | # Directory Breadcrumbs |
317 | directory_breadcrumbs = ( |
318 | util.directory_breadcrumbs( |
319 | @@ -111,25 +137,18 @@ |
320 | self._branch.is_root, |
321 | 'changes')) |
322 | |
323 | - return { |
324 | - 'branch': self._branch, |
325 | - 'revid': revid, |
326 | - 'change': change, |
327 | - 'file_changes': file_changes, |
328 | - 'diff_chunks': diff_chunks, |
329 | + values.update({ |
330 | + 'history': self._history, |
331 | 'link_data': simplejson.dumps(link_data), |
332 | - 'specific_path': path, |
333 | 'json_specific_path': simplejson.dumps(path), |
334 | 'path_to_id': simplejson.dumps(path_to_id), |
335 | - 'start_revid': start_revid, |
336 | + 'directory_breadcrumbs': directory_breadcrumbs, |
337 | + 'navigation': navigation, |
338 | + 'remember': remember, |
339 | + 'compare_revid': self.compare_revid, |
340 | 'filter_file_id': filter_file_id, |
341 | - 'util': util, |
342 | - 'history': h, |
343 | - 'merged_in': merged_in, |
344 | - 'navigation': navigation, |
345 | + 'diff_chunks': diff_chunks, |
346 | 'query': query, |
347 | - 'remember': remember, |
348 | - 'compare_revid': compare_revid, |
349 | - 'url': self._branch.context_url, |
350 | - 'directory_breadcrumbs': directory_breadcrumbs, |
351 | - } |
352 | + 'specific_path': path, |
353 | + 'start_revid': start_revid, |
354 | + }) |
355 | |
356 | === modified file 'loggerhead/controllers/revlog_ui.py' |
357 | --- loggerhead/controllers/revlog_ui.py 2009-03-19 00:44:05 +0000 |
358 | +++ loggerhead/controllers/revlog_ui.py 2011-06-28 16:21:27 +0000 |
359 | @@ -1,12 +1,12 @@ |
360 | import urllib |
361 | |
362 | -from loggerhead import util |
363 | from loggerhead.controllers import TemplatedBranchView |
364 | |
365 | |
366 | class RevLogUI(TemplatedBranchView): |
367 | |
368 | template_path = 'loggerhead.templates.revlog' |
369 | + supports_json = True |
370 | |
371 | def get_values(self, path, kwargs, headers): |
372 | history = self._history |
373 | @@ -18,10 +18,7 @@ |
374 | history.add_branch_nicks(change) |
375 | |
376 | return { |
377 | - 'branch': self._branch, |
378 | 'entry': change, |
379 | 'file_changes': file_changes, |
380 | - 'util': util, |
381 | 'revid': revid, |
382 | - 'url': self._branch.context_url, |
383 | } |
384 | |
385 | === modified file 'loggerhead/tests/test_controllers.py' |
386 | --- loggerhead/tests/test_controllers.py 2011-06-28 10:58:27 +0000 |
387 | +++ loggerhead/tests/test_controllers.py 2011-06-28 16:21:27 +0000 |
388 | @@ -1,47 +1,27 @@ |
389 | -from cStringIO import StringIO |
390 | -import logging |
391 | - |
392 | -from paste.httpexceptions import HTTPServerError |
393 | - |
394 | -from bzrlib import errors |
395 | +import simplejson |
396 | |
397 | from loggerhead.apps.branch import BranchWSGIApp |
398 | from loggerhead.controllers.annotate_ui import AnnotateUI |
399 | from loggerhead.controllers.inventory_ui import InventoryUI |
400 | from loggerhead.controllers.revision_ui import RevisionUI |
401 | -from loggerhead.tests.test_simple import BasicTests |
402 | +from loggerhead.tests.test_simple import BasicTests, consume_app |
403 | from loggerhead import util |
404 | |
405 | |
406 | class TestInventoryUI(BasicTests): |
407 | |
408 | + def make_bzrbranch_for_tree_shape(self, shape): |
409 | + tree = self.make_branch_and_tree('.') |
410 | + self.build_tree(shape) |
411 | + tree.smart_add([]) |
412 | + tree.commit('') |
413 | + self.addCleanup(tree.branch.lock_read().unlock) |
414 | + return tree.branch |
415 | + |
416 | def make_bzrbranch_and_inventory_ui_for_tree_shape(self, shape): |
417 | - tree = self.make_branch_and_tree('.') |
418 | - self.build_tree(shape) |
419 | - tree.smart_add([]) |
420 | - tree.commit('') |
421 | - tree.branch.lock_read() |
422 | - self.addCleanup(tree.branch.unlock) |
423 | - branch_app = BranchWSGIApp(tree.branch, '') |
424 | - branch_app.log.setLevel(logging.CRITICAL) |
425 | - # These are usually set in BranchWSGIApp.app(), which is set from env |
426 | - # settings set by BranchesFromTransportRoot, so we fake it. |
427 | - branch_app._static_url_base = '/' |
428 | - branch_app._url_base = '/' |
429 | - return tree.branch, InventoryUI(branch_app, branch_app.get_history) |
430 | - |
431 | - def consume_app(self, app, extra_environ=None): |
432 | - env = {'SCRIPT_NAME': '/files', 'PATH_INFO': ''} |
433 | - if extra_environ is not None: |
434 | - env.update(extra_environ) |
435 | - body = StringIO() |
436 | - start = [] |
437 | - def start_response(status, headers, exc_info=None): |
438 | - start.append((status, headers, exc_info)) |
439 | - return body.write |
440 | - extra_content = list(app(env, start_response)) |
441 | - body.writelines(extra_content) |
442 | - return start[0], body.getvalue() |
443 | + branch = self.make_bzrbranch_for_tree_shape(shape) |
444 | + branch_app = self.make_branch_app(branch) |
445 | + return branch, InventoryUI(branch_app, branch_app.get_history) |
446 | |
447 | def test_get_filelist(self): |
448 | bzrbranch, inv_ui = self.make_bzrbranch_and_inventory_ui_for_tree_shape( |
449 | @@ -52,7 +32,8 @@ |
450 | def test_smoke(self): |
451 | bzrbranch, inv_ui = self.make_bzrbranch_and_inventory_ui_for_tree_shape( |
452 | ['filename']) |
453 | - start, content = self.consume_app(inv_ui) |
454 | + start, content = consume_app(inv_ui, |
455 | + {'SCRIPT_NAME': '/files', 'PATH_INFO': ''}) |
456 | self.assertEqual(('200 OK', [('Content-Type', 'text/html')], None), |
457 | start) |
458 | self.assertContainsRe(content, 'filename') |
459 | @@ -60,43 +41,74 @@ |
460 | def test_no_content_for_HEAD(self): |
461 | bzrbranch, inv_ui = self.make_bzrbranch_and_inventory_ui_for_tree_shape( |
462 | ['filename']) |
463 | - start, content = self.consume_app(inv_ui, |
464 | - extra_environ={'REQUEST_METHOD': 'HEAD'}) |
465 | + start, content = consume_app(inv_ui, |
466 | + {'SCRIPT_NAME': '/files', 'PATH_INFO': '', |
467 | + 'REQUEST_METHOD': 'HEAD'}) |
468 | self.assertEqual(('200 OK', [('Content-Type', 'text/html')], None), |
469 | start) |
470 | self.assertEqual('', content) |
471 | |
472 | + def test_get_values_smoke(self): |
473 | + branch = self.make_bzrbranch_for_tree_shape(['a-file']) |
474 | + branch_app = self.make_branch_app(branch) |
475 | + env = {'SCRIPT_NAME': '', 'PATH_INFO': '/files'} |
476 | + inv_ui = branch_app.lookup_app(env) |
477 | + inv_ui.parse_args(env) |
478 | + values = inv_ui.get_values('', {}, {}) |
479 | + self.assertEqual('a-file', values['filelist'][0].filename) |
480 | + |
481 | + def test_json_render_smoke(self): |
482 | + branch = self.make_bzrbranch_for_tree_shape(['a-file']) |
483 | + branch_app = self.make_branch_app(branch) |
484 | + env = {'SCRIPT_NAME': '', 'PATH_INFO': '/+json/files'} |
485 | + inv_ui = branch_app.lookup_app(env) |
486 | + self.assertOkJsonResponse(inv_ui, env) |
487 | + |
488 | |
489 | class TestRevisionUI(BasicTests): |
490 | |
491 | - def make_bzrbranch_and_revision_ui_for_tree_shapes(self, shape1, shape2): |
492 | + def make_branch_app_for_revision_ui(self, shape1, shape2): |
493 | tree = self.make_branch_and_tree('.') |
494 | self.build_tree_contents(shape1) |
495 | tree.smart_add([]) |
496 | - tree.commit('') |
497 | + tree.commit('msg 1', rev_id='rev-1') |
498 | self.build_tree_contents(shape2) |
499 | tree.smart_add([]) |
500 | - tree.commit('') |
501 | - tree.branch.lock_read() |
502 | - self.addCleanup(tree.branch.unlock) |
503 | - branch_app = BranchWSGIApp(tree.branch) |
504 | - branch_app._environ = { |
505 | - 'wsgi.url_scheme':'', |
506 | - 'SERVER_NAME':'', |
507 | - 'SERVER_PORT':'80', |
508 | - } |
509 | - branch_app._url_base = '' |
510 | - branch_app.friendly_name = '' |
511 | - return tree.branch, RevisionUI(branch_app, branch_app.get_history) |
512 | + tree.commit('msg 2', rev_id='rev-2') |
513 | + branch = tree.branch |
514 | + self.addCleanup(branch.lock_read().unlock) |
515 | + return self.make_branch_app(branch) |
516 | |
517 | def test_get_values(self): |
518 | - branch, rev_ui = self.make_bzrbranch_and_revision_ui_for_tree_shapes( |
519 | - [], []) |
520 | - rev_ui.args = ['2'] |
521 | - util.set_context({}) |
522 | - self.assertIsInstance( |
523 | - rev_ui.get_values('', {}, []), |
524 | - dict) |
525 | + branch_app = self.make_branch_app_for_revision_ui([], []) |
526 | + env = {'SCRIPT_NAME': '', 'PATH_INFO': '/revision/2'} |
527 | + rev_ui = branch_app.lookup_app(env) |
528 | + rev_ui.parse_args(env) |
529 | + self.assertIsInstance(rev_ui.get_values('', {}, []), dict) |
530 | + |
531 | + def test_get_values_smoke(self): |
532 | + branch_app = self.make_branch_app_for_revision_ui( |
533 | + [('file', 'content\n'), ('other-file', 'other\n')], |
534 | + [('file', 'new content\n')]) |
535 | + env = {'SCRIPT_NAME': '/', |
536 | + 'PATH_INFO': '/revision/head:'} |
537 | + revision_ui = branch_app.lookup_app(env) |
538 | + revision_ui.parse_args(env) |
539 | + values = revision_ui.get_values('', {}, {}) |
540 | + |
541 | + self.assertEqual(values['revid'], 'rev-2') |
542 | + self.assertEqual(values['change'].comment, 'msg 2') |
543 | + self.assertEqual(values['file_changes'].modified[0].filename, 'file') |
544 | + self.assertEqual(values['merged_in'], None) |
545 | + |
546 | + def test_json_render_smoke(self): |
547 | + branch_app = self.make_branch_app_for_revision_ui( |
548 | + [('file', 'content\n'), ('other-file', 'other\n')], |
549 | + [('file', 'new content\n')]) |
550 | + env = {'SCRIPT_NAME': '', 'PATH_INFO': '/+json/revision/head:'} |
551 | + revision_ui = branch_app.lookup_app(env) |
552 | + self.assertOkJsonResponse(revision_ui, env) |
553 | + |
554 | |
555 | |
556 | class TestAnnotateUI(BasicTests): |
557 | @@ -125,3 +137,70 @@ |
558 | self.assertEqual(2, len(annotated)) |
559 | self.assertEqual('2', annotated[1].change.revno) |
560 | self.assertEqual('1', annotated[2].change.revno) |
561 | + |
562 | + |
563 | +class TestFileDiffUI(BasicTests): |
564 | + |
565 | + def make_branch_app_for_filediff_ui(self): |
566 | + builder = self.make_branch_builder('branch') |
567 | + builder.start_series() |
568 | + builder.build_snapshot('rev-1-id', None, [ |
569 | + ('add', ('', 'root-id', 'directory', '')), |
570 | + ('add', ('filename', 'f-id', 'file', 'content\n'))], |
571 | + message="First commit.") |
572 | + builder.build_snapshot('rev-2-id', None, [ |
573 | + ('modify', ('f-id', 'new content\n'))]) |
574 | + builder.finish_series() |
575 | + branch = builder.get_branch() |
576 | + self.addCleanup(branch.lock_read().unlock) |
577 | + return self.make_branch_app(branch) |
578 | + |
579 | + def test_get_values_smoke(self): |
580 | + branch_app = self.make_branch_app_for_filediff_ui() |
581 | + env = {'SCRIPT_NAME': '/', |
582 | + 'PATH_INFO': '/+filediff/rev-2-id/rev-1-id/f-id'} |
583 | + filediff_ui = branch_app.lookup_app(env) |
584 | + filediff_ui.parse_args(env) |
585 | + values = filediff_ui.get_values('', {}, {}) |
586 | + chunks = values['chunks'] |
587 | + self.assertEqual('insert', chunks[0].diff[1].type) |
588 | + self.assertEqual('new content', chunks[0].diff[1].line) |
589 | + |
590 | + def test_json_render_smoke(self): |
591 | + branch_app = self.make_branch_app_for_filediff_ui() |
592 | + env = {'SCRIPT_NAME': '/', |
593 | + 'PATH_INFO': '/+json/+filediff/rev-2-id/rev-1-id/f-id'} |
594 | + filediff_ui = branch_app.lookup_app(env) |
595 | + self.assertOkJsonResponse(filediff_ui, env) |
596 | + |
597 | + |
598 | +class TestRevLogUI(BasicTests): |
599 | + |
600 | + def make_branch_app_for_revlog_ui(self): |
601 | + builder = self.make_branch_builder('branch') |
602 | + builder.start_series() |
603 | + builder.build_snapshot('rev-id', None, [ |
604 | + ('add', ('', 'root-id', 'directory', '')), |
605 | + ('add', ('filename', 'f-id', 'file', 'content\n'))], |
606 | + message="First commit.") |
607 | + builder.finish_series() |
608 | + branch = builder.get_branch() |
609 | + self.addCleanup(branch.lock_read().unlock) |
610 | + return self.make_branch_app(branch) |
611 | + |
612 | + def test_get_values_smoke(self): |
613 | + branch_app = self.make_branch_app_for_revlog_ui() |
614 | + env = {'SCRIPT_NAME': '/', |
615 | + 'PATH_INFO': '/+revlog/rev-id'} |
616 | + revlog_ui = branch_app.lookup_app(env) |
617 | + revlog_ui.parse_args(env) |
618 | + values = revlog_ui.get_values('', {}, {}) |
619 | + self.assertEqual(values['file_changes'].added[1].filename, 'filename') |
620 | + self.assertEqual(values['entry'].comment, "First commit.") |
621 | + |
622 | + def test_json_render_smoke(self): |
623 | + branch_app = self.make_branch_app_for_revlog_ui() |
624 | + env = {'SCRIPT_NAME': '', 'PATH_INFO': '/+json/+revlog/rev-id'} |
625 | + revlog_ui = branch_app.lookup_app(env) |
626 | + self.assertOkJsonResponse(revlog_ui, env) |
627 | + |
628 | |
629 | === modified file 'loggerhead/tests/test_simple.py' |
630 | --- loggerhead/tests/test_simple.py 2011-03-23 03:36:23 +0000 |
631 | +++ loggerhead/tests/test_simple.py 2011-06-28 16:21:27 +0000 |
632 | @@ -18,6 +18,8 @@ |
633 | import cgi |
634 | import logging |
635 | import re |
636 | +import simplejson |
637 | +from cStringIO import StringIO |
638 | |
639 | from bzrlib.tests import TestCaseWithTransport |
640 | try: |
641 | @@ -47,6 +49,23 @@ |
642 | branch_app = BranchWSGIApp(self.tree.branch, '', **kw).app |
643 | return TestApp(HTTPExceptionHandler(branch_app)) |
644 | |
645 | + def assertOkJsonResponse(self, app, env): |
646 | + start, content = consume_app(app, env) |
647 | + self.assertEqual('200 OK', start[0]) |
648 | + self.assertEqual('application/json', dict(start[1])['Content-Type']) |
649 | + self.assertEqual(None, start[2]) |
650 | + simplejson.loads(content) |
651 | + |
652 | + def make_branch_app(self, branch): |
653 | + branch_app = BranchWSGIApp(branch, friendly_name='friendly-name') |
654 | + branch_app._environ = { |
655 | + 'wsgi.url_scheme':'', |
656 | + 'SERVER_NAME':'', |
657 | + 'SERVER_PORT':'80', |
658 | + } |
659 | + branch_app._url_base = '' |
660 | + return branch_app |
661 | + |
662 | |
663 | class TestWithSimpleTree(BasicTests): |
664 | |
665 | @@ -228,6 +247,18 @@ |
666 | self.assertEqualDiff('', res.body) |
667 | |
668 | |
669 | +def consume_app(app, env): |
670 | + body = StringIO() |
671 | + start = [] |
672 | + def start_response(status, headers, exc_info=None): |
673 | + start.append((status, headers, exc_info)) |
674 | + return body.write |
675 | + extra_content = list(app(env, start_response)) |
676 | + body.writelines(extra_content) |
677 | + return start[0], body.getvalue() |
678 | + |
679 | + |
680 | + |
681 | #class TestGlobalConfig(BasicTests): |
682 | # """ |
683 | # Test that global config settings are respected |
684 | |
685 | === modified file 'loggerhead/util.py' |
686 | --- loggerhead/util.py 2011-03-23 05:21:34 +0000 |
687 | +++ loggerhead/util.py 2011-06-28 16:21:27 +0000 |
688 | @@ -663,3 +663,13 @@ |
689 | else: |
690 | raise |
691 | return new_application |
692 | + |
693 | + |
694 | +def convert_to_json_ready(obj): |
695 | + if isinstance(obj, Container): |
696 | + d = obj.__dict__.copy() |
697 | + del d['_properties'] |
698 | + return d |
699 | + elif isinstance(obj, datetime.datetime): |
700 | + return tuple(obj.utctimetuple()) |
701 | + raise TypeError(repr(obj) + " is not JSON serializable") |
This looks fine and brings some useful refactorings.
Did you get any insight about how to avoid regressions in the template/values separation ?