9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
1 |
#!/usr/bin/env python
|
2 |
# Copyright 2009 Canonical Ltd. This software is licensed under the
|
|
3 |
# GNU Affero General Public License version 3 (see the file LICENSE).
|
|
4 |
||
11128.13.45
by Jonathan Lange
Document the way I'm thinking about this problem. |
5 |
"""Run tests in a daemon.
|
6 |
||
7 |
* `EC2Runner` handles the daemonization and instance shutdown.
|
|
8 |
||
9 |
* `Request` knows everything about the test request we're handling (e.g.
|
|
10 |
"test merging foo-bar-bug-12345 into db-devel").
|
|
11 |
||
12 |
* `LaunchpadTester` knows how to actually run the tests and gather the
|
|
12414.1.4
by Jonathan Lange
Kill unused FlagFallStream. |
13 |
results. It uses `SummaryResult` to do so.
|
11128.13.45
by Jonathan Lange
Document the way I'm thinking about this problem. |
14 |
|
15 |
* `WebTestLogger` knows how to display the results to the user, and is given
|
|
16 |
the responsibility of handling the results that `LaunchpadTester` gathers.
|
|
17 |
"""
|
|
18 |
||
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
19 |
__metatype__ = type |
20 |
||
21 |
import datetime |
|
14612.2.5
by William Grant
Format the non-contrib bits of lib. |
22 |
from email import ( |
23 |
MIMEMultipart, |
|
24 |
MIMEText, |
|
25 |
)
|
|
10736.3.7
by Jonathan Lange
Import from the right place |
26 |
from email.mime.application import MIMEApplication |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
27 |
import errno |
10736.3.4
by Jonathan Lange
Try to make the email with a gzipped attachment. |
28 |
import gzip |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
29 |
import optparse |
30 |
import os |
|
31 |
import pickle |
|
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
32 |
from StringIO import StringIO |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
33 |
import subprocess |
34 |
import sys |
|
10736.3.4
by Jonathan Lange
Try to make the email with a gzipped attachment. |
35 |
import tempfile |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
36 |
import textwrap |
37 |
import time |
|
38 |
import traceback |
|
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
39 |
import unittest |
9668.1.2
by Gavin Panella
Properly escape data before inclusion in the web report. |
40 |
from xml.sax.saxutils import escape |
41 |
||
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
42 |
import bzrlib.branch |
43 |
import bzrlib.config |
|
12449.1.5
by William Grant
Merge devel. |
44 |
from bzrlib.email_message import EmailMessage |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
45 |
import bzrlib.errors |
12449.1.5
by William Grant
Merge devel. |
46 |
from bzrlib.smtp_connection import SMTPConnection |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
47 |
import bzrlib.workingtree |
12449.1.5
by William Grant
Merge devel. |
48 |
import simplejson |
10189.6.22
by Jonathan Lange
Remove the horrible hack -- it's now installed in the packages |
49 |
import subunit |
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
50 |
from testtools import MultiTestResult |
51 |
||
12449.1.4
by William Grant
Initialise bzrlib's plugins so pickle can import bzrlib.plugins.pqm. This wasn't necessary when bzr and bzr-pqm were installed in the same place, but newer bzrs are installed elsewhere. |
52 |
# We need to be able to unpickle objects from bzr-pqm, so make sure we
|
53 |
# can import it.
|
|
54 |
bzrlib.plugin.load_plugins() |
|
55 |
||
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
56 |
|
11407.2.16
by Jonathan Lange
Pass around a result, rather than booleans |
57 |
class NonZeroExitCode(Exception): |
58 |
"""Raised when the child process exits with a non-zero exit code."""
|
|
59 |
||
60 |
def __init__(self, retcode): |
|
61 |
super(NonZeroExitCode, self).__init__( |
|
62 |
'Test process died with exit code %r, but no tests failed.' |
|
63 |
% (retcode,)) |
|
64 |
||
65 |
||
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
66 |
class SummaryResult(unittest.TestResult): |
67 |
"""Test result object used to generate the summary."""
|
|
68 |
||
11128.13.4
by Jonathan Lange
Don't print out anything about the test results until we're done |
69 |
double_line = '=' * 70 |
70 |
single_line = '-' * 70 |
|
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
71 |
|
72 |
def __init__(self, output_stream): |
|
73 |
super(SummaryResult, self).__init__() |
|
74 |
self.stream = output_stream |
|
11128.13.4
by Jonathan Lange
Don't print out anything about the test results until we're done |
75 |
|
76 |
def _formatError(self, flavor, test, error): |
|
77 |
return '\n'.join( |
|
78 |
[self.double_line, |
|
79 |
'%s: %s' % (flavor, test), |
|
80 |
self.single_line, |
|
81 |
error, |
|
82 |
'']) |
|
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
83 |
|
84 |
def addError(self, test, error): |
|
85 |
super(SummaryResult, self).addError(test, error) |
|
11128.13.47
by Jonathan Lange
Log to the summary in real time. |
86 |
self.stream.write( |
11128.13.4
by Jonathan Lange
Don't print out anything about the test results until we're done |
87 |
self._formatError( |
88 |
'ERROR', test, self._exc_info_to_string(error, test))) |
|
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
89 |
|
90 |
def addFailure(self, test, error): |
|
91 |
super(SummaryResult, self).addFailure(test, error) |
|
11128.13.47
by Jonathan Lange
Log to the summary in real time. |
92 |
self.stream.write( |
11128.13.4
by Jonathan Lange
Don't print out anything about the test results until we're done |
93 |
self._formatError( |
94 |
'FAILURE', test, self._exc_info_to_string(error, test))) |
|
95 |
||
11128.13.102
by Jonathan Lange
Flush the stream when we reach the end of a test. |
96 |
def stopTest(self, test): |
97 |
super(SummaryResult, self).stopTest(test) |
|
98 |
# At the very least, we should be sure that a test's output has been
|
|
99 |
# completely displayed once it has stopped.
|
|
100 |
self.stream.flush() |
|
101 |
||
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
102 |
|
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
103 |
class FailureUpdateResult(unittest.TestResult): |
104 |
||
105 |
def __init__(self, logger): |
|
106 |
super(FailureUpdateResult, self).__init__() |
|
107 |
self._logger = logger |
|
108 |
||
109 |
def addError(self, *args, **kwargs): |
|
110 |
super(FailureUpdateResult, self).addError(*args, **kwargs) |
|
111 |
self._logger.got_failure() |
|
112 |
||
113 |
def addFailure(self, *args, **kwargs): |
|
114 |
super(FailureUpdateResult, self).addFailure(*args, **kwargs) |
|
115 |
self._logger.got_failure() |
|
116 |
||
117 |
||
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
118 |
class EC2Runner: |
119 |
"""Runs generic code in an EC2 instance.
|
|
120 |
||
121 |
Handles daemonization, instance shutdown, and email in the case of
|
|
122 |
catastrophic failure.
|
|
123 |
"""
|
|
124 |
||
11128.13.95
by Jonathan Lange
Make the XXXs nicer. |
125 |
# XXX: JonathanLange 2010-08-17: EC2Runner needs tests.
|
11128.13.77
by Jonathan Lange
Bunch of XXXs |
126 |
|
11128.13.22
by Jonathan Lange
Use fewer magic literals. |
127 |
# The number of seconds we give this script to clean itself up, and for
|
128 |
# 'ec2 test --postmortem' to grab control if needed. If we don't give
|
|
129 |
# --postmortem enough time to log in via SSH and take control, then this
|
|
130 |
# server will begin to shutdown on its own.
|
|
131 |
#
|
|
132 |
# (FWIW, "grab control" means sending SIGTERM to this script's process id,
|
|
133 |
# thus preventing fail-safe shutdown.)
|
|
134 |
SHUTDOWN_DELAY = 60 |
|
135 |
||
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
136 |
def __init__(self, daemonize, pid_filename, shutdown_when_done, |
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
137 |
smtp_connection=None, emails=None): |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
138 |
"""Make an EC2Runner.
|
139 |
||
140 |
:param daemonize: Whether or not we will daemonize.
|
|
141 |
:param pid_filename: The filename to store the pid in.
|
|
142 |
:param shutdown_when_done: Whether or not to shut down when the tests
|
|
143 |
are done.
|
|
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
144 |
:param smtp_connection: The `SMTPConnection` to use to send email.
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
145 |
:param emails: The email address(es) to send catastrophic failure
|
146 |
messages to. If not provided, the error disappears into the ether.
|
|
147 |
"""
|
|
148 |
self._should_daemonize = daemonize |
|
149 |
self._pid_filename = pid_filename |
|
150 |
self._shutdown_when_done = shutdown_when_done |
|
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
151 |
if smtp_connection is None: |
152 |
config = bzrlib.config.GlobalConfig() |
|
153 |
smtp_connection = SMTPConnection(config) |
|
154 |
self._smtp_connection = smtp_connection |
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
155 |
self._emails = emails |
156 |
self._daemonized = False |
|
157 |
||
158 |
def _daemonize(self): |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
159 |
"""Turn the testrunner into a forked daemon process."""
|
160 |
# This also writes our pidfile to disk to a specific location. The
|
|
161 |
# ec2test.py --postmortem command will look there to find our PID,
|
|
162 |
# in order to control this process.
|
|
11128.13.20
by Jonathan Lange
More name errors. |
163 |
daemonize(self._pid_filename) |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
164 |
self._daemonized = True |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
165 |
|
11128.13.23
by Jonathan Lange
Extract the code that shuts down the instance. |
166 |
def _shutdown_instance(self): |
167 |
"""Shut down this EC2 instance."""
|
|
168 |
# Make sure our process is daemonized, and has therefore disconnected
|
|
169 |
# the controlling terminal. This also disconnects the ec2test.py SSH
|
|
170 |
# connection, thus signalling ec2test.py that it may now try to take
|
|
171 |
# control of the server.
|
|
172 |
if not self._daemonized: |
|
173 |
# We only want to do this if we haven't already been daemonized.
|
|
174 |
# Nesting daemons is bad.
|
|
175 |
self._daemonize() |
|
176 |
||
177 |
time.sleep(self.SHUTDOWN_DELAY) |
|
178 |
||
13687.1.1
by Steve Kowalik
Make use of shutdown rather than at to kill the ec2 instance. |
179 |
# Cancel the running shutdown.
|
180 |
subprocess.call(['sudo', 'shutdown', '-c']) |
|
181 |
||
11128.13.23
by Jonathan Lange
Extract the code that shuts down the instance. |
182 |
# We'll only get here if --postmortem didn't kill us. This is our
|
183 |
# fail-safe shutdown, in case the user got disconnected or suffered
|
|
184 |
# some other mishap that would prevent them from shutting down this
|
|
185 |
# server on their own.
|
|
186 |
subprocess.call(['sudo', 'shutdown', '-P', 'now']) |
|
187 |
||
11128.13.11
by Jonathan Lange
Parametrize the name. |
188 |
def run(self, name, function, *args, **kwargs): |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
189 |
try: |
190 |
if self._should_daemonize: |
|
11128.13.11
by Jonathan Lange
Parametrize the name. |
191 |
print 'Starting %s daemon...' % (name,) |
11128.13.19
by Jonathan Lange
Name errors |
192 |
self._daemonize() |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
193 |
|
194 |
return function(*args, **kwargs) |
|
195 |
except: |
|
196 |
config = bzrlib.config.GlobalConfig() |
|
197 |
# Handle exceptions thrown by the test() or daemonize() methods.
|
|
198 |
if self._emails: |
|
11407.2.6
by Jonathan Lange
Use the same API for sending email everywhere. |
199 |
msg = EmailMessage( |
200 |
from_address=config.username(), |
|
201 |
to_address=self._emails, |
|
202 |
subject='%s FAILED' % (name,), |
|
203 |
body=traceback.format_exc()) |
|
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
204 |
self._smtp_connection.send_email(msg) |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
205 |
raise
|
206 |
finally: |
|
207 |
# When everything is over, if we've been ask to shut down, then
|
|
208 |
# make sure we're daemonized, then shutdown. Otherwise, if we're
|
|
209 |
# daemonized, just clean up the pidfile.
|
|
11128.13.60
by Jonathan Lange
GRR! Another one. |
210 |
if self._shutdown_when_done: |
11128.13.23
by Jonathan Lange
Extract the code that shuts down the instance. |
211 |
self._shutdown_instance() |
11128.13.19
by Jonathan Lange
Name errors |
212 |
elif self._daemonized: |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
213 |
# It would be nice to clean up after ourselves, since we won't
|
214 |
# be shutting down.
|
|
11128.13.14
by Jonathan Lange
If write_pidfile is a function, remove_pidfile might as well be too. |
215 |
remove_pidfile(self._pid_filename) |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
216 |
else: |
11128.13.34
by Jonathan Lange
Move responsibility for stdout output to the logger. |
217 |
# We're not a daemon, and we're not shutting down. The user
|
218 |
# most likely started this script manually, from a shell
|
|
219 |
# running on the instance itself.
|
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
220 |
pass
|
221 |
||
222 |
||
11128.13.27
by Jonathan Lange
Remove irrelevant test. |
223 |
class LaunchpadTester: |
224 |
"""Runs Launchpad tests and gathers their results in a useful way."""
|
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
225 |
|
11128.13.42
by Jonathan Lange
Little bit of LaunchpadTester constructor API tweaking. |
226 |
def __init__(self, logger, test_directory, test_options=()): |
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
227 |
"""Construct a TestOnMergeRunner.
|
228 |
||
229 |
:param logger: The WebTestLogger to log to.
|
|
11128.13.15
by Jonathan Lange
No more need for these global constants. |
230 |
:param test_directory: The directory to run the tests in. We expect
|
231 |
this directory to have a fully-functional checkout of Launchpad
|
|
232 |
and its dependent branches.
|
|
11128.13.42
by Jonathan Lange
Little bit of LaunchpadTester constructor API tweaking. |
233 |
:param test_options: A sequence of options to pass to the test runner.
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
234 |
"""
|
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
235 |
self._logger = logger |
11128.13.15
by Jonathan Lange
No more need for these global constants. |
236 |
self._test_directory = test_directory |
11128.13.42
by Jonathan Lange
Little bit of LaunchpadTester constructor API tweaking. |
237 |
self._test_options = ' '.join(test_options) |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
238 |
|
239 |
def build_test_command(self): |
|
240 |
"""Return the command that will execute the test suite.
|
|
241 |
||
242 |
Should return a list of command options suitable for submission to
|
|
243 |
subprocess.call()
|
|
244 |
||
245 |
Subclasses must provide their own implementation of this method.
|
|
246 |
"""
|
|
11407.2.2
by Jonathan Lange
Tests for LaunchpadTester. |
247 |
command = ['make', 'check'] |
248 |
if self._test_options: |
|
249 |
command.append('TESTOPTS="%s"' % self._test_options) |
|
11128.13.2
by Jonathan Lange
Move almost all of main() into the test runner class. |
250 |
return command |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
251 |
|
11407.2.2
by Jonathan Lange
Tests for LaunchpadTester. |
252 |
def _spawn_test_process(self): |
253 |
"""Actually run the tests.
|
|
254 |
||
255 |
:return: A `subprocess.Popen` object for the test run.
|
|
256 |
"""
|
|
257 |
call = self.build_test_command() |
|
258 |
self._logger.write_line("Running %s" % (call,)) |
|
259 |
# bufsize=0 means do not buffer any of the output. We want to
|
|
260 |
# display the test output as soon as it is generated.
|
|
261 |
return subprocess.Popen( |
|
262 |
call, bufsize=0, |
|
263 |
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, |
|
264 |
cwd=self._test_directory) |
|
265 |
||
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
266 |
def test(self): |
267 |
"""Run the tests, log the results.
|
|
268 |
||
269 |
Signals the ec2test process and cleans up the logs once all the tests
|
|
270 |
have completed. If necessary, submits the branch to PQM, and mails
|
|
271 |
the user the test results.
|
|
272 |
"""
|
|
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
273 |
self._logger.prepare() |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
274 |
try: |
11407.2.2
by Jonathan Lange
Tests for LaunchpadTester. |
275 |
popen = self._spawn_test_process() |
11407.2.16
by Jonathan Lange
Pass around a result, rather than booleans |
276 |
result = self._gather_test_output(popen.stdout, self._logger) |
277 |
retcode = popen.wait() |
|
278 |
# The process could have an error not indicated by an actual test
|
|
279 |
# result nor by a raised exception
|
|
280 |
if result.wasSuccessful() and retcode: |
|
281 |
raise NonZeroExitCode(retcode) |
|
10189.6.7
by Jonathan Lange
Factor the code with some more clear try/except logic |
282 |
except: |
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
283 |
self._logger.error_in_testrunner(sys.exc_info()) |
11407.2.12
by Jonathan Lange
Send only one email in the case of catastrophic failure. |
284 |
else: |
11407.2.16
by Jonathan Lange
Pass around a result, rather than booleans |
285 |
self._logger.got_result(result) |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
286 |
|
11128.13.34
by Jonathan Lange
Move responsibility for stdout output to the logger. |
287 |
def _gather_test_output(self, input_stream, logger): |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
288 |
"""Write the testrunner output to the logs."""
|
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
289 |
summary_stream = logger.get_summary_stream() |
12473.1.1
by William Grant
Only return the SummaryResult, not the MultiTestResult containing it and the FailureUpdateResult. MultiTestResults don't have the test counts that the email needs. |
290 |
summary_result = SummaryResult(summary_stream) |
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
291 |
result = MultiTestResult( |
12473.1.1
by William Grant
Only return the SummaryResult, not the MultiTestResult containing it and the FailureUpdateResult. MultiTestResults don't have the test counts that the email needs. |
292 |
summary_result, |
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
293 |
FailureUpdateResult(logger)) |
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
294 |
subunit_server = subunit.TestProtocolServer(result, summary_stream) |
10189.6.10
by Jonathan Lange
Use subunit to generate the summary output. |
295 |
for line in input_stream: |
296 |
subunit_server.lineReceived(line) |
|
11128.13.33
by Jonathan Lange
Let the logger decide how to write to output. |
297 |
logger.got_line(line) |
11407.2.2
by Jonathan Lange
Tests for LaunchpadTester. |
298 |
summary_stream.flush() |
12473.1.1
by William Grant
Only return the SummaryResult, not the MultiTestResult containing it and the FailureUpdateResult. MultiTestResults don't have the test counts that the email needs. |
299 |
return summary_result |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
300 |
|
301 |
||
12414.1.1
by Jonathan Lange
TODOs. |
302 |
# XXX: Publish a JSON file that includes the relevant details from this
|
303 |
# request.
|
|
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
304 |
class Request: |
305 |
"""A request to have a branch tested and maybe landed."""
|
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
306 |
|
11128.13.43
by Jonathan Lange
Document the constructor. |
307 |
def __init__(self, branch_url, revno, local_branch_path, sourcecode_path, |
11407.2.7
by Jonathan Lange
Pass around an SMTPConnection, removing some of the uglier mocking that we |
308 |
emails=None, pqm_message=None, smtp_connection=None): |
11128.13.43
by Jonathan Lange
Document the constructor. |
309 |
"""Construct a `Request`.
|
310 |
||
311 |
:param branch_url: The public URL to the Launchpad branch we are
|
|
312 |
testing.
|
|
313 |
:param revno: The revision number of the branch we are testing.
|
|
314 |
:param local_branch_path: A local path to the Launchpad branch we are
|
|
315 |
testing. This must be a branch of Launchpad with a working tree.
|
|
316 |
:param sourcecode_path: A local path to the sourcecode dependencies
|
|
317 |
directory (normally '$local_branch_path/sourcecode'). This must
|
|
318 |
contain up-to-date copies of all of Launchpad's sourcecode
|
|
319 |
dependencies.
|
|
320 |
:param emails: A list of emails to send the results to. If not
|
|
321 |
provided, no emails are sent.
|
|
322 |
:param pqm_message: The message to submit to PQM. If not provided, we
|
|
323 |
don't submit to PQM.
|
|
11407.2.7
by Jonathan Lange
Pass around an SMTPConnection, removing some of the uglier mocking that we |
324 |
:param smtp_connection: The `SMTPConnection` to use to send email.
|
11128.13.43
by Jonathan Lange
Document the constructor. |
325 |
"""
|
11128.13.44
by Jonathan Lange
Internal variables have internal names. |
326 |
self._branch_url = branch_url |
327 |
self._revno = revno |
|
328 |
self._local_branch_path = local_branch_path |
|
329 |
self._sourcecode_path = sourcecode_path |
|
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
330 |
self._emails = emails |
331 |
self._pqm_message = pqm_message |
|
11128.13.69
by Jonathan Lange
Don't pass the configuration around. It's just an internal implementation detail. |
332 |
# Used for figuring out how to send emails.
|
333 |
self._bzr_config = bzrlib.config.GlobalConfig() |
|
11407.2.7
by Jonathan Lange
Pass around an SMTPConnection, removing some of the uglier mocking that we |
334 |
if smtp_connection is None: |
335 |
smtp_connection = SMTPConnection(self._bzr_config) |
|
336 |
self._smtp_connection = smtp_connection |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
337 |
|
11128.13.69
by Jonathan Lange
Don't pass the configuration around. It's just an internal implementation detail. |
338 |
def _send_email(self, message): |
11128.13.64
by Jonathan Lange
Extract email sending code so we can patch. |
339 |
"""Actually send 'message'."""
|
11407.2.7
by Jonathan Lange
Pass around an SMTPConnection, removing some of the uglier mocking that we |
340 |
self._smtp_connection.send_email(message) |
11128.13.64
by Jonathan Lange
Extract email sending code so we can patch. |
341 |
|
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
342 |
def _format_test_list(self, header, tests): |
343 |
if not tests: |
|
344 |
return [] |
|
345 |
tests = [' ' + test.id() for test, error in tests] |
|
346 |
return [header, '-' * len(header)] + tests + [''] |
|
347 |
||
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
348 |
def format_result(self, result, start_time, end_time): |
349 |
duration = end_time - start_time |
|
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
350 |
output = [ |
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
351 |
'Tests started at approximately %s' % start_time, |
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
352 |
]
|
353 |
source = self.get_source_details() |
|
354 |
if source: |
|
355 |
output.append('Source: %s r%s' % source) |
|
356 |
target = self.get_target_details() |
|
357 |
if target: |
|
358 |
output.append('Target: %s r%s' % target) |
|
359 |
output.extend([ |
|
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
360 |
'', |
361 |
'%s tests run in %s, %s failures, %s errors' % ( |
|
362 |
result.testsRun, duration, len(result.failures), |
|
363 |
len(result.errors)), |
|
364 |
'', |
|
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
365 |
])
|
366 |
||
367 |
bad_tests = ( |
|
368 |
self._format_test_list('Failing tests', result.failures) + |
|
369 |
self._format_test_list('Tests with errors', result.errors)) |
|
370 |
output.extend(bad_tests) |
|
371 |
||
372 |
if bad_tests: |
|
373 |
full_error_stream = StringIO() |
|
374 |
copy_result = SummaryResult(full_error_stream) |
|
375 |
for test, error in result.failures: |
|
376 |
full_error_stream.write( |
|
377 |
copy_result._formatError('FAILURE', test, error)) |
|
378 |
for test, error in result.errors: |
|
379 |
full_error_stream.write( |
|
380 |
copy_result._formatError('ERROR', test, error)) |
|
381 |
output.append(full_error_stream.getvalue()) |
|
382 |
||
383 |
subject = self._get_pqm_subject() |
|
384 |
if subject: |
|
385 |
if result.wasSuccessful(): |
|
386 |
output.append('SUBMITTED TO PQM:') |
|
387 |
else: |
|
388 |
output.append('**NOT** submitted to PQM:') |
|
389 |
output.extend([subject, '']) |
|
390 |
output.extend(['(See the attached file for the complete log)', '']) |
|
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
391 |
return '\n'.join(output) |
392 |
||
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
393 |
def get_target_details(self): |
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
394 |
"""Return (branch_url, revno) for trunk."""
|
11128.13.105
by Jonathan Lange
Fix stupid mistake. Sigh. |
395 |
branch = bzrlib.branch.Branch.open(self._local_branch_path) |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
396 |
return branch.get_parent().encode('utf-8'), branch.revno() |
397 |
||
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
398 |
def get_source_details(self): |
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
399 |
"""Return (branch_url, revno) for the branch we're merging in.
|
400 |
||
401 |
If we're not merging in a branch, but instead just testing a trunk,
|
|
402 |
then return None.
|
|
403 |
"""
|
|
11128.13.44
by Jonathan Lange
Internal variables have internal names. |
404 |
tree = bzrlib.workingtree.WorkingTree.open(self._local_branch_path) |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
405 |
parent_ids = tree.get_parent_ids() |
11128.13.57
by Jonathan Lange
Tests for get_branch_details. |
406 |
if len(parent_ids) < 2: |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
407 |
return None |
11128.13.44
by Jonathan Lange
Internal variables have internal names. |
408 |
return self._branch_url.encode('utf-8'), self._revno |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
409 |
|
11128.13.67
by Jonathan Lange
More tests! |
410 |
def _last_segment(self, url): |
411 |
"""Return the last segment of a URL."""
|
|
412 |
return url.strip('/').split('/')[-1] |
|
413 |
||
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
414 |
def get_nick(self): |
415 |
"""Get the nick of the branch we are testing."""
|
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
416 |
details = self.get_source_details() |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
417 |
if not details: |
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
418 |
details = self.get_target_details() |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
419 |
url, revno = details |
11128.13.67
by Jonathan Lange
More tests! |
420 |
return self._last_segment(url) |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
421 |
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
422 |
def get_revno(self): |
423 |
"""Get the revno of the branch we are testing."""
|
|
424 |
if self._revno is not None: |
|
425 |
return self._revno |
|
426 |
return bzrlib.branch.Branch.open(self._local_branch_path).revno() |
|
427 |
||
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
428 |
def get_merge_description(self): |
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
429 |
"""Get a description of the merge request.
|
430 |
||
431 |
If we're merging a branch, return '$SOURCE_NICK => $TARGET_NICK', if
|
|
432 |
we're just running tests for a trunk branch without merging return
|
|
433 |
'$TRUNK_NICK'.
|
|
434 |
"""
|
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
435 |
source = self.get_source_details() |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
436 |
if not source: |
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
437 |
return '%s r%s' % (self.get_nick(), self.get_revno()) |
438 |
target = self.get_target_details() |
|
11128.13.67
by Jonathan Lange
More tests! |
439 |
return '%s => %s' % ( |
440 |
self._last_segment(source[0]), self._last_segment(target[0])) |
|
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
441 |
|
442 |
def get_summary_commit(self): |
|
11128.13.68
by Jonathan Lange
Tests for some of the more complex bits. |
443 |
"""Get a message summarizing the change from the commit log.
|
444 |
||
445 |
Returns the last commit message of the merged branch, or None.
|
|
446 |
"""
|
|
447 |
# XXX: JonathanLange 2010-08-17: I don't actually know why we are
|
|
448 |
# using this commit message as a summary message. It's used in the
|
|
449 |
# test logs and the EC2 hosted web page.
|
|
11128.13.105
by Jonathan Lange
Fix stupid mistake. Sigh. |
450 |
branch = bzrlib.branch.Branch.open(self._local_branch_path) |
11128.13.44
by Jonathan Lange
Internal variables have internal names. |
451 |
tree = bzrlib.workingtree.WorkingTree.open(self._local_branch_path) |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
452 |
parent_ids = tree.get_parent_ids() |
453 |
if len(parent_ids) == 1: |
|
454 |
return None |
|
455 |
summary = ( |
|
456 |
branch.repository.get_revision(parent_ids[1]).get_summary()) |
|
457 |
return summary.encode('utf-8') |
|
458 |
||
11128.13.71
by Jonathan Lange
Rename send_email to send_report_email, in what I hope is a clearer name. |
459 |
def _build_report_email(self, successful, body_text, full_log_gz): |
11128.13.65
by Jonathan Lange
Extract the part of the code that builds the email. |
460 |
"""Build a MIME email summarizing the test results.
|
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
461 |
|
11128.13.56
by Jonathan Lange
Fix yet another name error. Looking forward to having unit tests. |
462 |
:param successful: True for pass, False for failure.
|
11128.13.37
by Jonathan Lange
send_email now gets given exactly what it should send. |
463 |
:param body_text: The body of the email to send to the requesters.
|
464 |
:param full_log_gz: A gzip of the full log.
|
|
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
465 |
"""
|
466 |
message = MIMEMultipart.MIMEMultipart() |
|
11128.13.18
by Jonathan Lange
Tweaks. |
467 |
message['To'] = ', '.join(self._emails) |
11128.13.69
by Jonathan Lange
Don't pass the configuration around. It's just an internal implementation detail. |
468 |
message['From'] = self._bzr_config.username() |
11128.13.56
by Jonathan Lange
Fix yet another name error. Looking forward to having unit tests. |
469 |
if successful: |
470 |
status = 'SUCCESS' |
|
471 |
else: |
|
472 |
status = 'FAILURE' |
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
473 |
subject = 'Test results: %s: %s' % ( |
474 |
self.get_merge_description(), status) |
|
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
475 |
message['Subject'] = subject |
476 |
||
477 |
# Make the body.
|
|
11128.13.37
by Jonathan Lange
send_email now gets given exactly what it should send. |
478 |
body = MIMEText.MIMEText(body_text, 'plain', 'utf8') |
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
479 |
body['Content-Disposition'] = 'inline' |
480 |
message.attach(body) |
|
481 |
||
482 |
# Attach the gzipped log.
|
|
11128.13.37
by Jonathan Lange
send_email now gets given exactly what it should send. |
483 |
zipped_log = MIMEApplication(full_log_gz, 'x-gzip') |
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
484 |
zipped_log.add_header( |
485 |
'Content-Disposition', 'attachment', |
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
486 |
filename='%s-r%s.subunit.gz' % ( |
487 |
self.get_nick(), self.get_revno())) |
|
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
488 |
message.attach(zipped_log) |
11128.13.65
by Jonathan Lange
Extract the part of the code that builds the email. |
489 |
return message |
490 |
||
11128.13.71
by Jonathan Lange
Rename send_email to send_report_email, in what I hope is a clearer name. |
491 |
def send_report_email(self, successful, body_text, full_log_gz): |
11128.13.65
by Jonathan Lange
Extract the part of the code that builds the email. |
492 |
"""Send an email summarizing the test results.
|
493 |
||
494 |
:param successful: True for pass, False for failure.
|
|
495 |
:param body_text: The body of the email to send to the requesters.
|
|
496 |
:param full_log_gz: A gzip of the full log.
|
|
497 |
"""
|
|
11128.13.71
by Jonathan Lange
Rename send_email to send_report_email, in what I hope is a clearer name. |
498 |
message = self._build_report_email(successful, body_text, full_log_gz) |
11128.13.64
by Jonathan Lange
Extract email sending code so we can patch. |
499 |
self._send_email(message) |
11128.13.17
by Jonathan Lange
Move responsibility for deciding what to do with the test results away |
500 |
|
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
501 |
def iter_dependency_branches(self): |
502 |
"""Iterate through the Bazaar branches we depend on."""
|
|
11128.13.68
by Jonathan Lange
Tests for some of the more complex bits. |
503 |
for name in sorted(os.listdir(self._sourcecode_path)): |
11128.13.44
by Jonathan Lange
Internal variables have internal names. |
504 |
path = os.path.join(self._sourcecode_path, name) |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
505 |
if os.path.isdir(path): |
506 |
try: |
|
11128.13.68
by Jonathan Lange
Tests for some of the more complex bits. |
507 |
branch = bzrlib.branch.Branch.open(path) |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
508 |
except bzrlib.errors.NotBranchError: |
509 |
continue
|
|
510 |
yield name, branch.get_parent(), branch.revno() |
|
511 |
||
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
512 |
def _get_pqm_subject(self): |
513 |
if not self._pqm_message: |
|
514 |
return
|
|
515 |
return self._pqm_message.get('Subject') |
|
516 |
||
11128.13.69
by Jonathan Lange
Don't pass the configuration around. It's just an internal implementation detail. |
517 |
def submit_to_pqm(self, successful): |
11128.13.36
by Jonathan Lange
Finer split of responsibilities. The request knows how to submit itself |
518 |
"""Submit this request to PQM, if successful & configured to do so."""
|
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
519 |
subject = self._get_pqm_subject() |
520 |
if subject and successful: |
|
11128.13.64
by Jonathan Lange
Extract email sending code so we can patch. |
521 |
self._send_email(self._pqm_message) |
11128.13.36
by Jonathan Lange
Finer split of responsibilities. The request knows how to submit itself |
522 |
return subject |
523 |
||
11128.13.37
by Jonathan Lange
send_email now gets given exactly what it should send. |
524 |
@property
|
525 |
def wants_email(self): |
|
526 |
"""Do the requesters want emails sent to them?"""
|
|
527 |
return bool(self._emails) |
|
528 |
||
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
529 |
|
530 |
class WebTestLogger: |
|
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
531 |
"""Logs test output to disk and a simple web page.
|
532 |
||
533 |
:ivar successful: Whether the logger has received only successful input up
|
|
534 |
until now.
|
|
535 |
"""
|
|
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
536 |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
537 |
def __init__(self, full_log_filename, summary_filename, index_filename, |
538 |
request, echo_to_stdout): |
|
11128.13.34
by Jonathan Lange
Move responsibility for stdout output to the logger. |
539 |
"""Construct a WebTestLogger.
|
540 |
||
11128.13.88
by Jonathan Lange
More tests, more documentation. |
541 |
Because this writes an HTML file with links to the summary and full
|
542 |
logs, you should construct this object with
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
543 |
`WebTestLogger.make_in_directory`, which guarantees that the files
|
11128.13.88
by Jonathan Lange
More tests, more documentation. |
544 |
are available in the correct locations.
|
545 |
||
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
546 |
:param full_log_filename: Path to a file that will have the full
|
547 |
log output written to it. The file will be overwritten.
|
|
548 |
:param summary_file: Path to a file that will have a human-readable
|
|
549 |
summary written to it. The file will be overwritten.
|
|
550 |
:param index_file: Path to a file that will have an HTML page
|
|
551 |
written to it. The file will be overwritten.
|
|
11128.13.34
by Jonathan Lange
Move responsibility for stdout output to the logger. |
552 |
:param request: A `Request` object representing the thing that's being
|
553 |
tested.
|
|
554 |
:param echo_to_stdout: Whether or not we should echo output to stdout.
|
|
555 |
"""
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
556 |
self._full_log_filename = full_log_filename |
557 |
self._summary_filename = summary_filename |
|
558 |
self._index_filename = index_filename |
|
12414.1.5
by Jonathan Lange
Beginning of storing information as json file. |
559 |
self._info_json = os.path.join( |
560 |
os.path.dirname(index_filename), 'info.json') |
|
11128.13.86
by Jonathan Lange
Docstring. |
561 |
self._request = request |
562 |
self._echo_to_stdout = echo_to_stdout |
|
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
563 |
# Actually set by prepare(), but setting to a dummy value to make
|
564 |
# testing easier.
|
|
565 |
self._start_time = datetime.datetime.utcnow() |
|
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
566 |
self.successful = True |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
567 |
|
11128.13.80
by Jonathan Lange
Make WebTestLogger slightly easier to test, start testing it. |
568 |
@classmethod
|
569 |
def make_in_directory(cls, www_dir, request, echo_to_stdout): |
|
11128.13.88
by Jonathan Lange
More tests, more documentation. |
570 |
"""Make a logger that logs to specific files in `www_dir`.
|
571 |
||
572 |
:param www_dir: The directory in which to log the files:
|
|
573 |
current_test.log, summary.log and index.html. These files
|
|
574 |
will be overwritten.
|
|
575 |
:param request: A `Request` object representing the thing that's being
|
|
576 |
tested.
|
|
577 |
:param echo_to_stdout: Whether or not we should echo output to stdout.
|
|
578 |
"""
|
|
11128.13.80
by Jonathan Lange
Make WebTestLogger slightly easier to test, start testing it. |
579 |
files = [ |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
580 |
os.path.join(www_dir, 'current_test.log'), |
581 |
os.path.join(www_dir, 'summary.log'), |
|
582 |
os.path.join(www_dir, 'index.html')] |
|
11128.13.80
by Jonathan Lange
Make WebTestLogger slightly easier to test, start testing it. |
583 |
files.extend([request, echo_to_stdout]) |
584 |
return cls(*files) |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
585 |
|
11128.13.39
by Jonathan Lange
Delegate responsibility for reporting catastrophic failures to the logger. |
586 |
def error_in_testrunner(self, exc_info): |
587 |
"""Called when there is a catastrophic error in the test runner."""
|
|
588 |
exc_type, exc_value, exc_tb = exc_info |
|
11128.13.85
by Jonathan Lange
More comments. |
589 |
# XXX: JonathanLange 2010-08-17: This should probably log to the full
|
590 |
# log as well.
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
591 |
summary = self.get_summary_stream() |
592 |
summary.write('\n\nERROR IN TESTRUNNER\n\n') |
|
593 |
traceback.print_exception(exc_type, exc_value, exc_tb, file=summary) |
|
594 |
summary.flush() |
|
11407.2.12
by Jonathan Lange
Send only one email in the case of catastrophic failure. |
595 |
if self._request.wants_email: |
596 |
self._write_to_filename( |
|
597 |
self._summary_filename, |
|
598 |
'\n(See the attached file for the complete log)\n') |
|
599 |
summary = self.get_summary_contents() |
|
600 |
full_log_gz = gzip_data(self.get_full_log_contents()) |
|
601 |
self._request.send_report_email(False, summary, full_log_gz) |
|
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
602 |
|
11128.13.88
by Jonathan Lange
More tests, more documentation. |
603 |
def get_index_contents(self): |
604 |
"""Return the contents of the index.html page."""
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
605 |
return self._get_contents(self._index_filename) |
11128.13.88
by Jonathan Lange
More tests, more documentation. |
606 |
|
11128.13.83
by Jonathan Lange
Make the tests for Logger use a public API. |
607 |
def get_full_log_contents(self): |
608 |
"""Return the contents of the complete log."""
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
609 |
return self._get_contents(self._full_log_filename) |
11128.13.83
by Jonathan Lange
Make the tests for Logger use a public API. |
610 |
|
611 |
def get_summary_contents(self): |
|
612 |
"""Return the contents of the summary log."""
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
613 |
return self._get_contents(self._summary_filename) |
11128.13.83
by Jonathan Lange
Make the tests for Logger use a public API. |
614 |
|
11128.13.41
by Jonathan Lange
Make all the internal state variables begin with underscores. |
615 |
def get_summary_stream(self): |
616 |
"""Return a stream that, when written to, writes to the summary."""
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
617 |
return open(self._summary_filename, 'a') |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
618 |
|
11128.13.33
by Jonathan Lange
Let the logger decide how to write to output. |
619 |
def got_line(self, line): |
620 |
"""Called when we get a line of output from our child processes."""
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
621 |
self._write_to_filename(self._full_log_filename, line) |
11128.13.34
by Jonathan Lange
Move responsibility for stdout output to the logger. |
622 |
if self._echo_to_stdout: |
623 |
sys.stdout.write(line) |
|
624 |
sys.stdout.flush() |
|
11128.13.33
by Jonathan Lange
Let the logger decide how to write to output. |
625 |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
626 |
def _get_contents(self, filename): |
627 |
"""Get the full contents of 'filename'."""
|
|
628 |
try: |
|
629 |
return open(filename, 'r').read() |
|
630 |
except IOError, e: |
|
631 |
if e.errno == errno.ENOENT: |
|
632 |
return '' |
|
11128.13.80
by Jonathan Lange
Make WebTestLogger slightly easier to test, start testing it. |
633 |
|
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
634 |
def got_failure(self): |
635 |
"""Called when we receive word that a test has failed."""
|
|
636 |
self.successful = False |
|
637 |
self._dump_json() |
|
638 |
||
11407.2.16
by Jonathan Lange
Pass around a result, rather than booleans |
639 |
def got_result(self, result): |
11128.13.35
by Jonathan Lange
The tester now only knows about the logger. |
640 |
"""The tests are done and the results are known."""
|
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
641 |
self._end_time = datetime.datetime.utcnow() |
11407.2.16
by Jonathan Lange
Pass around a result, rather than booleans |
642 |
successful = result.wasSuccessful() |
11128.13.69
by Jonathan Lange
Don't pass the configuration around. It's just an internal implementation detail. |
643 |
self._handle_pqm_submission(successful) |
11128.13.37
by Jonathan Lange
send_email now gets given exactly what it should send. |
644 |
if self._request.wants_email: |
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
645 |
email_text = self._request.format_result( |
646 |
result, self._start_time, self._end_time) |
|
11128.13.93
by Jonathan Lange
Test the contents of the email. |
647 |
full_log_gz = gzip_data(self.get_full_log_contents()) |
11407.2.18
by Jonathan Lange
Use the format_result method as the email method. |
648 |
self._request.send_report_email(successful, email_text, full_log_gz) |
11128.13.35
by Jonathan Lange
The tester now only knows about the logger. |
649 |
|
11128.13.69
by Jonathan Lange
Don't pass the configuration around. It's just an internal implementation detail. |
650 |
def _handle_pqm_submission(self, successful): |
651 |
subject = self._request.submit_to_pqm(successful) |
|
11128.13.36
by Jonathan Lange
Finer split of responsibilities. The request knows how to submit itself |
652 |
if not subject: |
11128.13.35
by Jonathan Lange
The tester now only knows about the logger. |
653 |
return
|
11128.13.78
by Jonathan Lange
Hide more unused methods. |
654 |
self.write_line('') |
655 |
self.write_line('') |
|
11128.13.35
by Jonathan Lange
The tester now only knows about the logger. |
656 |
if successful: |
11128.13.78
by Jonathan Lange
Hide more unused methods. |
657 |
self.write_line('SUBMITTED TO PQM:') |
11128.13.35
by Jonathan Lange
The tester now only knows about the logger. |
658 |
else: |
11128.13.78
by Jonathan Lange
Hide more unused methods. |
659 |
self.write_line('**NOT** submitted to PQM:') |
660 |
self.write_line(subject) |
|
11128.13.35
by Jonathan Lange
The tester now only knows about the logger. |
661 |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
662 |
def _write_to_filename(self, filename, msg): |
663 |
fd = open(filename, 'a') |
|
664 |
fd.write(msg) |
|
665 |
fd.flush() |
|
666 |
fd.close() |
|
667 |
||
11128.13.78
by Jonathan Lange
Hide more unused methods. |
668 |
def _write(self, msg): |
11128.13.29
by Jonathan Lange
Might as well give the logger some public methods for writing to its bits. |
669 |
"""Write to the summary and full log file."""
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
670 |
self._write_to_filename(self._full_log_filename, msg) |
671 |
self._write_to_filename(self._summary_filename, msg) |
|
11128.13.29
by Jonathan Lange
Might as well give the logger some public methods for writing to its bits. |
672 |
|
673 |
def write_line(self, msg): |
|
674 |
"""Write to the summary and full log file with a newline."""
|
|
11128.13.78
by Jonathan Lange
Hide more unused methods. |
675 |
self._write(msg + '\n') |
11128.13.29
by Jonathan Lange
Might as well give the logger some public methods for writing to its bits. |
676 |
|
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
677 |
def _dump_json(self): |
678 |
fd = open(self._info_json, 'w') |
|
679 |
simplejson.dump( |
|
680 |
{'description': self._request.get_merge_description(), |
|
12414.1.11
by Jonathan Lange
Some clarification |
681 |
'failed-yet': not self.successful, |
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
682 |
}, fd) |
683 |
fd.close() |
|
12414.1.5
by Jonathan Lange
Beginning of storing information as json file. |
684 |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
685 |
def prepare(self): |
686 |
"""Prepares the log files on disk.
|
|
687 |
||
688 |
Writes three log files: the raw output log, the filtered "summary"
|
|
689 |
log file, and a HTML index page summarizing the test run paramters.
|
|
690 |
"""
|
|
12414.1.6
by Jonathan Lange
Make sure the logger knows as soon as a test fails, and that this information is available via JSON. |
691 |
self._dump_json() |
11128.13.95
by Jonathan Lange
Make the XXXs nicer. |
692 |
# XXX: JonathanLange 2010-07-18: Mostly untested.
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
693 |
log = self.write_line |
694 |
||
11128.13.101
by Jonathan Lange
Mark down another glitch. Fix the way we only ever append to the index. |
695 |
# Clear the existing index file.
|
696 |
index = open(self._index_filename, 'w') |
|
697 |
index.truncate(0) |
|
698 |
index.close() |
|
699 |
||
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
700 |
def add_to_html(html): |
701 |
return self._write_to_filename( |
|
702 |
self._index_filename, textwrap.dedent(html)) |
|
703 |
||
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
704 |
self._start_time = datetime.datetime.utcnow() |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
705 |
msg = 'Tests started at approximately %(now)s UTC' % { |
11407.2.17
by Jonathan Lange
Add a format_result method that produces something nice. |
706 |
'now': self._start_time.strftime('%a, %d %b %Y %H:%M:%S')} |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
707 |
add_to_html('''\ |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
708 |
<html>
|
709 |
<head>
|
|
710 |
<title>Testing</title>
|
|
711 |
</head>
|
|
712 |
<body>
|
|
713 |
<h1>Testing</h1>
|
|
714 |
<p>%s</p> |
|
715 |
<ul>
|
|
716 |
<li><a href="summary.log">Summary results</a></li>
|
|
717 |
<li><a href="current_test.log">Full results</a></li>
|
|
718 |
</ul>
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
719 |
''' % (msg,)) |
720 |
log(msg) |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
721 |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
722 |
add_to_html('''\ |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
723 |
<h2>Branches Tested</h2>
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
724 |
''') |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
725 |
|
726 |
# Describe the trunk branch.
|
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
727 |
trunk, trunk_revno = self._request.get_target_details() |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
728 |
msg = '%s, revision %d\n' % (trunk, trunk_revno) |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
729 |
add_to_html('''\ |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
730 |
<p><strong>%s</strong></p> |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
731 |
''' % (escape(msg),)) |
732 |
log(msg) |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
733 |
|
11407.2.1
by Jonathan Lange
Change the subject and attachment name of the emails sent by ec2 test. |
734 |
branch_details = self._request.get_source_details() |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
735 |
if not branch_details: |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
736 |
add_to_html('<p>(no merged branch)</p>\n') |
737 |
log('(no merged branch)') |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
738 |
else: |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
739 |
branch_name, branch_revno = branch_details |
740 |
data = {'name': branch_name, |
|
741 |
'revno': branch_revno, |
|
11128.13.24
by Jonathan Lange
Name error |
742 |
'commit': self._request.get_summary_commit()} |
9668.1.1
by Gavin Panella
Encode the revision summary. |
743 |
msg = ('%(name)s, revision %(revno)d ' |
744 |
'(commit message: %(commit)s)\n' % data) |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
745 |
add_to_html('''\ |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
746 |
<p>Merged with<br />%(msg)s</p> |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
747 |
''' % {'msg': escape(msg)}) |
748 |
log("Merged with") |
|
749 |
log(msg) |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
750 |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
751 |
add_to_html('<dl>\n') |
752 |
log('\nDEPENDENCY BRANCHES USED\n') |
|
11128.13.31
by Jonathan Lange
Missed this in a previous refactoring. |
753 |
for name, branch, revno in self._request.iter_dependency_branches(): |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
754 |
data = {'name': name, 'branch': branch, 'revno': revno} |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
755 |
log( |
11128.13.12
by Jonathan Lange
Extract the branch iteration logic. |
756 |
'- %(name)s\n %(branch)s\n %(revno)d\n' % data) |
757 |
escaped_data = {'name': escape(name), |
|
11128.13.50
by Jonathan Lange
We already know the parent. |
758 |
'branch': escape(branch), |
11128.13.51
by Jonathan Lange
Oy! Use the revno we're given. |
759 |
'revno': revno} |
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
760 |
add_to_html('''\ |
11128.13.12
by Jonathan Lange
Extract the branch iteration logic. |
761 |
<dt>%(name)s</dt> |
762 |
<dd>%(branch)s</dd> |
|
763 |
<dd>%(revno)s</dd> |
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
764 |
''' % escaped_data) |
765 |
add_to_html('''\ |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
766 |
</dl>
|
767 |
</body>
|
|
11128.13.100
by Jonathan Lange
Change WebTestLogger to take filenames so that we can open them after |
768 |
</html>''') |
769 |
log('\n\nTEST RESULTS FOLLOW\n\n') |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
770 |
|
771 |
||
772 |
def daemonize(pid_filename): |
|
773 |
# this seems like the sort of thing that ought to be in the
|
|
774 |
# standard library :-/
|
|
775 |
pid = os.fork() |
|
11128.13.96
by Jonathan Lange
Slightly more verbose comments, plus less sleeping waiting for pidfile. |
776 |
if (pid == 0): # Child 1 |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
777 |
os.setsid() |
778 |
pid = os.fork() |
|
11128.13.96
by Jonathan Lange
Slightly more verbose comments, plus less sleeping waiting for pidfile. |
779 |
if (pid == 0): # Child 2, the daemon. |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
780 |
pass # lookie, we're ready to do work in the daemon |
781 |
else: |
|
782 |
os._exit(0) |
|
11128.13.96
by Jonathan Lange
Slightly more verbose comments, plus less sleeping waiting for pidfile. |
783 |
else: # Parent |
784 |
# Make sure the pidfile is written before we exit, so that people
|
|
11128.13.97
by Jonathan Lange
Fix a problem in my previous attempt to make this faster. |
785 |
# who've chosen to daemonize can quickly rectify their mistake. Since
|
786 |
# the daemon might terminate itself very, very quickly, we cannot poll
|
|
787 |
# for the existence of the pidfile. Instead, we just sleep for a
|
|
788 |
# reasonable amount of time.
|
|
789 |
time.sleep(1) |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
790 |
os._exit(0) |
791 |
||
792 |
# write a pidfile ASAP
|
|
793 |
write_pidfile(pid_filename) |
|
794 |
||
795 |
# Iterate through and close all file descriptors.
|
|
796 |
import resource |
|
797 |
maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] |
|
798 |
assert maxfd != resource.RLIM_INFINITY |
|
799 |
for fd in range(0, maxfd): |
|
800 |
try: |
|
801 |
os.close(fd) |
|
802 |
except OSError: |
|
803 |
# we assume fd was closed
|
|
804 |
pass
|
|
805 |
os.open(os.devnull, os.O_RDWR) # this will be 0 |
|
806 |
os.dup2(0, 1) |
|
807 |
os.dup2(0, 2) |
|
808 |
||
809 |
||
11128.13.93
by Jonathan Lange
Test the contents of the email. |
810 |
def gunzip_data(data): |
811 |
"""Decompress 'data'.
|
|
812 |
||
813 |
:param data: The gzip data to decompress.
|
|
814 |
:return: The decompressed data.
|
|
815 |
"""
|
|
816 |
fd, path = tempfile.mkstemp() |
|
817 |
os.write(fd, data) |
|
818 |
os.close(fd) |
|
819 |
try: |
|
820 |
return gzip.open(path, 'r').read() |
|
821 |
finally: |
|
822 |
os.unlink(path) |
|
823 |
||
824 |
||
825 |
def gzip_data(data): |
|
826 |
"""Compress 'data'.
|
|
827 |
||
828 |
:param data: The data to compress.
|
|
829 |
:return: The gzip-compressed data.
|
|
11128.13.8
by Jonathan Lange
Extract the gzip bit. |
830 |
"""
|
831 |
fd, path = tempfile.mkstemp() |
|
832 |
os.close(fd) |
|
833 |
gz = gzip.open(path, 'wb') |
|
11128.13.93
by Jonathan Lange
Test the contents of the email. |
834 |
gz.writelines(data) |
11128.13.8
by Jonathan Lange
Extract the gzip bit. |
835 |
gz.close() |
11128.13.93
by Jonathan Lange
Test the contents of the email. |
836 |
try: |
837 |
return open(path).read() |
|
838 |
finally: |
|
839 |
os.unlink(path) |
|
11128.13.8
by Jonathan Lange
Extract the gzip bit. |
840 |
|
841 |
||
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
842 |
def write_pidfile(pid_filename): |
843 |
"""Write a pidfile for the current process."""
|
|
844 |
pid_file = open(pid_filename, "w") |
|
845 |
pid_file.write(str(os.getpid())) |
|
846 |
pid_file.close() |
|
847 |
||
848 |
||
11128.13.14
by Jonathan Lange
If write_pidfile is a function, remove_pidfile might as well be too. |
849 |
def remove_pidfile(pid_filename): |
850 |
if os.path.exists(pid_filename): |
|
851 |
os.remove(pid_filename) |
|
852 |
||
853 |
||
11128.13.1
by Jonathan Lange
Start clean up |
854 |
def parse_options(argv): |
855 |
"""Make an `optparse.OptionParser` for running the tests remotely.
|
|
856 |
"""
|
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
857 |
parser = optparse.OptionParser( |
858 |
usage="%prog [options] [-- test options]", |
|
859 |
description=("Build and run tests for an instance.")) |
|
860 |
parser.add_option( |
|
861 |
'-e', '--email', action='append', dest='email', default=None, |
|
862 |
help=('Email address to which results should be mailed. Defaults to ' |
|
863 |
'the email address from `bzr whoami`. May be supplied multiple '
|
|
10542.2.1
by Jelmer Vernooij
Use configured email address in Bazaar as From address. |
864 |
'times. `bzr whoami` will be used as the From: address.')) |
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
865 |
parser.add_option( |
866 |
'-s', '--submit-pqm-message', dest='pqm_message', default=None, |
|
867 |
help=('A base64-encoded pickle (string) of a pqm message ' |
|
868 |
'(bzrib.plugins.pqm.pqm_submit.PQMEmailMessage) to submit if '
|
|
869 |
'the test run is successful.')) |
|
870 |
parser.add_option( |
|
871 |
'--daemon', dest='daemon', default=False, |
|
872 |
action='store_true', help=('Run test in background as daemon.')) |
|
873 |
parser.add_option( |
|
874 |
'--debug', dest='debug', default=False, |
|
875 |
action='store_true', |
|
876 |
help=('Drop to pdb trace as soon as possible.')) |
|
877 |
parser.add_option( |
|
878 |
'--shutdown', dest='shutdown', default=False, |
|
879 |
action='store_true', |
|
880 |
help=('Terminate (shutdown) instance after completion.')) |
|
881 |
parser.add_option( |
|
882 |
'--public-branch', dest='public_branch', default=None, |
|
883 |
help=('The URL of the public branch being tested.')) |
|
884 |
parser.add_option( |
|
885 |
'--public-branch-revno', dest='public_branch_revno', |
|
886 |
type="int", default=None, |
|
887 |
help=('The revision number of the public branch being tested.')) |
|
888 |
||
11128.13.1
by Jonathan Lange
Start clean up |
889 |
return parser.parse_args(argv) |
890 |
||
891 |
||
11128.13.2
by Jonathan Lange
Move almost all of main() into the test runner class. |
892 |
def main(argv): |
893 |
options, args = parse_options(argv) |
|
9265.1.1
by Jonathan Lange
Add lp-dev-tools that lack license confusion, changing the license to match |
894 |
|
895 |
if options.debug: |
|
896 |
import pdb; pdb.set_trace() |
|
897 |
if options.pqm_message is not None: |
|
898 |
pqm_message = pickle.loads( |
|
899 |
options.pqm_message.decode('string-escape').decode('base64')) |
|
900 |
else: |
|
901 |
pqm_message = None |
|
902 |
||
11128.13.15
by Jonathan Lange
No more need for these global constants. |
903 |
# Locations for Launchpad. These are actually defined by the configuration
|
904 |
# of the EC2 image that we use.
|
|
905 |
LAUNCHPAD_DIR = '/var/launchpad' |
|
906 |
TEST_DIR = os.path.join(LAUNCHPAD_DIR, 'test') |
|
907 |
SOURCECODE_DIR = os.path.join(TEST_DIR, 'sourcecode') |
|
908 |
||
11128.13.3
by Jonathan Lange
Make logger & pid_filename parameters to the test runner. |
909 |
pid_filename = os.path.join(LAUNCHPAD_DIR, 'ec2test-remote.pid') |
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
910 |
|
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
911 |
smtp_connection = SMTPConnection(bzrlib.config.GlobalConfig()) |
912 |
||
11128.13.16
by Jonathan Lange
Split out a class that represents the request that we are processing. |
913 |
request = Request( |
914 |
options.public_branch, options.public_branch_revno, TEST_DIR, |
|
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
915 |
SOURCECODE_DIR, options.email, pqm_message, smtp_connection) |
11128.13.34
by Jonathan Lange
Move responsibility for stdout output to the logger. |
916 |
# Only write to stdout if we are running as the foreground process.
|
917 |
echo_to_stdout = not options.daemon |
|
11128.13.80
by Jonathan Lange
Make WebTestLogger slightly easier to test, start testing it. |
918 |
logger = WebTestLogger.make_in_directory( |
919 |
'/var/www', request, echo_to_stdout) |
|
11128.13.3
by Jonathan Lange
Make logger & pid_filename parameters to the test runner. |
920 |
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
921 |
runner = EC2Runner( |
11407.2.8
by Jonathan Lange
Pass SMTPConnection through to EC2Runner. |
922 |
options.daemon, pid_filename, options.shutdown, |
923 |
smtp_connection, options.email) |
|
11128.13.10
by Jonathan Lange
Split out the daemonizing / shutdown logic from the test running logic. |
924 |
|
11128.13.62
by Jonathan Lange
Specify options correctly, I hope. |
925 |
tester = LaunchpadTester(logger, TEST_DIR, test_options=args[1:]) |
11128.13.11
by Jonathan Lange
Parametrize the name. |
926 |
runner.run("Test runner", tester.test) |
11128.13.2
by Jonathan Lange
Move almost all of main() into the test runner class. |
927 |
|
928 |
||
929 |
if __name__ == '__main__': |
|
930 |
main(sys.argv) |