Merge lp:~spiv/loggerhead/jsonify into lp:loggerhead

Proposed by Andrew Bennetts
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
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/revision/head: is the raw 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.

To post a comment you must log in.
Revision history for this message
Vincent Ladeuil (vila) wrote :

This looks fine and brings some useful refactorings.

Did you get any insight about how to avoid regressions in the template/values separation ?

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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")

Subscribers

People subscribed via source and target branches