← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/partial-ancestry-scanner into lp:launchpad/devel

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/partial-ancestry-scanner into lp:launchpad/devel.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  #638637 Stop loading the entire ancestry of a branch during the scan
  https://bugs.launchpad.net/bugs/638637


= Summary =
Fix bug #638637: Stop loading the entire ancestry of a branch during the scan

== Proposed fix ==
Use the set of revisions added to the ancestry, in most cases.

== Pre-implementation notes ==
Pre- and mid- implementation with thumper.

== Implementation details ==
I took the opportunity to refactor how things work a lot.  Removing
whole-ancestry traversal made retrieveBranchDetails trivial, so I replaced it
with Branch.revision_history.

I also replaced BzrSync.getRevisions with BzrSync.revisionsToInsert, which
can use a partial ancestry and last-revno to generate the required info.

I also factored code out, e.g. getHistoryDelta.

Along the way, I added a write_locked Context Manager and added parent_ids to
makeBranchRevision so I could test getAncestryDelta.

_getRevisionGraph is designed to handle the case where not only has the tip
changed, but the old tip isn't present in the new repository.  This requires
generating a Graph using the Revisions associated with the branch.  However,
it's an extremely rare case, so I didn't optimize it.

== Tests ==
bin/test -vv scanner

== Demo and Q/A ==
None.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/codehosting/scanner/tests/test_bzrsync.py
  lib/lp/codehosting/scanner/tests/test_mergedetection.py
  lib/lp/codehosting/scanner/events.py
  lib/lp/codehosting/bzrutils.py
  lib/lp/testing/factory.py
  lib/lp/codehosting/scanner/bzrsync.py
  lib/lp/codehosting/scanner/mergedetection.py

./lib/lp/codehosting/scanner/tests/test_mergedetection.py
     374: E231 missing whitespace after ','
     121: Line exceeds 78 characters.
-- 
https://code.launchpad.net/~abentley/launchpad/partial-ancestry-scanner/+merge/38582
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/partial-ancestry-scanner into lp:launchpad/devel.
=== modified file 'lib/lp/codehosting/bzrutils.py'
--- lib/lp/codehosting/bzrutils.py	2010-09-21 14:39:59 +0000
+++ lib/lp/codehosting/bzrutils.py	2010-10-15 18:41:34 +0000
@@ -162,6 +162,7 @@
 
 def make_oops_logging_exception_hook(error_utility, request):
     """Make a hook for logging OOPSes."""
+
     def log_oops():
         error_utility.raising(sys.exc_info(), request)
     return log_oops
@@ -340,6 +341,7 @@
 
 def makeURLChecker(allowed_scheme):
     """Make a callable that rejects URLs not on the given scheme."""
+
     def checkURL(url):
         """Check that `url` is safe to open."""
         if URI(url).scheme != allowed_scheme:
@@ -374,3 +376,13 @@
         yield
     finally:
         branch.unlock()
+
+
+@contextmanager
+def write_locked(branch):
+    """Provide a context in which the branch is write-locked."""
+    branch.lock_write()
+    try:
+        yield
+    finally:
+        branch.unlock()

=== modified file 'lib/lp/codehosting/scanner/bzrsync.py'
--- lib/lp/codehosting/scanner/bzrsync.py	2010-09-09 02:26:42 +0000
+++ lib/lp/codehosting/scanner/bzrsync.py	2010-10-15 18:41:34 +0000
@@ -16,8 +16,11 @@
 
 import logging
 
+from bzrlib.graph import DictParentsProvider
+from bzrlib.revision import NULL_REVISION
 import pytz
 import transaction
+from storm.locals import Store
 from zope.component import getUtility
 from zope.event import notify
 
@@ -25,6 +28,8 @@
 
 from lp.code.interfaces.branchjob import IRosettaUploadJobSource
 from lp.code.interfaces.revision import IRevisionSet
