Merge lp:~jameinel/loggerhead/trunk-into-experimental into lp:~loggerhead-team/loggerhead/experimental

Proposed by John A Meinel
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: 452
Merged at revision: 450
Proposed branch: lp:~jameinel/loggerhead/trunk-into-experimental
Merge into: lp:~loggerhead-team/loggerhead/experimental
Diff against target: 1124 lines (+781/-79)
17 files modified
HACKING (+59/-0)
NEWS (+15/-2)
__init__.py (+24/-49)
load_test_scripts/multiple_instances.script (+19/-0)
load_test_scripts/simple.script (+9/-0)
loggerhead/controllers/__init__.py (+6/-3)
loggerhead/controllers/raw_ui.py (+8/-5)
loggerhead/controllers/view_ui.py (+3/-3)
loggerhead/load_test.py (+233/-0)
loggerhead/static/css/global.css (+5/-5)
loggerhead/templates/inventory.pt (+7/-0)
loggerhead/templates/macros.pt (+0/-8)
loggerhead/templates/revision.pt (+0/-1)
loggerhead/tests/__init__.py (+1/-0)
loggerhead/tests/test_controllers.py (+43/-1)
loggerhead/tests/test_load_test.py (+337/-0)
loggerhead/tests/test_simple.py (+12/-2)
To merge this branch: bzr merge lp:~jameinel/loggerhead/trunk-into-experimental
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+49310@code.launchpad.net

Commit message

Merge lp:loggerhead back into the experimental branch, to prepare it for future synchronization.

Description of the change

This merges lp:loggerhead back into the experimental branch, and resolves all the small conflicts therein.

It shouldn't be too controversial. The small conflicts were mostly because of similar code being done differently between the old pqm branch and the old trunk.

The one thing is that this changes the inventory page to show the View and Download links, rather than showing Raw and Download links.

To post a comment you must log in.
452. By John A Meinel

Bring in the load-test updates as well.

Revision history for this message
Michael Hudson-Doyle (mwhudson) :
review: Approve
Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Why are you changing the color scheme and removing the search box?

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 2/10/2011 5:54 PM, Max Kanat-Alexander wrote:
> Why are you changing the color scheme and removing the search box?

I'm merging what was in trunk into experimental, and what is in trunk is
what was in pqm.

If you have suggestions for how to restore it, great! It certainly
wasn't intended to remove functionality, just get things up to speed.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk1Ue2AACgkQJdeBCYSNAAN+6gCfa1M+pk2k1oB0VEBZCxJvujV5
x78AniD7Mfh/FeDFCBybtR29amwvtldU
=I4Ja
-----END PGP SIGNATURE-----

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Ah, don't remove the search box, and don't change global.css. Merging what was in pqm into what was in experimental should not have been necessary, except for the fixes that you made to pqm.

Revision history for this message
Robert Collins (lifeless) wrote :

On Fri, Feb 11, 2011 at 1:11 PM, Max Kanat-Alexander
<email address hidden> wrote:
> Ah, don't remove the search box, and don't change global.css. Merging what was in pqm into what was in experimental should not have been necessary, except for the fixes that you made to pqm.

Actually merging trunk to experimental is a great idea because trunk
is where development is happening. It makes experimental reflect what
the merged branch will look like, when the issues with experimental
are resolved.

That said, the search box was and is a cute widget, but it scales very
poorly. I suspect we need a switch to control it (in trunk). It would
be nice to have it available, I think I missed that it was being
changed in the merge from pqm -> trunk.

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Yes, of course experimental should be kept up.

However, you made "trunk" into something that's Launchpad-specific, so there is some stuff that doesn't apply to other people, on "trunk" now. We removed features on the pqm branch because Launchpad couldn't use them, but everybody else still uses them.

So during this one merge, it's important to not merge the Launchpad-specific removals (or the color scheme change) back into experimental (and in fact they should not normally be there on something called "trunk" either).

Most of the changes in this merge are fine, but this one merge needs to be looked over more carefully. I imagine future merges from trunk will be fine.

Revision history for this message
Robert Collins (lifeless) wrote :

On Fri, Feb 11, 2011 at 1:25 PM, Max Kanat-Alexander
<email address hidden> wrote:
> Yes, of course experimental should be kept up.
>
> However, you made "trunk" into something that's Launchpad-specific, so there is some stuff that doesn't apply to other people, on "trunk" now. We removed features on the pqm branch because Launchpad couldn't use them, but everybody else still uses them.

Actually, we didn't make trunk Launchpad /specific/. We made it
Launchpad /acceptable/ : thats a big difference. The implementation
may have missed some nuances however, which will get sorted out soon.

> So during this one merge, it's important to not merge the Launchpad-specific removals (or the color scheme change) back into experimental (and in fact they should not normally be there on something called "trunk" either).

Whats appropriate for trunk is appropriate for experimental; it would
be a mistake to filter these things at the experimental layer, rather
the mistakes need to be fi
> --
> https://code.launchpad.net/~jameinel/loggerhead/trunk-into-experimental/+merge/49310
> Your team Loggerhead-team is subscribed to branch lp:~loggerhead-team/loggerhead/experimental.
>

xed in trunk.

> Most of the changes in this merge are fine, but this one merge needs to be looked over more carefully. I imagine future merges from trunk will be fine.

A merge from trunk should always be a no brainer. As should this one.

-Rob

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Right, so I'll try that again:

1) Don't change the color scheme to Launchpad's, because the upstream loggerhead color scheme is better.

2) Don't remove the search box from experimental.

3) Don't comment out the raw link in experimental.

Revision history for this message
Robert Collins (lifeless) wrote :

On Fri, Feb 11, 2011 at 2:17 PM, Max Kanat-Alexander
<email address hidden> wrote:
> Right, so I'll try that again:
>
> 1) Don't change the color scheme to Launchpad's, because the upstream loggerhead color scheme is better.

> 2) Don't remove the search box from experimental.
>
> 3) Don't comment out the raw link in experimental.

So these changes already hit trunk; I think we should:
 - propogate them to experimental
 - fix in trunk and propogate that fix likewise

otherwise we're just creating conflicts on future merges to experimental.

Could I get a screenshot of the two colour schemes to show to a few folk?

-Rob

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 2/10/2011 7:17 PM, Max Kanat-Alexander wrote:
> Right, so I'll try that again:
>
> 1) Don't change the color scheme to Launchpad's, because the upstream loggerhead color scheme is better.
>
> 2) Don't remove the search box from experimental.
>
> 3) Don't comment out the raw link in experimental.

I'll put together a proposal for the first two. But I'd like to ask for
more info on the 3rd.

I like having *access* to the raw content, but feel the link should be
on the View page. So the standard linking is:

Inventory => View => Annotate
                  => Raw

At least, IMO, leaving the HTML world of links and navigation should be
a last/final step, not a standard step while browsing.

