Merge lp:~jtv/launchpad/bug-409686 into lp:launchpad

Proposed by Jeroen T. Vermeulen on 2010-05-27
Status: Merged
Approved by: Jeroen T. Vermeulen on 2010-05-28
Approved revision: no longer in the source branch.
Merged at revision: 10941
Proposed branch: lp:~jtv/launchpad/bug-409686
Merge into: lp:launchpad
Diff against target: 356 lines (+205/-15)
4 files modified
lib/lp/translations/doc/translations-export-to-branch.txt (+57/-8)
lib/lp/translations/emailtemplates/unpushed-branch.txt (+38/-0)
lib/lp/translations/scripts/tests/test_translations_to_branch.py (+68/-4)
lib/lp/translations/scripts/translations_to_branch.py (+42/-3)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-409686
Reviewer Review Type Date Requested Status
Abel Deuring (community) code 2010-05-27 Approve on 2010-05-28
Review via email: mp+26179@code.launchpad.net

Commit Message

Notify owners of unpushed translations branches.

Description of the Change

= Bug 409686 =

The Code UI unfortunately allows users to "register" a branch without populating it. From the Launchpad database's point of view, this is a perfectly good branch. From bzr's point of view, it just doesn't exist. With our daily translations exports to bzr branches, this situation has been showing up in logs as NotBranchError exceptions. I've been notifying these people of the problem by hand, but I got tired of it and the LOSA team got sick of looking at the log output on error-reports.

Here's a fix that catches this condition, logs it separately from "real" errors, and notifies the branch owner by email. (The branch owner is normally also the project owner; where they're not the same, it's the branch owner who should perform the necessary bzr steps). The branch owner will be receiving this every day, hopefully pushing towards a rapid resolution.

We won't be able to Q/A this on staging, beyond ensuring that the script doesn't break altogether. On production, we can pick a translatable project and temporarily enable translations exports to a branch we registered ourselves (but did not push to the server). Of course we'll also want to make sure that the regular exports still work as well.

To test:
{{{
./bin/test -vv -t 'translations.*to.branch'
}}}

No lint.

Jeroen

To post a comment you must log in.
Abel Deuring (adeuring) wrote :

Hi Jeroen,

a nice branch. Just one remark:

Reading

> + bzr launchpad-login LOGIN
> +
> +Instead of LOGIN, type your Launchpad login name. Then:

raised a mental parser warning for me. This may be due to an undercaffeinated brain, but "...login LOGIN" looks a bit odd. What about "bzr launchpad-login your-launchpad-user-name" instead?

review: Approve (code)
Jeroen T. Vermeulen (jtv) wrote :

Thanks. Replaced with USERNAME as per IRC.

Robert Collins (lifeless) wrote :