+from lp.code.model.branchrevision import (BranchRevision)
+from lp.code.model.revision import Revision
 from lp.codehosting import iter_list_chunks
 from lp.codehosting.scanner import events
 from lp.translations.interfaces.translationtemplatesbuildjob import (
@@ -65,7 +70,8 @@
 
         * Revision: there must be one Revision row for each revision in the
           branch ancestry. If the row for a revision that has just been added
-          to the branch is already present, it must be checked for consistency.
+          to the branch is already present, it must be checked for
+          consistency.
 
         * BranchRevision: there must be one BrancheRevision row for each
           revision in the branch ancestry. If history revisions became merged
@@ -78,20 +84,21 @@
         self.logger.info("    from %s", bzr_branch.base)
         # Get the history and ancestry from the branch first, to fail early
         # if something is wrong with the branch.
-        bzr_ancestry, bzr_history = self.retrieveBranchDetails(bzr_branch)
+        self.logger.info("Retrieving history from bzrlib.")
+        bzr_history = bzr_branch.revision_history()
         # The BranchRevision, Revision and RevisionParent tables are only
         # written to by the branch-scanner, so they are not subject to
         # write-lock contention. Update them all in a single transaction to
         # improve the performance and allow garbage collection in the future.
         db_ancestry, db_history = self.retrieveDatabaseAncestry()
 
-        (added_ancestry, branchrevisions_to_delete,
+        (new_ancestry, branchrevisions_to_delete,
             revids_to_insert) = self.planDatabaseChanges(
-            bzr_branch, bzr_ancestry, bzr_history, db_ancestry, db_history)
-        added_ancestry.difference_update(
-            getUtility(IRevisionSet).onlyPresent(added_ancestry))
-        self.logger.info("Adding %s new revisions.", len(added_ancestry))
-        for revids in iter_list_chunks(list(added_ancestry), 1000):
+            bzr_branch, bzr_history, db_ancestry, db_history)
+        new_db_revs = (
+            new_ancestry - getUtility(IRevisionSet).onlyPresent(new_ancestry))
+        self.logger.info("Adding %s new revisions.", len(new_db_revs))
+        for revids in iter_list_chunks(list(new_db_revs), 1000):
             revisions = self.getBazaarRevisions(bzr_branch, revids)
             for revision in revisions:
                 # This would probably go much faster if we found some way to
@@ -122,7 +129,7 @@
         self.updateBranchStatus(bzr_history)
         notify(
             events.ScanCompleted(
-                self.db_branch, bzr_branch, bzr_ancestry, self.logger))
+                self.db_branch, bzr_branch, self.logger, new_ancestry))
         transaction.commit()
 
     def retrieveDatabaseAncestry(self):
@@ -131,28 +138,40 @@
         db_ancestry, db_history = self.db_branch.getScannerData()
         return db_ancestry, db_history
 
