Merge lp:~jtv/launchpad/db-bug-435655 into lp:launchpad/db-devel

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Brad Crittenden
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~jtv/launchpad/db-bug-435655
Merge into: lp:launchpad/db-devel
Diff against target: 108 lines
3 files modified
lib/lp/translations/doc/translations-export-to-branch.txt (+0/-1)
lib/lp/translations/scripts/tests/test_translations_to_branch.py (+6/-1)
lib/lp/translations/scripts/translations_to_branch.py (+23/-2)
To merge this branch: bzr merge lp:~jtv/launchpad/db-bug-435655
Reviewer Review Type Date Requested Status
Brad Crittenden (community) release-critical Approve
Michael Nelson (community) code Approve
Review via email: mp+12333@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

= Bug 435655 =

This branch fixes critical breakage in the translations-to-branch export
script.

In 3.0 we gave this script the ability to skip exports for POFiles in a
branch that hadn't changed since their previous export run. This change
broke because the timestamp on the bzr revision that we got this
information from is not a datetime. It's a float representing seconds
since the epoch, UTC-based.

As it turns out there was a test that exercised this code, and it did
trigger the exception, but since it happened in a script run and was
handled there, the exception did not propagate into the test. Explicit
checks of the script run's results were needed.

I had to do some reverse engineering to figure out the details. As it
turns out, even the bzr experts did not know exactly how to interpret
and convert the timestamp.

You may notice that I also made the script avoid its commit when there
are no changes. This is mostly duplicative of a similar check further
down the call tree, in DirectBranchCommit. But it also circumvents the
check for commit conflicts, which simplifies the tests. Otherwise, I'd
either have to patch up the last_scanned_id for the branch object, or
let the test spit out a "branch has changed" error when it's really not
supposed to change anything in the branch anyway.

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

Q/A is difficult, since this script must run on the codehosting server
where the branches are hosted. This pretty much rules out staging.

Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (6.9 KiB)

Great stuff jtv.
r=me - two questions below.

Thanks!

> = Bug 435655 =
>
> This branch fixes critical breakage in the translations-to-branch export
> script.
>
> In 3.0 we gave this script the ability to skip exports for POFiles in a
> branch that hadn't changed since their previous export run. This change
> broke because the timestamp on the bzr revision that we got this
> information from is not a datetime. It's a float representing seconds
> since the epoch, UTC-based.
>
> As it turns out there was a test that exercised this code, and it did
> trigger the exception, but since it happened in a script run and was
> handled there, the exception did not propagate into the test. Explicit
> checks of the script run's results were needed.

Great! OK, the setup in your test has convinced me that translations is
the new soyuz ;). I've just annotated your changes to ensure I've
understood the situation correctly.

>
> I had to do some reverse engineering to figure out the details. As it
> turns out, even the bzr experts did not know exactly how to interpret
> and convert the timestamp.
>
> You may notice that I also made the script avoid its commit when there
> are no changes. This is mostly duplicative of a similar check further
> down the call tree, in DirectBranchCommit. But it also circumvents the
> check for commit conflicts, which simplifies the tests. Otherwise, I'd
> either have to patch up the last_scanned_id for the branch object, or
> let the test spit out a "branch has changed" error when it's really not
> supposed to change anything in the branch anyway.
>
> Test:
> {{{
> ./bin/test -vv -t translations.*to.branch
> }}}
>
> Q/A is difficult, since this script must run on the codehosting server
> where the branches are hosted. This pretty much rules out staging.

> === modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
> --- lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-16 06:57:35 +0000
> +++ lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-24 08:57:28 +0000
> @@ -187,7 +187,6 @@
> INFO Exporting to translations branches.
> INFO Exporting Gazblachko trunk series.
> DEBUG Last commit was at ....
> - INFO Committed 0 file(s).
> INFO Unlock.
> INFO Processed 1 item(s); 0 failure(s).
>

OK, so this is due to skipping the commit when there are no changes.