Rather than unpushed, I suggest calling them 'neverpushed' - its a bit
clearer IMO.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
2--- lib/lp/translations/doc/translations-export-to-branch.txt 2010-04-01 04:29:46 +0000
3+++ lib/lp/translations/doc/translations-export-to-branch.txt 2010-05-28 07:47:34 +0000
4@@ -14,7 +14,7 @@
5 >>> print stderr
6 INFO Creating lockfile: /var/lock/launchpad-translations-export-to-branch.lock
7 INFO Exporting to translations branches.
8- INFO Processed 0 item(s); 0 failure(s).
9+ INFO Processed 0 item(s); 0 failure(s), 0 unpushed branch(es).
10
11 >>> from lp.translations.scripts.translations_to_branch import (
12 ... ExportTranslationsToBranch)
13@@ -157,7 +157,7 @@
14 DEBUG ...
15 INFO Committed 2 file(s).
16 INFO Unlock.
17- INFO Processed 1 item(s); 0 failure(s).
18+ INFO Processed 1 item(s); 0 failure(s), 0 unpushed branch(es).
19
20 When Gazblachko stops using Launchpad for Translations, the exports stop
21 also.
22@@ -166,7 +166,7 @@
23 >>> transaction.commit()
24 >>> script.main()
25 INFO Exporting to translations branches.
26- INFO Processed 0 item(s); 0 failure(s).
27+ INFO Processed 0 item(s); 0 failure(s), 0 unpushed branch(es).
28
29 >>> gazblachko.official_rosetta = True
30 >>> transaction.commit()
31@@ -192,7 +192,7 @@
32 DEBUG ....
33 DEBUG Last commit was at ....
34 INFO Unlock.
35- INFO Processed 1 item(s); 0 failure(s).
36+ INFO Processed 1 item(s); 0 failure(s), 0 unpushed branch(es).
37
38 If one of the files is updated, it is exported again. Unchanged files
39 are not.
40@@ -207,7 +207,56 @@
41 INFO ...
42 INFO Committed 1 file(s).
43 INFO Unlock.
44- INFO Processed 1 item(s); 0 failure(s).
45+ INFO Processed 1 item(s); 0 failure(s), 0 unpushed branch(es).
46+
47+
48+Unpushed branches
49+-----------------
50+
51+The Launchpad UI allows users to register branches in the Launchpad
52+database without populating them in bzr. Exporting to such a branch
53+won't work, so we email a notification to the branch owner.
54+
55+ >>> from email import message_from_string
56+ >>> from lp.services.mail import stub
57+ >>> from lp.codehosting.vfs import get_rw_server
58+ >>> productseries = factory.makeProductSeries()
59+ >>> productseries.translations_branch = factory.makeBranch()
60+ >>> template = factory.makePOTemplate(productseries=productseries)
61+ >>> potmsgset = factory.makePOTMsgSet(template)
62+ >>> pofile = removeSecurityProxy(
63+ ... factory.makePOFile('nl', potemplate=template))
64+ >>> tm = factory.makeTranslationMessage(
65+ ... pofile=pofile, potmsgset=potmsgset, translator=template.owner)
66+
67+ >>> server = get_rw_server(direct_database=True)
68+ >>> server.start_server()
69+ >>> real_script = ExportTranslationsToBranch(
70+ ... 'export-to-branch', test_args=[])
71+ >>> real_script.logger = FakeLogger()
72+ >>> try:
73+ ... real_script._exportToBranches([productseries])
74+ ... finally:
75+ ... server.destroy()
76+ INFO Exporting ...
77+ INFO Processed 1 item(s); 0 failure(s), 1 unpushed branch(es).
78+
79+ # Give the email a chance to arrive in the test mailbox.
80+ >>> transaction.commit()
81+
82+ >>> sender, recipients, body = stub.test_emails.pop()
83+ >>> message = message_from_string(body)
84+ >>> print message['Subject']
85+ Launchpad: translations branch has not been set up.
86+
87+ >>> print message.get_payload()
88+ Hello,
89+ There was a problem with translations branch synchronization for
90+ ...
91+ Branch synchronization for this release series has been set up to
92+ commit translations snapshots to the bzr branch at lp://...
93+
94+For the full message text, see emailtemplates/unpushed-branch.txt.
95
96
97 Race conditions
98@@ -243,7 +292,7 @@
99 DEBUG ...
100 INFO Unlock.
101 ERROR Failure: ConcurrentUpdateError(...Simulated race condition...)
102- INFO Processed 1 item(s); 1 failure(s).
103+ INFO Processed 1 item(s); 1 failure(s), 0 unpushed branch(es).
104
105
106 === Pending imports from same branch ===
107@@ -264,7 +313,7 @@
108 ERROR Failure: ConcurrentUpdateError(...Translations branch for
109 Gazblachko trunk series has pending translations changes.
110 Not committing...)
111- INFO Processed 1 item(s); 1 failure(s).
112+ INFO Processed 1 item(s); 1 failure(s), 0 unpushed branch(es).
113
114 There is one problem with detecting this race condition. Jobs are never
115 cleaned up. So if the job failed for whatever reason, an unfinished job
116@@ -282,4 +331,4 @@
117 INFO Writing file 'po/main/nl.po':
118 INFO ...
119 INFO Unlock.
120- INFO Processed 1 item(s); 0 failure(s).
121+ INFO Processed 1 item(s); 0 failure(s), 0 unpushed branch(es).
122
123=== added file 'lib/lp/translations/emailtemplates/unpushed-branch.txt'
124--- lib/lp/translations/emailtemplates/unpushed-branch.txt 1970-01-01 00:00:00 +0000
125+++ lib/lp/translations/emailtemplates/unpushed-branch.txt 2010-05-28 07:47:34 +0000
126@@ -0,0 +1,38 @@
127+Hello,
128+
129+There was a problem with translations branch synchronization for
130+%(productseries)s.
131+
132+Branch synchronization for this release series has been set up to commit
133+translations snapshots to the bzr branch at %(branch_url)s.
134+
135+That branch does not appear to have been pushed to Launchpad yet.
136+Please create a branch on your own system that matches your needs, and
137+then push it to Launchpad using "bzr push %(branch_url)s".
138+
139+You'll need the bzr tool. If all you want is an empty branch for the
140+translations to go into, you can create one using the following command
141+lines:
142+
143+ bzr launchpad-login USERNAME
144+
145+Instead of USERNAME, type your Launchpad login name. Then:
146+
147+ bzr init translations-export
148+ cd translations-export
149+ bzr commit --unchanged -m "Translations branch."
150+ bzr push %(branch_url)s
151+
152+This will create an empty branch on Launchpad. It should be updated
153+with translations within the day. You can update the branch on your
154+system with the latest changes on Launchpad by going back into the
155+translations-export directory and typing this command:
156+
157+ bzr pull --remember %(branch_url)s
158+
159+More extensive help can be found at
160+
161+ https://help.launchpad.net/Code
162+
163+--
164+Automatic message from Launchpad.net.
165
166=== modified file 'lib/lp/translations/scripts/tests/test_translations_to_branch.py'
167--- lib/lp/translations/scripts/tests/test_translations_to_branch.py 2010-04-23 08:27:40 +0000
168+++ lib/lp/translations/scripts/tests/test_translations_to_branch.py 2010-05-28 07:47:34 +0000
169@@ -11,6 +11,10 @@
170
171 from zope.security.proxy import removeSecurityProxy
172
173+from bzrlib.errors import NotBranchError
174+
175+from canonical.config import config
176+
177 from canonical.launchpad.scripts.logger import QuietFakeLogger
178 from canonical.launchpad.scripts.tests import run_script
179 from canonical.testing import ZopelessAppServerLayer
180@@ -23,7 +27,7 @@
181
182
183 class GruesomeException(Exception):
184- """CPU on fire. Or some other kind of failure, like."""
185+ """CPU on fire. Or some other kind of failure."""
186
187
188 class TestExportTranslationsToBranch(TestCaseWithFactory):
189@@ -76,10 +80,13 @@
190
191 self.assertEqual('', stdout)
192 self.assertEqual(
193- 'INFO Creating lockfile: /var/lock/launchpad-translations-export-to-branch.lock\n'
194+ 'INFO '
195+ 'Creating lockfile: '
196+ '/var/lock/launchpad-translations-export-to-branch.lock\n'
197 'INFO Exporting to translations branches.\n'
198 'INFO Exporting Committobranch trunk series.\n'
199- 'INFO Processed 1 item(s); 0 failure(s).',
200+ 'INFO '
201+ 'Processed 1 item(s); 0 failure(s), 0 unpushed branch(es).',
202 self._filterOutput(stderr))
203 self.assertIn('No previous translations commit found.', stderr)
204 self.assertEqual(0, retcode)
205@@ -123,7 +130,9 @@
206 ['-vvv', '--no-fudge'])
207 self.assertEqual(0, retcode)
208 self.assertIn('Last commit was at', stderr)
209- self.assertIn("Processed 1 item(s); 0 failure(s).", stderr)
210+ self.assertIn(
211+ "Processed 1 item(s); 0 failure(s), 0 unpushed branch(es).",
212+ stderr)
213 self.assertEqual(
214 None, re.search("INFO\s+Committed [0-9]+ file", stderr))
215
216@@ -144,6 +153,61 @@
217 self.assertTrue(message.startswith("ERROR"))
218 self.assertTrue("GruesomeException" in message)
219
220+ def test_exportToBranches_handles_unpushed_branches(self):
221+ # bzrlib raises NotBranchError when accessing a nonexistent
222+ # branch. The exporter deals with that by calling
223+ # _handleUnpushedBranch.
224+ exporter = ExportTranslationsToBranch(test_args=[])
225+ exporter.logger = QuietFakeLogger()
226+ productseries = self.factory.makeProductSeries()
227+ productseries.translations_branch = self.factory.makeBranch()
228+
229+ # _handleUnpushedBranch is called if _exportToBranch raises
230+ # NotBranchError.
231+ exporter._handleUnpushedBranch = FakeMethod()
232+ exporter._exportToBranch = FakeMethod(failure=NotBranchError("No!"))
233+ exporter._exportToBranches([productseries])
234+ self.assertEqual(1, exporter._handleUnpushedBranch.call_count)
235+
236+ # This does not happen if the export succeeds.
237+ exporter._handleUnpushedBranch = FakeMethod()
238+ exporter._exportToBranch = FakeMethod()
239+ exporter._exportToBranches([productseries])
240+ self.assertEqual(0, exporter._handleUnpushedBranch.call_count)
241+
242+ # Nor does it happen if the export fails in some other way.
243+ exporter._handleUnpushedBranch = FakeMethod()
244+ exporter._exportToBranch = FakeMethod(failure=IndexError("Ayyeee!"))
245+ exporter._exportToBranches([productseries])
246+ self.assertEqual(0, exporter._handleUnpushedBranch.call_count)
247+
248+ def test_handleUnpushedBranch_mails_branch_owner(self):
249+ exporter = ExportTranslationsToBranch(test_args=[])
250+ exporter.logger = QuietFakeLogger()
251+ productseries = self.factory.makeProductSeries()
252+ email = self.factory.getUniqueEmailAddress()
253+ branch_owner = self.factory.makePerson(email=email)
254+ productseries.translations_branch = self.factory.makeBranch(
255+ owner=branch_owner)
256+ exporter._exportToBranch = FakeMethod(failure=NotBranchError("Ow"))
257+ exporter._sendMail = FakeMethod()
258+
259+ exporter._exportToBranches([productseries])
260+
261+ self.assertEqual(1, exporter._sendMail.call_count)
262+ (sender, recipients, subject, text), kwargs = (
263+ exporter._sendMail.calls[-1])
264+ self.assertIn(config.canonical.noreply_from_address, sender)
265+ self.assertIn(email, recipients)
266+ self.assertEqual(
267+ "Launchpad: translations branch has not been set up.", subject)
268+
269+ self.assertIn(
270+ "problem with translations branch synchronization", text)
271+ self.assertIn(productseries.title, text)
272+ self.assertIn(productseries.translations_branch.bzr_identity, text)
273+ self.assertIn('bzr push lp://', text)
274+
275
276 class TestExportToStackedBranch(TestCaseWithFactory):
277 """Test workaround for bzr bug 375013."""
278
279=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
280--- lib/lp/translations/scripts/translations_to_branch.py 2010-04-23 03:09:57 +0000
281+++ lib/lp/translations/scripts/translations_to_branch.py 2010-05-28 07:47:34 +0000
282@@ -14,9 +14,14 @@
283
284 from zope.component import getUtility
285
286+from bzrlib.errors import NotBranchError
287+
288 from storm.expr import Join, SQL
289
290-from canonical.launchpad.helpers import shortlist
291+from canonical.config import config
292+from canonical.launchpad.helpers import (
293+ get_contact_email_addresses, get_email_template, shortlist)
294+from lp.services.mail.sendmail import format_address, simple_sendmail
295 from lp.codehosting.vfs import get_rw_server
296 from lp.translations.interfaces.potemplate import IPOTemplateSet
297 from canonical.launchpad.webapp.interfaces import (
298@@ -225,6 +230,7 @@
299 """Loop over `productseries_iter` and export their translations."""
300 items_done = 0
301 items_failed = 0
302+ unpushed_branches = 0
303
304 productseries = shortlist(productseries_iter, longest_expected=2000)
305
306@@ -236,6 +242,13 @@
307 self.txn.commit()
308 except (KeyboardInterrupt, SystemExit):
309 raise
310+ except NotBranchError:
311+ unpushed_branches += 1
312+ if self.txn:
313+ self.txn.abort()
314+ self._handleUnpushedBranch(source)
315+ if self.txn:
316+ self.txn.commit()
317 except Exception, e:
318 items_failed += 1
319 self.logger.error("Failure: %s" % repr(e))
320@@ -244,8 +257,34 @@
321
322 items_done += 1
323
324- self.logger.info("Processed %d item(s); %d failure(s)." % (
325- items_done, items_failed))
326+ self.logger.info(
327+ "Processed %d item(s); %d failure(s), %d unpushed branch(es)." % (
328+ items_done, items_failed, unpushed_branches))
329+
330+ def _sendMail(self, sender, recipients, subject, text):
331+ """Wrapper for `simple_sendmail`. Fakeable for easy testing."""
332+ simple_sendmail(sender, recipients, subject, text)
333+
334+ def _handleUnpushedBranch(self, productseries):
335+ """Branch has never been scanned. Notify owner.
336+
337+ This means that as far as the Launchpad database knows, there is
338+ no actual bzr branch behind this `IBranch` yet.
339+ """
340+ branch = productseries.translations_branch
341+ self.logger.info("Notifying %s of unpushed branch %s." % (
342+ branch.owner.name, branch.bzr_identity))
343+
344+ template = get_email_template('unpushed-branch.txt', 'translations')
345+ text = template % {
346+ 'productseries': productseries.title,
347+ 'branch_url': branch.bzr_identity,
348+ }
349+ recipients = get_contact_email_addresses(branch.owner)
350+ sender = format_address(
351+ "Launchpad Translations", config.canonical.noreply_from_address)
352+ subject = "Launchpad: translations branch has not been set up."
353+ self._sendMail(sender, recipients, subject, text)
354
355 def main(self):
356 """See `LaunchpadScript`."""