Especially since you can also do neat things like share a link to an
exact line, rather than just the whole document, etc.

I'm certainly interested in hearing what you think, though. Is it just a
performance thing?

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk1UsK8ACgkQJdeBCYSNAAOxLgCfYc8zDN8qZvqB35lJzRIx8Nrg
7ZgAnjheqLoufFrrpADu69azI22Bovgr
=Vm81
-----END PGP SIGNATURE-----

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Well, the raw controller was originally added with the idea that if your view was taking too long, you could get an instantaneous raw view--it was implemented for performance reasons. Also, the number of links on View is getting pretty large. However, I do agree that for navigational purposes, it does make more sense on the View page.

Revision history for this message
Max Kanat-Alexander (mkanat) wrote :

Also, you could put the link in both places--it's just a tiny icon on the Inventory page.

Revision history for this message
John A Meinel (jameinel) wrote :

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 2/10/2011 7:17 PM, Max Kanat-Alexander wrote:
> Right, so I'll try that again:
>
> 1) Don't change the color scheme to Launchpad's, because the upstream loggerhead color scheme is better.
>

I don't know how we will settle this one, but for now it is:
https://bugs.launchpad.net/loggerhead/+bug/718968

> 2) Don't remove the search box from experimental.
>

https://bugs.launchpad.net/loggerhead/+bug/718978

This should be as simple as a 'tal:condition' that uses some config
entry to set it as available. I imagine the default should be available
if bzr-search is available, but Launchpad would want it disabled.

> 3) Don't comment out the raw link in experimental.

https://bugs.launchpad.net/loggerhead/+bug/718982