>
> === modified file 'lib/lp/translations/scripts/tests/test_translations_to_branch.py'
> --- lib/lp/translations/scripts/tests/test_translations_to_branch.py 2009-09-15 15:30:59 +0000
> +++ lib/lp/translations/scripts/tests/test_translations_to_branch.py 2009-09-24 08:57:28 +0000
> @@ -111,8 +111,11 @@
> # anything because it sees that the POFile has not been changed
> # since the last export.
> retcode, stdout, stderr = run_script(
> - 'cronscripts/translations-export-to-branch.py', ['-vvv'])
> + 'cronscripts/translations-export-to-branch.py',
> + ['-vvv', '--no-fudge'])
> + self.assertEqual(0, retcode)
> self.assertIn('Last commit was at', stderr)
> + self.assertIn("Processed 1 item(s); 0 failure(s)....

Read more...

review: Approve (code)
Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (3.5 KiB)

> Great stuff jtv.
> r=me - two questions below.

Thanks for the fast review! Read on...

> > === modified file 'lib/lp/translations/doc/translations-export-to-
> branch.txt'
> > --- lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-16
> 06:57:35 +0000
> > +++ lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-24
> 08:57:28 +0000
> > @@ -187,7 +187,6 @@
> > INFO Exporting to translations branches.
> > INFO Exporting Gazblachko trunk series.
> > DEBUG Last commit was at ....
> > - INFO Committed 0 file(s).
> > INFO Unlock.
> > INFO Processed 1 item(s); 0 failure(s).
> >
>
> OK, so this is due to skipping the commit when there are no changes.

Yes.

> > === modified file
> 'lib/lp/translations/scripts/tests/test_translations_to_branch.py'
> > --- lib/lp/translations/scripts/tests/test_translations_to_branch.py
> 2009-09-15 15:30:59 +0000
> > +++ lib/lp/translations/scripts/tests/test_translations_to_branch.py
> 2009-09-24 08:57:28 +0000
> > @@ -111,8 +111,11 @@
> > # anything because it sees that the POFile has not been changed
> > # since the last export.
> > retcode, stdout, stderr = run_script(
> > - 'cronscripts/translations-export-to-branch.py', ['-vvv'])
> > + 'cronscripts/translations-export-to-branch.py',
> > + ['-vvv', '--no-fudge'])
> > + self.assertEqual(0, retcode)
> > self.assertIn('Last commit was at', stderr)
> > + self.assertIn("Processed 1 item(s); 0 failure(s).", stderr)
> >
>
> Right - so now you're checking that it didn't error, and that it did
> process an item. Is it worth here explicitely asserting that
> 'INFO Committed' is not in stderr? (It's already part of the documentation
> above.)

Excellent idea! I'm going with it. Using regexes to search for it.

> > === modified file 'lib/lp/translations/scripts/translations_to_branch.py'
> > --- lib/lp/translations/scripts/translations_to_branch.py 2009-09-15
> 15:30:59 +0000
> > +++ lib/lp/translations/scripts/translations_to_branch.py 2009-09-24

> > @@ -81,6 +88,13 @@
> > """Is `revision` an automatic translations commit?"""
> > return revision.message == self.commit_message
> >
> > + def _getRevisionTime(self, revision):
> > + """Get timestamp of `revision`."""
> > + # The bzr timestamp is a float representing UTC-based seconds
> > + # since the epoch. It stores the timezone as well, but we can
> > + # ignore it here.
> > + return datetime.fromtimestamp(revision.timestamp, UTC)
> > +
>
> Is this a separate method because you thought you'd need to re-use it? or
> just to encapsulate the comment? Normally I'd not do that for a single
> line unless it was being re-used. It could simply go in the method below
> right?

It's mostly because getting it right turned out to be difficult. If even
lifeless doesn't know how to get a piece of information from bzrlib, when
you do finally figure it out, it's worth isolating!

I know, it doesn't look like much. But what we have in the revision is: a
"timestamp" of unclear type, and a "timezone" which isn't a timezone but an
...

Read more...

