← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~jtv/launchpad/bug-791204 into lp:launchpad

 

Jeroen T. Vermeulen has proposed merging lp:~jtv/launchpad/bug-791204 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #791204 in Launchpad itself: "Fix the 'queue' script tool to work with copy-type PackageUploads"
  https://bugs.launchpad.net/launchpad/+bug/791204

For more details, see:
https://code.launchpad.net/~jtv/launchpad/bug-791204/+merge/64390

= Summary =

The soyuz queue script and +queue page are going to display PackageUploads with PackageCopyJobs.  This type of PackageUpload isn't currently displayed because DistroSeries.getQueueItems doesn't find them; we're replacing it with DistroSeries.getPackageUploads which does.

First we must make sure that the script and the view can handle this type of job.  In this branch you'll see some of the groundwork for that.


== Proposed fix ==

Several attributes that a PackageUpload previously got from its source or build upload may now have to be gleaned from its PackageCopyJob instead.  This requires encapsulating them in PackageUpload, which knows where to get the information, rather than have the TAL or anyone just poking around its details.


== Pre-implementation notes ==

This does not actually replace getQueueItems calls with getPackageUploads calls yet.  That involves more work, such as supporting a few more filters that getQueueItems has and sorting out the ordering story.


== Implementation details ==

Working with Julian on this.  Separate branches will add functionality to bring getPackageUploads up to par with getQueueItems, and convert getQueueItems calls to use getPackageUploads instead.  We're also still working out how to sort.


== Tests ==

{{{
./bin/tests lp.soyuz.scripts.tests.test_queue
./bin/tests lp.soyuz.browser.tests.test_queue
./bin/tests lp.soyuz.tests.test_packageupload
}}}


== Demo and Q/A ==

The queue script will work as before, as will the +queue page.


= Launchpad lint =

There's some lint that I fixed in a separate branch, which should be about to go into PQM.  At that point the diff should become a bit smaller, because some of the fixes are duplicated.


Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/soyuz/browser/tests/test_queue.py
  lib/lp/soyuz/model/queue.py
  lib/lp/soyuz/interfaces/queue.py
  lib/lp/soyuz/scripts/tests/test_queue.py
  lib/lp/soyuz/browser/queue.py
  lib/lp/soyuz/configure.zcml
  lib/lp/soyuz/tests/test_packageupload.py
  lib/lp/soyuz/scripts/queue.py

./lib/lp/soyuz/browser/queue.py
     328: local variable 'header' is assigned to but never used
     361: E203 whitespace before ':'
     501: W391 blank line at end of file
-- 
https://code.launchpad.net/~jtv/launchpad/bug-791204/+merge/64390
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~jtv/launchpad/bug-791204 into lp:launchpad.
=== modified file 'lib/lp/soyuz/browser/queue.py'
--- lib/lp/soyuz/browser/queue.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/browser/queue.py	2011-06-13 11:44:37 +0000
@@ -479,7 +479,7 @@
         used to upload the contained source.
         """
         if self.is_delayed_copy:
-            return self.sources[0].sourcepackagerelease.upload_changesfile
+            return self.sourcepackagerelease.upload_changesfile
         return self.context.changesfile
 
     @property

=== modified file 'lib/lp/soyuz/browser/tests/test_queue.py'
--- lib/lp/soyuz/browser/tests/test_queue.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/browser/tests/test_queue.py	2011-06-13 11:44:37 +0000
@@ -10,10 +10,12 @@
     getUtility,
     queryMultiAdapter,
     )
+from zope.security.proxy import removeSecurityProxy
 
 from canonical.launchpad.webapp.servers import LaunchpadTestRequest
 from canonical.testing.layers import LaunchpadFunctionalLayer
 from lp.archiveuploader.tests import datadir
+from lp.soyuz.adapters.overrides import SourceOverride
 from lp.soyuz.enums import PackageUploadStatus
 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
 from lp.soyuz.interfaces.queue import IPackageUploadSet
@@ -21,8 +23,10 @@
 from lp.testing import (
     login,
     logout,
+    person_logged_in,
     TestCaseWithFactory,
     )
+from lp.testing.views import create_initialized_view
 
 
 class TestAcceptQueueUploads(TestCaseWithFactory):
@@ -186,3 +190,43 @@
         self.assertEquals(
             'NEW',
             getUtility(IPackageUploadSet).get(package_upload_id).status.name)
+
+
+class TestQueueItemsView(TestCaseWithFactory):
+    """Unit tests for `QueueItemsView`."""
+
+    layer = LaunchpadFunctionalLayer
+
+    def makeView(self, distroseries, user, form):
+        return create_initialized_view(
+            distroseries, name='+queue', principal=user)
+
+    def makeCopyJobUpload(self, distroseries, component):
+        job = self.factory.makePlainPackageCopyJob(
+            target_distroseries=distroseries)
+        job.addSourceOverride(SourceOverride(
+            self.factory.makeSourcePackageName().name,
+            component=component, section=self.factory.makeSection()))
+        naked_job = removeSecurityProxy(job).context
+        return self.factory.makePackageUpload(
+            distroseries=distroseries, package_copy_job=naked_job)
+
+    def test_copy_upload_does_not_break_rendering(self):
+        # The presence of a PackageUpload with a PackageCopyJob does not
+        # break rendering of the page.
+        # XXX JeroenVermeulen 2011-06-13 bug=394645: the current reason
+        # why this doesn't break is that the view uses getQueueItems,
+        # which won't return PackageUploads with copy jobs.  This test
+        # may start breaking once the view uses getPackageUploads.
+        distroseries = self.factory.makeDistroSeries()
+        component = self.factory.makeComponent()
+        upload = self.makeCopyJobUpload(distroseries, component=component)
+        queue_admin = self.factory.makeArchiveAdmin(distroseries.main_archive)
+        form = {
+            'queue_state': PackageUploadStatus.NEW.value,
+            'Accept': 'Accept',
+            'QUEUE_ID': [upload.id],
+            }
+        with person_logged_in(queue_admin):
+            view = self.makeView(distroseries, queue_admin, form)
+            view()

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2011-06-03 08:53:14 +0000
+++ lib/lp/soyuz/configure.zcml	2011-06-13 11:44:37 +0000
@@ -166,6 +166,7 @@
                 custom_file_urls
                 date_created
                 sourcepackagerelease
+                component_name
                 contains_source
                 contains_build
                 contains_translation
@@ -178,6 +179,9 @@
                 is_delayed_copy
                 isPPA
                 package_copy_job
+                package_name
+                package_version
+                section_name
                 components"/>
         <require
             permission="launchpad.Edit"

=== modified file 'lib/lp/soyuz/interfaces/queue.py'
--- lib/lp/soyuz/interfaces/queue.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/interfaces/queue.py	2011-06-13 11:44:37 +0000
@@ -41,7 +41,10 @@
     )
 
 from canonical.launchpad import _
+<<<<<<< TREE
 from lp.soyuz.enums import PackageUploadStatus
+=======
+>>>>>>> MERGE-SOURCE
 from lp.soyuz.interfaces.packagecopyjob import IPackageCopyJob
 
 
@@ -168,7 +171,7 @@
         exported_as="display_name")
     displayversion = exported(
         TextLine(
-            title=_("The source package version for this item"),
+            title=_("This item's displayable source package version"),
             readonly=True),
         exported_as="display_version")
     displayarchs = exported(
@@ -179,6 +182,15 @@
     sourcepackagerelease = Attribute(
         "The source package release for this item")
 
+    package_name = TextLine(
+        title=_("Name of the uploaded source package"), readonly=True)
+
+    package_version = TextLine(
+        title=_("Source package version"), readonly=True)
+
+    component_name = TextLine(
+        title=_("Source package component name"), readonly=True)
+
     contains_source = Attribute("whether or not this upload contains sources")
     contains_build = Attribute("whether or not this upload contains binaries")
     contains_installer = Attribute(
@@ -202,6 +214,9 @@
         on all the binarypackagerelease records arising from the build.
         """)
 
+    section_name = TextLine(
+        title=_("Source package sectio name"), readonly=True)
+
     def setNew():
         """Set queue state to NEW."""
 

=== modified file 'lib/lp/soyuz/model/queue.py'
--- lib/lp/soyuz/model/queue.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/model/queue.py	2011-06-13 11:44:37 +0000
@@ -537,12 +537,52 @@
         return (PackageUploadCustomFormat.DDTP_TARBALL
                 in self._customFormats)
 
+    @property
+    def package_name(self):
+        """See `IPackageUpload`."""
+        if self.package_copy_job_id is not None:
+            return self.package_copy_job.package_name
+        elif self.sourcepackagerelease is not None:
+            return self.sourcepackagerelease.sourcepackagename.name
+        else:
+            return None
+
+    @property
+    def package_version(self):
+        """See `IPackageUpload`."""
+        if self.package_copy_job_id is not None:
+            return self.package_copy_job.metadata["package_version"]
+        elif self.sourcepackagerelease is not None:
+            return self.sourcepackagerelease.version
+        else:
+            return None
+
+    @property
+    def component_name(self):
+        """See `IPackageUpload`."""
+        if self.package_copy_job_id is not None:
+            return self.package_copy_job.metadata["component_override"]
+        elif self.sourcepackagerelease is not None:
+            return self.sourcepackagerelease.component.name
+        else:
+            return None
+
+    @property
+    def section_name(self):
+        """See `IPackageUpload`."""
+        if self.package_copy_job_id is not None:
+            return self.package_copy_job.metadata["section_override"]
+        elif self.sourcepackagerelease is not None:
+            return self.sourcepackagerelease.section.name
+        else:
+            return None
+
     @cachedproperty
     def displayname(self):
         """See `IPackageUpload`."""
         names = []
-        for queue_source in self.sources:
-            names.append(queue_source.sourcepackagerelease.name)
+        if self.contains_source or self.package_copy_job_id is not None:
+            names.append(self.package_name)
         for queue_build in self.builds:
             names.append(queue_build.build.source_package_release.name)
         for queue_custom in self.customfiles:
@@ -558,7 +598,7 @@
     def displayarchs(self):
         """See `IPackageUpload`"""
         archs = []
-        for queue_source in self.sources:
+        if self.contains_source or self.package_copy_job_id is not None:
             archs.append('source')
         for queue_build in self.builds:
             archs.append(queue_build.build.distro_arch_series.architecturetag)
@@ -569,42 +609,20 @@
     @cachedproperty
     def displayversion(self):
         """See `IPackageUpload`"""
-        if self.sources:
-            return self.sources[0].sourcepackagerelease.version
-        if self.builds:
-            return self.builds[0].build.source_package_release.version
-        if self.customfiles:
+        package_version = self.package_version
+        if package_version is not None:
+            return package_version
+        elif self.customfiles:
             return '-'
+        else:
+            return None
 
     @cachedproperty
     def sourcepackagerelease(self):
-        """The source package release related to this queue item.
-
-        This is currently heuristic but may be more easily calculated later.
-        """
-        if self.sources:
-            return self.sources[0].sourcepackagerelease
-        elif self.builds:
-            return self.builds[0].build.source_package_release
-        else:
-            return None
-
-    @property
-    def my_source_package_release(self):
-        """The source package release related to this queue item.
-
-        al-maisan, Wed, 30 Sep 2009 17:58:31 +0200:
-        The cached property version above behaves very finicky in
-        tests and I've had a *hell* of a time revising these and
-        making them pass.
-
-        In any case, Celso's advice was to stay away from it
-        and I am hence introducing this non-cached variant for
-        usage inside the content class.
-        """
-        if self.sources is not None and bool(self.sources):
-            return self.sources[0].sourcepackagerelease
-        elif self.builds is not None and bool(self.builds):
+        """See `IPackageUpload`."""
+        if self.contains_source:
+            return self.sources[0].sourcepackagerelease
+        elif self.contains_build:
             return self.builds[0].build.source_package_release
         else:
             return None

=== modified file 'lib/lp/soyuz/scripts/queue.py'
--- lib/lp/soyuz/scripts/queue.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/scripts/queue.py	2011-06-13 11:44:37 +0000
@@ -129,8 +129,13 @@
             try:
                 self.distroseries, self.pocket = (
                     self.distribution.getDistroSeriesAndPocket(
+<<<<<<< TREE
                         self.suite_name))
             except NotFoundError:
+=======
+                    self.suite_name))
+            except NotFoundError:
+>>>>>>> MERGE-SOURCE
                 raise QueueActionError('Context not found: "%s/%s"'
                                        % (self.distribution.name,
                                           self.suite_name))
@@ -194,6 +199,9 @@
                     term, version = term.strip().split('/')
 
                 # Expand SQLObject results.
+                # XXX 2011-06-13 JeroenVermeulen bug=394645: This should
+                # use getPackageUploads, not getQueueItems, so that it
+                # will also include copy-job uploads.
                 for item in self.distroseries.getQueueItems(
                     status=self.queue, name=term, version=version,
                     exact_match=self.exact_match, pocket=self.pocket):
@@ -267,11 +275,14 @@
         Optionally pass a binarypackagename via 'only' argument to display
         only exact matches within the selected build queue items.
         """
-        for source in queue_item.sources:
-            spr = source.sourcepackagerelease
-            self.display("\t | * %s/%s Component: %s Section: %s"
-                         % (spr.sourcepackagename.name, spr.version,
-                            spr.component.name, spr.section.name))
+        if queue_item.package_copy_job or not queue_item.sources.is_empty():
+            self.display(
+                "\t | * %s/%s Component: %s Section: %s" % (
+                    queue_item.package_name,
+                    queue_item.package_version,
+                    queue_item.component_name,
+                    queue_item.section_name,
+                    ))
 
         for queue_build in queue_item.builds:
             for bpr in queue_build.build.binarypackages:

=== modified file 'lib/lp/soyuz/scripts/tests/test_queue.py'
--- lib/lp/soyuz/scripts/tests/test_queue.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/scripts/tests/test_queue.py	2011-06-13 11:44:37 +0000
@@ -20,7 +20,12 @@
 from canonical.database.sqlbase import ISOLATION_LEVEL_READ_COMMITTED
 from canonical.launchpad.database.librarian import LibraryFileAlias
 from canonical.launchpad.interfaces.librarian import ILibraryFileAliasSet
-from canonical.librarian.testing.server import fillLibrarianFile
+<<<<<<< TREE
+from canonical.librarian.testing.server import fillLibrarianFile
+=======
+from canonical.launchpad.interfaces.lpstorm import IStore
+from canonical.librarian.testing.server import fillLibrarianFile
+>>>>>>> MERGE-SOURCE
 from canonical.librarian.utils import filechunks
 from canonical.testing.layers import (
     DatabaseFunctionalLayer,
@@ -42,15 +47,23 @@
 from lp.registry.interfaces.person import IPersonSet
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
+from lp.services.job.interfaces.job import SuspendJobException
 from lp.services.log.logger import DevNullLogger
 from lp.services.mail import stub
+from lp.soyuz.adapters.overrides import SourceOverride
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackagePublishingStatus,
     PackageUploadStatus,
     )
-from lp.soyuz.interfaces.archive import IArchiveSet
-from lp.soyuz.interfaces.queue import IPackageUploadSet
+<<<<<<< TREE
+from lp.soyuz.interfaces.archive import IArchiveSet
+from lp.soyuz.interfaces.queue import IPackageUploadSet
+=======
+from lp.soyuz.interfaces.archive import IArchiveSet
+from lp.soyuz.interfaces.packagecopyjob import IPlainPackageCopyJobSource
+from lp.soyuz.interfaces.queue import IPackageUploadSet
+>>>>>>> MERGE-SOURCE
 from lp.soyuz.model.queue import PackageUploadBuild
 from lp.soyuz.scripts.processaccepted import (
     close_bugs_for_sourcepackagerelease,
@@ -59,15 +72,17 @@
     CommandRunner,
     CommandRunnerError,
     name_queue_map,
+    QueueAction,
     )
 from lp.testing import (
     celebrity_logged_in,
     person_logged_in,
     TestCaseWithFactory,
     )
-
-
-class TestQueueBase(TestCase):
+from lp.testing.fakemethod import FakeMethod
+
+
+class TestQueueBase:
     """Base methods for queue tool test classes."""
 
     def setUp(self):
@@ -107,7 +122,7 @@
         self.assertEqual(to_addrs, expected_to_addrs)
 
 
-class TestQueueTool(TestQueueBase):
+class TestQueueTool(TestQueueBase, TestCase):
     layer = LaunchpadZopelessLayer
     dbuser = config.uploadqueue.dbuser
 
@@ -924,6 +939,128 @@
             'override binary pmount', component_name='partner')
 
 
+class TestQueueActionLite(TestCaseWithFactory):
+    """A lightweight unit test case for `QueueAction`.
+
+    Meant for detailed tests that would be too expensive for full end-to-end
+    tests.
+    """
+
+    layer = LaunchpadZopelessLayer
+
+    def makePackageCopyJob(self, sourcepackagerelease=None):
+        """Create a `PlainPackageCopyJob` for use with a `PackageUpload`."""
+        job_source = getUtility(IPlainPackageCopyJobSource)
+        if sourcepackagerelease is None:
+            sourcepackagerelease = self.factory.makeSourcePackageRelease()
+        distroseries = self.factory.makeDistroSeries()
+        job = job_source.create(
+            sourcepackagerelease.sourcepackagename.name,
+            sourcepackagerelease.upload_archive,
+            distroseries.main_archive,
+            distroseries,
+            PackagePublishingPocket.RELEASE,
+            package_version=sourcepackagerelease.version)
+        job.addSourceOverride(SourceOverride(
+            source_package_name=sourcepackagerelease.sourcepackagename,
+            component=sourcepackagerelease.component,
+            section=sourcepackagerelease.section))
+        IStore(removeSecurityProxy(job).context).flush()
+        return job
+
+    def makeQueueAction(self, package_upload, distroseries=None):
+        """Create a `QueueAction` for use with a `PackageUpload`.
+
+        The action's `display` method is set to a `FakeMethod`.
+        """
+        if distroseries is None:
+            distroseries = self.factory.makeDistroSeries()
+        distro = distroseries.distribution
+        if package_upload is None:
+            package_upload = self.factory.makePackageUpload(
+                distroseries=distroseries, archive=distro.main_archive)
+        component = self.factory.makeComponent()
+        section = self.factory.makeSection()
+        suite = "%s-%s" % (distroseries.name, "release")
+        queue = None
+        priority_name = "STANDARD"
+        display = FakeMethod()
+        terms = ['*']
+        return QueueAction(
+            distro.name, suite, queue, terms, component.name,
+            section.name, priority_name, display)
+
+    def makeSourceUpload(self):
+        """Create a `PackageUpload` with source attached."""
+        upload = self.factory.makePackageUpload()
+        upload.addSource(self.factory.makeSourcePackageRelease())
+        return upload
+
+    def makeCopyJobUpload(self):
+        """Create a `PackageUpload` with a package copy job attached."""
+        spph = self.factory.makeSourcePackagePublishingHistory()
+        job = self.makePackageCopyJob(spph.sourcepackagerelease)
+        # Let the job create a PackageUpload and suspend itself for
+        # future approval or rejection.
+        try:
+            job.run()
+        except SuspendJobException:
+            # Expected exception.
+            job.suspend()
+        upload_set = getUtility(IPackageUploadSet)
+        return upload_set.getByPackageCopyJobIDs([job.id]).one()
+
+    def test_display_actions_have_privileges_for_PackageCopyJob(self):
+        # The methods that display uploads have privileges to work with
+        # a PackageUpload that has a copy job.
+        # Bundling tests for multiple operations into one test because
+        # the database user change requires a costly commit.
+        upload = self.makeCopyJobUpload()
+        action = self.makeQueueAction(upload)
+        self.layer.txn.commit()
+        self.layer.switchDbUser(config.uploadqueue.dbuser)
+
+        action.displayItem(upload)
+        self.assertNotEqual(0, action.display.call_count)
+        action.display.calls = []
+        action.displayInfo(upload)
+        self.assertNotEqual(0, action.display.call_count)
+
+    def test_accept_actions_have_privileges_for_PackageCopyJob(self):
+        upload = self.makeCopyJobUpload()
+        self.layer.txn.commit()
+        self.layer.switchDbUser(config.uploadqueue.dbuser)
+        upload.acceptFromQueue(DevNullLogger(), dry_run=True)
+        # Flush changes to make sure we're not caching any updates that
+        # the database won't allow.  If this passes, we've got the
+        # privileges.
+        IStore(upload).flush()
+
+    def test_displayItem_displays_PackageUpload_with_source(self):
+        upload = self.makeSourceUpload()
+        action = self.makeQueueAction(upload)
+        action.displayItem(upload)
+        self.assertNotEqual(0, action.display.call_count)
+
+    def test_displayItem_displays_PackageUpload_with_PackageCopyJob(self):
+        upload = self.makeCopyJobUpload()
+        action = self.makeQueueAction(upload)
+        action.displayItem(upload)
+        self.assertNotEqual(0, action.display.call_count)
+
+    def test_displayInfo_displays_PackageUpload_with_source(self):
+        upload = self.makeSourceUpload()
+        action = self.makeQueueAction(upload)
+        action.displayInfo(upload)
+        self.assertNotEqual(0, action.display.call_count)
+
+    def test_displayInfo_displays_PackageUpload_with_PackageCopyJob(self):
+        upload = self.makeCopyJobUpload()
+        action = self.makeQueueAction(upload)
+        action.displayInfo(upload)
+        self.assertNotEqual(0, action.display.call_count)
+
+
 class TestQueuePageClosingBugs(TestCaseWithFactory):
     # The distroseries +queue page can close bug when accepting
     # packages.  Unit tests for that belong here.
@@ -954,7 +1091,7 @@
             self.assertEqual(bug_task.status, BugTaskStatus.FIXRELEASED)
 
 
-class TestQueueToolInJail(TestQueueBase):
+class TestQueueToolInJail(TestQueueBase, TestCase):
     layer = LaunchpadZopelessLayer
     dbuser = config.uploadqueue.dbuser
 

=== modified file 'lib/lp/soyuz/tests/test_packageupload.py'
--- lib/lp/soyuz/tests/test_packageupload.py	2011-06-13 07:01:15 +0000
+++ lib/lp/soyuz/tests/test_packageupload.py	2011-06-13 11:44:37 +0000
@@ -22,6 +22,7 @@
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.log.logger import BufferLogger
 from lp.services.mail import stub
+from lp.soyuz.adapters.overrides import SourceOverride
 from lp.soyuz.enums import (
     ArchivePurpose,
     PackagePublishingStatus,
@@ -350,6 +351,117 @@
         pub = package_upload.realiseUpload()[0]
         self.assertEqual("partner", pub.archive.name)
 
+    def test_package_name_and_version(self):
+        # The PackageUpload knows the name and version of the package
+        # being uploaded.  Internally, it gets this information from the
+        # SourcePackageRelease.
+        upload = self.factory.makePackageUpload()
+        spr = self.factory.makeSourcePackageRelease()
+        upload.addSource(spr)
+        self.assertEqual(spr.sourcepackagename.name, upload.package_name)
+        self.assertEqual(spr.version, upload.package_version)
+
+
+class TestPackageUploadWithPackageCopyJob(TestCaseWithFactory):
+
+    layer = LaunchpadZopelessLayer
+    dbuser = config.uploadqueue.dbuser
+
+    def makeUploadWithPackageCopyJob(self, sourcepackagename=None):
+        """Create a `PackageUpload` plus attached `PlainPackageCopyJob`."""
+        job_factory_args = {}
+        if sourcepackagename is not None:
+            job_factory_args['package_name'] = sourcepackagename.name
+            job_factory_args['package_version'] = '1.0'
+        job = self.factory.makePlainPackageCopyJob(**job_factory_args)
+        naked_job = removeSecurityProxy(job).context
+        upload = self.factory.makePackageUpload(package_copy_job=naked_job)
+        return upload, job
+
+    def test_package_copy_job_property(self):
+        # Test that we can set and get package_copy_job.
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        self.assertEqual(
+            removeSecurityProxy(pcj).context, pu.package_copy_job)
+
+    def test_getByPackageCopyJobIDs(self):
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        result = getUtility(IPackageUploadSet).getByPackageCopyJobIDs(
+            [pcj.id])
+        self.assertEqual(pu, result.one())
+
+    def test_overrideSource_with_copy_job(self):
+        # The overrides should be stored in the job's metadata.
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        component = getUtility(IComponentSet)['restricted']
+        section = getUtility(ISectionSet)['games']
+
+        expected_metadata = {
+            'component_override': component.name,
+            'section_override': section.name,
+        }
+        expected_metadata.update(pcj.metadata)
+
+        pu.overrideSource(component, section, allowed_components=[component])
+
+        self.assertEqual(expected_metadata, pcj.metadata)
+
+    def test_acceptFromQueue_with_copy_job(self):
+        # acceptFromQueue should accept the upload and resume the copy
+        # job.
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        self.assertEqual(PackageUploadStatus.NEW, pu.status)
+        pcj.suspend()
+
+        pu.acceptFromQueue()
+
+        self.assertEqual(PackageUploadStatus.ACCEPTED, pu.status)
+        self.assertEqual(JobStatus.WAITING, pcj.status)
+
+    def test_rejectFromQueue_with_copy_job(self):
+        # rejectFromQueue will reject the upload and fail the copy job.
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        pcj.suspend()
+
+        pu.rejectFromQueue()
+
+        self.assertEqual(PackageUploadStatus.REJECTED, pu.status)
+        self.assertEqual(JobStatus.FAILED, pcj.status)
+
+        # It cannot be resurrected after rejection.
+        self.assertRaises(
+            QueueInconsistentStateError, pu.acceptFromQueue, None)
+
+    def test_package_name_and_version_are_as_in_job(self):
+        # The PackageUpload knows the name and version of the package
+        # being uploaded.  It gets this information from the
+        # PlainPackageCopyJob.
+        upload, job = self.makeUploadWithPackageCopyJob()
+        self.assertEqual(job.package_name, upload.package_name)
+        self.assertEqual(job.package_version, upload.package_version)
+
+    def test_displayarchs_for_copy_job_is_source(self):
+        upload, job = self.makeUploadWithPackageCopyJob()
+        self.assertEqual('source', upload.displayarchs)
+
+    def test_component_and_section_name(self):
+        spn = self.factory.makeSourcePackageName()
+        upload, job = self.makeUploadWithPackageCopyJob(sourcepackagename=spn)
+        component = self.factory.makeComponent()
+        section = self.factory.makeSection()
+        job.addSourceOverride(SourceOverride(
+            source_package_name=spn, component=component, section=section))
+        self.assertEqual(component.name, upload.component_name)
+
+    def test_displayname_is_package_name(self):
+        spn = self.factory.makeSourcePackageName()
+        upload, job = self.makeUploadWithPackageCopyJob(sourcepackagename=spn)
+        self.assertEqual(spn.name, upload.displayname)
+
+    def test_upload_with_copy_job_has_no_source_package_release(self):
+        pu, pcj = self.makeUploadWithPackageCopyJob()
+        self.assertIs(None, pu.sourcepackagerelease)
+
 
 class TestPackageUploadWithPackageCopyJob(TestCaseWithFactory):
 


Follow ups