-    def retrieveBranchDetails(self, bzr_branch):
-        """Retrieve ancestry from the the bzr branch on disk."""
-        self.logger.info("Retrieving ancestry from bzrlib.")
-        last_revision = bzr_branch.last_revision()
-        # Make bzr_ancestry a set for consistency with db_ancestry.
-        bzr_ancestry_ordered = (
-            bzr_branch.repository.get_ancestry(last_revision))
-        first_ancestor = bzr_ancestry_ordered.pop(0)
-        assert first_ancestor is None, 'history horizons are not supported'
-        bzr_ancestry = set(bzr_ancestry_ordered)
-        bzr_history = bzr_branch.revision_history()
-        return bzr_ancestry, bzr_history
-
-    def planDatabaseChanges(self, bzr_branch, bzr_ancestry, bzr_history,
-                            db_ancestry, db_history):
-        """Plan database changes to synchronize with bzrlib data.
-
-        Use the data retrieved by `retrieveDatabaseAncestry` and
-        `retrieveBranchDetails` to plan the changes to apply to the database.
-        """
-        self.logger.info("Planning changes.")
-        # Find the length of the common history.
+    def _getRevisionGraph(self, bzr_branch, db_last):
+        if bzr_branch.repository.has_revision(db_last):
+            return bzr_branch.repository.get_graph()
+        revisions = Store.of(self.db_branch).find(Revision,
+                BranchRevision.branch_id == self.db_branch.id,
+                Revision.id == BranchRevision.revision_id)
+        parent_map = dict(
+            (r.revision_id, r.parent_ids) for r in revisions)
+        parents_provider = DictParentsProvider(parent_map)
+
+        class PPSource:
+
+            @staticmethod
+            def _make_parents_provider():
+                return parents_provider
+
+        return bzr_branch.repository.get_graph(PPSource)
+
+    def getAncestryDelta(self, bzr_branch):
+        bzr_last = bzr_branch.last_revision()
+        db_last = self.db_branch.last_scanned_id
+        if db_last is None:
+            added_ancestry = set(bzr_branch.repository.get_ancestry(bzr_last))
+            added_ancestry.discard(None)
+            removed_ancestry = set()
+        else:
+            graph = self._getRevisionGraph(bzr_branch, db_last)
+            added_ancestry, removed_ancestry = (
+                graph.find_difference(bzr_last, db_last))
+            added_ancestry.discard(NULL_REVISION)
+        return added_ancestry, removed_ancestry
+
+    def getHistoryDelta(self, bzr_history, db_history):
+        self.logger.info("Calculating history delta.")
         common_len = min(len(bzr_history), len(db_history))
         while common_len > 0:
             # The outer conditional improves efficiency. Without it, the
@@ -165,39 +184,44 @@
                 if db_history[:common_len] == bzr_history[:common_len]:
                     break
             common_len -= 1
-
-        # Revisions added to the branch's ancestry.
-        added_ancestry = bzr_ancestry.difference(db_ancestry)
-
         # Revision added or removed from the branch's history. These lists may
         # include revisions whose history position has merely changed.
         removed_history = db_history[common_len:]
         added_history = bzr_history[common_len:]
+        return added_history, removed_history
+
+    def planDatabaseChanges(self, bzr_branch, bzr_history, db_ancestry,
+                            db_history):
+        """Plan database changes to synchronize with bzrlib data.
+
+        Use the data retrieved by `retrieveDatabaseAncestry` and
+        `retrieveBranchDetails` to plan the changes to apply to the database.
+        """
+        self.logger.info("Planning changes.")
+        # Find the length of the common history.
+        added_history, removed_history = self.getHistoryDelta(
+            bzr_history, db_history)
+        added_ancestry, removed_ancestry = self.getAncestryDelta(bzr_branch)
 
         notify(
             events.RevisionsRemoved(
                 self.db_branch, bzr_branch, removed_history))
 
-        # Merged (non-history) revisions in the database and the bzr branch.
-        old_merged = db_ancestry.difference(db_history)
-        new_merged = bzr_ancestry.difference(bzr_history)
-
-        # Revisions added or removed from the set of merged revisions.
-        removed_merged = old_merged.difference(new_merged)
-        added_merged = new_merged.difference(old_merged)
-
         # We must delete BranchRevision rows for all revisions which where
         # removed from the ancestry or whose sequence value has changed.
-        branchrevisions_to_delete = list(
-            removed_merged.union(removed_history))
+        branchrevisions_to_delete = set(removed_history)
+        branchrevisions_to_delete.update(removed_ancestry)
+        branchrevisions_to_delete.update(
+            set(added_history).difference(added_ancestry))
 
         # We must insert BranchRevision rows for all revisions which were
         # added to the ancestry or whose sequence value has changed.
+        last_revno = len(bzr_history)
         revids_to_insert = dict(
-            self.getRevisions(
-                bzr_history, added_merged.union(added_history)))
+            self.revisionsToInsert(
+                added_history, last_revno, added_ancestry))
 
-        return (added_ancestry, branchrevisions_to_delete,
+        return (added_ancestry, list(branchrevisions_to_delete),
                 revids_to_insert)
 
     def getBazaarRevisions(self, bzr_branch, revisions):
@@ -228,17 +252,20 @@
                 self.db_branch, bzr_branch, db_revision, bzr_revision,
                 revids_to_insert[revision_id]))
 
-    def getRevisions(self, bzr_history, revision_subset):
-        """Iterate over '(revid, revno)' pairs in a branch's ancestry.
+    @staticmethod
+    def revisionsToInsert(added_history, last_revno, added_ancestry):
+        """Calculate the revisions to insert and their revnos.
 
-        Generate a sequence of (revision-id, sequence) pairs to be inserted
-        into the branchrevision table.
+        :param added_history: A list of revision ids added to the revision
+            history in parent-to-child order.
+        :param last_revno: The revno of the last revision.
+        :param added_ancestry: A set of revisions that have been added to the
+            ancestry of the branch.  May overlap with added_history.
         """
-        for (index, revision_id) in enumerate(bzr_history):
-            if revision_id in revision_subset:
-                # sequence numbers start from 1
-                yield revision_id, index + 1
-        for revision_id in revision_subset.difference(set(bzr_history)):
+        start_revno = last_revno - len(added_history) + 1
+        for (revno, revision_id) in enumerate(added_history, start_revno):
+            yield revision_id, revno
+        for revision_id in added_ancestry.difference(added_history):
             yield revision_id, None
 
     def deleteBranchRevisions(self, revision_ids_to_delete):

=== modified file 'lib/lp/codehosting/scanner/events.py'
--- lib/lp/codehosting/scanner/events.py	2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/scanner/events.py	2010-10-15 18:41:34 +0000
@@ -111,6 +111,7 @@
         ScannerEvent.__init__(self, db_branch, bzr_branch)
         self.removed_history = removed_history
 
+
 class IScanCompleted(IObjectEvent):
     """The scan has been completed and the database is up-to-date."""
 
@@ -120,7 +121,7 @@
 
     implements(IScanCompleted)
 
-    def __init__(self, db_branch, bzr_branch, bzr_ancestry, logger):
+    def __init__(self, db_branch, bzr_branch, logger, new_ancestry):
         """Construct a `ScanCompleted` event.
 
         :param db_branch: The database branch.
@@ -131,7 +132,7 @@
             information, such as merges that we find.
         """
         ScannerEvent.__init__(self, db_branch, bzr_branch)
-        self.bzr_ancestry = bzr_ancestry
+        self.new_ancestry = new_ancestry
         # This is kind of ick. In a strict Zope sense, the logger should
         # probably be a registered utility.
         self.logger = logger

=== modified file 'lib/lp/codehosting/scanner/mergedetection.py'
--- lib/lp/codehosting/scanner/mergedetection.py	2010-08-20 20:31:18 +0000
+++ lib/lp/codehosting/scanner/mergedetection.py	2010-10-15 18:41:34 +0000
@@ -78,7 +78,7 @@
     determine which other branches this branch has been merged into.
     """
     db_branch = scan_completed.db_branch
-    bzr_ancestry = scan_completed.bzr_ancestry
+    new_ancestry = scan_completed.new_ancestry
     logger = scan_completed.logger
 
     # XXX: JonathanLange 2009-05-05 spec=package-branches: Yet another thing
@@ -112,7 +112,7 @@
             # If the tip revisions are the same, then it is the same
             # branch, not one merged into the other.
             pass
-        elif last_scanned in bzr_ancestry:
+        elif last_scanned in new_ancestry:
             merge_detected(logger, branch, db_branch)
 
 
@@ -140,7 +140,7 @@
 def auto_merge_proposals(scan_completed):
     """Detect merged proposals."""
     db_branch = scan_completed.db_branch
-    bzr_ancestry = scan_completed.bzr_ancestry
+    new_ancestry = scan_completed.new_ancestry
     logger = scan_completed.logger
 
     # Check landing candidates in non-terminal states to see if their tip
@@ -159,7 +159,7 @@
             scan_completed.bzr_branch.iter_merge_sorted_revisions())
     for proposal in db_branch.landing_candidates:
         tip_rev_id = proposal.source_branch.last_scanned_id
-        if tip_rev_id in bzr_ancestry:
+        if tip_rev_id in new_ancestry:
             merged_revno = find_merged_revno(merge_sorted, tip_rev_id)
             # Remember so we can find the merged revision number.
             merge_detected(

=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
--- lib/lp/codehosting/scanner/tests/test_bzrsync.py	2010-10-04 19:50:45 +0000
+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py	2010-10-15 18:41:34 +0000
@@ -38,9 +38,10 @@
     RevisionAuthor,
     RevisionParent,
     )
+from lp.codehosting.bzrutils import write_locked
 from lp.codehosting.scanner.bzrsync import BzrSync
 from lp.services.osutils import override_environ
-from lp.testing import TestCaseWithFactory
+from lp.testing import TestCaseWithFactory, temp_dir
 from lp.translations.interfaces.translations import (
     TranslationsBranchImportMode,
     )
@@ -391,37 +392,99 @@
         self.assertEqual(rev_1.revision_date, dt)
         self.assertEqual(rev_2.revision_date, dt)
 
-    def test_get_revisions_empty(self):
+    def getAncestryDelta_test(self, clean_repository=False):
+        """"Test various ancestry delta calculations.
+
+        :param clean_repository: If True, perform calculations with a branch
+            whose repository contains only revisions in the ancestry of the
+            tip.
+        """
+        (db_branch, bzr_tree), ignored = self.makeBranchWithMerge(
+            'base', 'trunk', 'branch', 'merge')
+        bzr_branch = bzr_tree.branch
+        self.factory.makeBranchRevision(db_branch, 'base', 0)
+        self.factory.makeBranchRevision(
+            db_branch, 'trunk', 1, parent_ids=['base'])
+        self.factory.makeBranchRevision(
+            db_branch, 'branch', None, parent_ids=['base'])
+        self.factory.makeBranchRevision(
+            db_branch, 'merge', 2, parent_ids=['trunk', 'branch'])
+        sync = self.makeBzrSync(db_branch)
+        self.useContext(write_locked(bzr_branch))
+
+        def get_delta(bzr_rev, db_rev):
+            db_branch.last_scanned_id = db_rev
+            graph = bzr_branch.repository.get_graph()
+            revno = graph.find_distance_to_null(bzr_rev, [])
+            if clean_repository:
+                tempdir = self.useContext(temp_dir())
+                delta_branch = self.createBranchAtURL(tempdir)
+                self.useContext(write_locked(delta_branch))
+                delta_branch.pull(bzr_branch, stop_revision=bzr_rev)
+            else:
+                bzr_branch.set_last_revision_info(revno, bzr_rev)
+                delta_branch = bzr_branch
+            return sync.getAncestryDelta(delta_branch)
+
+        added_ancestry, removed_ancestry = get_delta('merge', None)
+        # All revisions are new for an unscanned branch
+        self.assertEqual(
+            set(['base', 'trunk', 'branch', 'merge']), added_ancestry)
+        self.assertEqual(set(), removed_ancestry)
+        added_ancestry, removed_ancestry = get_delta('merge', 'base')
+        self.assertEqual(
+            set(['trunk', 'branch', 'merge']), added_ancestry)
+        self.assertEqual(set(), removed_ancestry)
+        added_ancestry, removed_ancestry = get_delta(NULL_REVISION, 'merge')
+        self.assertEqual(
+            set(), added_ancestry)
+        self.assertEqual(
+            set(['base', 'trunk', 'branch', 'merge']), removed_ancestry)
+        added_ancestry, removed_ancestry = get_delta('base', 'merge')
+        self.assertEqual(
+            set(), added_ancestry)
+        self.assertEqual(
+            set(['trunk', 'branch', 'merge']), removed_ancestry)
+        added_ancestry, removed_ancestry = get_delta('trunk', 'branch')
+        self.assertEqual(set(['trunk']), added_ancestry)
+        self.assertEqual(set(['branch']), removed_ancestry)
+
+    def test_getAncestryDelta(self):
+        """"Test ancestry delta calculations with a dirty repository."""
+        return self.getAncestryDelta_test()
+
+    def test_getAncestryDelta_clean_repository(self):
+        """"Test ancestry delta calculations with a clean repository."""
+        return self.getAncestryDelta_test(clean_repository=True)
+
+    def test_revisionsToInsert_empty(self):
         # An empty branch should have no revisions.
-        bzrsync = self.makeBzrSync(self.db_branch)
-        bzr_ancestry, bzr_history = (
-            bzrsync.retrieveBranchDetails(self.bzr_branch))
         self.assertEqual(
-            [], list(bzrsync.getRevisions(bzr_history, bzr_ancestry)))
+            [], list(BzrSync.revisionsToInsert([], 0, set())))
 
-    def test_get_revisions_linear(self):
-        # If the branch has a linear ancestry, getRevisions() should yield
-        # each revision along with a sequence number, starting at 1.
+    def test_revisionsToInsert_linear(self):
+        # If the branch has a linear ancestry, revisionsToInsert() should
+        # yield each revision along with a sequence number, starting at 1.
         self.commitRevision(rev_id='rev-1')
         bzrsync = self.makeBzrSync(self.db_branch)
-        bzr_ancestry, bzr_history = (
-            bzrsync.retrieveBranchDetails(self.bzr_branch))
-        self.assertEqual(
-            [('rev-1', 1)],
-            list(bzrsync.getRevisions(bzr_history, bzr_ancestry)))
+        bzr_history = self.bzr_branch.revision_history()
+        added_ancestry = bzrsync.getAncestryDelta(self.bzr_branch)[0]
+        result = bzrsync.revisionsToInsert(
+            bzr_history, self.bzr_branch.revno(), added_ancestry)
+        self.assertEqual({'rev-1': 1}, dict(result))
 
-    def test_get_revisions_branched(self):
+    def test_revisionsToInsert_branched(self):
         # Confirm that these revisions are generated by getRevisions with None
         # as the sequence 'number'.
         (db_branch, bzr_tree), ignored = self.makeBranchWithMerge(
             'base', 'trunk', 'branch', 'merge')
         bzrsync = self.makeBzrSync(db_branch)
-        bzr_ancestry, bzr_history = (
-            bzrsync.retrieveBranchDetails(bzr_tree.branch))
-        expected = set(
-            [('base', 1), ('trunk', 2), ('merge', 3), ('branch', None)])
+        bzr_history = bzr_tree.branch.revision_history()
+        added_ancestry = bzrsync.getAncestryDelta(bzr_tree.branch)[0]
+        expected = {'base': 1, 'trunk': 2, 'merge': 3, 'branch': None}
         self.assertEqual(
-            expected, set(bzrsync.getRevisions(bzr_history, bzr_ancestry)))
+            expected, dict(bzrsync.revisionsToInsert(bzr_history,
+                bzr_tree.branch.revno(), added_ancestry)))
 
     def test_sync_with_merged_branches(self):
         # Confirm that when we syncHistory, all of the revisions are included
@@ -460,19 +523,6 @@
         expected = set([(1, 'base'), (2, 'branch')])
         self.assertEqual(self.getBranchRevisions(db_trunk), expected)
 
-    def test_retrieveBranchDetails(self):
-        # retrieveBranchDetails should set last_revision, bzr_ancestry and
-        # bzr_history on the BzrSync instance to match the information in the
-        # Bazaar branch.
-        (db_trunk, trunk_tree), ignored = self.makeBranchWithMerge(
-            'base', 'trunk', 'branch', 'merge')
-        bzrsync = self.makeBzrSync(db_trunk)
-        bzr_ancestry, bzr_history = (
-            bzrsync.retrieveBranchDetails(trunk_tree.branch))
-        expected_ancestry = set(['base', 'trunk', 'branch', 'merge'])
-        self.assertEqual(expected_ancestry, bzr_ancestry)
-        self.assertEqual(['base', 'trunk', 'merge'], bzr_history)
-
     def test_retrieveDatabaseAncestry(self):
         # retrieveDatabaseAncestry should set db_ancestry and db_history to
         # Launchpad's current understanding of the branch state.

=== modified file 'lib/lp/codehosting/scanner/tests/test_mergedetection.py'
--- lib/lp/codehosting/scanner/tests/test_mergedetection.py	2010-10-04 19:50:45 +0000
+++ lib/lp/codehosting/scanner/tests/test_mergedetection.py	2010-10-15 18:41:34 +0000
@@ -197,11 +197,11 @@
         mergedetection.merge_detected = self._original_merge_detected
         TestCaseWithFactory.tearDown(self)
 
-    def autoMergeBranches(self, db_branch, bzr_ancestry):
+    def autoMergeBranches(self, db_branch, new_ancestry):
         mergedetection.auto_merge_branches(
             events.ScanCompleted(
                 db_branch=db_branch, bzr_branch=None,
-                bzr_ancestry=bzr_ancestry, logger=None))
+                logger=None, new_ancestry=new_ancestry))
 
     def mergeDetected(self, logger, source, target):
         # Record the merged branches
@@ -360,7 +360,7 @@
         target = self.factory.makeBranchTargetBranch(source.target)
         target.product.development_focus.branch = target
         logger = logging.getLogger('test')
-        notify(events.ScanCompleted(target, None, ['23foo'], logger))
+        notify(events.ScanCompleted(target, None, logger, ['23foo']))
         self.assertEqual(
             BranchLifecycleStatus.MERGED, source.lifecycle_status)
 

=== modified file 'lib/lp/testing/factory.py'
--- lib/lp/testing/factory.py	2010-10-14 12:57:25 +0000
+++ lib/lp/testing/factory.py	2010-10-15 18:41:34 +0000
@@ -1310,8 +1310,10 @@
             '', parent.revision_id, None, None, None)
         branch.updateScannedDetails(parent, sequence)
 
-    def makeBranchRevision(self, branch, revision_id, sequence=None):
-        revision = self.makeRevision(rev_id=revision_id)
+    def makeBranchRevision(self, branch, revision_id, sequence=None,
+                           parent_ids=None):
+        revision = self.makeRevision(
+            rev_id=revision_id, parent_ids=parent_ids)
         return branch.createBranchRevision(sequence, revision)
 
     def makeBug(self, product=None, owner=None, bug_watch_url=None,
@@ -1844,22 +1846,6 @@
             expires=expires, restricted=restricted)
         return library_file_alias
 
-    def makePackageDiff(self, from_spr=None, to_spr=None):
-        """Make a completed package diff."""
-        if from_spr is None:
-            from_spr = self.makeSourcePackageRelease()
-        if to_spr is None:
-            to_spr = self.makeSourcePackageRelease()
-
-        diff = from_spr.requestDiffTo(
-            from_spr.creator, to_spr)
-
-        naked_diff = removeSecurityProxy(diff)
-        naked_diff.status = PackageDiffStatus.COMPLETED
-        naked_diff.diff_content = self.makeLibraryFileAlias()
-        naked_diff.date_fulfilled = UTC_NOW
-        return diff
-
     def makeDistribution(self, name=None, displayname=None, owner=None,
                          members=None, title=None, aliases=None):
         """Make a new distribution."""