← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~wgrant/launchpad/spr-bug-closure-move into lp:launchpad

 

William Grant has proposed merging lp:~wgrant/launchpad/spr-bug-closure-move into lp:launchpad with lp:~wgrant/launchpad/process-accepted-cleanup as a prerequisite.

Commit message:
Move all the bug closure code and tests from scripts.processaccepted to model.processacceptedbugsjob, since ProcessAcceptedBugsJob should ultimately be the only callsite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~wgrant/launchpad/spr-bug-closure-move/+merge/230202

Move all the bug closure code and tests from scripts.processaccepted to model.processacceptedbugsjob, since ProcessAcceptedBugsJob should ultimately be the only callsite.
-- 
https://code.launchpad.net/~wgrant/launchpad/spr-bug-closure-move/+merge/230202
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~wgrant/launchpad/spr-bug-closure-move into lp:launchpad.
=== modified file 'lib/lp/soyuz/model/processacceptedbugsjob.py'
--- lib/lp/soyuz/model/processacceptedbugsjob.py	2013-06-20 05:50:00 +0000
+++ lib/lp/soyuz/model/processacceptedbugsjob.py	2014-08-09 09:28:06 +0000
@@ -1,15 +1,17 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2012-2014 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 __metaclass__ = type
 
 __all__ = [
-    "close_bug_ids_for_sourcepackagerelease",
+    'close_bugs_for_queue_item',
+    'close_bugs_for_sourcepublication',
     "ProcessAcceptedBugsJob",
     ]
 
 import logging
 
+from debian.deb822 import Deb822Dict
 from storm.locals import (
     And,
     Int,
@@ -21,10 +23,13 @@
     classProvides,
     implements,
     )
+from zope.security.management import getSecurityPolicy
 
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
+from lp.archiveuploader.tagfiles import parse_tagfile_content
 from lp.bugs.interfaces.bug import IBugSet
 from lp.bugs.interfaces.bugtask import BugTaskStatus
+from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.model.distroseries import DistroSeries
 from lp.services.config import config
 from lp.services.database.interfaces import (
@@ -34,6 +39,13 @@
 from lp.services.database.stormbase import StormBase
 from lp.services.job.model.job import Job
 from lp.services.job.runner import BaseRunnableJob
+from lp.services.webapp.authorization import LaunchpadPermissiveSecurityPolicy
+from lp.soyuz.enums import (
+    ArchivePurpose,
+    re_bug_numbers,
+    re_closes,
+    re_lp_closes,
+    )
 from lp.soyuz.interfaces.processacceptedbugsjob import (
     IProcessAcceptedBugsJob,
     IProcessAcceptedBugsJobSource,
@@ -61,6 +73,142 @@
                 content=content)
 
 
+def get_bug_ids_from_changes_file(changes_file):
+    """Parse the changes file and return a list of bug IDs referenced by it.
+
+    The bugs is specified in the Launchpad-bugs-fixed header, and are
+    separated by a space character. Nonexistent bug ids are ignored.
+    """
+    tags = Deb822Dict(parse_tagfile_content(changes_file.read()))
+    bugs_fixed = tags.get('Launchpad-bugs-fixed', '').split()
+    return [int(bug_id) for bug_id in bugs_fixed if bug_id.isdigit()]
+
+
+def get_bug_ids_from_changelog_entry(sourcepackagerelease, since_version):
+    """Parse the changelog_entry in the sourcepackagerelease and return a
+    list of bug IDs referenced by it.
+    """
+    changelog = sourcepackagerelease.aggregate_changelog(since_version)
+    closes = []
+    # There are 2 main regexes to match.  Each match from those can then
+    # have further multiple matches from the 3rd regex:
+    # closes: NNN, NNN
+    # lp: #NNN, #NNN
+    regexes = (
+        re_closes.finditer(changelog), re_lp_closes.finditer(changelog))
+    for regex in regexes:
+        for match in regex:
+            bug_match = re_bug_numbers.findall(match.group(0))
+            closes += map(int, bug_match)
+    return closes
+
+
+def can_close_bugs(target):
+    """Whether or not bugs should be closed in the given target.
+
+    ISourcePackagePublishingHistory and IPackageUpload are the
+    currently supported targets.
+
+    Source publications or package uploads targeted to pockets
+    PROPOSED/BACKPORTS or any other archive purpose than PRIMARY will
+    not automatically close bugs.
+    """
+    banned_pockets = (
+        PackagePublishingPocket.PROPOSED,
+        PackagePublishingPocket.BACKPORTS)
+
+    if (target.pocket in banned_pockets or
+       target.archive.purpose != ArchivePurpose.PRIMARY):
+        return False
+
+    return True
+
+
+def close_bugs_for_queue_item(queue_item, changesfile_object=None):
+    """Close bugs for a given queue item.
+
+    'queue_item' is an IPackageUpload instance and is given by the user.
+
+    'changesfile_object' is optional if not given this function will try
+    to use the IPackageUpload.changesfile, which is only available after
+    the upload is processed and committed.
+
+    In practice, 'changesfile_object' is only set when we are closing bugs
+    in upload-time (see nascentupload-closing-bugs.txt).
+
+    Skip bug-closing if the upload is target to pocket PROPOSED or if
+    the upload is for a PPA.
+
+    Set the package bugtask status to Fix Released and the changelog is added
+    as a comment.
+    """
+    if not can_close_bugs(queue_item):
+        return
+
+    if changesfile_object is None:
+        changesfile_object = queue_item.changesfile
+
+    for source_queue_item in queue_item.sources:
+        close_bugs_for_sourcepackagerelease(
+            queue_item.distroseries, source_queue_item.sourcepackagerelease,
+            changesfile_object)
+
+
+def close_bugs_for_sourcepublication(source_publication, since_version=None):
+    """Close bugs for a given sourcepublication.
+
+    Given a `ISourcePackagePublishingHistory` close bugs mentioned in
+    upload changesfile.
+    """
+    if not can_close_bugs(source_publication):
+        return
+
+    sourcepackagerelease = source_publication.sourcepackagerelease
+    changesfile_object = sourcepackagerelease.upload_changesfile
+
+    close_bugs_for_sourcepackagerelease(
+        source_publication.distroseries, sourcepackagerelease,
+        changesfile_object, since_version)
+
+
+def close_bugs_for_sourcepackagerelease(distroseries, source_release,
+                                        changesfile_object,
+                                        since_version=None):
+    """Close bugs for a given source.
+
+    Given an `IDistroSeries`, an `ISourcePackageRelease`, and a
+    corresponding changesfile object, close bugs mentioned in the
+    changesfile in the context of the source.
+
+    If changesfile_object is None and since_version is supplied,
+    close all the bugs in changelog entries made after that version and up
+    to and including the source_release's version.  It does this by parsing
+    the changelog on the sourcepackagerelease.  This could be extended in
+    the future to deal with the changes file as well but there is no
+    requirement to do so right now.
+    """
+    if since_version and source_release.changelog:
+        bug_ids_to_close = get_bug_ids_from_changelog_entry(
+            source_release, since_version=since_version)
+    elif changesfile_object:
+        bug_ids_to_close = get_bug_ids_from_changes_file(changesfile_object)
+    else:
+        return
+
+    # No bugs to be closed by this upload, move on.
+    if not bug_ids_to_close:
+        return
+
+    if getSecurityPolicy() == LaunchpadPermissiveSecurityPolicy:
+        # We're already running in a script, so we can just close the bugs
+        # directly.
+        close_bug_ids_for_sourcepackagerelease(
+            distroseries, source_release, bug_ids_to_close)
+    else:
+        job_source = getUtility(IProcessAcceptedBugsJobSource)
+        job_source.create(distroseries, source_release, bug_ids_to_close)
+
+
 class ProcessAcceptedBugsJob(StormBase, BaseRunnableJob):
     """Base class for jobs to close bugs for accepted package uploads."""
 

=== modified file 'lib/lp/soyuz/scripts/processaccepted.py'
--- lib/lp/soyuz/scripts/processaccepted.py	2014-08-09 09:28:06 +0000
+++ lib/lp/soyuz/scripts/processaccepted.py	2014-08-09 09:28:06 +0000
@@ -5,25 +5,16 @@
 
 __metaclass__ = type
 __all__ = [
-    'close_bugs_for_queue_item',
-    'close_bugs_for_sourcepackagerelease',
-    'close_bugs_for_sourcepublication',
-    'get_bug_ids_from_changes_file',
     'ProcessAccepted',
     ]
 
 from optparse import OptionValueError
 import sys
 
-from debian.deb822 import Deb822Dict
 from zope.component import getUtility
-from zope.security.management import getSecurityPolicy
 
 from lp.archivepublisher.publishing import GLOBAL_PUBLISHER_LOCK
 from lp.archivepublisher.scripts.base import PublisherScript
-from lp.archiveuploader.tagfiles import parse_tagfile_content
-from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.services.webapp.authorization import LaunchpadPermissiveSecurityPolicy
 from lp.services.webapp.errorlog import (
     ErrorReportingUtility,
     ScriptRequest,
@@ -31,153 +22,9 @@
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackageUploadStatus,
-    re_bug_numbers,
-    re_closes,
-    re_lp_closes,
     )
 from lp.soyuz.interfaces.archive import IArchiveSet
-from lp.soyuz.interfaces.processacceptedbugsjob import (
-    IProcessAcceptedBugsJobSource,
-    )
-from lp.soyuz.model.processacceptedbugsjob import (
-    close_bug_ids_for_sourcepackagerelease,
-    )
-
-
-def get_bug_ids_from_changes_file(changes_file):
-    """Parse the changes file and return a list of bug IDs referenced by it.
-
-    The bugs is specified in the Launchpad-bugs-fixed header, and are
-    separated by a space character. Nonexistent bug ids are ignored.
-    """
-    tags = Deb822Dict(parse_tagfile_content(changes_file.read()))
-    bugs_fixed = tags.get('Launchpad-bugs-fixed', '').split()
-    return [int(bug_id) for bug_id in bugs_fixed if bug_id.isdigit()]
-
-
-def get_bug_ids_from_changelog_entry(sourcepackagerelease, since_version):
-    """Parse the changelog_entry in the sourcepackagerelease and return a
-    list of bug IDs referenced by it.
-    """
-    changelog = sourcepackagerelease.aggregate_changelog(since_version)
-    closes = []
-    # There are 2 main regexes to match.  Each match from those can then
-    # have further multiple matches from the 3rd regex:
-    # closes: NNN, NNN
-    # lp: #NNN, #NNN
-    regexes = (
-        re_closes.finditer(changelog), re_lp_closes.finditer(changelog))
-    for regex in regexes:
-        for match in regex:
-            bug_match = re_bug_numbers.findall(match.group(0))
-            closes += map(int, bug_match)
-    return closes
-
-
-def can_close_bugs(target):
-    """Whether or not bugs should be closed in the given target.
-
-    ISourcePackagePublishingHistory and IPackageUpload are the
-    currently supported targets.
-
-    Source publications or package uploads targeted to pockets
-    PROPOSED/BACKPORTS or any other archive purpose than PRIMARY will
-    not automatically close bugs.
-    """
-    banned_pockets = (
-        PackagePublishingPocket.PROPOSED,
-        PackagePublishingPocket.BACKPORTS)
-
-    if (target.pocket in banned_pockets or
-       target.archive.purpose != ArchivePurpose.PRIMARY):
-        return False
-
-    return True
-
-
-def close_bugs_for_queue_item(queue_item, changesfile_object=None):
-    """Close bugs for a given queue item.
-
-    'queue_item' is an IPackageUpload instance and is given by the user.
-
-    'changesfile_object' is optional if not given this function will try
-    to use the IPackageUpload.changesfile, which is only available after
-    the upload is processed and committed.
-
-    In practice, 'changesfile_object' is only set when we are closing bugs
-    in upload-time (see nascentupload-closing-bugs.txt).
-
-    Skip bug-closing if the upload is target to pocket PROPOSED or if
-    the upload is for a PPA.
-
-    Set the package bugtask status to Fix Released and the changelog is added
-    as a comment.
-    """
-    if not can_close_bugs(queue_item):
-        return
-
-    if changesfile_object is None:
-        changesfile_object = queue_item.changesfile
-
-    for source_queue_item in queue_item.sources:
-        close_bugs_for_sourcepackagerelease(
-            queue_item.distroseries, source_queue_item.sourcepackagerelease,
-            changesfile_object)
-
-
-def close_bugs_for_sourcepublication(source_publication, since_version=None):
-    """Close bugs for a given sourcepublication.
-
-    Given a `ISourcePackagePublishingHistory` close bugs mentioned in
-    upload changesfile.
-    """
-    if not can_close_bugs(source_publication):
-        return
-
-    sourcepackagerelease = source_publication.sourcepackagerelease
-    changesfile_object = sourcepackagerelease.upload_changesfile
-
-    close_bugs_for_sourcepackagerelease(
-        source_publication.distroseries, sourcepackagerelease,
-        changesfile_object, since_version)
-
-
-def close_bugs_for_sourcepackagerelease(distroseries, source_release,
-                                        changesfile_object,
-                                        since_version=None):
-    """Close bugs for a given source.
-
-    Given an `IDistroSeries`, an `ISourcePackageRelease`, and a
-    corresponding changesfile object, close bugs mentioned in the
-    changesfile in the context of the source.
-
-    If changesfile_object is None and since_version is supplied,
-    close all the bugs in changelog entries made after that version and up
-    to and including the source_release's version.  It does this by parsing
-    the changelog on the sourcepackagerelease.  This could be extended in
-    the future to deal with the changes file as well but there is no
-    requirement to do so right now.
-    """
-    if since_version and source_release.changelog:
-        bug_ids_to_close = get_bug_ids_from_changelog_entry(
-            source_release, since_version=since_version)
-    elif changesfile_object:
-        bug_ids_to_close = get_bug_ids_from_changes_file(changesfile_object)
-    else:
-        return
-
-    # No bugs to be closed by this upload, move on.
-    if not bug_ids_to_close:
-        return
-
-    if getSecurityPolicy() == LaunchpadPermissiveSecurityPolicy:
-        # We're already running in a script, so we can just close the bugs
-        # directly.
-        close_bug_ids_for_sourcepackagerelease(
-            distroseries, source_release, bug_ids_to_close)
-    else:
-        job_source = getUtility(IProcessAcceptedBugsJobSource)
-        job_source.create(distroseries, source_release, bug_ids_to_close)
+from lp.soyuz.model.processacceptedbugsjob import close_bugs_for_queue_item
 
 
 class ProcessAccepted(PublisherScript):

=== removed file 'lib/lp/soyuz/scripts/tests/test_processaccepted.py'
--- lib/lp/soyuz/scripts/tests/test_processaccepted.py	2014-04-24 06:45:51 +0000
+++ lib/lp/soyuz/scripts/tests/test_processaccepted.py	1970-01-01 00:00:00 +0000
@@ -1,177 +0,0 @@
-# Copyright 2009-2012 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-__metaclass__ = type
-
-from StringIO import StringIO
-from textwrap import dedent
-
-from zope.component import getUtility
-from zope.security.interfaces import ForbiddenAttribute
-from zope.security.proxy import removeSecurityProxy
-
-from lp.app.enums import InformationType
-from lp.bugs.interfaces.bugtask import BugTaskStatus
-from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.soyuz.interfaces.processacceptedbugsjob import (
-    IProcessAcceptedBugsJobSource,
-    )
-from lp.soyuz.scripts.processaccepted import (
-    close_bugs_for_sourcepackagerelease,
-    close_bugs_for_sourcepublication,
-    )
-from lp.testing import (
-    celebrity_logged_in,
-    person_logged_in,
-    TestCaseWithFactory,
-    )
-from lp.testing.layers import (
-    DatabaseFunctionalLayer,
-    LaunchpadZopelessLayer,
-    )
-
-
-class TestClosingBugs(TestCaseWithFactory):
-    """Test the various bug closing methods in processaccepted.py.
-
-    Tests are currently spread around the codebase; this is an attempt to
-    start a unification in a single file and those other tests need
-    migrating here.
-    See also:
-        * lib/lp/soyuz/doc/closing-bugs-from-changelogs.txt
-        * lib/lp/archiveuploader/tests/nascentupload-closing-bugs.txt
-    """
-    layer = LaunchpadZopelessLayer
-
-    def makeChangelogWithBugs(self, spr, target_series=None):
-        """Create a changelog for the passed sourcepackagerelease that has
-        6 bugs referenced.
-
-        :param spr: The sourcepackagerelease that needs a changelog.
-        :param target_distro: the distribution context for the source package
-            bug target.  If None, default to its uploaded distribution.
-
-        :return: A tuple which is a list of (bug, bugtask)
-        """
-        # Make 4 bugs and corresponding bugtasks and put them in an array
-        # as tuples.
-        bugs = []
-        for i in range(6):
-            if target_series is None:
-                target_series = spr.upload_distroseries
-            target = target_series.getSourcePackage(spr.sourcepackagename)
-            bug = self.factory.makeBug()
-            bugtask = self.factory.makeBugTask(target=target, bug=bug)
-            bugs.append((bug, bugtask))
-        # Make a changelog entry for a package which contains the IDs of
-        # the 6 bugs separated across 3 releases.
-        changelog = dedent("""
-            foo (1.0-3) unstable; urgency=low
-
-              * closes: %s, %s
-              * lp: #%s, #%s
-
-             -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
-
-            foo (1.0-2) unstable; urgency=low
-
-              * closes: %s
-
-             -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
-
-            foo (1.0-1) unstable; urgency=low
-
-              * closes: %s
-
-             -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
-
-            """ % (
-            bugs[0][0].id,
-            bugs[1][0].id,
-            bugs[2][0].id,
-            bugs[3][0].id,
-            bugs[4][0].id,
-            bugs[5][0].id,
-            ))
-        lfa = self.factory.makeLibraryFileAlias(content=changelog)
-        removeSecurityProxy(spr).changelog = lfa
-        self.layer.txn.commit()
-        return bugs
-
-    def test_close_bugs_for_sourcepackagerelease_with_no_changes_file(self):
-        # If there's no changes file it should read the changelog_entry on
-        # the sourcepackagerelease.
-
-        spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
-        bugs = self.makeChangelogWithBugs(spr)
-
-        # Call the method and test it's closed the bugs.
-        close_bugs_for_sourcepackagerelease(
-            spr.upload_distroseries, spr, None, since_version="1.0-1")
-        for bug, bugtask in bugs:
-            if bug.id != bugs[5][0].id:
-                self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status)
-            else:
-                self.assertEqual(BugTaskStatus.NEW, bugtask.status)
-
-    def test__close_bugs_for_sourcepublication__uses_right_distro(self):
-        # If a source was originally uploaded to a different distro,
-        # closing bugs based on a publication of the same source in a new
-        # distro should work.
-
-        # Create a source package that was originally uploaded to one
-        # distro and publish it in a second distro.
-        spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
-        target_distro = self.factory.makeDistribution()
-        target_distroseries = self.factory.makeDistroSeries(target_distro)
-        bugs = self.makeChangelogWithBugs(
-            spr, target_series=target_distroseries)
-        target_spph = self.factory.makeSourcePackagePublishingHistory(
-            sourcepackagerelease=spr, distroseries=target_distroseries,
-            archive=target_distro.main_archive,
-            pocket=PackagePublishingPocket.RELEASE)
-
-        # The test depends on this pre-condition.
-        self.assertNotEqual(spr.upload_distroseries.distribution,
-                            target_distroseries.distribution)
-
-        close_bugs_for_sourcepublication(target_spph, since_version="1.0")
-
-        for bug, bugtask in bugs:
-            self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status)
-
-
-class TestClosingPrivateBugs(TestCaseWithFactory):
-    # The distroseries +queue page can close private bugs when accepting
-    # packages.
-
-    layer = DatabaseFunctionalLayer
-
-    def test_close_bugs_for_sourcepackagerelease_with_private_bug(self):
-        """close_bugs_for_sourcepackagerelease works with private bugs."""
-        changes_file_template = "Format: 1.7\nLaunchpad-bugs-fixed: %s\n"
-        # changelog_entry is required for an assertion inside the function
-        # we're testing.
-        spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
-        archive_admin = self.factory.makePerson()
-        series = spr.upload_distroseries
-        dsp = series.distribution.getSourcePackage(spr.sourcepackagename)
-        bug = self.factory.makeBug(
-            target=dsp, information_type=InformationType.USERDATA)
-        changes = StringIO(changes_file_template % bug.id)
-
-        with person_logged_in(archive_admin):
-            # The archive admin user can't normally see this bug.
-            self.assertRaises(ForbiddenAttribute, bug, 'status')
-            # But the bug closure should work.
-            close_bugs_for_sourcepackagerelease(series, spr, changes)
-
-        # Rather than closing the bugs immediately, this creates a
-        # ProcessAcceptedBugsJob.
-        with celebrity_logged_in("admin"):
-            self.assertEqual(BugTaskStatus.NEW, bug.default_bugtask.status)
-        job_source = getUtility(IProcessAcceptedBugsJobSource)
-        [job] = list(job_source.iterReady())
-        self.assertEqual(series, job.distroseries)
-        self.assertEqual(spr, job.sourcepackagerelease)
-        self.assertEqual([bug.id], job.bug_ids)

=== modified file 'lib/lp/soyuz/tests/test_processaccepted.py'
--- lib/lp/soyuz/tests/test_processaccepted.py	2014-08-09 09:28:06 +0000
+++ lib/lp/soyuz/tests/test_processaccepted.py	2014-08-09 09:28:06 +0000
@@ -3,10 +3,8 @@
 
 """Test process-accepted.py"""
 
-from cStringIO import StringIO
 from optparse import OptionValueError
 
-from debian.deb822 import Changes
 from testtools.matchers import LessThan
 import transaction
 
@@ -20,10 +18,7 @@
     PackageUploadStatus,
     )
 from lp.soyuz.model.queue import PackageUpload
-from lp.soyuz.scripts.processaccepted import (
-    get_bug_ids_from_changes_file,
-    ProcessAccepted,
-    )
+from lp.soyuz.scripts.processaccepted import ProcessAccepted
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import TestCaseWithFactory
 from lp.testing.dbuser import switch_dbuser
@@ -190,62 +185,3 @@
         script = ProcessAccepted(
             test_args=['--all-derived', '-d', distro.name])
         self.assertRaises(OptionValueError, script.validateArguments)
-
-
-class TestBugIDsFromChangesFile(TestCaseWithFactory):
-    """Test get_bug_ids_from_changes_file."""
-
-    layer = LaunchpadZopelessLayer
-    dbuser = config.uploadqueue.dbuser
-
-    def setUp(self):
-        super(TestBugIDsFromChangesFile, self).setUp()
-        self.changes = Changes({
-            'Format': '1.8',
-            'Source': 'swat',
-            })
-
-    def getBugIDs(self):
-        """Serialize self.changes and use get_bug_ids_from_changes_file to
-        extract bug IDs from it.
-        """
-        stream = StringIO()
-        self.changes.dump(stream)
-        stream.seek(0)
-        return get_bug_ids_from_changes_file(stream)
-
-    def test_no_bugs(self):
-        # An empty list is returned if there are no bugs
-        # mentioned.
-        self.assertEqual([], self.getBugIDs())
-
-    def test_invalid_bug_id(self):
-        # Invalid bug ids (i.e. containing non-digit characters) are ignored.
-        self.changes["Launchpad-Bugs-Fixed"] = "bla"
-        self.assertEqual([], self.getBugIDs())
-
-    def test_unknown_bug_id(self):
-        # Unknown bug ids are passed through; they will be ignored later, by
-        # close_bug_ids_for_sourcepackagerelease.
-        self.changes["Launchpad-Bugs-Fixed"] = "45120"
-        self.assertEqual([45120], self.getBugIDs())
-
-    def test_valid_bug(self):
-        # For valid bug ids the bug object is returned.
-        bug = self.factory.makeBug()
-        self.changes["Launchpad-Bugs-Fixed"] = "%d" % bug.id
-        self.assertEqual([bug.id], self.getBugIDs())
-
-    def test_case_sensitivity(self):
-        # The spelling of Launchpad-Bugs-Fixed is case-insensitive.
-        bug = self.factory.makeBug()
-        self.changes["LaUnchpad-Bugs-fixed"] = "%d" % bug.id
-        self.assertEqual([bug.id], self.getBugIDs())
-
-    def test_multiple_bugs(self):
-        # Multiple bug ids can be specified, separated by spaces.
-        bug1 = self.factory.makeBug()
-        bug2 = self.factory.makeBug()
-        self.changes["Launchpad-Bugs-Fixed"] = "%d invalid %d" % (
-            bug1.id, bug2.id)
-        self.assertEqual([bug1.id, bug2.id], self.getBugIDs())