If you have a chance, please look at the bugs to make sure I've captured
your concerns and the state of the discussion.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk1ZlIMACgkQJdeBCYSNAAMSogCgtQKuhOSMCIy9PHIyHwQWxfID
hQ0AoJRMzhybOeIpMgmjUS8gOLrlkoxF
=hzIr
-----END PGP SIGNATURE-----

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'HACKING'
2--- HACKING 1970-01-01 00:00:00 +0000
3+++ HACKING 2011-02-10 23:12:12 +0000
4@@ -0,0 +1,59 @@
5+Loggerhead
6+==========
7+
8+Overview
9+--------
10+
11+This document attempts to give some hints for people that are wanting to work
12+on Loggerhead.
13+
14+
15+Testing
16+-------
17+
18+You can run the loggerhead test suite as a bzr plugin. To run just the
19+loggerhead tests::
20+
21+ bzr selftest -s bp.loggerhead
22+
23+
24+Load Testing
25+------------
26+
27+As a web service, Loggerhead will often be hit by multiple requests. We want
28+to make sure that loggerhead can scale with many requests, without performing
29+poorly or crashing under the load.
30+
31+There is a command ``bzr load-test-loggerhead`` that can be run to stress
32+loggerhead. A script is given, describing what requests to make, against what
33+URLs, and for what level of parallel activity.
34+
35+
36+Load Testing Multiple Instances
37+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
38+
39+One way that Launchpad provides both high availability and performance scaling
40+is by running multiple instances of loggerhead, serving the same content. A
41+proxy is then used to load balance the requests. This also allows us to shut
42+down one instance for upgrading, without interupting service (requests are
43+just routed to the other instance).
44+
45+However, multiple processes poses an even greater risk that caches will
46+conflict. As such, it is useful to test that changes don't introduce coherency
47+issues at load. ``bzr load-test-loggerhead`` can be configured with a script
48+that will make requests against multiple loggerhead instances concurrently.
49+
50+To run multiple instances, it is often sufficient to just spawn multiple
51+servers on different ports. For example::
52+
53+ $ bzr serve --http --port=8080 &
54+ $ bzr serve --http --port=8081 &
55+
56+There is a simple example script already in the source tree::
57+
58+ $ bzr load-test-loggerhead load_test_scripts/multiple_instances.script
59+
60+
61+
62+.. vim: ft=rst tw=78
63+
64
65=== modified file 'NEWS'
66--- NEWS 2011-01-11 00:09:54 +0000
67+++ NEWS 2011-02-10 23:12:12 +0000
68@@ -4,13 +4,26 @@
69 dev [future]
70 ------------
71
72- - Remove ``start-loggerhead`` and ``stop-loggerhead`` which were already
73- deprecated. (John Arbash Meinel)
74+ - Add ``bzr load-test-loggerhead`` as a way to make sure loggerhead can
75+ handle concurrent requests, etc. Scripts can be written that spawn
76+ multiple threads, and issue concurrent requests.
77+ (John Arbash Meinel)
78+
79+ - If we get a HEAD request, there is no reason to expand the template, we
80+ shouldn't be returning body content anyway.
81+ (John Arbash Meinel, #716201, #716217)
82
83 - Make the /download/ URLs use revnos instead of revids. This makes
84 download URLs simpler and allows you to have a single download URL
85 for the latest version of a file. (Max Kanat-Alexander, #322295)
86
87+ - Merge the pqm changes back into trunk, after trunk was reverted to an old
88+ revision. (John Arbash Meinel, #716152)
89+
90+ - Remove ``start-loggerhead`` and ``stop-loggerhead`` which were already
91+ deprecated. (John Arbash Meinel)
92+
93+
94 1.18 [10Nov2010]
95 ----------------
96
97
98=== modified file '__init__.py'
99--- __init__.py 2010-11-11 03:36:24 +0000
100+++ __init__.py 2011-02-10 23:12:12 +0000
101@@ -1,4 +1,4 @@
102-# Copyright 2009, 2010 Canonical Ltd
103+# Copyright 2009, 2010, 2011 Canonical Ltd
104 #
105 # This program is free software; you can redistribute it and/or modify
106 # it under the terms of the GNU General Public License as published by
107@@ -38,21 +38,11 @@
108 if __name__ == 'bzrlib.plugins.loggerhead':
109 import bzrlib
110 from bzrlib.api import require_any_api
111+ from bzrlib import commands
112
113 require_any_api(bzrlib, bzr_compatible_versions)
114
115- # NB: Normally plugins should lazily load almost everything, but this
116- # seems reasonable to have in-line here: bzrlib.commands and options are
117- # normally loaded, and the rest of loggerhead won't be loaded until serve
118- # --http is run.
119-
120- # transport_server_registry was added in bzr 1.16. When we drop support for
121- # older releases, we can remove the code to override cmd_serve.
122-
123- try:
124- from bzrlib.transport import transport_server_registry
125- except ImportError:
126- transport_server_registry = None
127+ from bzrlib.transport import transport_server_registry
128
129 DEFAULT_HOST = '0.0.0.0'
130 DEFAULT_PORT = 8080
131@@ -83,7 +73,6 @@
132
133
134
135-
136 def _ensure_loggerhead_path():
137 """Ensure that you can 'import loggerhead' and get the root."""
138 # loggerhead internal code will try to 'import loggerhead', so
139@@ -116,41 +105,27 @@
140 app = HTTPExceptionHandler(app)
141 serve(app, host=host, port=port)
142
143- if transport_server_registry is not None:
144- transport_server_registry.register('http', serve_http, help=HELP)
145- else:
146- import bzrlib.builtins
147- from bzrlib.commands import get_cmd_object, register_command
148- from bzrlib.option import Option
149-
150- _original_command = get_cmd_object('serve')
151-
152- class cmd_serve(bzrlib.builtins.cmd_serve):
153- __doc__ = _original_command.__doc__
154-
155- takes_options = _original_command.takes_options + [
156- Option('http', help=HELP)]
157-
158- def run(self, *args, **kw):
159- if 'http' in kw:
160- from bzrlib.transport import get_transport
161- allow_writes = kw.get('allow_writes', False)
162- path = kw.get('directory', '.')
163- port = kw.get('port', DEFAULT_PORT)
164- # port might be an int already...
165- if isinstance(port, basestring) and ':' in port:
166- host, port = port.split(':')
167- else:
168- host = DEFAULT_HOST
169- if allow_writes:
170- transport = get_transport(path)
171- else:
172- transport = get_transport('readonly+' + path)
173- serve_http(transport, host, port)
174- else:
175- super(cmd_serve, self).run(*args, **kw)
176-
177- register_command(cmd_serve)
178+ transport_server_registry.register('http', serve_http, help=HELP)
179+
180+ class cmd_load_test_loggerhead(commands.Command):
181+ """Run a load test against a live loggerhead instance.
182+
183+ Pass in the name of a script file to run. See loggerhead/load_test.py
184+ for a description of the file format.
185+ """
186+
187+ takes_args = ["filename"]
188+
189+ def run(self, filename):
190+ from bzrlib.plugins.loggerhead.loggerhead import load_test
191+ script = load_test.run_script(filename)
192+ for thread_id in sorted(script._threads):
193+ worker = script._threads[thread_id][0]
194+ for url, success, time in worker.stats:
195+ self.outf.write(' %5.3fs %s %s\n'
196+ % (time, str(success)[0], url))
197+
198+ commands.register_command(cmd_load_test_loggerhead)
199
200 def load_tests(standard_tests, module, loader):
201 _ensure_loggerhead_path()
202
203=== added directory 'load_test_scripts'
204=== added file 'load_test_scripts/multiple_instances.script'
205--- load_test_scripts/multiple_instances.script 1970-01-01 00:00:00 +0000
206+++ load_test_scripts/multiple_instances.script 2011-02-10 23:12:12 +0000
207@@ -0,0 +1,19 @@
208+{
209+ "comment": "Connect to multiple loggerhead instances and make requests on each. One should be on :8080, one should be on :8081. Multiple threads will place requests on each.",
210+ "parameters": {"base_url": "http://localhost"},
211+ "requests": [
212+ {"thread": "1", "relpath": ":8080/changes"},
213+ {"thread": "2", "relpath": ":8080/files"},
214+ {"thread": "3", "relpath": ":8081/files"},
215+ {"thread": "4", "relpath": ":8081/changes"},
216+ {"thread": "1", "relpath": ":8080/changes"},
217+ {"thread": "2", "relpath": ":8080/files"},
218+ {"thread": "3", "relpath": ":8081/files"},
219+ {"thread": "4", "relpath": ":8081/changes"},
220+ {"thread": "1", "relpath": ":8080/changes"},
221+ {"thread": "2", "relpath": ":8080/files"},
222+ {"thread": "3", "relpath": ":8081/files"},
223+ {"thread": "4", "relpath": ":8081/changes"}
224+ ]
225+}
226+
227
228=== added file 'load_test_scripts/simple.script'
229--- load_test_scripts/simple.script 1970-01-01 00:00:00 +0000
230+++ load_test_scripts/simple.script 2011-02-10 23:12:12 +0000
231@@ -0,0 +1,9 @@
232+{
233+ "comment": "A fairly trivial load test script. It just loads the main two pages from a loggerhead install running directly on a branch.",
234+ "parameters": {"base_url": "http://localhost:8080"},
235+ "requests": [
236+ {"relpath": "/changes"},
237+ {"relpath": "/files"}
238+ ]
239+}
240+
241
242=== modified file 'loggerhead/controllers/__init__.py'
243--- loggerhead/controllers/__init__.py 2011-01-11 00:30:17 +0000
244+++ loggerhead/controllers/__init__.py 2011-02-10 23:12:12 +0000
245@@ -84,9 +84,9 @@
246 if len(args) > 1:
247 path = unicode('/'.join(args[1:]), 'utf-8')
248
249- return self.get_output(path, kwargs, start_response)
250+ return self.get_output(path, kwargs, start_response, environ)
251
252- def get_output(self, path, kwargs, start_response):
253+ def get_output(self, path, kwargs, start_response, environ):
254 vals = {
255 'static_url': self._branch.static_url,
256 'branch': self._branch,
257@@ -103,6 +103,9 @@
258 if 'Content-Type' not in headers:
259 headers['Content-Type'] = 'text/html'
260 writer = start_response("200 OK", headers.items())
261+ if environ.get('REQUEST_METHOD') == 'HEAD':
262+ # No content for a HEAD request
263+ return []
264 template = load_template(self.template_path)
265 z = time.time()
266 w = BufferingWriter(writer, 8192)
267@@ -151,4 +154,4 @@
268
269 def tree_for(self, file_id, revid):
270 file_revid = self._history.get_inventory(revid)[file_id].revision
271- return self._history._branch.repository.revision_tree(file_revid)
272\ No newline at end of file
273+ return self._history._branch.repository.revision_tree(file_revid)
274
275=== modified file 'loggerhead/controllers/raw_ui.py'
276--- loggerhead/controllers/raw_ui.py 2011-01-11 00:02:53 +0000
277+++ loggerhead/controllers/raw_ui.py 2011-02-10 23:12:12 +0000
278@@ -24,9 +24,12 @@
279
280 from loggerhead.controllers import TemplatedBranchView
281
282+# Note: RawUI should probably not be a TemplatedBranchView, because, quite
283+# frankly, it doesn't use a template...
284+
285 class RawUI(TemplatedBranchView):
286
287- def get_output(self, path, kwargs, start_response):
288+ def get_output(self, path, kwargs, start_response, environ):
289 # /raw/<revno>/[path]
290 file_info = self.file_info(path, kwargs)
291 revid = file_info['revid']
292@@ -37,13 +40,13 @@
293 tree = self.tree_for(file_id, revid)
294 content = tree.get_file_lines(file_id)
295 size = tree.inventory[file_id].text_size
296-
297+
298 mime_type = self.get_mime_type(content, file_info)
299 headers = self.get_headers(mime_type, size, file_info)
300-
301+
302 self.log.info('/raw %s @ %s (%d bytes): %.3f secs',
303 file_info['path'], revid, size, time.time() - self._call_start)
304-
305+
306 start_response('200 OK', headers)
307 return content
308
309@@ -64,4 +67,4 @@
310 ('Content-Type', mime_type),
311 ('Content-Length', str(size)),
312 ('X-Content-Type-Options', 'nosniff'),
313- ]
314\ No newline at end of file
315+ ]
316
317=== modified file 'loggerhead/controllers/view_ui.py'
318--- loggerhead/controllers/view_ui.py 2010-12-03 17:11:21 +0000
319+++ loggerhead/controllers/view_ui.py 2011-02-10 23:12:12 +0000
320@@ -37,11 +37,11 @@
321 class ViewUI(TemplatedBranchView):
322
323 template_path = 'loggerhead.templates.view'
324-
325+
326 def text_lines(self, file_info):
327 file_id = file_info['file_id']
328 revid = file_info['revid']
329-
330+
331 tree = self.tree_for(file_id, revid)
332 file_text = tree.get_file_text(file_id)
333 encoding = 'utf-8'
334@@ -54,7 +54,7 @@
335 file_lines = bzrlib.osutils.split_lines(file_text)
336 # This can throw bzrlib.errors.BinaryFile (which our caller catches).
337 bzrlib.textfile.check_text_lines(file_lines)
338-
339+
340 if highlight is not None:
341 hl_lines = highlight(file_info['filename'], file_text, encoding)
342 # highlight strips off extra newlines at the end of the file.
343
344=== added file 'loggerhead/load_test.py'
345--- loggerhead/load_test.py 1970-01-01 00:00:00 +0000
346+++ loggerhead/load_test.py 2011-02-10 23:12:12 +0000
347@@ -0,0 +1,233 @@
348+# Copyright 2011 Canonical Ltd
349+#
350+# This program is free software; you can redistribute it and/or modify
351+# it under the terms of the GNU General Public License as published by
352+# the Free Software Foundation; either version 2 of the License, or
353+# (at your option) any later version.
354+#
355+# This program is distributed in the hope that it will be useful,
356+# but WITHOUT ANY WARRANTY; without even the implied warranty of
357+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
358+# GNU General Public License for more details.
359+#
360+# You should have received a copy of the GNU General Public License
361+# along with this program; if not, write to the Free Software
362+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
363+
364+"""Code to do some load testing against a loggerhead instance.
365+
366+This is basically meant to take a list of actions to take, and run it against a
367+real host, and see how the results respond.::
368+
369+ {"parameters": {
370+ "base_url": "http://localhost:8080",
371+ },
372+ "requests": [
373+ {"thread": "1", "relpath": "/changes"},
374+ {"thread": "1", "relpath": "/changes"},
375+ {"thread": "1", "relpath": "/changes"},
376+ {"thread": "1", "relpath": "/changes"}
377+ ],
378+ }
379+
380+All threads have a Queue length of 1. When a third request for a given thread
381+is seen, no more requests are queued until that thread finishes its current
382+job. So this results in all requests being issued sequentially::
383+
384+ {"thread": "1", "relpath": "/changes"},
385+ {"thread": "1", "relpath": "/changes"},
386+ {"thread": "1", "relpath": "/changes"},
387+ {"thread": "1", "relpath": "/changes"}
388+
389+While this would cause all requests to be sent in parallel:
390+
391+ {"thread": "1", "relpath": "/changes"},
392+ {"thread": "2", "relpath": "/changes"},
393+ {"thread": "3", "relpath": "/changes"},
394+ {"thread": "4", "relpath": "/changes"}
395+
396+This should keep 2 threads pipelined with activity, as long as they finish in
397+approximately the same speed. We'll start the first thread running, and the
398+second thread, and queue up both with a second request once the first finishes.
399+When we get to the third request for thread "1", we block on queuing up more
400+work until the first thread 1 request has finished.
401+ {"thread": "1", "relpath": "/changes"},
402+ {"thread": "2", "relpath": "/changes"},
403+ {"thread": "1", "relpath": "/changes"},
404+ {"thread": "2", "relpath": "/changes"},
405+ {"thread": "1", "relpath": "/changes"},
406+ {"thread": "2", "relpath": "/changes"}
407+
408+There is not currently a way to say "run all these requests keeping exactly 2
409+threads active". Though if you know the load pattern, you could approximate
410+this.
411+"""
412+
413+import threading
414+import time
415+import Queue
416+
417+try:
418+ import simplejson
419+except ImportError:
420+ import json as simplejson
421+
422+from bzrlib import (
423+ errors,
424+ transport,
425+ urlutils,
426+ )
427+
428+# This code will be doing multi-threaded requests against bzrlib.transport
429+# code. We want to make sure to load everything ahead of time, so we don't get
430+# lazy-import failures
431+_ = transport.get_transport('http://example.com')
432+
433+
434+class RequestDescription(object):
435+ """Describes info about a request."""
436+
437+ def __init__(self, descrip_dict):
438+ self.thread = descrip_dict.get('thread', '1')
439+ self.relpath = descrip_dict['relpath']
440+
441+
442+class RequestWorker(object):
443+ """Process requests in a worker thread."""
444+
445+ _timer = time.time
446+
447+ def __init__(self, identifier, blocking_time=1.0, _queue_size=1):
448+ self.identifier = identifier
449+ self.queue = Queue.Queue(_queue_size)
450+ self.start_time = self.end_time = None
451+ self.stats = []
452+ self.blocking_time = blocking_time
453+
454+ def step_next(self):
455+ url = self.queue.get(True, self.blocking_time)
456+ if url == '<noop>':
457+ # This is usually an indicator that we want to stop, so just skip
458+ # this one.
459+ self.queue.task_done()
460+ return
461+ self.start_time = self._timer()
462+ success = self.process(url)
463+ self.end_time = self._timer()
464+ self.update_stats(url, success)
465+ self.queue.task_done()
466+
467+ def run(self, stop_event):
468+ while not stop_event.isSet():
469+ try:
470+ self.step_next()
471+ except Queue.Empty:
472+ pass
473+
474+ def process(self, url):
475+ base, path = urlutils.split(url)
476+ t = transport.get_transport(base)
477+ try:
478+ # TODO: We should probably look into using some part of
479+ # blocking_timeout to decide when to stop trying to read
480+ # content
481+ content = t.get_bytes(path)
482+ except (errors.TransportError, errors.NoSuchFile):
483+ return False
484+ return True
485+
486+ def update_stats(self, url, success):
487+ self.stats.append((url, success, self.end_time - self.start_time))
488+
489+
490+class ActionScript(object):
491+ """This tracks the actions that we want to perform."""
492+
493+ _worker_class = RequestWorker
494+ _default_base_url = 'http://localhost:8080'
495+ _default_blocking_timeout = 60.0
496+
497+ def __init__(self):
498+ self.base_url = self._default_base_url
499+ self.blocking_timeout = self._default_blocking_timeout
500+ self._requests = []
501+ self._threads = {}
502+ self.stop_event = threading.Event()
503+
504+ @classmethod
505+ def parse(cls, content):
506+ script = cls()
507+ json_dict = simplejson.loads(content)
508+ if 'parameters' not in json_dict:
509+ raise ValueError('Missing "parameters" section')
510+ if 'requests' not in json_dict:
511+ raise ValueError('Missing "requests" section')
512+ param_dict = json_dict['parameters']
513+ request_list = json_dict['requests']
514+ base_url = param_dict.get('base_url', None)
515+ if base_url is not None:
516+ script.base_url = base_url
517+ blocking_timeout = param_dict.get('blocking_timeout', None)
518+ if blocking_timeout is not None:
519+ script.blocking_timeout = blocking_timeout
520+ for request_dict in request_list:
521+ script.add_request(request_dict)
522+ return script
523+
524+ def add_request(self, request_dict):
525+ request = RequestDescription(request_dict)
526+ self._requests.append(request)
527+
528+ def _get_worker(self, thread_id):
529+ if thread_id in self._threads:
530+ return self._threads[thread_id][0]
531+ handler = self._worker_class(thread_id,
532+ blocking_time=self.blocking_timeout/10.0)
533+
534+ t = threading.Thread(target=handler.run, args=(self.stop_event,),
535+ name='Thread-%s' % (thread_id,))
536+ self._threads[thread_id] = (handler, t)
537+ t.start()
538+ return handler
539+
540+ def finish_queues(self):
541+ """Wait for all queues of all children to finish."""
542+ for h, t in self._threads.itervalues():
543+ h.queue.join()
544+
545+ def stop_and_join(self):
546+ """Stop all running workers, and return.
547+
548+ This will stop even if workers still have work items.
549+ """
550+ self.stop_event.set()
551+ for h, t in self._threads.itervalues():
552+ # Signal the queue that it should stop blocking, we don't have to
553+ # wait for the queue to empty, because we may see stop_event before
554+ # we see the <noop>
555+ h.queue.put('<noop>')
556+ # And join the controlling thread
557+ for i in range(10):
558+ t.join(self.blocking_timeout / 10.0)
559+ if not t.isAlive():
560+ break
561+
562+ def _full_url(self, relpath):
563+ return self.base_url + relpath
564+
565+ def run(self):
566+ self.stop_event.clear()
567+ for request in self._requests:
568+ full_url = self._full_url(request.relpath)
569+ worker = self._get_worker(request.thread)
570+ worker.queue.put(full_url, True, self.blocking_timeout)
571+ self.finish_queues()
572+ self.stop_and_join()
573+
574+
575+def run_script(filename):
576+ with open(filename, 'rb') as f:
577+ content = f.read()
578+ script = ActionScript.parse(content)
579+ script.run()
580+ return script
581
582=== modified file 'loggerhead/static/css/global.css'
583--- loggerhead/static/css/global.css 2009-10-17 06:55:25 +0000
584+++ loggerhead/static/css/global.css 2011-02-10 23:12:12 +0000
585@@ -96,27 +96,27 @@
586 text-align:center;
587 color:#fff;
588 text-decoration:none;
589- background:#2f40a5;
590+ background:#d18b39;
591 }
592 ul#submenuTabs li {
593 float:left;
594 display:inline;
595 margin-left:1px;
596 color:#fff;
597- background:#2f40a5;
598+ background:#d18b39;
599 }
600 ul#submenuTabs li a:hover {
601 float:left;
602 display:inline;
603 color:#fff;
604- background:#2f40a5;
605+ background:#d18b39;
606 text-decoration:underline;
607 }
608 ul#submenuTabs li#first a, ul#submenuTabs li#first a:link, ul#submenuTabs li#first a:visited, ul#submenuTabs li#first a:hover {
609- background:#2f40a5 url(../images/bg_submenuTabs.gif) top left no-repeat;
610+ background:#d18b39 url(../images/bg_submenuTabs.gif) top left no-repeat;
611 }
612 ul#submenuTabs li#last a, ul#submenuTabs li#last a:link, ul#submenuTabs li#last a:visited, ul#submenuTabs li#last a:hover {
613- background:#2f40a5 url(../images/bg_submenuTabs.gif) bottom right no-repeat;
614+ background:#d18b39 url(../images/bg_submenuTabs.gif) bottom right no-repeat;
615 }
616
617 /* Search & RSS */
618
619=== modified file 'loggerhead/static/images/bg_Tabs.gif'
620Binary files loggerhead/static/images/bg_Tabs.gif 2008-07-25 19:23:15 +0000 and loggerhead/static/images/bg_Tabs.gif 2011-02-10 23:12:12 +0000 differ
621=== modified file 'loggerhead/static/images/bg_menuTabs.gif'
622Binary files loggerhead/static/images/bg_menuTabs.gif 2008-07-25 19:23:15 +0000 and loggerhead/static/images/bg_menuTabs.gif 2011-02-10 23:12:12 +0000 differ
623=== modified file 'loggerhead/static/images/bg_submenuTabs.gif'
624Binary files loggerhead/static/images/bg_submenuTabs.gif 2008-07-25 19:23:15 +0000 and loggerhead/static/images/bg_submenuTabs.gif 2011-02-10 23:12:12 +0000 differ
625=== modified file 'loggerhead/templates/inventory.pt'
626--- loggerhead/templates/inventory.pt 2011-01-11 00:02:53 +0000
627+++ loggerhead/templates/inventory.pt 2011-02-10 23:12:12 +0000
628@@ -129,12 +129,19 @@
629 <td class="date" tal:content="python:util.date_time(file.change.date)"></td>
630 <td class="timedate2" tal:content="python:util.human_size(file.size)"></td>
631 <td class="expcell">
632+ <a class="view_link"
633+ tal:attributes="href python:url(['/view', revno_url, file.absolutepath]);
634+ title string:View ${file/filename}">
635+ <img tal:attributes="src python:branch.static_url('/static/images/ico_planilla.gif')" alt="Diff" />
636+ </a>
637+<!--
638 <a class="raw_link"
639 tal:attributes="href python:url(['/raw', revno_url, file.absolutepath]);
640 title string:Raw content of ${file/filename}">
641 <img tal:attributes="src python:branch.static_url('/static/images/ico_file.gif')"
642 alt="View raw file" />
643 </a>
644+-->
645 <a class="download_link"
646 tal:attributes="href python:url(['/download', revno_url, file.absolutepath]);
647 title string:Download ${file/filename} at revision ${file/change/revno}">
648
649=== modified file 'loggerhead/templates/macros.pt'
650--- loggerhead/templates/macros.pt 2009-10-21 19:17:16 +0000
651+++ loggerhead/templates/macros.pt 2011-02-10 23:12:12 +0000
652@@ -45,14 +45,6 @@
653
654 <body>
655 <!-- Loggerhead Content Area -->
656- <div id="finderBox">
657- <tal:search-box define="navigation navigation"
658- replace="structure python:search_box(branch,
659- navigation)" />
660- <div>
661- <tal:feed-link replace="structure python:feed_link(branch, url)" />
662- </div>
663- </div>
664
665 <tal:menu define="fileview_active fileview_active"
666 replace="structure python:menu(branch, url, fileview_active)" />
667
668=== modified file 'loggerhead/templates/revision.pt'
669--- loggerhead/templates/revision.pt 2010-11-30 10:18:40 +0000
670+++ loggerhead/templates/revision.pt 2011-02-10 23:12:12 +0000
671@@ -20,7 +20,6 @@
672 </head>
673
674 <body>
675-
676 <tal:block metal:fill-slot="heading">
677 <h1>
678 <tal:has-link condition="branch/branch_link">
679
680=== modified file 'loggerhead/tests/__init__.py'
681--- loggerhead/tests/__init__.py 2011-01-28 22:44:19 +0000
682+++ loggerhead/tests/__init__.py 2011-02-10 23:12:12 +0000
683@@ -21,6 +21,7 @@
684 'history_db_tests',
685 'test_controllers',
686 'test_corners',
687+ 'test_load_test',
688 'test_simple',
689 'test_templating',
690 ]]))
691
692=== modified file 'loggerhead/tests/test_controllers.py'
693--- loggerhead/tests/test_controllers.py 2011-01-31 21:27:06 +0000
694+++ loggerhead/tests/test_controllers.py 2011-02-10 23:12:12 +0000
695@@ -1,3 +1,10 @@
696+from cStringIO import StringIO
697+import logging
698+
699+from paste.httpexceptions import HTTPServerError
700+
701+from bzrlib import errors
702+
703 from loggerhead.apps.branch import BranchWSGIApp
704 from loggerhead.controllers.annotate_ui import AnnotateUI
705 from loggerhead.controllers.inventory_ui import InventoryUI
706@@ -15,15 +22,50 @@
707 tree.commit('')
708 tree.branch.lock_read()
709 self.addCleanup(tree.branch.unlock)
710- branch_app = BranchWSGIApp(tree.branch)
711+ branch_app = BranchWSGIApp(tree.branch, '')
712+ branch_app.log.setLevel(logging.CRITICAL)
713+ # These are usually set in BranchWSGIApp.app(), which is set from env
714+ # settings set by BranchesFromTransportRoot, so we fake it.
715+ branch_app._static_url_base = '/'
716+ branch_app._url_base = '/'
717 return tree.branch, InventoryUI(branch_app, branch_app.get_history)
718
719+ def consume_app(self, app, extra_environ=None):
720+ env = {'SCRIPT_NAME': '/files', 'PATH_INFO': ''}
721+ if extra_environ is not None:
722+ env.update(extra_environ)
723+ body = StringIO()
724+ start = []
725+ def start_response(status, headers, exc_info=None):
726+ start.append((status, headers, exc_info))
727+ return body.write
728+ extra_content = list(app(env, start_response))
729+ body.writelines(extra_content)
730+ return start[0], body.getvalue()
731+
732 def test_get_filelist(self):
733 bzrbranch, inv_ui = self.make_bzrbranch_and_inventory_ui_for_tree_shape(
734 ['filename'])
735 inv = bzrbranch.repository.get_inventory(bzrbranch.last_revision())
736 self.assertEqual(1, len(inv_ui.get_filelist(inv, '', 'filename')))
737
738+ def test_smoke(self):
739+ bzrbranch, inv_ui = self.make_bzrbranch_and_inventory_ui_for_tree_shape(
740+ ['filename'])
741+ start, content = self.consume_app(inv_ui)
742+ self.assertEqual(('200 OK', [('Content-Type', 'text/html')], None),
743+ start)
744+ self.assertContainsRe(content, 'filename')
745+
746+ def test_no_content_for_HEAD(self):
747+ bzrbranch, inv_ui = self.make_bzrbranch_and_inventory_ui_for_tree_shape(
748+ ['filename'])
749+ start, content = self.consume_app(inv_ui,
750+ extra_environ={'REQUEST_METHOD': 'HEAD'})
751+ self.assertEqual(('200 OK', [('Content-Type', 'text/html')], None),
752+ start)
753+ self.assertEqual('', content)
754+
755
756 class TestRevisionUI(BasicTests):
757
758
759=== added file 'loggerhead/tests/test_load_test.py'
760--- loggerhead/tests/test_load_test.py 1970-01-01 00:00:00 +0000
761+++ loggerhead/tests/test_load_test.py 2011-02-10 23:12:12 +0000
762@@ -0,0 +1,337 @@
763+# Copyright 2011 Canonical Ltd
764+#
765+# This program is free software; you can redistribute it and/or modify
766+# it under the terms of the GNU General Public License as published by
767+# the Free Software Foundation; either version 2 of the License, or
768+# (at your option) any later version.
769+#
770+# This program is distributed in the hope that it will be useful,
771+# but WITHOUT ANY WARRANTY; without even the implied warranty of
772+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
773+#
774+
775+"""Tests for the load testing code."""
776+
777+import socket
778+import time
779+import threading
780+import Queue
781+
782+from bzrlib import tests
783+from bzrlib.tests import http_server
784+
785+from bzrlib.plugins.loggerhead.loggerhead import load_test
786+
787+
788+empty_script = """{
789+ "parameters": {},
790+ "requests": []
791+}"""
792+
793+class TestRequestDescription(tests.TestCase):
794+
795+ def test_init_from_dict(self):
796+ rd = load_test.RequestDescription({'thread': '10', 'relpath': '/foo'})
797+ self.assertEqual('10', rd.thread)
798+ self.assertEqual('/foo', rd.relpath)
799+
800+ def test_default_thread_is_1(self):
801+ rd = load_test.RequestDescription({'relpath': '/bar'})
802+ self.assertEqual('1', rd.thread)
803+ self.assertEqual('/bar', rd.relpath)
804+
805+
806+_cur_time = time.time()
807+def one_sec_timer():
808+ """Every time this timer is called, it increments by 1 second."""
809+ global _cur_time
810+ _cur_time += 1.0
811+ return _cur_time
812+
813+
814+class NoopRequestWorker(load_test.RequestWorker):
815+
816+ # Every call to _timer will increment by one
817+ _timer = staticmethod(one_sec_timer)
818+
819+ # Ensure that process never does anything
820+ def process(self, url):
821+ return True
822+
823+
824+class TestRequestWorkerInfrastructure(tests.TestCase):
825+ """Tests various infrastructure bits, without doing actual requests."""
826+
827+ def test_step_next_tracks_time(self):
828+ rt = NoopRequestWorker('id')
829+ rt.queue.put('item')
830+ rt.step_next()
831+ self.assertTrue(rt.queue.empty())
832+ self.assertEqual([('item', True, 1.0)], rt.stats)
833+
834+ def test_step_multiple_items(self):
835+ rt = NoopRequestWorker('id')
836+ rt.queue.put('item')
837+ rt.step_next()
838+ rt.queue.put('next-item')
839+ rt.step_next()
840+ self.assertTrue(rt.queue.empty())
841+ self.assertEqual([('item', True, 1.0), ('next-item', True, 1.0)],
842+ rt.stats)
843+
844+ def test_step_next_does_nothing_for_noop(self):
845+ rt = NoopRequestWorker('id')
846+ rt.queue.put('item')
847+ rt.step_next()
848+ rt.queue.put('<noop>')
849+ rt.step_next()
850+ self.assertEqual([('item', True, 1.0)], rt.stats)
851+
852+ def test_step_next_will_timeout(self):
853+ # We don't want step_next to block forever
854+ rt = NoopRequestWorker('id', blocking_time=0.001)
855+ self.assertRaises(Queue.Empty, rt.step_next)
856+
857+ def test_run_stops_for_stop_event(self):
858+ rt = NoopRequestWorker('id', blocking_time=0.001, _queue_size=2)
859+ rt.queue.put('item1')
860+ rt.queue.put('item2')
861+ event = threading.Event()
862+ t = threading.Thread(target=rt.run, args=(event,))
863+ t.start()
864+ # Wait for the queue to be processed
865+ rt.queue.join()
866+ # Now we can queue up another one, and wait for it
867+ rt.queue.put('item3')
868+ rt.queue.join()
869+ # Now set the stopping event
870+ event.set()
871+ # Add another item to the queue, which might get processed, but the
872+ # next item won't
873+ rt.queue.put('item4')
874+ rt.queue.put('item5')
875+ t.join()
876+ self.assertEqual([('item1', True, 1.0), ('item2', True, 1.0),
877+ ('item3', True, 1.0)],
878+ rt.stats[:3])
879+ # The last event might be item4 or might be item3, the important thing
880+ # is that even though there are still queued events, we won't
881+ # process anything past the first
882+ self.assertNotEqual('item5', rt.stats[-1][0])
883+
884+
885+class TestRequestWorker(tests.TestCaseWithTransport):
886+
887+ def setUp(self):
888+ super(TestRequestWorker, self).setUp()
889+ self.transport_readonly_server = http_server.HttpServer
890+
891+ def test_request_items(self):
892+ rt = load_test.RequestWorker('id', blocking_time=0.01, _queue_size=2)
893+ self.build_tree(['file1', 'file2'])
894+ readonly_url1 = self.get_readonly_url('file1')
895+ self.assertStartsWith(readonly_url1, 'http://')
896+ readonly_url2 = self.get_readonly_url('file2')
897+ rt.queue.put(readonly_url1)
898+ rt.queue.put(readonly_url2)
899+ rt.step_next()
900+ rt.step_next()
901+ self.assertEqual(readonly_url1, rt.stats[0][0])
902+ self.assertEqual(readonly_url2, rt.stats[1][0])
903+
904+ def test_request_nonexistant_items(self):
905+ rt = load_test.RequestWorker('id', blocking_time=0.01, _queue_size=2)
906+ readonly_url1 = self.get_readonly_url('no-file1')
907+ rt.queue.put(readonly_url1)
908+ rt.step_next()
909+ self.assertEqual(readonly_url1, rt.stats[0][0])
910+ self.assertEqual(False, rt.stats[0][1])
911+
912+ def test_no_server(self):
913+ s = socket.socket()
914+ # Bind to a port, but don't listen on it
915+ s.bind(('localhost', 0))
916+ url = 'http://%s:%s/path' % s.getsockname()
917+ rt = load_test.RequestWorker('id', blocking_time=0.01, _queue_size=2)
918+ rt.queue.put(url)
919+ rt.step_next()
920+ self.assertEqual((url, False), rt.stats[0][:2])
921+
922+
923+class NoActionScript(load_test.ActionScript):
924+
925+ _thread_class = NoopRequestWorker
926+ _default_blocking_timeout = 0.01
927+
928+
929+class TestActionScriptInfrastructure(tests.TestCase):
930+
931+ def test_parse_requires_parameters_and_requests(self):
932+ self.assertRaises(ValueError,
933+ load_test.ActionScript.parse, '')
934+ self.assertRaises(ValueError,
935+ load_test.ActionScript.parse, '{}')
936+ self.assertRaises(ValueError,
937+ load_test.ActionScript.parse, '{"parameters": {}}')
938+ self.assertRaises(ValueError,
939+ load_test.ActionScript.parse, '{"requests": []}')
940+ load_test.ActionScript.parse(
941+ '{"parameters": {}, "requests": [], "comment": "section"}')
942+ script = load_test.ActionScript.parse(
943+ empty_script)
944+ self.assertIsNot(None, script)
945+
946+ def test_parse_default_base_url(self):
947+ script = load_test.ActionScript.parse(empty_script)
948+ self.assertEqual('http://localhost:8080', script.base_url)
949+
950+ def test_parse_find_base_url(self):
951+ script = load_test.ActionScript.parse(
952+ '{"parameters": {"base_url": "http://example.com"},'
953+ ' "requests": []}')
954+ self.assertEqual('http://example.com', script.base_url)
955+
956+ def test_parse_default_blocking_timeout(self):
957+ script = load_test.ActionScript.parse(empty_script)
958+ self.assertEqual(60.0, script.blocking_timeout)
959+
960+ def test_parse_find_blocking_timeout(self):
961+ script = load_test.ActionScript.parse(
962+ '{"parameters": {"blocking_timeout": 10.0},'
963+ ' "requests": []}'
964+ )
965+ self.assertEqual(10.0, script.blocking_timeout)
966+
967+ def test_parse_finds_requests(self):
968+ script = load_test.ActionScript.parse(
969+ '{"parameters": {}, "requests": ['
970+ ' {"relpath": "/foo"},'
971+ ' {"relpath": "/bar"}'
972+ ' ]}')
973+ self.assertEqual(2, len(script._requests))
974+ self.assertEqual("/foo", script._requests[0].relpath)
975+ self.assertEqual("/bar", script._requests[1].relpath)
976+
977+ def test__get_worker(self):
978+ script = NoActionScript()
979+ # If an id is found, then we should create it
980+ self.assertEqual({}, script._threads)
981+ worker = script._get_worker('id')
982+ self.assertTrue('id' in script._threads)
983+ # We should have set the blocking timeout
984+ self.assertEqual(script.blocking_timeout / 10.0,
985+ worker.blocking_time)
986+
987+ # Another request will return the identical object
988+ self.assertIs(worker, script._get_worker('id'))
989+
990+ # And the stop event will stop the thread
991+ script.stop_and_join()
992+
993+ def test__full_url(self):
994+ script = NoActionScript()
995+ self.assertEqual('http://localhost:8080/path',
996+ script._full_url('/path'))
997+ self.assertEqual('http://localhost:8080/path/to/foo',
998+ script._full_url('/path/to/foo'))
999+ script.base_url = 'http://example.com'
1000+ self.assertEqual('http://example.com/path/to/foo',
1001+ script._full_url('/path/to/foo'))
1002+ script.base_url = 'http://example.com/base'
1003+ self.assertEqual('http://example.com/base/path/to/foo',
1004+ script._full_url('/path/to/foo'))
1005+ script.base_url = 'http://example.com'
1006+ self.assertEqual('http://example.com:8080/path',
1007+ script._full_url(':8080/path'))
1008+
1009+ def test_single_threaded(self):
1010+ script = NoActionScript.parse("""{
1011+ "parameters": {"base_url": ""},
1012+ "requests": [
1013+ {"thread": "1", "relpath": "first"},
1014+ {"thread": "1", "relpath": "second"},
1015+ {"thread": "1", "relpath": "third"},
1016+ {"thread": "1", "relpath": "fourth"}
1017+ ]}""")
1018+ script.run()
1019+ worker = script._get_worker("1")
1020+ self.assertEqual(["first", "second", "third", "fourth"],
1021+ [s[0] for s in worker.stats])
1022+
1023+ def test_two_threads(self):
1024+ script = NoActionScript.parse("""{
1025+ "parameters": {"base_url": ""},
1026+ "requests": [
1027+ {"thread": "1", "relpath": "first"},
1028+ {"thread": "2", "relpath": "second"},
1029+ {"thread": "1", "relpath": "third"},
1030+ {"thread": "2", "relpath": "fourth"}
1031+ ]}""")
1032+ script.run()
1033+ worker = script._get_worker("1")
1034+ self.assertEqual(["first", "third"],
1035+ [s[0] for s in worker.stats])
1036+ worker = script._get_worker("2")
1037+ self.assertEqual(["second", "fourth"],
1038+ [s[0] for s in worker.stats])
1039+
1040+
1041+class TestActionScriptIntegration(tests.TestCaseWithTransport):
1042+
1043+ def setUp(self):
1044+ super(TestActionScriptIntegration, self).setUp()
1045+ self.transport_readonly_server = http_server.HttpServer
1046+
1047+ def test_full_integration(self):
1048+ self.build_tree(['first', 'second', 'third', 'fourth'])
1049+ url = self.get_readonly_url()
1050+ script = load_test.ActionScript.parse("""{
1051+ "parameters": {"base_url": "%s", "blocking_timeout": 0.1},
1052+ "requests": [
1053+ {"thread": "1", "relpath": "first"},
1054+ {"thread": "2", "relpath": "second"},
1055+ {"thread": "1", "relpath": "no-this"},
1056+ {"thread": "2", "relpath": "or-this"},
1057+ {"thread": "1", "relpath": "third"},
1058+ {"thread": "2", "relpath": "fourth"}
1059+ ]}""" % (url,))
1060+ script.run()
1061+ worker = script._get_worker("1")
1062+ self.assertEqual([("first", True), ('no-this', False),
1063+ ("third", True)],
1064+ [(s[0].rsplit('/', 1)[1], s[1])
1065+ for s in worker.stats])
1066+ worker = script._get_worker("2")
1067+ self.assertEqual([("second", True), ('or-this', False),
1068+ ("fourth", True)],
1069+ [(s[0].rsplit('/', 1)[1], s[1])
1070+ for s in worker.stats])
1071+
1072+
1073+class TestRunScript(tests.TestCaseWithTransport):
1074+
1075+ def setUp(self):
1076+ super(TestRunScript, self).setUp()
1077+ self.transport_readonly_server = http_server.HttpServer
1078+
1079+ def test_run_script(self):
1080+ self.build_tree(['file1', 'file2', 'file3', 'file4'])
1081+ url = self.get_readonly_url()
1082+ self.build_tree_contents([('localhost.script', """{
1083+ "parameters": {"base_url": "%s", "blocking_timeout": 0.1},
1084+ "requests": [
1085+ {"thread": "1", "relpath": "file1"},
1086+ {"thread": "2", "relpath": "file2"},
1087+ {"thread": "1", "relpath": "file3"},
1088+ {"thread": "2", "relpath": "file4"}
1089+ ]
1090+}""" % (url,))])
1091+ script = load_test.run_script('localhost.script')
1092+ worker = script._threads["1"][0]
1093+ self.assertEqual([("file1", True), ('file3', True)],
1094+ [(s[0].rsplit('/', 1)[1], s[1])
1095+ for s in worker.stats])
1096+ worker = script._threads["2"][0]
1097+ self.assertEqual([("file2", True), ("file4", True)],
1098+ [(s[0].rsplit('/', 1)[1], s[1])
1099+ for s in worker.stats])
1100
1101=== modified file 'loggerhead/tests/test_simple.py'
1102--- loggerhead/tests/test_simple.py 2010-05-14 10:58:20 +0000
1103+++ loggerhead/tests/test_simple.py 2011-02-10 23:12:12 +0000
1104@@ -95,8 +95,18 @@
1105 def test_annotate(self):
1106 app = self.setUpLoggerhead()
1107 res = app.get('/annotate', params={'file_id': self.fileid})
1108- for line in self.filecontents.splitlines():
1109- res.mustcontain(cgi.escape(line))
1110+ # This now fails because pigments is highlighting the lines.
1111+ # Specifically, instead of getting:
1112+ # 'with&lt;htmlspecialchars' we are now getting
1113+ # '<span class='pyg-n'>with</span><span class='pyg-o'>&lt;</span>'
1114+ # '<span class='pyg-n'>htmlspecialchars</span>
1115+ # So pygments is breaking up the special characters with custom
1116+ # highlighting. The alternative is to figure out how to disable
1117+ # pygments for this test.
1118+ ## for line in self.filecontents.splitlines():
1119+ ## res.mustcontain(cgi.escape(line))
1120+ self.assertContainsRe(res.body,
1121+ "with.*&lt;.*htmlspecialchars")
1122
1123 def test_inventory(self):
1124 app = self.setUpLoggerhead()

Subscribers

People subscribed via source and target branches

to all changes: