Merge lp:~matsubara/tarmac/bundle-lander into lp:~launchpad/tarmac/lp-tarmac
- bundle-lander
- Merge into lp-tarmac
Status: | Merged |
---|---|
Approved by: | Diogo Matsubara |
Approved revision: | 398 |
Merged at revision: | 384 |
Proposed branch: | lp:~matsubara/tarmac/bundle-lander |
Merge into: | lp:~launchpad/tarmac/lp-tarmac |
Diff against target: |
834 lines (+661/-35) 7 files modified
tarmac/bin/commands.py (+20/-0) tarmac/pidfile.py (+77/-0) tarmac/plugins/bundlecommand.py (+215/-30) tarmac/plugins/tests/test_bundlecommand.py (+280/-0) tarmac/tests/mock.py (+17/-0) tarmac/tests/test_commands.py (+21/-5) tarmac/tests/test_pidfile.py (+31/-0) |
To merge this branch: | bzr merge lp:~matsubara/tarmac/bundle-lander |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gary Poster (community) | Approve | ||
Review via email: mp+43396@code.launchpad.net |
Commit message
Implements a lock mechanism and a way to properly format the test results similar to the way they are formatted in ec2test and email those results to merge proposal subscribers.
Description of the change
This branch implements a simple lock mechanism for the bundle merge command and implements a way to properly format the test resuls similar to the way they are formatted in ec2test and email those results to merge proposal subscribers.
All tests pass.
Gary Poster (gary) wrote : | # |
Gary Poster (gary) wrote : | # |
And by the way, very nice branch. :-) Thank you.
Diogo Matsubara (matsubara) wrote : | # |
On Mon, Dec 13, 2010 at 3:03 PM, Gary Poster <email address hidden> wrote:
> Lock file: here are some things that would be nice, in decreasing order of importance in my mind.
> 1 lock file is actually a pid file. pid is reported in error message. This gives SAs a chance to quickly see if it is dead or not.
I did this one by reusing Tarmac's PID_FILE in the config.
> 2 if lock file exists and has been around for fewer than N seconds, exit with status code 0. If lock file has been around for greater than or equal to N seconds, exit with a non-0 status code. N should be configurable. This gives SAs a chance to care about serious problems, and ignore typical problems.
Done. SAs need to change the tarmac config file to look like:
allowed_lock_period = X where X is the amount of seconds allowed for
the lock to be considered OK.
If the lock file is in place for less than the amount configured,
tarmac will return and log the amount. If the lock file is in place
for more than the amount configured, tarmac will return an error and
show the PID and the lock file age.
> 3 if lock file exists, error includes how long it has been around. That's easy enough to do without (just look at the file yourself), but a nice convenience.
Done. It'll log the lock file age and pid as explained above.
> 4 if lock file exists, with pid, and identified pid doesn't exist, delete the pid file, mention what you are doing, and proceed to run tarmac.
Didn't do this one.
>
> FWIW, lib/canonical/
>
> I think I'll say that I want #1 unless you convince me otherwise. I'd be happy if you threw in #2 and #3, but I'm fine with ignoring it for now. I don't think you should pursue #4 right now.
>
> Email: cool. A small note is that "(See the attached file for the complete log)" will be included in the merge proposal output, and that will be incorrect. Might be mildly confusing. Would be nice to omit it in the merge proposal, and only include it when sending the mail.
Done.
>
> Nice tests. It test_run_failure, is it possible/reasonable to show that the merge proposal was changed, and that emails were sent? That seems like it would be nice.
Done.
Thanks for the review.
--
Diogo M. Matsubara
Diogo Matsubara (matsubara) wrote : | # |
Hi Gary,
based in our conversation on IRC I added tests to the lock file mechanism and to the functions in the pidfile.py module. I removed an XXX in the bundle merge command test to make it more complete.
Let me know what you think and if this is ok to be merged.
Thanks for the detailed review and help.
Gary Poster (gary) wrote : | # |
Hi Diogo. We talked about a couple of things that will probably be important for merging this into tarmac trunk, but since we are not doing that now, I'm not worrying about it yet. Thank you!
Here are the notes I have that might be pertinent when we consider merging this to tarmac trunk.
- We are reusing some sort of tarmac pid configuration value that didn't seem to actually be used in tarmac. Therefore, we are not sure if what we are doing makes sense in the larger context.
- We are only using the lock for certain commands, not help. Does that make sense, or should we make the lock pervasive, or pervasive for everything other than help? I don't know, and it's not necessary to figure out the answer yet, but it will be when a merge is possible.
Diogo Matsubara (matsubara) wrote : | # |
There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.
Preview Diff
1 | === modified file 'tarmac/bin/commands.py' | |||
2 | --- tarmac/bin/commands.py 2010-12-15 16:32:58 +0000 | |||
3 | +++ tarmac/bin/commands.py 2010-12-15 16:55:13 +0000 | |||
4 | @@ -2,6 +2,7 @@ | |||
5 | 2 | import httplib2 | 2 | import httplib2 |
6 | 3 | import logging | 3 | import logging |
7 | 4 | import os | 4 | import os |
8 | 5 | import time | ||
9 | 5 | 6 | ||
10 | 6 | from bzrlib.commands import Command | 7 | from bzrlib.commands import Command |
11 | 7 | from bzrlib.help import help_commands | 8 | from bzrlib.help import help_commands |
12 | @@ -14,6 +15,7 @@ | |||
13 | 14 | from tarmac.log import set_up_debug_logging, set_up_logging | 15 | from tarmac.log import set_up_debug_logging, set_up_logging |
14 | 15 | from tarmac.exceptions import TarmacCommandError, TarmacMergeError | 16 | from tarmac.exceptions import TarmacCommandError, TarmacMergeError |
15 | 16 | from tarmac.plugin import load_plugins | 17 | from tarmac.plugin import load_plugins |
16 | 18 | from tarmac.pidfile import get_pid, make_pidfile | ||
17 | 17 | 19 | ||
18 | 18 | 20 | ||
19 | 19 | class TarmacCommand(Command): | 21 | class TarmacCommand(Command): |
20 | @@ -235,6 +237,24 @@ | |||
21 | 235 | """ | 237 | """ |
22 | 236 | 238 | ||
23 | 237 | def _do_merges(self, branch_url, imply_commit_message): | 239 | def _do_merges(self, branch_url, imply_commit_message): |
24 | 240 | existing_pid = get_pid() | ||
25 | 241 | if existing_pid is None: | ||
26 | 242 | make_pidfile() | ||
27 | 243 | else: | ||
28 | 244 | # Check how long the lock has been there. | ||
29 | 245 | lock_creation_time = os.stat(self.config.PID_FILE)[-1] | ||
30 | 246 | now = time.time() | ||
31 | 247 | locked_period = now - lock_creation_time | ||
32 | 248 | # If the locked_period is greater than the allowed, it probably | ||
33 | 249 | # means the test suite is stuck. | ||
34 | 250 | message = 'Tarmac lock file in place for %d seconds, PID: %d' | ||
35 | 251 | if locked_period > int(self.config.allowed_lock_period): | ||
36 | 252 | self.logger.info(message % (locked_period, get_pid())) | ||
37 | 253 | raise TarmacCommandError(message % (locked_period, get_pid())) | ||
38 | 254 | else: | ||
39 | 255 | self.logger.info(message % (locked_period, get_pid())) | ||
40 | 256 | return | ||
41 | 257 | |||
42 | 238 | target = Branch.create( | 258 | target = Branch.create( |
43 | 239 | self.launchpad.branches.getByUrl(url=branch_url), self.config, | 259 | self.launchpad.branches.getByUrl(url=branch_url), self.config, |
44 | 240 | imply_commit_message=imply_commit_message) | 260 | imply_commit_message=imply_commit_message) |
45 | 241 | 261 | ||
46 | === added file 'tarmac/pidfile.py' | |||
47 | --- tarmac/pidfile.py 1970-01-01 00:00:00 +0000 | |||
48 | +++ tarmac/pidfile.py 2010-12-15 16:55:13 +0000 | |||
49 | @@ -0,0 +1,77 @@ | |||
50 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
51 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
52 | 3 | |||
53 | 4 | # Code from lib/canonical/lazr/pidfile.py modified to be used here. | ||
54 | 5 | |||
55 | 6 | __metaclass__ = type | ||
56 | 7 | |||
57 | 8 | import tempfile | ||
58 | 9 | import os | ||
59 | 10 | import atexit | ||
60 | 11 | import sys | ||
61 | 12 | from signal import signal, SIGTERM | ||
62 | 13 | |||
63 | 14 | from tarmac.config import TarmacConfig | ||
64 | 15 | |||
65 | 16 | |||
66 | 17 | def make_pidfile(): | ||
67 | 18 | """Write the current process id to a PID file. | ||
68 | 19 | |||
69 | 20 | Also installs an atexit handler to remove the file on process termination. | ||
70 | 21 | |||
71 | 22 | Also installs a SIGTERM signal handler to remove the file on SIGTERM. | ||
72 | 23 | If you install your own handler, you will want to call remove_pidfile | ||
73 | 24 | inside it. | ||
74 | 25 | """ | ||
75 | 26 | pidfile = TarmacConfig().PID_FILE | ||
76 | 27 | if os.path.exists(pidfile): | ||
77 | 28 | raise RuntimeError("PID file %s already exists. Already running?" % | ||
78 | 29 | pidfile) | ||
79 | 30 | |||
80 | 31 | atexit.register(remove_pidfile) | ||
81 | 32 | def remove_pidfile_handler(*ignored): | ||
82 | 33 | sys.exit(-1 * SIGTERM) | ||
83 | 34 | signal(SIGTERM, remove_pidfile_handler) | ||
84 | 35 | |||
85 | 36 | fd, tempname = tempfile.mkstemp(dir=os.path.dirname(pidfile)) | ||
86 | 37 | outf = os.fdopen(fd, 'w') | ||
87 | 38 | outf.write(str(os.getpid())+'\n') | ||
88 | 39 | outf.flush() | ||
89 | 40 | outf.close() | ||
90 | 41 | os.rename(tempname, pidfile) | ||
91 | 42 | |||
92 | 43 | |||
93 | 44 | def remove_pidfile(): | ||
94 | 45 | """Remove the PID file. | ||
95 | 46 | |||
96 | 47 | This should only be needed if you are overriding the default SIGTERM | ||
97 | 48 | signal handler. | ||
98 | 49 | """ | ||
99 | 50 | pidfile = TarmacConfig().PID_FILE | ||
100 | 51 | pid = get_pid() | ||
101 | 52 | if pid is None: | ||
102 | 53 | return | ||
103 | 54 | if pid == os.getpid(): | ||
104 | 55 | os.unlink(pidfile) | ||
105 | 56 | |||
106 | 57 | |||
107 | 58 | def get_pid(): | ||
108 | 59 | """Return the PID as an integer, or None | ||
109 | 60 | |||
110 | 61 | May raise a ValueError if the PID file is corrupt. | ||
111 | 62 | |||
112 | 63 | This method will only be needed by service or monitoring scripts. | ||
113 | 64 | |||
114 | 65 | Currently no checking is done to ensure that the process is actually | ||
115 | 66 | running, is healthy, or died horribly a while ago and its PID being | ||
116 | 67 | used by something else. What we have is probably good enough. | ||
117 | 68 | """ | ||
118 | 69 | pidfile = TarmacConfig().PID_FILE | ||
119 | 70 | try: | ||
120 | 71 | pid = open(pidfile).read() | ||
121 | 72 | return int(pid) | ||
122 | 73 | except IOError: | ||
123 | 74 | return None | ||
124 | 75 | except ValueError: | ||
125 | 76 | raise ValueError("Invalid PID %s" % repr(pid)) | ||
126 | 77 | |||
127 | 0 | 78 | ||
128 | === modified file 'tarmac/plugins/bundlecommand.py' | |||
129 | --- tarmac/plugins/bundlecommand.py 2010-11-29 13:51:42 +0000 | |||
130 | +++ tarmac/plugins/bundlecommand.py 2010-12-15 16:55:13 +0000 | |||
131 | @@ -20,16 +20,101 @@ | |||
132 | 20 | os = None | 20 | os = None |
133 | 21 | subprocess = None | 21 | subprocess = None |
134 | 22 | 22 | ||
135 | 23 | import datetime | ||
136 | 24 | from email import MIMEMultipart, MIMEText | ||
137 | 25 | from email.mime.application import MIMEApplication | ||
138 | 26 | import gzip | ||
139 | 27 | from StringIO import StringIO | ||
140 | 28 | import tempfile | ||
141 | 29 | import unittest | ||
142 | 30 | |||
143 | 31 | import bzrlib.config | ||
144 | 32 | |||
145 | 23 | from bzrlib.lazy_import import lazy_import | 33 | from bzrlib.lazy_import import lazy_import |
146 | 34 | from bzrlib.smtp_connection import SMTPConnection | ||
147 | 24 | lazy_import(globals(), ''' | 35 | lazy_import(globals(), ''' |
148 | 25 | import os | 36 | import os |
149 | 26 | import subprocess | 37 | import subprocess |
150 | 27 | ''') | 38 | ''') |
151 | 28 | 39 | ||
152 | 40 | import subunit | ||
153 | 41 | |||
154 | 29 | from tarmac.exceptions import TarmacMergeError | 42 | from tarmac.exceptions import TarmacMergeError |
155 | 30 | from tarmac.hooks import TarmacHookPoint, tarmac_hooks | 43 | from tarmac.hooks import TarmacHookPoint, tarmac_hooks |
156 | 31 | from tarmac.plugins import TarmacPlugin | 44 | from tarmac.plugins import TarmacPlugin |
157 | 32 | 45 | ||
158 | 46 | _double_line = '=' * 70 | ||
159 | 47 | _single_line = '-' * 70 | ||
160 | 48 | |||
161 | 49 | def format_error(flavor, test, error): | ||
162 | 50 | return '\n'.join( | ||
163 | 51 | [_double_line, | ||
164 | 52 | '%s: %s' % (flavor, test), | ||
165 | 53 | _single_line, | ||
166 | 54 | error, | ||
167 | 55 | '']) | ||
168 | 56 | |||
169 | 57 | |||
170 | 58 | class TestResultFormatter: | ||
171 | 59 | |||
172 | 60 | def __init__(self, start_time, end_time, result, proposal): | ||
173 | 61 | self.result = result | ||
174 | 62 | self.start_time = start_time | ||
175 | 63 | self.source_branch = proposal.source_branch.display_name | ||
176 | 64 | self.source_revno = proposal.source_branch.revision_count | ||
177 | 65 | self.target_branch = proposal.target_branch.display_name | ||
178 | 66 | self.target_revno = proposal.target_branch.revision_count | ||
179 | 67 | self.num_tests = self.result.testsRun | ||
180 | 68 | self.duration = end_time - start_time | ||
181 | 69 | self.num_failures = len(self.result.failures) | ||
182 | 70 | self.num_errors = len(self.result.errors) | ||
183 | 71 | |||
184 | 72 | def _format_test_list(self, header, tests): | ||
185 | 73 | if not tests: | ||
186 | 74 | return [] | ||
187 | 75 | tests = [' ' + test.id() for test, error in tests] | ||
188 | 76 | return [header, '-' * len(header)] + tests + [''] | ||
189 | 77 | |||
190 | 78 | def get_full_result(self): | ||
191 | 79 | return '\n'.join(self.full_result) | ||
192 | 80 | |||
193 | 81 | def format_result(self, attached_log=False): | ||
194 | 82 | output = [ | ||
195 | 83 | 'Tests started at approximately %s' % self.start_time, | ||
196 | 84 | ] | ||
197 | 85 | output.append('Source: %s r%s' % ( | ||
198 | 86 | self.source_branch, self.source_revno)) | ||
199 | 87 | output.append('Target: %s r%s' % ( | ||
200 | 88 | self.target_branch, self.target_revno)) | ||
201 | 89 | output.extend([ | ||
202 | 90 | '', | ||
203 | 91 | '%s tests run in %s, %s failures, %s errors' % ( | ||
204 | 92 | self.num_tests, self.duration, self.num_failures, | ||
205 | 93 | self.num_errors), | ||
206 | 94 | '', | ||
207 | 95 | ]) | ||
208 | 96 | |||
209 | 97 | bad_tests = ( | ||
210 | 98 | self._format_test_list('Failing tests', self.result.failures) + | ||
211 | 99 | self._format_test_list('Tests with errors', self.result.errors)) | ||
212 | 100 | output.extend(bad_tests) | ||
213 | 101 | |||
214 | 102 | if bad_tests: | ||
215 | 103 | email_summary = StringIO() | ||
216 | 104 | for test, error in self.result.failures: | ||
217 | 105 | email_summary.write( | ||
218 | 106 | format_error('FAILURE', test, error)) | ||
219 | 107 | for test, error in self.result.errors: | ||
220 | 108 | email_summary.write( | ||
221 | 109 | format_error('ERROR', test, error)) | ||
222 | 110 | output.append(email_summary.getvalue()) | ||
223 | 111 | |||
224 | 112 | # Include notification about the attached log. Useful when the output | ||
225 | 113 | # is sent as an email. | ||
226 | 114 | if attached_log: | ||
227 | 115 | output.extend(['(See the attached file for the complete log)', '']) | ||
228 | 116 | return '\n'.join(output) | ||
229 | 117 | |||
230 | 33 | 118 | ||
231 | 34 | class VerifyCommandFailed(TarmacMergeError): | 119 | class VerifyCommandFailed(TarmacMergeError): |
232 | 35 | """Running the verify_command failed.""" | 120 | """Running the verify_command failed.""" |
233 | @@ -37,6 +122,12 @@ | |||
234 | 37 | 122 | ||
235 | 38 | class BundleCommand(TarmacPlugin): | 123 | class BundleCommand(TarmacPlugin): |
236 | 39 | 124 | ||
237 | 125 | def __init__(self, smtp_connection=None): | ||
238 | 126 | super(BundleCommand, self).__init__() | ||
239 | 127 | if smtp_connection is None: | ||
240 | 128 | smtp_connection = SMTPConnection(bzrlib.config.GlobalConfig()) | ||
241 | 129 | self._smtp_connection = smtp_connection | ||
242 | 130 | |||
243 | 40 | def run(self, command, target, collected_proposals): | 131 | def run(self, command, target, collected_proposals): |
244 | 41 | """Plugin for running the registered bundle verify command on target. | 132 | """Plugin for running the registered bundle verify command on target. |
245 | 42 | 133 | ||
246 | @@ -58,45 +149,84 @@ | |||
247 | 58 | except AttributeError: | 149 | except AttributeError: |
248 | 59 | return | 150 | return |
249 | 60 | 151 | ||
250 | 152 | start_time = datetime.datetime.utcnow() | ||
251 | 61 | self.logger.debug('Running test command: %s' % self.verify_command) | 153 | self.logger.debug('Running test command: %s' % self.verify_command) |
252 | 62 | cwd = os.getcwd() | 154 | cwd = os.getcwd() |
253 | 63 | os.chdir(target.tree.abspath('')) | 155 | os.chdir(target.tree.abspath('')) |
277 | 64 | proc = subprocess.Popen( | 156 | output = tempfile.TemporaryFile() |
278 | 65 | self.verify_command, | 157 | try: |
279 | 66 | shell=True, | 158 | proc = subprocess.Popen( |
280 | 67 | stdout=subprocess.PIPE, | 159 | self.verify_command, |
281 | 68 | stderr=subprocess.PIPE) | 160 | shell=True, |
282 | 69 | stdout_value, stderr_value = proc.communicate() | 161 | stdout=output, |
283 | 70 | return_code = proc.wait() | 162 | stderr=subprocess.STDOUT) |
284 | 71 | os.chdir(cwd) | 163 | return_code = proc.wait() |
285 | 72 | self.logger.debug('Completed test command: %s' % self.verify_command) | 164 | output.seek(0) |
286 | 73 | 165 | os.chdir(cwd) | |
287 | 74 | if return_code != 0: | 166 | self.logger.debug('Completed test command: %s' % self.verify_command) |
288 | 75 | for source, proposal in collected_proposals: | 167 | end_time = datetime.datetime.utcnow() |
289 | 76 | self.do_failed(proposal, stdout_value, stderr_value) | 168 | |
290 | 77 | message = u'Test command "%s" failed.' % self.verify_command | 169 | if return_code != 0: |
291 | 78 | comment = ( | 170 | for source, proposal in collected_proposals: |
292 | 79 | u'Bundle lander failed to land proposals: %s' % | 171 | self.do_failed( |
293 | 80 | collected_proposals) | 172 | start_time, end_time, proposal, output) |
294 | 81 | raise VerifyCommandFailed(message, comment) | 173 | message = u'Test command "%s" failed.' % self.verify_command |
295 | 82 | else: | 174 | comment = ( |
296 | 83 | self.logger.info('==========') | 175 | u'Bundle lander failed to land proposals: %s' % |
297 | 84 | self.logger.info(stdout_value) | 176 | collected_proposals) |
298 | 85 | 177 | raise VerifyCommandFailed(message, comment) | |
299 | 86 | def do_failed(self, proposal, stdout_value, stderr_value): | 178 | else: |
300 | 179 | self.logger.info('==========') | ||
301 | 180 | self.logger.info(output.read()) | ||
302 | 181 | finally: | ||
303 | 182 | output.close() | ||
304 | 183 | |||
305 | 184 | def do_failed(self, start_time, end_time, proposal, output): | ||
306 | 87 | '''Perform failure tests. | 185 | '''Perform failure tests. |
307 | 88 | 186 | ||
311 | 89 | In this case, the output of the test command is posted as a comment, | 187 | In this case, the full failure is emailed to the proposal subscribers, |
312 | 90 | and the merge proposal is then set to "Needs review" so that Tarmac | 188 | a summary of the test failure is posted as a comment and the |
313 | 91 | doesn't attempt to merge it again without human interaction. | 189 | proposal is then set to "Needs Review" so that Tarmac doesn't attempt |
314 | 190 | to merge it again without human interaction. | ||
315 | 92 | ''' | 191 | ''' |
316 | 192 | # Make output into a Result object, and create a tgz of the | ||
317 | 193 | # full log. | ||
318 | 194 | # We will use these for the gzip. | ||
319 | 195 | fd, path = tempfile.mkstemp() | ||
320 | 196 | os.close(fd) | ||
321 | 197 | gz = gzip.open(path, 'wb') | ||
322 | 198 | # These are for creating the test result object. | ||
323 | 199 | result = unittest.TestResult() | ||
324 | 200 | discard = tempfile.TemporaryFile() | ||
325 | 201 | # The temporary file gets things that subunit doesn't understand. | ||
326 | 202 | # It prints them to stdout by default. We don't want the output on | ||
327 | 203 | # stdout, so we get it and discard it. (Note that the things subunit | ||
328 | 204 | # doesn't understand are still put in the full_result, so that the | ||
329 | 205 | # email will include them.) | ||
330 | 206 | subunit_server = subunit.TestProtocolServer(result, discard) | ||
331 | 207 | for line in output: | ||
332 | 208 | subunit_server.lineReceived(line) | ||
333 | 209 | gz.write(line) | ||
334 | 210 | gz.close() | ||
335 | 211 | try: | ||
336 | 212 | gzipped_log = open(path).read() | ||
337 | 213 | finally: | ||
338 | 214 | os.unlink(path) | ||
339 | 215 | formatter = TestResultFormatter( | ||
340 | 216 | start_time, end_time, result, proposal) | ||
341 | 217 | output.seek(0) | ||
342 | 218 | |||
343 | 219 | emails = self.get_subscribers_emails(proposal) | ||
344 | 220 | self.send_report_email(emails, formatter, gzipped_log) | ||
345 | 221 | |||
346 | 222 | summary = formatter.format_result() | ||
347 | 93 | message = u'Test command "%s" failed.' % self.verify_command | 223 | message = u'Test command "%s" failed.' % self.verify_command |
348 | 94 | comment = (u'The attempt to merge %(source)s into %(target)s failed. ' | 224 | comment = (u'The attempt to merge %(source)s into %(target)s failed. ' |
351 | 95 | u'Below is the output from the failed tests.\n\n' | 225 | u'Below is a summary from the failed tests.\n\n' |
352 | 96 | u'%(output)s') % { | 226 | u'%(summary)s') % { |
353 | 97 | 'source': proposal.source_branch.display_name, | 227 | 'source': proposal.source_branch.display_name, |
354 | 98 | 'target': proposal.target_branch.display_name, | 228 | 'target': proposal.target_branch.display_name, |
356 | 99 | 'output': u'\n'.join([stdout_value, stderr_value]), | 229 | 'summary': summary, |
357 | 100 | } | 230 | } |
358 | 101 | 231 | ||
359 | 102 | self.logger.warn( | 232 | self.logger.warn( |
360 | @@ -111,9 +241,64 @@ | |||
361 | 111 | 241 | ||
362 | 112 | proposal.createComment(subject=subject, | 242 | proposal.createComment(subject=subject, |
363 | 113 | content=comment) | 243 | content=comment) |
365 | 114 | proposal.setStatus(status=u'Needs review') | 244 | proposal.setStatus(status=u'Needs Review') |
366 | 115 | proposal.lp_save() | 245 | proposal.lp_save() |
367 | 116 | 246 | ||
368 | 247 | def get_subscribers_emails(self, proposal): | ||
369 | 248 | """Return a list of the proposal subscribers emails.""" | ||
370 | 249 | subscribers_emails = set() | ||
371 | 250 | # Get all the reviewers. | ||
372 | 251 | for vote in proposal.votes: | ||
373 | 252 | subscribers_emails.add( | ||
374 | 253 | vote.reviewer.preferred_email_address.email) | ||
375 | 254 | # Get source branch subscribers. | ||
376 | 255 | for subscriber in proposal.source_branch.subscribers: | ||
377 | 256 | subscribers_emails.add(subscriber.preferred_email_address.email) | ||
378 | 257 | return subscribers_emails | ||
379 | 258 | |||
380 | 259 | def send_report_email(self, emails, formatter, full_log_gz): | ||
381 | 260 | """Send an email summarizing the test results. | ||
382 | 261 | |||
383 | 262 | :param emails: The list of emails to mail to. | ||
384 | 263 | :param formatter: The TestResultFormatter object. | ||
385 | 264 | :param full_test_result: The full log. | ||
386 | 265 | """ | ||
387 | 266 | message = self._build_report_email(emails, formatter, full_log_gz) | ||
388 | 267 | self._send_email(message) | ||
389 | 268 | |||
390 | 269 | def _build_report_email(self, emails, formatter, full_log_gz): | ||
391 | 270 | """Build a MIME email summarizing the test results. | ||
392 | 271 | |||
393 | 272 | :param emails: The list of emails to mail to. | ||
394 | 273 | :param formatter: The TestResultFormatter object. | ||
395 | 274 | :param full_log_gz: A gzip of the full log. | ||
396 | 275 | """ | ||
397 | 276 | message = MIMEMultipart.MIMEMultipart() | ||
398 | 277 | message['To'] = ', '.join(emails) | ||
399 | 278 | message['From'] = "Tarmac Bundle lander." | ||
400 | 279 | subject = 'Test results: %s => %s: %s' % ( | ||
401 | 280 | formatter.source_branch, formatter.target_branch, "FAILURE") | ||
402 | 281 | message['Subject'] = subject | ||
403 | 282 | |||
404 | 283 | body_text = formatter.format_result(attached_log=True) | ||
405 | 284 | # Make the body. | ||
406 | 285 | body = MIMEText.MIMEText(body_text, 'plain', 'utf8') | ||
407 | 286 | body['Content-Disposition'] = 'inline' | ||
408 | 287 | message.attach(body) | ||
409 | 288 | |||
410 | 289 | # Attach the gzipped log. | ||
411 | 290 | zipped_log = MIMEApplication(full_log_gz, 'x-gzip') | ||
412 | 291 | zipped_log.add_header( | ||
413 | 292 | 'Content-Disposition', 'attachment', | ||
414 | 293 | filename='%s-r%s.subunit.gz' % ( | ||
415 | 294 | formatter.source_branch, formatter.source_revno)) | ||
416 | 295 | message.attach(zipped_log) | ||
417 | 296 | return message | ||
418 | 297 | |||
419 | 298 | def _send_email(self, message): | ||
420 | 299 | """Actually send 'message'.""" | ||
421 | 300 | self._smtp_connection.send_email(message) | ||
422 | 301 | |||
423 | 117 | # Create the hook here so we don't change the tarmac_hooks API with this very | 302 | # Create the hook here so we don't change the tarmac_hooks API with this very |
424 | 118 | # specific hook. | 303 | # specific hook. |
425 | 119 | tarmac_hooks.create_hook(TarmacHookPoint('tarmac_bundle_merge', | 304 | tarmac_hooks.create_hook(TarmacHookPoint('tarmac_bundle_merge', |
426 | 120 | 305 | ||
427 | === added file 'tarmac/plugins/tests/test_bundlecommand.py' | |||
428 | --- tarmac/plugins/tests/test_bundlecommand.py 1970-01-01 00:00:00 +0000 | |||
429 | +++ tarmac/plugins/tests/test_bundlecommand.py 2010-12-15 16:55:13 +0000 | |||
430 | @@ -0,0 +1,280 @@ | |||
431 | 1 | """Tests for the BundleCommmand plug-in.""" | ||
432 | 2 | |||
433 | 3 | from datetime import datetime, timedelta | ||
434 | 4 | import doctest | ||
435 | 5 | import os | ||
436 | 6 | import unittest | ||
437 | 7 | |||
438 | 8 | from testtools import TestCase, TestResult | ||
439 | 9 | from testtools.matchers import DocTestMatches | ||
440 | 10 | |||
441 | 11 | from tarmac.bin.registry import CommandRegistry | ||
442 | 12 | from tarmac.plugins.bundlecommand import ( | ||
443 | 13 | BundleCommand, TestResultFormatter, VerifyCommandFailed) | ||
444 | 14 | from tarmac.tests import TarmacTestCase | ||
445 | 15 | from tarmac.tests.test_commands import FakeCommand | ||
446 | 16 | from tarmac.tests.mock import Thing, MockMergeProposal | ||
447 | 17 | |||
448 | 18 | |||
449 | 19 | class LoggingSMTPConnection(object): | ||
450 | 20 | """An SMTPConnection double that logs sent email.""" | ||
451 | 21 | |||
452 | 22 | def __init__(self, log): | ||
453 | 23 | self._log = log | ||
454 | 24 | |||
455 | 25 | def send_email(self, message): | ||
456 | 26 | self._log.append(message) | ||
457 | 27 | |||
458 | 28 | |||
459 | 29 | class Helpers: | ||
460 | 30 | |||
461 | 31 | def make_formatter(self): | ||
462 | 32 | proposal = Thing( | ||
463 | 33 | source_branch=Thing( | ||
464 | 34 | revision_count=123, | ||
465 | 35 | display_name="lp:project/source"), | ||
466 | 36 | target_branch=Thing( | ||
467 | 37 | revision_count=120, | ||
468 | 38 | display_name="lp:project")) | ||
469 | 39 | start_time = datetime.utcnow() | ||
470 | 40 | end_time = start_time + timedelta(hours=1) | ||
471 | 41 | |||
472 | 42 | class SomeTest(TestCase): | ||
473 | 43 | def test_ok(self): | ||
474 | 44 | pass | ||
475 | 45 | def test_fail(self): | ||
476 | 46 | self.fail("oh no") | ||
477 | 47 | def test_error(self): | ||
478 | 48 | 1/0 | ||
479 | 49 | self.fail_test = SomeTest('test_fail') | ||
480 | 50 | self.error_test = SomeTest('test_error') | ||
481 | 51 | test = unittest.TestSuite( | ||
482 | 52 | [self.fail_test, self.error_test, SomeTest('test_ok')]) | ||
483 | 53 | result = TestResult() | ||
484 | 54 | test.run(result) | ||
485 | 55 | |||
486 | 56 | return TestResultFormatter(start_time, end_time, result, proposal) | ||
487 | 57 | |||
488 | 58 | def lp_save(self, *args, **kwargs): | ||
489 | 59 | """Dummy lp_save method.""" | ||
490 | 60 | pass | ||
491 | 61 | |||
492 | 62 | |||
493 | 63 | class TestTestResultFormatter(TestCase, Helpers): | ||
494 | 64 | """Tests for `TestResultFormatter`.""" | ||
495 | 65 | |||
496 | 66 | expected_failure_summary_email = ("""\ | ||
497 | 67 | Tests started at approximately %(start_time)s | ||
498 | 68 | Source: %(source_branch)s r%(source_revno)s | ||
499 | 69 | Target: %(target_branch)s r%(target_revno)s | ||
500 | 70 | <BLANKLINE> | ||
501 | 71 | %(num_tests)s tests run in %(duration)s, %(num_failures)s failures, %(num_errors)s errors | ||
502 | 72 | <BLANKLINE> | ||
503 | 73 | Failing tests | ||
504 | 74 | ------------- | ||
505 | 75 | %(fail_id)s | ||
506 | 76 | <BLANKLINE> | ||
507 | 77 | Tests with errors | ||
508 | 78 | ----------------- | ||
509 | 79 | %(error_id)s | ||
510 | 80 | <BLANKLINE> | ||
511 | 81 | ====================================================================== | ||
512 | 82 | FAILURE: test_fail... | ||
513 | 83 | ---------------------------------------------------------------------- | ||
514 | 84 | Text attachment: traceback | ||
515 | 85 | ------------ | ||
516 | 86 | Traceback (most recent call last): | ||
517 | 87 | ... | ||
518 | 88 | ------------ | ||
519 | 89 | <BLANKLINE> | ||
520 | 90 | ====================================================================== | ||
521 | 91 | ERROR: test_error... | ||
522 | 92 | ---------------------------------------------------------------------- | ||
523 | 93 | Text attachment: traceback | ||
524 | 94 | ------------ | ||
525 | 95 | Traceback (most recent call last): | ||
526 | 96 | ... | ||
527 | 97 | ------------ | ||
528 | 98 | <BLANKLINE> | ||
529 | 99 | <BLANKLINE> | ||
530 | 100 | (See the attached file for the complete log) | ||
531 | 101 | """) | ||
532 | 102 | |||
533 | 103 | expected_failure_summary = ("""\ | ||
534 | 104 | Tests started at approximately %(start_time)s | ||
535 | 105 | Source: %(source_branch)s r%(source_revno)s | ||
536 | 106 | Target: %(target_branch)s r%(target_revno)s | ||
537 | 107 | <BLANKLINE> | ||
538 | 108 | %(num_tests)s tests run in %(duration)s, %(num_failures)s failures, %(num_errors)s errors | ||
539 | 109 | <BLANKLINE> | ||
540 | 110 | Failing tests | ||
541 | 111 | ------------- | ||
542 | 112 | %(fail_id)s | ||
543 | 113 | <BLANKLINE> | ||
544 | 114 | Tests with errors | ||
545 | 115 | ----------------- | ||
546 | 116 | %(error_id)s | ||
547 | 117 | <BLANKLINE> | ||
548 | 118 | ====================================================================== | ||
549 | 119 | FAILURE: test_fail... | ||
550 | 120 | ---------------------------------------------------------------------- | ||
551 | 121 | Text attachment: traceback | ||
552 | 122 | ------------ | ||
553 | 123 | Traceback (most recent call last): | ||
554 | 124 | ... | ||
555 | 125 | ------------ | ||
556 | 126 | <BLANKLINE> | ||
557 | 127 | ====================================================================== | ||
558 | 128 | ERROR: test_error... | ||
559 | 129 | ---------------------------------------------------------------------- | ||
560 | 130 | Text attachment: traceback | ||
561 | 131 | ------------ | ||
562 | 132 | Traceback (most recent call last): | ||
563 | 133 | ... | ||
564 | 134 | ------------ | ||
565 | 135 | <BLANKLINE> | ||
566 | 136 | """) | ||
567 | 137 | |||
568 | 138 | def test_format_result_with_attached_log(self): | ||
569 | 139 | formatter = self.make_formatter() | ||
570 | 140 | summary = formatter.format_result(attached_log=True) | ||
571 | 141 | data = { | ||
572 | 142 | 'start_time': formatter.start_time, | ||
573 | 143 | 'source_branch': formatter.source_branch, | ||
574 | 144 | 'source_revno': formatter.source_revno, | ||
575 | 145 | 'target_branch': formatter.target_branch, | ||
576 | 146 | 'target_revno': formatter.target_revno, | ||
577 | 147 | 'num_tests': formatter.num_tests, | ||
578 | 148 | 'duration': formatter.duration, | ||
579 | 149 | 'num_failures': formatter.num_failures, | ||
580 | 150 | 'num_errors': formatter.num_errors, | ||
581 | 151 | 'fail_id': self.fail_test.id(), | ||
582 | 152 | 'error_id': self.error_test.id() | ||
583 | 153 | } | ||
584 | 154 | self.assertThat(summary, DocTestMatches( | ||
585 | 155 | self.expected_failure_summary_email % data, doctest.REPORT_NDIFF| | ||
586 | 156 | doctest.ELLIPSIS)) | ||
587 | 157 | |||
588 | 158 | def test_format_result_without_attached_log(self): | ||
589 | 159 | formatter = self.make_formatter() | ||
590 | 160 | summary = formatter.format_result(attached_log=False) | ||
591 | 161 | data = { | ||
592 | 162 | 'start_time': formatter.start_time, | ||
593 | 163 | 'source_branch': formatter.source_branch, | ||
594 | 164 | 'source_revno': formatter.source_revno, | ||
595 | 165 | 'target_branch': formatter.target_branch, | ||
596 | 166 | 'target_revno': formatter.target_revno, | ||
597 | 167 | 'num_tests': formatter.num_tests, | ||
598 | 168 | 'duration': formatter.duration, | ||
599 | 169 | 'num_failures': formatter.num_failures, | ||
600 | 170 | 'num_errors': formatter.num_errors, | ||
601 | 171 | 'fail_id': self.fail_test.id(), | ||
602 | 172 | 'error_id': self.error_test.id() | ||
603 | 173 | } | ||
604 | 174 | self.assertThat(summary, DocTestMatches( | ||
605 | 175 | self.expected_failure_summary % data, doctest.REPORT_NDIFF| | ||
606 | 176 | doctest.ELLIPSIS)) | ||
607 | 177 | |||
608 | 178 | |||
609 | 179 | class BundleCommandTests(TarmacTestCase, Helpers): | ||
610 | 180 | """Test the BundleCommand.""" | ||
611 | 181 | |||
612 | 182 | def setUp(self): | ||
613 | 183 | """Set up additional data we need for tests.""" | ||
614 | 184 | super(BundleCommandTests, self).setUp() | ||
615 | 185 | self.collected_proposals = [ | ||
616 | 186 | (Thing(), | ||
617 | 187 | MockMergeProposal( | ||
618 | 188 | Thing( | ||
619 | 189 | display_name='lp:project/source', | ||
620 | 190 | revision_count=123, | ||
621 | 191 | subscribers=[ | ||
622 | 192 | Thing(preferred_email_address=Thing( | ||
623 | 193 | email='foo@example.com'))]), | ||
624 | 194 | Thing( | ||
625 | 195 | display_name='lp:project', | ||
626 | 196 | revision_count=120), | ||
627 | 197 | status="Approved", | ||
628 | 198 | votes=[ | ||
629 | 199 | Thing(reviewer=Thing( | ||
630 | 200 | preferred_email_address=Thing( | ||
631 | 201 | email='foo.bar@example.com')))])), | ||
632 | 202 | (Thing(), | ||
633 | 203 | MockMergeProposal( | ||
634 | 204 | Thing( | ||
635 | 205 | display_name='lp:project/source2', | ||
636 | 206 | revision_count=124, | ||
637 | 207 | subscribers=[ | ||
638 | 208 | Thing(preferred_email_address=Thing( | ||
639 | 209 | email='foo@example.com'))]), | ||
640 | 210 | Thing( | ||
641 | 211 | display_name='lp:project', | ||
642 | 212 | revision_count=120), | ||
643 | 213 | status="Approved", | ||
644 | 214 | votes=[ | ||
645 | 215 | Thing(reviewer=Thing( | ||
646 | 216 | preferred_email_address=Thing( | ||
647 | 217 | email='foo.bar@example.com')))])), | ||
648 | 218 | ] | ||
649 | 219 | # Patch the plugin smtp_connection so emails are never actually sent. | ||
650 | 220 | email_log = [] | ||
651 | 221 | smtp_connection = LoggingSMTPConnection(email_log) | ||
652 | 222 | self.plugin = BundleCommand(smtp_connection=smtp_connection) | ||
653 | 223 | registry = CommandRegistry(config=self.config) | ||
654 | 224 | self.command = FakeCommand(registry) | ||
655 | 225 | |||
656 | 226 | def test_run(self): | ||
657 | 227 | """Test that the plug-in run without errors.""" | ||
658 | 228 | target = Thing(config=Thing( | ||
659 | 229 | bundle_verify_command="/bin/true"), | ||
660 | 230 | tree=Thing(abspath=os.path.abspath)) | ||
661 | 231 | self.plugin.run( | ||
662 | 232 | command=self.command, target=target, | ||
663 | 233 | collected_proposals=self.collected_proposals) | ||
664 | 234 | |||
665 | 235 | def test_run_failure(self): | ||
666 | 236 | """Test that a failure raises the correct exception.""" | ||
667 | 237 | # No email in the log. | ||
668 | 238 | self.assertEqual(self.plugin._smtp_connection._log, []) | ||
669 | 239 | # Proposal status are approved. | ||
670 | 240 | proposal1, proposal2 = self.collected_proposals | ||
671 | 241 | self.assertEqual(proposal1[1].queue_status, "Approved") | ||
672 | 242 | self.assertEqual(proposal2[1].queue_status, "Approved") | ||
673 | 243 | |||
674 | 244 | target = Thing(config=Thing( | ||
675 | 245 | bundle_verify_command="/bin/false"), | ||
676 | 246 | tree=Thing(abspath=os.path.abspath)) | ||
677 | 247 | self.assertRaises(VerifyCommandFailed, | ||
678 | 248 | self.plugin.run, | ||
679 | 249 | command=self.command, target=target, | ||
680 | 250 | collected_proposals=self.collected_proposals) | ||
681 | 251 | |||
682 | 252 | # There are two emails in the log. One for each proposal. | ||
683 | 253 | email = self.plugin._smtp_connection._log.pop() | ||
684 | 254 | self.assertEqual( | ||
685 | 255 | email.get('Subject'), | ||
686 | 256 | 'Test results: lp:project/source2 => lp:project: FAILURE') | ||
687 | 257 | email = self.plugin._smtp_connection._log.pop() | ||
688 | 258 | self.assertEqual( | ||
689 | 259 | email.get('Subject'), | ||
690 | 260 | 'Test results: lp:project/source => lp:project: FAILURE') | ||
691 | 261 | |||
692 | 262 | # Proposals status is updated. | ||
693 | 263 | proposal1, proposal2 = self.collected_proposals | ||
694 | 264 | self.assertEqual(proposal1[1].queue_status, "Needs Review") | ||
695 | 265 | self.assertEqual(proposal2[1].queue_status, "Needs Review") | ||
696 | 266 | |||
697 | 267 | def test_send_report_email(self): | ||
698 | 268 | """Test send_report_email().""" | ||
699 | 269 | formatter = self.make_formatter() | ||
700 | 270 | |||
701 | 271 | emails = self.plugin.get_subscribers_emails( | ||
702 | 272 | self.collected_proposals[0][1]) | ||
703 | 273 | self.plugin.send_report_email( | ||
704 | 274 | emails, formatter, 'some gzip content') | ||
705 | 275 | email = self.plugin._smtp_connection._log.pop() | ||
706 | 276 | self.assertEqual( | ||
707 | 277 | email.get('To'), 'foo@example.com, foo.bar@example.com') | ||
708 | 278 | self.assertEqual( | ||
709 | 279 | email.get('Subject'), | ||
710 | 280 | "Test results: lp:project/source => lp:project: FAILURE") | ||
711 | 0 | 281 | ||
712 | === modified file 'tarmac/tests/mock.py' | |||
713 | --- tarmac/tests/mock.py 2010-11-10 14:00:22 +0000 | |||
714 | +++ tarmac/tests/mock.py 2010-12-15 16:55:13 +0000 | |||
715 | @@ -58,6 +58,23 @@ | |||
716 | 58 | self.landing_candidates = [] | 58 | self.landing_candidates = [] |
717 | 59 | 59 | ||
718 | 60 | 60 | ||
719 | 61 | class MockMergeProposal(object): | ||
720 | 62 | """A mock LP merge proposal.""" | ||
721 | 63 | |||
722 | 64 | def __init__(self, source_branch, target_branch, status, votes=()): | ||
723 | 65 | self.createComment = self.lp_save | ||
724 | 66 | self.queue_status = status | ||
725 | 67 | self.source_branch = source_branch | ||
726 | 68 | self.target_branch = target_branch | ||
727 | 69 | self.votes = votes | ||
728 | 70 | |||
729 | 71 | def lp_save(self, *args, **kwargs): | ||
730 | 72 | pass | ||
731 | 73 | |||
732 | 74 | def setStatus(self, status="Needs Review"): | ||
733 | 75 | self.queue_status = status | ||
734 | 76 | |||
735 | 77 | |||
736 | 61 | class cmd_mock(TarmacCommand): | 78 | class cmd_mock(TarmacCommand): |
737 | 62 | '''A mock command.''' | 79 | '''A mock command.''' |
738 | 63 | 80 | ||
739 | 64 | 81 | ||
740 | === modified file 'tarmac/tests/test_commands.py' | |||
741 | --- tarmac/tests/test_commands.py 2010-12-08 00:14:43 +0000 | |||
742 | +++ tarmac/tests/test_commands.py 2010-12-15 16:55:13 +0000 | |||
743 | @@ -1,18 +1,19 @@ | |||
744 | 1 | '''Tests for tarmac.bin.commands.py.''' | 1 | '''Tests for tarmac.bin.commands.py.''' |
745 | 2 | from cStringIO import StringIO | 2 | from cStringIO import StringIO |
746 | 3 | import os | 3 | import os |
747 | 4 | import shutil | ||
748 | 5 | import sys | 4 | import sys |
749 | 6 | 5 | ||
750 | 7 | from tarmac.bin import commands | 6 | from tarmac.bin import commands |
751 | 8 | from tarmac.bin.registry import CommandRegistry | 7 | from tarmac.bin.registry import CommandRegistry |
752 | 9 | from tarmac.branch import Branch | 8 | from tarmac.branch import Branch |
753 | 10 | from tarmac.config import TarmacConfig | 9 | from tarmac.config import TarmacConfig |
755 | 11 | from tarmac.exceptions import TarmacMergeError, UnapprovedChanges | 10 | from tarmac.exceptions import ( |
756 | 11 | TarmacCommandError, TarmacMergeError, UnapprovedChanges) | ||
757 | 12 | from tarmac.hooks import TarmacHookPoint, tarmac_hooks | 12 | from tarmac.hooks import TarmacHookPoint, tarmac_hooks |
758 | 13 | from tarmac.pidfile import make_pidfile | ||
759 | 13 | from tarmac.plugins import TarmacPlugin | 14 | from tarmac.plugins import TarmacPlugin |
760 | 14 | from tarmac.tests import TarmacTestCase, BranchTestCase | 15 | from tarmac.tests import TarmacTestCase, BranchTestCase |
762 | 15 | from tarmac.tests.mock import MockLPBranch, Thing | 16 | from tarmac.tests.mock import Thing |
763 | 16 | 17 | ||
764 | 17 | 18 | ||
765 | 18 | class FakeCommand(commands.TarmacCommand): | 19 | class FakeCommand(commands.TarmacCommand): |
766 | @@ -329,6 +330,8 @@ | |||
767 | 329 | 330 | ||
768 | 330 | self.launchpad = Thing(branches=Thing(getByUrl=self.getBranchByUrl)) | 331 | self.launchpad = Thing(branches=Thing(getByUrl=self.getBranchByUrl)) |
769 | 331 | self.error = None | 332 | self.error = None |
770 | 333 | # Add the allowed_lock_period option to the config. | ||
771 | 334 | self.config.set('Tarmac', 'allowed_lock_period', '10') | ||
772 | 332 | registry = CommandRegistry(config=self.config) | 335 | registry = CommandRegistry(config=self.config) |
773 | 333 | registry.register_command('bundle_merge', commands.cmd_bundle_merge) | 336 | registry.register_command('bundle_merge', commands.cmd_bundle_merge) |
774 | 334 | 337 | ||
775 | @@ -412,8 +415,21 @@ | |||
776 | 412 | self.assertEqual( | 415 | self.assertEqual( |
777 | 413 | rev.properties.get('merge_url'), | 416 | rev.properties.get('merge_url'), |
778 | 414 | u'http://code.launchpad.net/proposal2') | 417 | u'http://code.launchpad.net/proposal2') |
781 | 415 | #XXX need to grab the previuous revision as well and do the | 418 | |
782 | 416 | # assertions. | 419 | previous_to_last_revno = target.revno() - 1 |
783 | 420 | previous_to_last_rev = target.get_rev_id(previous_to_last_revno) | ||
784 | 421 | rev = target.repository.get_revision(previous_to_last_rev) | ||
785 | 422 | self.assertEqual(rev.get_summary(), u"Commit this.") | ||
786 | 423 | self.assertEqual(rev.committer, "Tarmac") | ||
787 | 424 | self.assertEqual( | ||
788 | 425 | rev.properties.get('merge_url'), | ||
789 | 426 | u'http://code.launchpad.net/proposal1') | ||
790 | 427 | |||
791 | 428 | def test_run_locked_fails(self): | ||
792 | 429 | make_pidfile() | ||
793 | 430 | self.config.set('Tarmac', 'allowed_lock_period', '0') | ||
794 | 431 | self.assertRaises( | ||
795 | 432 | TarmacCommandError, self.command.run, launchpad=self.launchpad) | ||
796 | 417 | 433 | ||
797 | 418 | def test_plugin_merge_errors_are_handled(self): | 434 | def test_plugin_merge_errors_are_handled(self): |
798 | 419 | # Setup | 435 | # Setup |
799 | 420 | 436 | ||
800 | === added file 'tarmac/tests/test_pidfile.py' | |||
801 | --- tarmac/tests/test_pidfile.py 1970-01-01 00:00:00 +0000 | |||
802 | +++ tarmac/tests/test_pidfile.py 2010-12-15 16:55:13 +0000 | |||
803 | @@ -0,0 +1,31 @@ | |||
804 | 1 | |||
805 | 2 | import os | ||
806 | 3 | |||
807 | 4 | from tarmac.pidfile import make_pidfile, get_pid, remove_pidfile | ||
808 | 5 | from tarmac.tests import TarmacTestCase | ||
809 | 6 | |||
810 | 7 | |||
811 | 8 | class TestPidFile(TarmacTestCase): | ||
812 | 9 | """Tests for the functions in the pidfile.py module.""" | ||
813 | 10 | |||
814 | 11 | def test_make_pidfile(self): | ||
815 | 12 | self.assertFalse(os.path.exists(self.config.PID_FILE)) | ||
816 | 13 | make_pidfile() | ||
817 | 14 | self.assertTrue(os.path.exists(self.config.PID_FILE)) | ||
818 | 15 | |||
819 | 16 | def test_make_pidfile_raises_if_exists(self): | ||
820 | 17 | self.assertFalse(os.path.exists(self.config.PID_FILE)) | ||
821 | 18 | make_pidfile() | ||
822 | 19 | self.assertRaises(RuntimeError, make_pidfile) | ||
823 | 20 | |||
824 | 21 | def test_get_pid(self): | ||
825 | 22 | self.assertFalse(os.path.exists(self.config.PID_FILE)) | ||
826 | 23 | make_pidfile() | ||
827 | 24 | self.assertEqual(os.getpid(), get_pid()) | ||
828 | 25 | |||
829 | 26 | def test_remove_pidfile(self): | ||
830 | 27 | self.assertFalse(os.path.exists(self.config.PID_FILE)) | ||
831 | 28 | make_pidfile() | ||
832 | 29 | self.assertTrue(os.path.exists(self.config.PID_FILE)) | ||
833 | 30 | remove_pidfile() | ||
834 | 31 | self.assertFalse(os.path.exists(self.config.PID_FILE)) |
Lock file: here are some things that would be nice, in decreasing order of importance in my mind.
1 lock file is actually a pid file. pid is reported in error message. This gives SAs a chance to quickly see if it is dead or not.
2 if lock file exists and has been around for fewer than N seconds, exit with status code 0. If lock file has been around for greater than or equal to N seconds, exit with a non-0 status code. N should be configurable. This gives SAs a chance to care about serious problems, and ignore typical problems.
3 if lock file exists, error includes how long it has been around. That's easy enough to do without (just look at the file yourself), but a nice convenience.
4 if lock file exists, with pid, and identified pid doesn't exist, delete the pid file, mention what you are doing, and proceed to run tarmac.
FWIW, lib/canonical/ lazr/pidfile. py has some simple code to steal for #1.
I think I'll say that I want #1 unless you convince me otherwise. I'd be happy if you threw in #2 and #3, but I'm fine with ignoring it for now. I don't think you should pursue #4 right now.
Email: cool. A small note is that "(See the attached file for the complete log)" will be included in the merge proposal output, and that will be incorrect. Might be mildly confusing. Would be nice to omit it in the merge proposal, and only include it when sending the mail.
Nice tests. It test_run_failure, is it possible/reasonable to show that the merge proposal was changed, and that emails were sent? That seems like it would be nice.