=== modified file 'lib/lp/soyuz/tests/test_processacceptedbugsjob.py'
--- lib/lp/soyuz/tests/test_processacceptedbugsjob.py	2014-04-24 06:45:51 +0000
+++ lib/lp/soyuz/tests/test_processacceptedbugsjob.py	2014-08-09 09:28:06 +0000
@@ -1,17 +1,22 @@
-# Copyright 2012 Canonical Ltd.  This software is licensed under the
+# Copyright 2012-2014 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
 """Tests for jobs to close bugs for accepted package uploads."""
 
+from cStringIO import StringIO
 from itertools import product
+from textwrap import dedent
 
+from debian.deb822 import Changes
 from testtools.content import text_content
 import transaction
 from zope.component import getUtility
+from zope.security.interfaces import ForbiddenAttribute
 from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import InformationType
 from lp.bugs.interfaces.bugtask import BugTaskStatus
+from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.config import config
 from lp.services.features.testing import FeatureFixture
@@ -24,9 +29,14 @@
     )
 from lp.soyuz.model.processacceptedbugsjob import (
     close_bug_ids_for_sourcepackagerelease,
+    close_bugs_for_sourcepackagerelease,
+    close_bugs_for_sourcepublication,
+    get_bug_ids_from_changes_file,
     )
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
 from lp.testing import (
+    celebrity_logged_in,
+    person_logged_in,
     run_script,
     TestCaseWithFactory,
     verifyObject,
@@ -34,10 +44,216 @@
 from lp.testing.fakemethod import FakeMethod
 from lp.testing.layers import (
     CeleryJobLayer,
+    DatabaseFunctionalLayer,
     LaunchpadZopelessLayer,
     )
 
 
+class TestBugIDsFromChangesFile(TestCaseWithFactory):
+    """Test get_bug_ids_from_changes_file."""
+
+    layer = LaunchpadZopelessLayer
+    dbuser = config.uploadqueue.dbuser
+
+    def setUp(self):
+        super(TestBugIDsFromChangesFile, self).setUp()
+        self.changes = Changes({
+            'Format': '1.8',
+            'Source': 'swat',
+            })
+
+    def getBugIDs(self):
+        """Serialize self.changes and use get_bug_ids_from_changes_file to
+        extract bug IDs from it.
+        """
+        stream = StringIO()
+        self.changes.dump(stream)
+        stream.seek(0)
+        return get_bug_ids_from_changes_file(stream)
+
+    def test_no_bugs(self):
+        # An empty list is returned if there are no bugs
+        # mentioned.
+        self.assertEqual([], self.getBugIDs())
+
+    def test_invalid_bug_id(self):
+        # Invalid bug ids (i.e. containing non-digit characters) are ignored.
+        self.changes["Launchpad-Bugs-Fixed"] = "bla"
+        self.assertEqual([], self.getBugIDs())
+
+    def test_unknown_bug_id(self):
+        # Unknown bug ids are passed through; they will be ignored later, by
+        # close_bug_ids_for_sourcepackagerelease.
+        self.changes["Launchpad-Bugs-Fixed"] = "45120"
+        self.assertEqual([45120], self.getBugIDs())
+
+    def test_valid_bug(self):
+        # For valid bug ids the bug object is returned.
+        bug = self.factory.makeBug()
+        self.changes["Launchpad-Bugs-Fixed"] = "%d" % bug.id
+        self.assertEqual([bug.id], self.getBugIDs())
+
+    def test_case_sensitivity(self):
+        # The spelling of Launchpad-Bugs-Fixed is case-insensitive.
+        bug = self.factory.makeBug()
+        self.changes["LaUnchpad-Bugs-fixed"] = "%d" % bug.id
+        self.assertEqual([bug.id], self.getBugIDs())
+
+    def test_multiple_bugs(self):
+        # Multiple bug ids can be specified, separated by spaces.
+        bug1 = self.factory.makeBug()
+        bug2 = self.factory.makeBug()
+        self.changes["Launchpad-Bugs-Fixed"] = "%d invalid %d" % (
+            bug1.id, bug2.id)
+        self.assertEqual([bug1.id, bug2.id], self.getBugIDs())
+
+
+class TestClosingBugs(TestCaseWithFactory):
+    """Test the various bug closing methods in processaccepted.py.
+
+    Tests are currently spread around the codebase; this is an attempt to
+    start a unification in a single file and those other tests need
+    migrating here.
+    See also:
+        * lib/lp/soyuz/doc/closing-bugs-from-changelogs.txt
+        * lib/lp/archiveuploader/tests/nascentupload-closing-bugs.txt
+    """
+    layer = LaunchpadZopelessLayer
+
+    def makeChangelogWithBugs(self, spr, target_series=None):
+        """Create a changelog for the passed sourcepackagerelease that has
+        6 bugs referenced.
+
+        :param spr: The sourcepackagerelease that needs a changelog.
+        :param target_distro: the distribution context for the source package
+            bug target.  If None, default to its uploaded distribution.
+
+        :return: A tuple which is a list of (bug, bugtask)
+        """
+        # Make 4 bugs and corresponding bugtasks and put them in an array
+        # as tuples.
+        bugs = []
+        for i in range(6):
+            if target_series is None:
+                target_series = spr.upload_distroseries
+            target = target_series.getSourcePackage(spr.sourcepackagename)
+            bug = self.factory.makeBug()
+            bugtask = self.factory.makeBugTask(target=target, bug=bug)
+            bugs.append((bug, bugtask))
+        # Make a changelog entry for a package which contains the IDs of
+        # the 6 bugs separated across 3 releases.
+        changelog = dedent("""
+            foo (1.0-3) unstable; urgency=low
+
+              * closes: %s, %s
+              * lp: #%s, #%s
+
+             -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
+
+            foo (1.0-2) unstable; urgency=low
+
+              * closes: %s
+
+             -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
+
+            foo (1.0-1) unstable; urgency=low
+
+              * closes: %s
+
+             -- Foo Bar <foo@xxxxxxxxxxx>  Tue, 01 Jan 1970 01:50:41 +0000
+
+            """ % (
+            bugs[0][0].id,
+            bugs[1][0].id,
+            bugs[2][0].id,
+            bugs[3][0].id,
+            bugs[4][0].id,
+            bugs[5][0].id,
+            ))
+        lfa = self.factory.makeLibraryFileAlias(content=changelog)
+        removeSecurityProxy(spr).changelog = lfa
+        self.layer.txn.commit()
+        return bugs
+
+    def test_close_bugs_for_sourcepackagerelease_with_no_changes_file(self):
+        # If there's no changes file it should read the changelog_entry on
+        # the sourcepackagerelease.
+
+        spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
+        bugs = self.makeChangelogWithBugs(spr)
+
+        # Call the method and test it's closed the bugs.
+        close_bugs_for_sourcepackagerelease(
+            spr.upload_distroseries, spr, None, since_version="1.0-1")
+        for bug, bugtask in bugs:
+            if bug.id != bugs[5][0].id:
+                self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status)
+            else:
+                self.assertEqual(BugTaskStatus.NEW, bugtask.status)
+
+    def test__close_bugs_for_sourcepublication__uses_right_distro(self):
+        # If a source was originally uploaded to a different distro,
+        # closing bugs based on a publication of the same source in a new
+        # distro should work.
+
+        # Create a source package that was originally uploaded to one
+        # distro and publish it in a second distro.
+        spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
+        target_distro = self.factory.makeDistribution()
+        target_distroseries = self.factory.makeDistroSeries(target_distro)
+        bugs = self.makeChangelogWithBugs(
+            spr, target_series=target_distroseries)
+        target_spph = self.factory.makeSourcePackagePublishingHistory(
+            sourcepackagerelease=spr, distroseries=target_distroseries,
+            archive=target_distro.main_archive,
+            pocket=PackagePublishingPocket.RELEASE)
+
+        # The test depends on this pre-condition.
+        self.assertNotEqual(spr.upload_distroseries.distribution,
+                            target_distroseries.distribution)
+
+        close_bugs_for_sourcepublication(target_spph, since_version="1.0")
+
+        for bug, bugtask in bugs:
+            self.assertEqual(BugTaskStatus.FIXRELEASED, bugtask.status)
+
+
+class TestClosingPrivateBugs(TestCaseWithFactory):
+    # The distroseries +queue page can close private bugs when accepting
+    # packages.
+
+    layer = DatabaseFunctionalLayer
+
+    def test_close_bugs_for_sourcepackagerelease_with_private_bug(self):
+        """close_bugs_for_sourcepackagerelease works with private bugs."""
+        changes_file_template = "Format: 1.7\nLaunchpad-bugs-fixed: %s\n"
+        # changelog_entry is required for an assertion inside the function
+        # we're testing.
+        spr = self.factory.makeSourcePackageRelease(changelog_entry="blah")
+        archive_admin = self.factory.makePerson()
+        series = spr.upload_distroseries
+        dsp = series.distribution.getSourcePackage(spr.sourcepackagename)
+        bug = self.factory.makeBug(
+            target=dsp, information_type=InformationType.USERDATA)
+        changes = StringIO(changes_file_template % bug.id)
+
+        with person_logged_in(archive_admin):
+            # The archive admin user can't normally see this bug.
+            self.assertRaises(ForbiddenAttribute, bug, 'status')
+            # But the bug closure should work.
+            close_bugs_for_sourcepackagerelease(series, spr, changes)
+
+        # Rather than closing the bugs immediately, this creates a
+        # ProcessAcceptedBugsJob.
+        with celebrity_logged_in("admin"):
+            self.assertEqual(BugTaskStatus.NEW, bug.default_bugtask.status)
+        job_source = getUtility(IProcessAcceptedBugsJobSource)
+        [job] = list(job_source.iterReady())
+        self.assertEqual(series, job.distroseries)
+        self.assertEqual(spr, job.sourcepackagerelease)
+        self.assertEqual([bug.id], job.bug_ids)
+
+
 class TestCloseBugIDsForSourcePackageRelease(TestCaseWithFactory):
 
     layer = LaunchpadZopelessLayer


Follow ups