← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~allenap/launchpad/do-copy-async-bug-766247 into lp:launchpad

 

Gavin Panella has proposed merging lp:~allenap/launchpad/do-copy-async-bug-766247 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #766247 in Launchpad itself: "do_copy() cannot run be asynchronously. Sort it out."
  https://bugs.launchpad.net/launchpad/+bug/766247

For more details, see:
https://code.launchpad.net/~allenap/launchpad/do-copy-async-bug-766247/+merge/58714

This branch takes the unused SyncPackageJob, renames it to
PackageCopyJob, and changes it to use do_copy() instead of going via
Archive.syncSource(). Apart from anything else, this means it can copy
multiple packages at a time.

It also removes the existing cronscript and instead changes it to run
as part of the job source group system, in the MAIN group. This is
already scheduled to run once every five minutes on loganberry, so no
LOSA intervention is required to make this work. If we want this to
run more frequently we should create a new group, FREQUENT perhaps,
and add it to the loganberry crontab.

-- 
https://code.launchpad.net/~allenap/launchpad/do-copy-async-bug-766247/+merge/58714
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~allenap/launchpad/do-copy-async-bug-766247 into lp:launchpad.
=== modified file 'configs/development/launchpad-lazr.conf'
--- configs/development/launchpad-lazr.conf	2011-04-21 01:13:19 +0000
+++ configs/development/launchpad-lazr.conf	2011-04-21 16:42:31 +0000
@@ -137,8 +137,8 @@
 oops_prefix: IDSJ
 error_dir: /var/tmp/soyuz.test
 
-[sync_packages]
-oops_prefix: SPJ
+[IPackageCopyJobSource]
+oops_prefix: PCJ
 error_dir: /var/tmp/soyuz.test
 
 [launchpad]

=== removed file 'cronscripts/sync-packages.py'
--- cronscripts/sync-packages.py	2010-11-12 10:52:15 +0000
+++ cronscripts/sync-packages.py	1970-01-01 00:00:00 +0000
@@ -1,25 +0,0 @@
-#!/usr/bin/python -S
-#
-# Copyright 2010 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-"""Synchronize packages."""
-
-__metaclass__ = type
-
-import _pythonpath
-
-from lp.services.job.runner import JobCronScript
-from lp.soyuz.interfaces.distributionjob import ISyncPackageJobSource
-
-
-class RunSyncPackageJob(JobCronScript):
-    """Run SyncPackageJob jobs."""
-
-    config_name = 'sync_packages'
-    source_interface = ISyncPackageJobSource
-
-
-if __name__ == '__main__':
-    script = RunSyncPackageJob()
-    script.lock_and_run()

=== modified file 'lib/canonical/config/schema-lazr.conf'
--- lib/canonical/config/schema-lazr.conf	2011-04-21 01:30:30 +0000
+++ lib/canonical/config/schema-lazr.conf	2011-04-21 16:42:31 +0000
@@ -977,8 +977,12 @@
 # See [error_reports].
 copy_to_zlog: false
 
-[sync_packages]
+[IPackageCopyJobSource]
+module: lp.soyuz.interfaces.distributionjob
+# XXX: GavinPanella 2011-04-20 bug=??????: The sync_packages database
+# user should be renamed to copy_packages.
 dbuser: sync_packages
+crontab_group: MAIN
 
 # See [error_reports].
 error_dir: none
@@ -2110,7 +2114,10 @@
 # Each job source class also needs its own config section to specify the
 # dbuser, the crontab_group, and the module that the job source class
 # can be loaded from.
-job_sources: IMembershipNotificationJobSource, IPersonMergeJobSource
+job_sources:
+    IMembershipNotificationJobSource,
+    IPackageCopyJobSource,
+    IPersonMergeJobSource,
 
 [IMembershipNotificationJobSource]
 # This section is used by cronscripts/process-job-source.py.

=== modified file 'lib/lp/soyuz/configure.zcml'
--- lib/lp/soyuz/configure.zcml	2011-04-11 14:34:55 +0000
+++ lib/lp/soyuz/configure.zcml	2011-04-21 16:42:31 +0000
@@ -908,14 +908,14 @@
         <allow interface="lp.soyuz.interfaces.distributionjob.IDistributionJob" />
     </class>
 
-    <!-- SyncPackageJobSource -->
+    <!-- PackageCopyJobSource -->
     <securedutility
-      component="lp.soyuz.model.syncpackagejob.SyncPackageJob"
-      provides="lp.soyuz.interfaces.distributionjob.ISyncPackageJobSource">
-        <allow interface="lp.soyuz.interfaces.distributionjob.ISyncPackageJobSource"/>
+      component="lp.soyuz.model.packagecopyjob.PackageCopyJob"
+      provides="lp.soyuz.interfaces.distributionjob.IPackageCopyJobSource">
+        <allow interface="lp.soyuz.interfaces.distributionjob.IPackageCopyJobSource"/>
     </securedutility>
-    <class class="lp.soyuz.model.syncpackagejob.SyncPackageJob">
-        <allow interface="lp.soyuz.interfaces.distributionjob.ISyncPackageJob" />
+    <class class="lp.soyuz.model.packagecopyjob.PackageCopyJob">
+        <allow interface="lp.soyuz.interfaces.distributionjob.IPackageCopyJob" />
         <allow interface="lp.soyuz.interfaces.distributionjob.IDistributionJob" />
     </class>
 

=== modified file 'lib/lp/soyuz/interfaces/distributionjob.py'
--- lib/lp/soyuz/interfaces/distributionjob.py	2011-04-15 15:08:20 +0000
+++ lib/lp/soyuz/interfaces/distributionjob.py	2011-04-21 16:42:31 +0000
@@ -10,14 +10,15 @@
     "IDistroSeriesDifferenceJobSource",
     "IInitialiseDistroSeriesJob",
     "IInitialiseDistroSeriesJobSource",
-    "ISyncPackageJob",
-    "ISyncPackageJobSource",
+    "IPackageCopyJob",
+    "IPackageCopyJobSource",
 ]
 
 from lazr.enum import (
     DBEnumeratedType,
     DBItem,
     )
+from lazr.restful.fields import Reference
 from zope.interface import (
     Attribute,
     Interface,
@@ -25,8 +26,10 @@
 from zope.schema import (
     Bool,
     Int,
+    List,
     Object,
     TextLine,
+    Tuple,
     )
 
 from canonical.launchpad import _
@@ -37,6 +40,7 @@
     IJobSource,
     IRunnableJob,
     )
+from lp.soyuz.interfaces.archive import IArchive
 
 
 class IDistributionJob(Interface):
@@ -72,8 +76,8 @@
         populating the archive from the parent distroseries.
         """)
 
-    SYNC_PACKAGE = DBItem(2, """
-        Synchronize a single package from another distribution.
+    COPY_PACKAGE = DBItem(2, """
+        Copy a single package from another distribution.
 
         This job copies a single package, optionally including binaries.
         """)
@@ -97,12 +101,26 @@
         """
 
 
-class ISyncPackageJobSource(IJobSource):
-    """An interface for acquiring IISyncPackageJobs."""
-
-    def create(source_archive, target_archive, distroseries, pocket,
-        source_package_name, version, include_binaries):
-        """Create a new sync package job."""
+class IPackageCopyJobSource(IJobSource):
+    """An interface for acquiring IIPackageCopyJobs."""
+
+    def create(cls, source_archive, source_packages,
+               target_archive, target_distroseries, target_pocket,
+               include_binaries=False):
+        """Create a new sync package job.
+
+        :param source_archive: The `IArchive` in which `source_packages` are
+            found.
+        :param source_packages: This is an iterable of `(source_package_name,
+            version)` tuples, where both `source_package_name` and `version`
+            are strings.
+        :param target_archive: The `IArchive` to which to copy the packages.
+        :param target_distroseries: The `IDistroSeries` to which to copy the
+            packages.
+        :param target_pocket: The pocket into which to copy the packages. Must
+            be a member of `PackagePublishingPocket`.
+        :param include_binaries: See `do_copy`.
+        """
 
     def getActiveJobs(archive):
         """Retrieve all active sync jobs for an archive."""
@@ -112,33 +130,46 @@
     """A Job that performs actions on a distribution."""
 
 
-class ISyncPackageJob(IRunnableJob):
+class IPackageCopyJob(IRunnableJob):
     """A Job that synchronizes packages."""
 
-    pocket = Int(
-            title=_('Target package publishing pocket'), required=True,
-            readonly=True,
-            )
-
-    source_archive = Int(
-            title=_('Source Archive ID'), required=True, readonly=True,
-            )
-
-    target_archive = Int(
-            title=_('Target Archive ID'), required=True, readonly=True,
-            )
-
-    source_package_name = TextLine(
-            title=_("Source Package Name"),
-            required=True, readonly=True)
-
-    source_package_version = TextLine(
-            title=_("Source Package Version"),
-            required=True, readonly=True)
+    source_packages = List(
+        title=_("Source Packages"),
+        value_type=Tuple(min_length=3, max_length=3),
+        required=True, readonly=True,
+        )
+
+    source_archive_id = Int(
+        title=_('Source Archive ID'), required=True, readonly=True,
+        )
+
+    source_archive = Reference(
+        schema=IArchive, title=_('Source Archive'),
+        required=True, readonly=True,
+        )
+
+    target_archive_id = Int(
+        title=_('Target Archive ID'), required=True, readonly=True,
+        )
+
+    target_archive = Reference(
+        schema=IArchive, title=_('Target Archive'),
+        required=True, readonly=True,
+        )
+
+    target_distroseries = Reference(
+        schema=IDistroSeries, title=_('Target DistroSeries.'),
+        required=True, readonly=True)
+
+    target_pocket = Int(
+        title=_('Target package publishing pocket'), required=True,
+        readonly=True,
+        )
 
     include_binaries = Bool(
-            title=_("Copy binaries"),
-            required=False, readonly=True)
+        title=_("Copy binaries"),
+        required=False, readonly=True,
+        )
 
 
 class IDistroSeriesDifferenceJob(IRunnableJob):

=== renamed file 'lib/lp/soyuz/model/syncpackagejob.py' => 'lib/lp/soyuz/model/packagecopyjob.py'
--- lib/lp/soyuz/model/syncpackagejob.py	2010-11-15 16:25:05 +0000
+++ lib/lp/soyuz/model/packagecopyjob.py	2011-04-21 16:42:31 +0000
@@ -4,7 +4,7 @@
 __metaclass__ = type
 
 __all__ = [
-    "SyncPackageJob",
+    "PackageCopyJob",
 ]
 
 from zope.component import getUtility
@@ -18,50 +18,53 @@
     IStore,
     )
 from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.soyuz.interfaces.archive import IArchiveSet
+from lp.soyuz.interfaces.archive import (
+    CannotCopy,
+    IArchiveSet,
+    )
 from lp.soyuz.interfaces.distributionjob import (
     DistributionJobType,
-    ISyncPackageJob,
-    ISyncPackageJobSource,
+    IPackageCopyJob,
+    IPackageCopyJobSource,
     )
 from lp.soyuz.model.distributionjob import (
     DistributionJob,
     DistributionJobDerived,
     )
-
-
-class SyncPackageJob(DistributionJobDerived):
+from lp.soyuz.scripts.packagecopier import do_copy
+
+
+class PackageCopyJob(DistributionJobDerived):
     """Job that copies a package between archives."""
 
-    implements(ISyncPackageJob)
+    implements(IPackageCopyJob)
 
-    class_job_type = DistributionJobType.SYNC_PACKAGE
-    classProvides(ISyncPackageJobSource)
+    class_job_type = DistributionJobType.COPY_PACKAGE
+    classProvides(IPackageCopyJobSource)
 
     @classmethod
-    def create(cls, source_archive, target_archive, distroseries,
-        pocket, source_package_name, source_package_version,
-        include_binaries):
-        """See `ISyncPackageJobSource`."""
+    def create(cls, source_packages, source_archive,
+               target_archive, target_distroseries, target_pocket,
+               include_binaries=False):
+        """See `IPackageCopyJobSource`."""
         metadata = {
+            'source_packages': source_packages,
             'source_archive_id': source_archive.id,
             'target_archive_id': target_archive.id,
-            'pocket': pocket.value,
-            'source_package_name': source_package_name,
-            'source_package_version': source_package_version,
+            'target_pocket': target_pocket.value,
             'include_binaries': include_binaries,
             }
         job = DistributionJob(
-            distroseries.distribution, distroseries, cls.class_job_type,
-            metadata)
+            target_distroseries.distribution, target_distroseries,
+            cls.class_job_type, metadata)
         IMasterStore(DistributionJob).add(job)
         return cls(job)
 
     @classmethod
     def getActiveJobs(cls, archive):
-        """See `ISyncPackageJobSource`."""
+        """See `IPackageCopyJobSource`."""
         # TODO: JRV 20101104. This iterates manually over all active
-        # SyncPackageJobs. This should usually be a short enough list,
+        # PackageCopyJobs. This should usually be a short enough list,
         # but if it really becomes an issue target_archive should
         # be moved into a separate database field.
         jobs = IStore(DistributionJob).find(
@@ -69,36 +72,59 @@
             DistributionJob.job_type == cls.class_job_type,
             DistributionJob.distribution == archive.distribution)
         jobs = [cls(job) for job in jobs]
-        return (job for job in jobs if job.target_archive == archive)
+        return (job for job in jobs if job.target_archive_id == archive.id)
+
+    @property
+    def source_packages(self):
+        getPublishedSources = self.source_archive.getPublishedSources
+        for name, version in self.metadata['source_packages']:
+            yield name, version, getPublishedSources(
+                name=name, version=version, exact_match=True).first()
+
+    @property
+    def source_archive_id(self):
+        return self.metadata['source_archive_id']
 
     @property
     def source_archive(self):
-        return getUtility(IArchiveSet).get(self.metadata['source_archive_id'])
+        return getUtility(IArchiveSet).get(self.source_archive_id)
+
+    @property
+    def target_archive_id(self):
+        return self.metadata['target_archive_id']
 
     @property
     def target_archive(self):
-        return getUtility(IArchiveSet).get(self.metadata['target_archive_id'])
-
-    @property
-    def pocket(self):
-        return PackagePublishingPocket.items[self.metadata['pocket']]
+        return getUtility(IArchiveSet).get(self.target_archive_id)
+
+    @property
+    def target_distroseries(self):
+        return self.distroseries
+
+    @property
+    def target_pocket(self):
+        return PackagePublishingPocket.items[self.metadata['target_pocket']]
 
     @property
     def include_binaries(self):
         return self.metadata['include_binaries']
 
-    @property
-    def source_package_name(self):
-        return self.metadata['source_package_name']
-
-    @property
-    def source_package_version(self):
-        return self.metadata['source_package_version']
-
     def run(self):
         """See `IRunnableJob`."""
-        self.target_archive.syncSource(
-            self.source_package_name, self.source_package_version,
-            self.source_archive, to_pocket=str(self.pocket),
-            to_series=self.distroseries.name,
+        if self.target_archive.is_ppa:
+            if self.target_pocket != PackagePublishingPocket.RELEASE:
+                raise CannotCopy(
+                    "Destination pocket must be 'release' for a PPA.")
+
+        source_packages = set()
+        for name, version, source_package in self.source_packages:
+            if source_package is None:
+                raise CannotCopy(
+                    "Package %r %r not found." % (name, version))
+            else:
+                source_packages.add(source_package)
+
+        do_copy(
+            sources=source_packages, archive=self.target_archive,
+            series=self.target_distroseries, pocket=self.target_pocket,
             include_binaries=self.include_binaries)

=== renamed file 'lib/lp/soyuz/tests/test_syncpackagejob.py' => 'lib/lp/soyuz/tests/test_packagecopyjob.py'
--- lib/lp/soyuz/tests/test_syncpackagejob.py	2011-03-03 00:43:44 +0000
+++ lib/lp/soyuz/tests/test_packagecopyjob.py	2011-04-21 16:42:31 +0000
@@ -3,48 +3,54 @@
 
 """Tests for sync package jobs."""
 
-import os
-import subprocess
-import sys
-
+from testtools.content import text_content
 import transaction
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
-from canonical.config import config
 from canonical.testing import LaunchpadZopelessLayer
-from lp.registry.errors import NoSuchSourcePackageName
 from lp.registry.interfaces.pocket import PackagePublishingPocket
+from lp.soyuz.interfaces.archive import CannotCopy
 from lp.soyuz.interfaces.distributionjob import (
-    ISyncPackageJob,
-    ISyncPackageJobSource,
+    IPackageCopyJob,
+    IPackageCopyJobSource,
     )
 from lp.soyuz.interfaces.publishing import PackagePublishingStatus
 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
-from lp.testing import TestCaseWithFactory
-
-
-class SyncPackageJobTests(TestCaseWithFactory):
-    """Test case for SyncPackageJob."""
+from lp.testing import (
+    run_script,
+    TestCaseWithFactory,
+    )
+
+
+class PackageCopyJobTests(TestCaseWithFactory):
+    """Test case for PackageCopyJob."""
 
     layer = LaunchpadZopelessLayer
 
     def test_create(self):
-        # A SyncPackageJob can be created and stores its arguments.
+        # A PackageCopyJob can be created and stores its arguments.
         distroseries = self.factory.makeDistroSeries()
         archive1 = self.factory.makeArchive(distroseries.distribution)
         archive2 = self.factory.makeArchive(distroseries.distribution)
-        source = getUtility(ISyncPackageJobSource)
-        job = source.create(archive1, archive2, distroseries,
-                PackagePublishingPocket.RELEASE,
-                "foo", "1.0-1", include_binaries=False)
-        self.assertProvides(job, ISyncPackageJob)
+        source = getUtility(IPackageCopyJobSource)
+        job = source.create(
+            source_packages=[("foo", "1.0-1"), ("bar", "2.4")],
+            source_archive=archive1, target_archive=archive2,
+            target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.RELEASE,
+            include_binaries=False)
+        self.assertProvides(job, IPackageCopyJob)
         self.assertEquals(distroseries, job.distroseries)
+        self.assertEquals(archive1.id, job.source_archive_id)
         self.assertEquals(archive1, job.source_archive)
+        self.assertEquals(archive2.id, job.target_archive_id)
         self.assertEquals(archive2, job.target_archive)
-        self.assertEquals(PackagePublishingPocket.RELEASE, job.pocket)
-        self.assertEquals("foo", job.source_package_name)
-        self.assertEquals("1.0-1", job.source_package_version)
+        self.assertEquals(distroseries, job.target_distroseries)
+        self.assertEquals(PackagePublishingPocket.RELEASE, job.target_pocket)
+        self.assertContentEqual(
+            job.source_packages,
+            [("foo", "1.0-1", None), ("bar", "2.4", None)])
         self.assertEquals(False, job.include_binaries)
 
     def test_getActiveJobs(self):
@@ -52,32 +58,39 @@
         distroseries = self.factory.makeDistroSeries()
         archive1 = self.factory.makeArchive(distroseries.distribution)
         archive2 = self.factory.makeArchive(distroseries.distribution)
-        source = getUtility(ISyncPackageJobSource)
-        job = source.create(archive1, archive2, distroseries,
-                PackagePublishingPocket.RELEASE,
-                "foo", "1.0-1", include_binaries=False)
+        source = getUtility(IPackageCopyJobSource)
+        job = source.create(
+            source_packages=[("foo", "1.0-1")], source_archive=archive1,
+            target_archive=archive2, target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.RELEASE,
+            include_binaries=False)
         self.assertContentEqual([job], source.getActiveJobs(archive2))
 
-    def test_cronscript(self):
-        # The cron script runs without problems.
-        script = os.path.join(
-            config.root, 'cronscripts', 'sync-packages.py')
-        args = [sys.executable, script, '-v']
-        process = subprocess.Popen(
-            args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
-        stdout, stderr = process.communicate()
-        self.assertEqual(process.returncode, 0)
-
     def test_run_unknown_package(self):
         # A job properly records failure.
         distroseries = self.factory.makeDistroSeries()
         archive1 = self.factory.makeArchive(distroseries.distribution)
         archive2 = self.factory.makeArchive(distroseries.distribution)
-        source = getUtility(ISyncPackageJobSource)
-        job = source.create(archive1, archive2, distroseries,
-                PackagePublishingPocket.RELEASE,
-                "foo", "1.0-1", include_binaries=False)
-        self.assertRaises(NoSuchSourcePackageName, job.run)
+        source = getUtility(IPackageCopyJobSource)
+        job = source.create(
+            source_packages=[("foo", "1.0-1")], source_archive=archive1,
+            target_archive=archive2, target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.RELEASE,
+            include_binaries=False)
+        self.assertRaises(CannotCopy, job.run)
+
+    def test_target_ppa_non_release_pocket(self):
+        # When copyingto a PPA archive the target must be the release pocket.
+        distroseries = self.factory.makeDistroSeries()
+        archive1 = self.factory.makeArchive(distroseries.distribution)
+        archive2 = self.factory.makeArchive(distroseries.distribution)
+        source = getUtility(IPackageCopyJobSource)
+        job = source.create(
+            source_packages=[], source_archive=archive1,
+            target_archive=archive2, target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.UPDATES,
+            include_binaries=False)
+        self.assertRaises(CannotCopy, job.run)
 
     def test_run(self):
         # A proper test run synchronizes packages.
@@ -88,19 +101,25 @@
         archive1 = self.factory.makeArchive(distroseries.distribution)
         archive2 = self.factory.makeArchive(distroseries.distribution)
 
-        publisher.getPubBinaries(
-            distroseries=distroseries, binaryname="libc",
-            version="2.8-1",
-            status=PackagePublishingStatus.PUBLISHED,
+        source_package = publisher.getPubSource(
+            distroseries=distroseries, sourcename="libc",
+            version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
             archive=archive1)
 
-        source = getUtility(ISyncPackageJobSource)
-        job = source.create(archive1, archive2, distroseries,
-                PackagePublishingPocket.RELEASE,
-                "libc", "2.8-1", include_binaries=False)
+        source = getUtility(IPackageCopyJobSource)
+        job = source.create(
+            source_packages=[("libc", "2.8-1")], source_archive=archive1,
+            target_archive=archive2, target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.RELEASE,
+            include_binaries=False)
+        self.assertContentEqual(
+            job.source_packages, [("libc", "2.8-1", source_package)])
+
         # Make sure everything hits the database, switching db users
         # aborts.
         transaction.commit()
+        # XXX: GavinPanella 2011-04-20 bug=??????: The sync_packages database
+        # user should be renamed to copy_packages.
         self.layer.switchDbUser('sync_packages')
         job.run()
 
@@ -113,10 +132,12 @@
         distroseries = self.factory.makeDistroSeries()
         archive1 = self.factory.makeArchive(distroseries.distribution)
         archive2 = self.factory.makeArchive(distroseries.distribution)
-        source = getUtility(ISyncPackageJobSource)
-        job = source.create(archive1, archive2, distroseries,
-                PackagePublishingPocket.RELEASE,
-                "foo", "1.0-1", include_binaries=False)
+        source = getUtility(IPackageCopyJobSource)
+        job = source.create(
+            source_packages=[("foo", "1.0-1")], source_archive=archive1,
+            target_archive=archive2, target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.RELEASE,
+            include_binaries=False)
         oops_vars = job.getOopsVars()
         naked_job = removeSecurityProxy(job)
         self.assertIn(
@@ -124,3 +145,32 @@
         self.assertIn(('distroseries_id', distroseries.id), oops_vars)
         self.assertIn(
             ('distribution_job_id', naked_job.context.id), oops_vars)
+
+    def test_smoke(self):
+        publisher = SoyuzTestPublisher()
+        publisher.prepareBreezyAutotest()
+        distroseries = publisher.breezy_autotest
+        archive1 = self.factory.makeArchive(distroseries.distribution)
+        archive2 = self.factory.makeArchive(distroseries.distribution)
+        publisher.getPubSource(
+            distroseries=distroseries, sourcename="libc",
+            version="2.8-1", status=PackagePublishingStatus.PUBLISHED,
+            archive=archive1)
+        getUtility(IPackageCopyJobSource).create(
+            source_packages=[("libc", "2.8-1")], source_archive=archive1,
+            target_archive=archive2, target_distroseries=distroseries,
+            target_pocket=PackagePublishingPocket.RELEASE,
+            include_binaries=False)
+        transaction.commit()
+
+        out, err, exit_code = run_script(
+            "LP_DEBUG_SQL=1 cronscripts/process-job-source.py -vv %s" % (
+                IPackageCopyJobSource.getName()))
+
+        self.addDetail("stdout", text_content(out))
+        self.addDetail("stderr", text_content(err))
+
+        self.assertEqual(0, exit_code)
+        copied_source_package = archive2.getPublishedSources(
+            name="libc", version="2.8-1", exact_match=True).first()
+        self.assertIsNot(copied_source_package, None)