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

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: William Grant
Approved revision: no longer in the source branch.
Merged at revision: 13618
Proposed branch: lp:~jtv/launchpad/bug-819674
Merge into: lp:launchpad
Diff against target: 109 lines (+75/-6)
2 files modified
lib/lp/archivepublisher/scripts/publish_ftpmaster.py (+14/-6)
lib/lp/archivepublisher/tests/test_publish_ftpmaster.py (+61/-0)
To merge this branch: bzr merge lp:~jtv/launchpad/bug-819674
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+70135@code.launchpad.net

Commit message

[r=wgrant][bug=819674] Make sure publish-ftpmaster handles ctrl-C.

Description of the change

= Summary =

If you hit ctrl-C while running publish-ftpmaster, the sensible thing happens: the only parts of the filesystem that are affected are the archive pool (which may get some extra files but won't reference them yet), and the working dists directory (which won't become the current dists directory until success).

This wasn't fully tested, so here goes.

I tried to do this the proper TDD way, and then found that the only real code change I needed was to make this behaviour testable. Then I discovered that the "except:" catches not just Exceptions but KeyboardInterrupts as well (the latter is no longer derived from the former in recent python versions). But I believe the tests are still useful.

I used FakeMethods in two of their four capacities here:

1. To force a method into raising a specific failure.

2. To keep track of calls to a method.

(The other two, which I did not use here, are to turn a modifying method into a no-op and to force a querying method into returning a specific value).

The actual work done by recoverWorkingDists (including its integration with the newly extracted recoverArchiveWorkingDirectory) is already covered by existing tests. The new tests cover new aspects of how publish and recoverWorkingDists fit together.

== Tests ==

{{{
./bin/test -vvc lp.archivepublisher.tests.test_publish_ftpmaster
}}}

== Demo and Q/A ==

Ongoing: make sure the new python-based publish-ftpmaster script works well enough to replace the old shell script in production.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/archivepublisher/scripts/publish_ftpmaster.py
  lib/lp/archivepublisher/tests/test_publish_ftpmaster.py

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) wrote :

As discussed on IRC, I'd like to see assertRaisesWithContent to simplify two of those tests.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/archivepublisher/scripts/publish_ftpmaster.py'
--- lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2011-07-27 10:54:53 +0000
+++ lib/lp/archivepublisher/scripts/publish_ftpmaster.py 2011-08-05 04:03:15 +0000
@@ -321,6 +321,19 @@
321 failure=LaunchpadScriptFailure(321 failure=LaunchpadScriptFailure(
322 "Failed to rsync new dists for %s." % purpose.title))322 "Failed to rsync new dists for %s." % purpose.title))
323323
324 def recoverArchiveWorkingDir(self, archive_config):
325 """Recover working dists dir for `archive_config`.
326
327 If there is a dists directory for `archive_config` in the working
328 location, kick it back to the backup location.
329 """
330 working_location = get_working_dists(archive_config)
331 if file_exists(working_location):
332 self.logger.info(
333 "Recovering working directory %s from failed run.",
334 working_location)
335 os.rename(working_location, get_backup_dists(archive_config))
336
324 def recoverWorkingDists(self):337 def recoverWorkingDists(self):
325 """Look for and recover any dists left in transient working state.338 """Look for and recover any dists left in transient working state.
326339
@@ -330,12 +343,7 @@
330 permanent location.343 permanent location.
331 """344 """
332 for archive_config in self.configs.itervalues():345 for archive_config in self.configs.itervalues():
333 working_location = get_working_dists(archive_config)346 self.recoverArchiveWorkingDir(archive_config)
334 if file_exists(working_location):
335 self.logger.info(
336 "Recovering working directory %s from failed run.",
337 working_location)
338 os.rename(working_location, get_backup_dists(archive_config))
339347
340 def setUpDirs(self):348 def setUpDirs(self):
341 """Create archive roots and such if they did not yet exist."""349 """Create archive roots and such if they did not yet exist."""
342350
=== modified file 'lib/lp/archivepublisher/tests/test_publish_ftpmaster.py'
--- lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2011-07-22 15:35:08 +0000
+++ lib/lp/archivepublisher/tests/test_publish_ftpmaster.py 2011-08-05 04:03:15 +0000
@@ -736,6 +736,67 @@
736 "Did not find expected marker for %s."736 "Did not find expected marker for %s."
737 % archive.purpose.title)737 % archive.purpose.title)
738738
739 def test_publish_reraises_exception(self):
740 # If an Exception comes up while publishing, it bubbles up out
741 # of the publish method even though the method must intercept
742 # it for its own purposes.
743 class MoonPhaseError(Exception):
744 """Simulated failure."""
745
746 message = self.factory.getUniqueString()
747 script = self.makeScript()
748 script.publishAllUploads = FakeMethod(failure=MoonPhaseError(message))
749 script.setUp()
750 self.assertRaisesWithContent(MoonPhaseError, message, script.publish)
751
752 def test_publish_obeys_keyboard_interrupt(self):
753 # Similar to an Exception, a keyboard interrupt does not get
754 # swallowed.
755 message = self.factory.getUniqueString()
756 script = self.makeScript()
757 script.publishAllUploads = FakeMethod(
758 failure=KeyboardInterrupt(message))
759 script.setUp()
760 self.assertRaisesWithContent(
761 KeyboardInterrupt, message, script.publish)
762
763 def test_publish_recovers_working_dists_on_exception(self):
764 # If an Exception comes up while publishing, the publish method
765 # recovers its working directory.
766 class MoonPhaseError(Exception):
767 """Simulated failure."""
768
769 failure = MoonPhaseError(self.factory.getUniqueString())
770
771 script = self.makeScript()
772 script.publishAllUploads = FakeMethod(failure=failure)
773 script.recoverArchiveWorkingDir = FakeMethod()
774 script.setUp()
775
776 try:
777 script.publish()
778 except MoonPhaseError:
779 pass
780
781 self.assertEqual(1, script.recoverArchiveWorkingDir.call_count)
782
783 def test_publish_recovers_working_dists_on_ctrl_C(self):
784 # If the user hits ctrl-C while publishing, the publish method
785 # recovers its working directory.
786 failure = KeyboardInterrupt("Ctrl-C!")
787
788 script = self.makeScript()
789 script.publishAllUploads = FakeMethod(failure=failure)
790 script.recoverArchiveWorkingDir = FakeMethod()
791 script.setUp()
792
793 try:
794 script.publish()
795 except KeyboardInterrupt:
796 pass
797
798 self.assertEqual(1, script.recoverArchiveWorkingDir.call_count)
799
739800
740class TestCreateDistroSeriesIndexes(TestCaseWithFactory, HelpersMixin):801class TestCreateDistroSeriesIndexes(TestCaseWithFactory, HelpersMixin):
741 """Test initial creation of archive indexes for a `DistroSeries`."""802 """Test initial creation of archive indexes for a `DistroSeries`."""