Merge lp:~jml/launchpad/dirty-parry into lp:launchpad

Proposed by Jonathan Lange
Status: Rejected
Rejected by: Jonathan Lange
Proposed branch: lp:~jml/launchpad/dirty-parry
Merge into: lp:launchpad
Diff against target: 254 lines (+130/-10)
3 files modified
lib/devscripts/ec2test/builtins.py (+43/-6)
lib/devscripts/ec2test/testrunner.py (+29/-4)
lib/devscripts/utils.py (+58/-0)
To merge this branch: bzr merge lp:~jml/launchpad/dirty-parry
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Needs Information
Review via email: mp+14617@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jonathan Lange (jml) wrote :

This branch works towards running the test suite in parallel. It takes a different approach to allenap's ec2-parry branch, hence the name "dirty-parry" -- it's a quick and dirty approach. It doesn't actually make the test suite runnable in parallel.

It adds two... abilities to the ec2 program. The first is that EC2TestRunner now populates a TestResult object, rather than simply dumping stuff to stdout. It does this by running the tests on the instance with a subunit test result object, and then parsing this output on the client side.

The other one is the ability to split the test suite up into N parts. This isn't really useful right now, but combining it with the previous ability basically gives us parallelization. Maybe I should do that before this branch lands.

Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Most of this branch looks fine... we have subunit in sourcecode already, right? However...

It seems this branch does 80% of what would actually be useful: why don't you actually boot num_instance instances? I guess this is the thing that you talked about with using Twisted or threads or whatever: you'd want to do this in parallel. That said, until num_instances >1 boots >1 instance, I'm not sure what the win from landing this would be.

review: Needs Information
Revision history for this message
Jonathan Lange (jml) wrote :

On Tue, Nov 10, 2009 at 2:18 PM, Michael Hudson
<email address hidden> wrote:
> Review: Needs Information
> Most of this branch looks fine... we have subunit in sourcecode already, right?  However...
>
> It seems this branch does 80% of what would actually be useful: why don't you actually boot num_instance instances?  I guess this is the thing that you talked about with using Twisted or threads or whatever: you'd want to do this in parallel.  That said, until num_instances >1 boots >1 instance, I'm not sure what the win from landing this would be.

OK, I'll make it work then.

jml

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/devscripts/ec2test/builtins.py'
2--- lib/devscripts/ec2test/builtins.py 2009-10-16 11:07:37 +0000
3+++ lib/devscripts/ec2test/builtins.py 2009-11-08 09:20:38 +0000
4@@ -9,6 +9,8 @@
5 import os
6 import pdb
7 import subprocess
8+import sys
9+import unittest
10
11 from bzrlib.bzrdir import BzrDir
12 from bzrlib.commands import Command
13@@ -25,6 +27,7 @@
14 AVAILABLE_INSTANCE_TYPES, DEFAULT_INSTANCE_TYPE, EC2Instance)
15 from devscripts.ec2test.session import EC2SessionName
16 from devscripts.ec2test.testrunner import EC2TestRunner, TRUNK_BRANCH
17+from devscripts.utils import split_list
18
19
20 # Options accepted by more than one command.
21@@ -239,6 +242,12 @@
22 'If the branch is local, then the bzr configuration is '
23 'consulted; for remote branches "Launchpad PQM '
24 '<launchpad@pqm.canonical.com>" is used by default.')),
25+ Option(
26+ 'num-instances', short_name='j', type=int,
27+ help=(
28+ 'The number of EC2 instances to spread the tests across. '
29+ 'Incompatible with --headless. Currently only runs a '
30+ 'fraction of the tests.')),
31 postmortem_option,
32 Option(
33 'headless',
34@@ -254,23 +263,45 @@
35
36 takes_args = ['test_branch?']
37
38+ def _load_tests(self):
39+ """Return the full list of tests."""
40+ tests = subprocess.Popen(
41+ ['./bin/test', '--list'], stdout=subprocess.PIPE).communicate()[0]
42+ return tests.splitlines()[:-1]
43+
44 def run(self, test_branch=None, branch=None, trunk=False, machine=None,
45 instance_type=DEFAULT_INSTANCE_TYPE,
46 file=None, email=None, test_options='-vv', noemail=False,
47 submit_pqm_message=None, pqm_public_location=None,
48 pqm_submit_location=None, pqm_email=None, postmortem=False,
49 headless=False, debug=False, open_browser=False,
50- include_download_cache_changes=False):
51+ include_download_cache_changes=False, num_instances=1):
52 if debug:
53 pdb.set_trace()
54 if branch is None:
55 branch = []
56 branches, test_branch = _get_branches_and_test_branch(
57 trunk, branch, test_branch)
58- if ((postmortem or file) and headless):
59+ if (postmortem or file) and headless:
60 raise BzrCommandError(
61- 'Headless mode currently does not support postmortem or file '
62- ' options.')
63+ 'Headless mode currently does not support postmortem or '
64+ 'file options.')
65+ if num_instances > 1:
66+ if headless:
67+ raise BzrCommandError(
68+ 'Headless mode currently does not support using more '
69+ 'than one instance.')
70+ if test_options != '-vv':
71+ raise BzrCommandError(
72+ "Cannot specify non-default options when running with "
73+ "multiple instances.")
74+ tests = split_list(self._load_tests(), num_instances)[0]
75+ # XXX: JonathanLange 2009-11-08: Email logic doesn't work properly
76+ # with this running only part of the test suite. Need to figure
77+ # this out before full parallel support can work.
78+ noemail = True
79+ else:
80+ tests = None
81 if noemail:
82 if email:
83 raise BzrCommandError(
84@@ -301,9 +332,15 @@
85 pqm_submit_location=pqm_submit_location,
86 open_browser=open_browser, pqm_email=pqm_email,
87 include_download_cache_changes=include_download_cache_changes,
88- instance=instance, launchpad_login=instance._launchpad_login)
89+ instance=instance, launchpad_login=instance._launchpad_login,
90+ tests=tests)
91
92- instance.set_up_and_run(postmortem, not headless, runner.run_tests)
93+ result = unittest._TextTestResult(
94+ unittest._WritelnDecorator(sys.stdout), 0, 2)
95+ result.getDescription = lambda test: test.id()
96+ instance.set_up_and_run(
97+ postmortem, not headless, runner.run_tests, result)
98+ result.printErrors()
99
100
101 class cmd_land(EC2Command):
102
103=== modified file 'lib/devscripts/ec2test/testrunner.py'
104--- lib/devscripts/ec2test/testrunner.py 2009-10-04 15:35:16 +0000
105+++ lib/devscripts/ec2test/testrunner.py 2009-11-08 09:20:38 +0000
106@@ -13,6 +13,7 @@
107 import pickle
108 import re
109 import sys
110+import tempfile
111
112 from bzrlib.branch import Branch
113 from bzrlib.bzrdir import BzrDir
114@@ -21,6 +22,8 @@
115 from bzrlib.plugins.pqm.pqm_submit import (
116 NoPQMSubmissionAddress, PQMSubmission)
117
118+from devscripts.utils import TestResultToStream
119+
120
121 TRUNK_BRANCH = 'bzr+ssh://bazaar.launchpad.net/~launchpad-pqm/launchpad/devel'
122
123@@ -115,7 +118,7 @@
124 pqm_submit_location=None,
125 open_browser=False, pqm_email=None,
126 include_download_cache_changes=None, instance=None,
127- launchpad_login=None):
128+ launchpad_login=None, tests=None):
129 """Create a new EC2TestRunner.
130
131 This sets the following attributes:
132@@ -137,6 +140,7 @@
133 self.open_browser = open_browser
134 self.file = file
135 self._launchpad_login = launchpad_login
136+ self._tests = tests
137
138 trunk_specified = False
139 trunk_branch = TRUNK_BRANCH
140@@ -446,7 +450,19 @@
141 # close ssh connection
142 user_connection.close()
143
144- def run_tests(self):
145+ def _copy_tests(self, connection, tests):
146+ """Copy a list of tests to the server."""
147+ tempdir = tempfile.mkdtemp()
148+ testfilepath = os.path.join(tempdir, 'test-list')
149+ testfile = open(testfilepath, 'wb')
150+ for test in tests:
151+ testfile.write(test)
152+ testfile.write('\n')
153+ testfile.close()
154+ connection.sftp.put(testfilepath, '/var/launchpad/test-list')
155+ return '/var/launchpad/test-list'
156+
157+ def run_tests(self, result):
158 self.configure_system()
159 self.prepare_tests()
160 user_connection = self._instance.connect()
161@@ -488,6 +504,11 @@
162
163 # Add any additional options for ec2test-remote.py
164 cmd.extend(self.get_remote_test_options())
165+
166+ if self._tests is not None:
167+ test_path = self._copy_tests(user_connection, self._tests)
168+ cmd.append('--load-list=%s' % test_path)
169+
170 self.log(
171 'Running tests... (output is available on '
172 'http://%s/)\n' % self._instance.hostname)
173@@ -505,7 +526,8 @@
174
175 # Run the remote script! Our execution will block here until the
176 # remote side disconnects from the terminal.
177- user_connection.perform(' '.join(cmd))
178+ user_connection.perform(
179+ ' '.join(cmd), out=TestResultToStream(result))
180 self._running = True
181
182 if not self.headless:
183@@ -536,4 +558,7 @@
184 # Run the normal testsuite with our Zope testrunner options.
185 # ec2test-remote.py wants the extra options to be after a double-
186 # dash.
187- return ('--', self.test_options)
188+ if self._tests is None:
189+ return ('--', '--subunit', self.test_options)
190+ else:
191+ return ('--', '--subunit')
192
193=== added file 'lib/devscripts/utils.py'
194--- lib/devscripts/utils.py 1970-01-01 00:00:00 +0000
195+++ lib/devscripts/utils.py 2009-11-08 09:20:38 +0000
196@@ -0,0 +1,58 @@
197+# Copyright 2009 Canonical Ltd. This software is licensed under the
198+# GNU Affero General Public License version 3 (see the file LICENSE).
199+
200+__metaclass__ = type
201+__all__ = [
202+ 'LineReceiver',
203+ 'split_list',
204+ 'TestResultToStream',
205+ ]
206+
207+from itertools import cycle, izip
208+
209+import subunit
210+
211+
212+def split_list(stuff, n):
213+ """Split a 'stuff' into 'n' similarly-sized chunks."""
214+ chunks = [[] for i in range(n)]
215+ for thing, chunk in izip(stuff, cycle(chunks)):
216+ chunk.append(thing)
217+ return chunks
218+
219+
220+class LineReceiver:
221+ """Stream-like interface that calls the 'lineReceived' callback."""
222+
223+ delimiter = '\n'
224+
225+ def __init__(self, lineReceived):
226+ self._buffer = ''
227+ self._lineReceived = lineReceived
228+
229+ def write(self, data):
230+ self._buffer += data
231+ lines = self._buffer.split(self.delimiter)
232+ self._buffer = lines.pop()
233+ for line in lines:
234+ self._lineReceived(line)
235+
236+ def flush(self):
237+ pass
238+
239+ def close(self):
240+ pass
241+
242+
243+class TestResultToStream(LineReceiver):
244+ """Wraps a result in a stream-like interface."""
245+
246+ def __init__(self, result):
247+ self._subunit = subunit.TestProtocolServer(result)
248+ super(TestResultToStream, self).__init__(self.lineReceived)
249+
250+ def lineReceived(self, line):
251+ self._subunit.lineReceived(line + '\n')
252+
253+ def close(self):
254+ self._subunit.lostConnection()