~launchpad-pqm/launchpad/devel

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)