Revision history for this message
Brad Crittenden (bac) :
review: Approve (release-critical)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/translations/doc/translations-export-to-branch.txt'
--- lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-16 06:57:35 +0000
+++ lib/lp/translations/doc/translations-export-to-branch.txt 2009-09-24 09:51:12 +0000
@@ -187,7 +187,6 @@
187 INFO Exporting to translations branches.187 INFO Exporting to translations branches.
188 INFO Exporting Gazblachko trunk series.188 INFO Exporting Gazblachko trunk series.
189 DEBUG Last commit was at ....189 DEBUG Last commit was at ....
190 INFO Committed 0 file(s).
191 INFO Unlock.190 INFO Unlock.
192 INFO Processed 1 item(s); 0 failure(s).191 INFO Processed 1 item(s); 0 failure(s).
193192
194193
=== modified file 'lib/lp/translations/scripts/tests/test_translations_to_branch.py'
--- lib/lp/translations/scripts/tests/test_translations_to_branch.py 2009-09-15 15:30:59 +0000
+++ lib/lp/translations/scripts/tests/test_translations_to_branch.py 2009-09-24 09:51:12 +0000
@@ -111,8 +111,13 @@
111 # anything because it sees that the POFile has not been changed111 # anything because it sees that the POFile has not been changed
112 # since the last export.112 # since the last export.
113 retcode, stdout, stderr = run_script(113 retcode, stdout, stderr = run_script(
114 'cronscripts/translations-export-to-branch.py', ['-vvv'])114 'cronscripts/translations-export-to-branch.py',
115 ['-vvv', '--no-fudge'])
116 self.assertEqual(0, retcode)
115 self.assertIn('Last commit was at', stderr)117 self.assertIn('Last commit was at', stderr)
118 self.assertIn("Processed 1 item(s); 0 failure(s).", stderr)
119 self.assertEqual(
120 None, re.search("INFO\s+Committed [0-9]+ file", stderr))
116121
117122
118def test_suite():123def test_suite():
119124
=== modified file 'lib/lp/translations/scripts/translations_to_branch.py'
--- lib/lp/translations/scripts/translations_to_branch.py 2009-09-15 15:30:59 +0000
+++ lib/lp/translations/scripts/translations_to_branch.py 2009-09-24 09:51:12 +0000
@@ -43,6 +43,13 @@
43 # factor.43 # factor.
44 fudge_factor = timedelta(hours=6)44 fudge_factor = timedelta(hours=6)
4545
46 def add_my_options(self):
47 """See `LaunchpadScript`."""
48 self.parser.add_option(
49 '-n', '--no-fudge', action='store_true', dest='no_fudge',
50 default=False,
51 help="For testing: no fudge period for POFile changes.")
52
46 def _checkForObjections(self, source):53 def _checkForObjections(self, source):
47 """Check for reasons why we can't commit to this branch.54 """Check for reasons why we can't commit to this branch.
4855
@@ -81,6 +88,13 @@
81 """Is `revision` an automatic translations commit?"""88 """Is `revision` an automatic translations commit?"""
82 return revision.message == self.commit_message89 return revision.message == self.commit_message
8390
91 def _getRevisionTime(self, revision):
92 """Get timestamp of `revision`."""
93 # The bzr timestamp is a float representing UTC-based seconds
94 # since the epoch. It stores the timezone as well, but we can
95 # ignore it here.
96 return datetime.fromtimestamp(revision.timestamp, UTC)
97
84 def _getLatestTranslationsCommit(self, branch):98 def _getLatestTranslationsCommit(self, branch):
85 """Get date of last translations commit to `branch`, if any."""99 """Get date of last translations commit to `branch`, if any."""
86 cutoff_date = datetime.now(UTC) - self.previous_commit_cutoff_age100 cutoff_date = datetime.now(UTC) - self.previous_commit_cutoff_age
@@ -89,7 +103,7 @@
89 repository = branch.repository103 repository = branch.repository
90 for rev_id in repository.iter_reverse_revision_history(current_rev):104 for rev_id in repository.iter_reverse_revision_history(current_rev):
91 revision = repository.get_revision(rev_id)105 revision = repository.get_revision(rev_id)
92 revision_date = revision.timestamp106 revision_date = self._getRevisionTime(revision)
93 if self._isTranslationsCommit(revision):107 if self._isTranslationsCommit(revision):
94 return revision_date108 return revision_date
95109
@@ -125,6 +139,8 @@
125 self.logger.debug("Last commit was at %s." % last_commit_date)139 self.logger.debug("Last commit was at %s." % last_commit_date)
126 changed_since = last_commit_date - self.fudge_factor140 changed_since = last_commit_date - self.fudge_factor
127141
142 change_count = 0
143
128 try:144 try:
129 subset = getUtility(IPOTemplateSet).getSubset(145 subset = getUtility(IPOTemplateSet).getSubset(
130 productseries=source, iscurrent=True)146 productseries=source, iscurrent=True)
@@ -144,6 +160,7 @@
144 pofile_contents = pofile.export()160 pofile_contents = pofile.export()
145161
146 committer.writeFile(pofile_path, pofile_contents)162 committer.writeFile(pofile_path, pofile_contents)
163 change_count += 1
147164
148 # We're not actually writing any changes to the165 # We're not actually writing any changes to the
149 # database, but it's not polite to stay in one166 # database, but it's not polite to stay in one
@@ -155,7 +172,8 @@
155 # anything about it any longer.172 # anything about it any longer.
156 template.clearPOFileCache()173 template.clearPOFileCache()
157174
158 self._commit(source, committer)175 if change_count > 0:
176 self._commit(source, committer)
159 finally:177 finally:
160 committer.unlock()178 committer.unlock()
161179
@@ -193,6 +211,9 @@
193 from lp.registry.model.product import Product211 from lp.registry.model.product import Product
194 from lp.registry.model.productseries import ProductSeries212 from lp.registry.model.productseries import ProductSeries
195213
214 if self.options.no_fudge:
215 self.fudge_factor = timedelta(0)
216
196 self.logger.info("Exporting to translations branches.")217 self.logger.info("Exporting to translations branches.")
197218
198 self.store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)219 self.store = getUtility(IStoreSelector).get(MAIN_STORE, SLAVE_FLAVOR)

Subscribers

People subscribed via source and target branches

to status/vote changes: