← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~abentley/launchpad/incremental-diff-job into lp:launchpad

 

Aaron Bentley has proposed merging lp:~abentley/launchpad/incremental-diff-job into lp:launchpad with lp:~abentley/launchpad/display-incremental as a prerequisite.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)


= Summary =
Implement jobs for generating incremental diffs for merge proposals, and create
these jobs when the merge proposal source banch tip changes.

== Proposed fix ==
See above

== Pre-implementation notes ==
Discussed with Thumper

== Implementation details ==
As a driveby, I extracted commit_file and create_example_merge so that they
could be reused more easily, without necessarily deriving from DiffTestCase.

I also added a job_type parameter to BranchMergeProposalJobSource.iterReady, to
make testing easier.

There were also a few lint fixes, like removing a semicolon.

== Tests ==
bin/test -t TestGenerateIncrementalDiffJob -t test_script_runs

== Demo and Q/A ==
Create a merge proposal.  After the initial diff has been generated, push new
changes to the source branch.  As well as updating the diff, this should show a
diff from the old branch tip to the new branch tip.


= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/codehosting/scanner/tests/test_bzrsync.py
  lib/lp/code/browser/configure.zcml
  lib/lp/code/configure.zcml
  lib/lp/code/model/tests/test_branchmergeproposaljobs.py
  lib/lp/code/interfaces/branchmergeproposal.py
  lib/lp/code/model/branchmergeproposal.py
  lib/lp/code/model/tests/test_branchmergeproposal.py
  lib/lp/code/browser/tests/test_branchmergeproposal.py
  lib/lp/code/scripts/tests/test_merge_proposal_jobs.py
  lib/lp/code/model/diff.py
  lib/lp/testing/factory.py
  lib/lp/code/model/tests/test_diff.py
  lib/lp/code/templates/incrementaldiffcomment-body.pt
  lib/lp/code/model/branch.py
  lib/lp/code/model/branchmergeproposaljob.py
  lib/lp/code/browser/branchmergeproposal.py
  utilities/sourcedeps.conf
  lib/lp/code/templates/incrementaldiffcomment-header.pt
  lib/lp/code/interfaces/diff.py

./lib/lp/codehosting/scanner/tests/test_bzrsync.py
     618: E202 whitespace before ')'
     635: E231 missing whitespace after ','
./lib/lp/code/model/tests/test_branchmergeproposal.py
     192: E231 missing whitespace after ','
     194: E231 missing whitespace after ','
./lib/lp/code/scripts/tests/test_merge_proposal_jobs.py
      15: E202 whitespace before ')'
./lib/lp/code/model/diff.py
     166: E301 expected 1 blank line, found 0
./lib/lp/code/model/branchmergeproposaljob.py
       5: E303 too many blank lines (3)
     292: E202 whitespace before ')'
./lib/lp/code/browser/branchmergeproposal.py
     168: E301 expected 1 blank line, found 0
     178: E301 expected 1 blank line, found 0
     201: E202 whitespace before '}'
    1471: E301 expected 1 blank line, found 0
-- 
https://code.launchpad.net/~abentley/launchpad/incremental-diff-job/+merge/36881
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~abentley/launchpad/incremental-diff-job into lp:launchpad.
=== modified file 'lib/lp/code/configure.zcml'
--- lib/lp/code/configure.zcml	2010-09-28 15:35:59 +0000
+++ lib/lp/code/configure.zcml	2010-09-28 15:36:01 +0000
@@ -308,6 +308,11 @@
     <allow interface="lp.code.interfaces.branchmergeproposal.IBranchMergeProposalJobSource"/>
   </securedutility>
 
+  <class
+  class="lp.code.model.branchmergeproposaljob.GenerateIncrementalDiffJob">
+    <allow interface="lp.code.interfaces.branchmergeproposal.IGenerateIncrementalDiffJob"/>
+    <allow interface="lp.code.interfaces.branchmergeproposal.IBranchMergeProposalJob" />
+  </class>
   <class class="lp.code.model.branchmergeproposaljob.UpdatePreviewDiffJob">
     <allow interface="lp.code.interfaces.branchmergeproposal.IUpdatePreviewDiffJob" />
     <allow interface="lp.code.interfaces.branchmergeproposal.IBranchMergeProposalJob" />

=== modified file 'lib/lp/code/interfaces/branchmergeproposal.py'
--- lib/lp/code/interfaces/branchmergeproposal.py	2010-09-15 20:41:46 +0000
+++ lib/lp/code/interfaces/branchmergeproposal.py	2010-09-28 15:36:01 +0000
@@ -17,6 +17,8 @@
     'ICodeReviewCommentEmailJobSource',
     'ICreateMergeProposalJob',
     'ICreateMergeProposalJobSource',
+    'IGenerateIncrementalDiffJob',
+    'IGenerateIncrementalDiffJobSource',
     'IMergeProposalCreatedJob',
     'IMergeProposalCreatedJobSource',
     'IMergeProposalUpdatedEmailJob',
@@ -555,6 +557,10 @@
     """A job source that will get all supported merge proposal jobs."""
 
 
+class IBranchMergeProposalJobSource(IJobSource):
+    """A job source that will get all supported merge proposal jobs."""
+
+
 class IBranchMergeProposalListingBatchNavigator(ITableBatchNavigator):
     """A marker interface for registering the appropriate listings."""
 
@@ -660,6 +666,20 @@
         """Return the UpdatePreviewDiffJob with this id."""
 
 
+class IGenerateIncrementalDiffJob(IRunnableJob):
+    """Interface for the job to update the diff for a merge proposal."""
+
+
+class IGenerateIncrementalDiffJobSource(Interface):
+    """Create or retrieve jobs that update preview diffs."""
+
+    def create(bmp, old_revision_id, new_revision_id):
+        """Create job to generate incremental diff for this merge proposal."""
+
+    def get(id):
+        """Return the GenerateIncrementalDiffJob with this id."""
+
+
 class ICodeReviewCommentEmailJob(IRunnableJob):
     """Interface for the job to send code review comment email."""
 
@@ -710,8 +730,6 @@
         """
 
 
-# XXX: JonathanLange 2010-01-06: This is only used in the scanner, perhaps it
-# should be moved there.
 def notify_modified(proposal, func, *args, **kwargs):
     """Call func, then notify about the changes it made.
 
@@ -721,6 +739,8 @@
     :param kwargs: Keyword arguments for the method.
     :return: The return value of the method.
     """
+    # XXX: JonathanLange 2010-01-06: This is only used in the scanner, perhaps
+    # it should be moved there.
     from lp.code.adapters.branch import BranchMergeProposalDelta
     snapshot = BranchMergeProposalDelta.snapshot(proposal)
     result = func(*args, **kwargs)

=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py	2010-09-22 07:12:37 +0000
+++ lib/lp/code/model/branch.py	2010-09-28 15:36:01 +0000
@@ -456,10 +456,18 @@
 
     def scheduleDiffUpdates(self):
         """See `IBranch`."""
-        from lp.code.model.branchmergeproposaljob import UpdatePreviewDiffJob
-        jobs = [UpdatePreviewDiffJob.create(target)
-                for target in self.active_landing_targets
-                if target.target_branch.last_scanned_id is not None]
+        from lp.code.model.branchmergeproposaljob import (
+                GenerateIncrementalDiffJob,
+                UpdatePreviewDiffJob,
+        )
+        jobs = []
+        for merge_proposal in self.active_landing_targets:
+            if merge_proposal.target_branch.last_scanned_id is None:
+                continue
+            jobs.append(UpdatePreviewDiffJob.create(merge_proposal))
+            for old, new in merge_proposal.getMissingIncrementalDiffs():
+                GenerateIncrementalDiffJob.create(
+                    merge_proposal, old.revision_id, new.revision_id)
         return jobs
 
     # XXX: Tim Penhey, 2008-06-18, bug 240881

=== modified file 'lib/lp/code/model/branchmergeproposal.py'
--- lib/lp/code/model/branchmergeproposal.py	2010-09-28 15:35:59 +0000
+++ lib/lp/code/model/branchmergeproposal.py	2010-09-28 15:36:01 +0000
@@ -881,6 +881,11 @@
         ranges = self.getIncrementalDiffs(self.getIncrementalDiffRanges())
         return [diff for diff in ranges if diff is not None]
 
+    def getMissingIncrementalDiffs(self):
+        ranges = self.getIncrementalDiffRanges()
+        diffs = self.getIncrementalDiffs(ranges)
+        return [range_ for range_, diff in zip(ranges, diffs) if diff is None]
+
 
 class BranchMergeProposalGetter:
     """See `IBranchMergeProposalGetter`."""

=== modified file 'lib/lp/code/model/branchmergeproposaljob.py'
--- lib/lp/code/model/branchmergeproposaljob.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/branchmergeproposaljob.py	2010-09-28 15:36:01 +0000
@@ -22,6 +22,7 @@
     'BranchMergeProposalJobType',
     'CodeReviewCommentEmailJob',
     'CreateMergeProposalJob',
+    'GenerateIncrementalDiffJob',
     'MergeProposalCreatedJob',
     'MergeProposalUpdatedEmailJob',
     'ReviewRequestedEmailJob',
@@ -86,6 +87,8 @@
     ICodeReviewCommentEmailJobSource,
     ICreateMergeProposalJob,
     ICreateMergeProposalJobSource,
+    IGenerateIncrementalDiffJob,
+    IGenerateIncrementalDiffJobSource,
     IMergeProposalCreatedJob,
     IMergeProposalCreatedJobSource,
     IMergeProposalUpdatedEmailJob,
@@ -95,6 +98,7 @@
     IUpdatePreviewDiffJob,
     IUpdatePreviewDiffJobSource,
     )
+from lp.code.interfaces.revision import IRevisionSet
 from lp.code.mail.branch import RecipientReason
 from lp.code.mail.branchmergeproposal import BMPMailer
 from lp.code.mail.codereviewcomment import CodeReviewCommentMailer
@@ -148,6 +152,11 @@
         that have been changed on the merge proposal itself.
         """)
 
+    GENERATE_INCREMENTAL_DIFF = DBItem(5, """
+        Generate incremental diff
+
+        This job generates an incremental diff for a merge proposal.""")
+
 
 class BranchMergeProposalJob(Storm):
     """Base class for jobs related to branch merge proposals."""
@@ -286,7 +295,7 @@
 
     def getOopsVars(self):
         """See `IRunnableJob`."""
-        vars =  BaseRunnableJob.getOopsVars(self)
+        vars = BaseRunnableJob.getOopsVars(self)
         bmp = self.context.branch_merge_proposal
         vars.extend([
             ('branchmergeproposal_job_id', self.context.id),
@@ -494,7 +503,7 @@
 
     def getOopsVars(self):
         """See `IRunnableJob`."""
-        vars =  BranchMergeProposalJobDerived.getOopsVars(self)
+        vars = BranchMergeProposalJobDerived.getOopsVars(self)
         vars.extend([
             ('code_review_comment', self.metadata['code_review_comment']),
             ])
@@ -556,7 +565,7 @@
 
     def getOopsVars(self):
         """See `IRunnableJob`."""
-        vars =  BranchMergeProposalJobDerived.getOopsVars(self)
+        vars = BranchMergeProposalJobDerived.getOopsVars(self)
         vars.extend([
             ('reviewer', self.metadata['reviewer']),
             ('requester', self.metadata['requester']),
@@ -605,7 +614,7 @@
     def getMetadata(delta_text, editor):
         metadata = {'delta_text': delta_text}
         if editor is not None:
-            metadata['editor'] = editor.name;
+            metadata['editor'] = editor.name
         return metadata
 
     @property
@@ -624,7 +633,7 @@
 
     def getOopsVars(self):
         """See `IRunnableJob`."""
-        vars =  BranchMergeProposalJobDerived.getOopsVars(self)
+        vars = BranchMergeProposalJobDerived.getOopsVars(self)
         vars.extend([
             ('editor', self.metadata.get('editor', '(not set)')),
             ('delta_text', self.metadata['delta_text']),
@@ -642,6 +651,70 @@
         return 'emailing subscribers about merge proposal changes'
 
 
+class GenerateIncrementalDiffJob(BranchMergeProposalJobDerived):
+    """A job to generate an incremental diff for a branch merge proposal.
+
+    Provides class methods to create and retrieve such jobs.
+    """
+
+    implements(IGenerateIncrementalDiffJob)
+
+    classProvides(IGenerateIncrementalDiffJobSource)
+
+    class_job_type = BranchMergeProposalJobType.GENERATE_INCREMENTAL_DIFF
+
+    def acquireLease(self, duration=600):
+        return self.job.acquireLease(duration)
+
+    def run(self):
+        revision_set = getUtility(IRevisionSet)
+        old_revision = revision_set.getByRevisionId(self.old_revision_id)
+        new_revision = revision_set.getByRevisionId(self.new_revision_id)
+        diff = self.branch_merge_proposal.generateIncrementalDiff(
+            old_revision, new_revision)
+
+    @classmethod
+    def create(cls, merge_proposal, old_revision_id, new_revision_id):
+        metadata = cls.getMetadata(old_revision_id, new_revision_id)
+        job = BranchMergeProposalJob(
+            merge_proposal, cls.class_job_type, metadata)
+        return cls(job)
+
+    @staticmethod
+    def getMetadata(old_revision_id, new_revision_id):
+        return {
+            'old_revision_id': old_revision_id,
+            'new_revision_id': new_revision_id,
+        }
+
+    @property
+    def old_revision_id(self):
+        """The old revision id for the diff."""
+        return self.metadata['old_revision_id']
+
+    @property
+    def new_revision_id(self):
+        """The new revision id for the diff."""
+        return self.metadata['new_revision_id']
+
+    def getOopsVars(self):
+        """See `IRunnableJob`."""
+        vars = BranchMergeProposalJobDerived.getOopsVars(self)
+        vars.extend([
+            ('old_revision_id', self.metadata['old_revision_id']),
+            ('new_revision_id', self.metadata['new_revision_id']),
+            ])
+        return vars
+
+    def getOperationDescription(self):
+        return ('generating an incremental diff for a merge proposal')
+
+    def getErrorRecipients(self):
+        """Return a list of email-ids to notify about user errors."""
+        registrant = self.branch_merge_proposal.registrant
+        return format_address_for_person(registrant)
+
+
 class BranchMergeProposalJobFactory:
     """Construct a derived merge proposal job for a BranchMergeProposalJob."""
 
@@ -656,6 +729,8 @@
             ReviewRequestedEmailJob,
         BranchMergeProposalJobType.MERGE_PROPOSAL_UPDATED:
             MergeProposalUpdatedEmailJob,
+        BranchMergeProposalJobType.GENERATE_INCREMENTAL_DIFF:
+            GenerateIncrementalDiffJob,
         }
 
     @classmethod
@@ -696,21 +771,23 @@
         return BranchMergeProposalJobFactory.create(job)
 
     @staticmethod
-    def iterReady():
+    def iterReady(job_type=None):
         from lp.code.model.branch import Branch
         store = getUtility(IStoreSelector).get(MAIN_STORE, MASTER_FLAVOR)
         SourceBranch = ClassAlias(Branch)
         TargetBranch = ClassAlias(Branch)
-        jobs = store.find(
-            (BranchMergeProposalJob, Job, BranchMergeProposal,
-             SourceBranch, TargetBranch),
-            And(BranchMergeProposalJob.job == Job.id,
+        clauses = [BranchMergeProposalJob.job == Job.id,
                 Job._status.is_in([JobStatus.WAITING, JobStatus.RUNNING]),
                 BranchMergeProposalJob.branch_merge_proposal
                     == BranchMergeProposal.id,
                 BranchMergeProposal.source_branch == SourceBranch.id,
                 BranchMergeProposal.target_branch == TargetBranch.id,
-                ))
+                ]
+        if job_type is not None:
+            clauses.append(BranchMergeProposalJob.job_type == job_type)
+        jobs = store.find(
+            (BranchMergeProposalJob, Job, BranchMergeProposal,
+             SourceBranch, TargetBranch), And(*clauses))
         # Order by the job status first (to get running before waiting), then
         # the date_created, then job type.  This should give us all creation
         # jobs before comment jobs.

=== modified file 'lib/lp/code/model/tests/test_branchmergeproposaljobs.py'
--- lib/lp/code/model/tests/test_branchmergeproposaljobs.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/model/tests/test_branchmergeproposaljobs.py	2010-09-28 15:36:01 +0000
@@ -33,6 +33,8 @@
     IBranchMergeProposalJobSource,
     ICodeReviewCommentEmailJob,
     ICodeReviewCommentEmailJobSource,
+    IGenerateIncrementalDiffJob,
+    IGenerateIncrementalDiffJobSource,
     IMergeProposalCreatedJob,
     IMergeProposalUpdatedEmailJob,
     IMergeProposalUpdatedEmailJobSource,
@@ -46,12 +48,13 @@
     BranchMergeProposalJobDerived,
     BranchMergeProposalJobType,
     CodeReviewCommentEmailJob,
+    GenerateIncrementalDiffJob,
     MergeProposalCreatedJob,
     MergeProposalUpdatedEmailJob,
     ReviewRequestedEmailJob,
     UpdatePreviewDiffJob,
     )
-from lp.code.model.tests.test_diff import DiffTestCase
+from lp.code.model.tests.test_diff import DiffTestCase, create_example_merge
 from lp.code.subscribers.branchmergeproposal import merge_proposal_modified
 from lp.services.job.model.job import Job
 from lp.services.job.runner import JobRunner
@@ -193,7 +196,7 @@
 
     def test_run(self):
         self.useBzrBranches(direct_database=True)
-        bmp = self.createExampleMerge()[0]
+        bmp = create_example_merge(self)[0]
         job = UpdatePreviewDiffJob.create(bmp)
         self.factory.makeRevisionsForBranch(bmp.source_branch, count=1)
         bmp.source_branch.next_mirror_time = None
@@ -227,13 +230,72 @@
 
     def test_10_minute_lease(self):
         self.useBzrBranches(direct_database=True)
-        bmp = self.createExampleMerge()[0]
+        bmp = create_example_merge(self)[0]
         job = UpdatePreviewDiffJob.create(bmp)
         job.acquireLease()
         expiry_delta = job.lease_expires - datetime.now(pytz.UTC)
         self.assertTrue(500 <= expiry_delta.seconds, expiry_delta)
 
 
+def make_runnable_incremental_diff_job(test_case):
+    test_case.useBzrBranches(direct_database=True)
+    bmp, source_rev_id, target_rev_id = create_example_merge(test_case)
+    repository = bmp.source_branch.getBzrBranch().repository
+    parent_id = repository.get_revision(source_rev_id).parent_ids[0]
+    test_case.factory.makeRevision(rev_id=source_rev_id)
+    test_case.factory.makeRevision(rev_id=parent_id)
+    return GenerateIncrementalDiffJob.create(bmp, parent_id, source_rev_id)
+
+
+class TestGenerateIncrementalDiffJob(DiffTestCase):
+
+    layer = LaunchpadZopelessLayer
+
+    def test_implement_interface(self):
+        """GenerateIncrementalDiffJob implements its interface."""
+        verifyObject(
+            IGenerateIncrementalDiffJobSource, GenerateIncrementalDiffJob)
+
+    def test_providesInterface(self):
+        """MergeProposalCreatedJob provides the expected interfaces."""
+        bmp = self.factory.makeBranchMergeProposal()
+        job = GenerateIncrementalDiffJob.create(bmp, 'old', 'new')
+        verifyObject(IGenerateIncrementalDiffJob, job)
+        verifyObject(IBranchMergeProposalJob, job)
+
+    def test_getOperationDescription(self):
+        bmp = self.factory.makeBranchMergeProposal()
+        job = GenerateIncrementalDiffJob.create(bmp, 'old', 'new')
+        self.assertEqual(
+            'generating an incremental diff for a merge proposal',
+            job.getOperationDescription())
+
+    def test_run(self):
+        job = make_runnable_incremental_diff_job(self)
+        transaction.commit()
+        self.layer.switchDbUser(config.merge_proposal_jobs.dbuser)
+        job.run()
+        transaction.commit()
+
+    def test_run_all(self):
+        job = make_runnable_incremental_diff_job(self)
+        transaction.commit()
+        self.layer.switchDbUser(config.merge_proposal_jobs.dbuser)
+        runner = JobRunner([job])
+        runner.runAll()
+        self.assertEqual([job], runner.completed_jobs)
+
+    def test_10_minute_lease(self):
+        self.useBzrBranches(direct_database=True)
+        bmp = create_example_merge(self)[0]
+        job = GenerateIncrementalDiffJob.create(bmp, 'old', 'new')
+        transaction.commit()
+        self.layer.switchDbUser(config.merge_proposal_jobs.dbuser)
+        job.acquireLease()
+        expiry_delta = job.lease_expires - datetime.now(pytz.UTC)
+        self.assertTrue(500 <= expiry_delta.seconds, expiry_delta)
+
+
 class TestBranchMergeProposalJobSource(TestCaseWithFactory):
 
     layer = LaunchpadZopelessLayer

=== modified file 'lib/lp/code/model/tests/test_diff.py'
--- lib/lp/code/model/tests/test_diff.py	2010-09-28 15:35:59 +0000
+++ lib/lp/code/model/tests/test_diff.py	2010-09-28 15:36:01 +0000
@@ -56,37 +56,41 @@
         self.records.append(record)
 
 
+def commit_file(branch, path, contents, merge_parents=None):
+    """Create a commit that updates a file to specified contents.
+
+    This will create or modify the file, as needed.
+    """
+    committer = DirectBranchCommit(
+        removeSecurityProxy(branch), no_race_check=True,
+        merge_parents=merge_parents)
+    committer.writeFile(path, contents)
+    try:
+        return committer.commit('committing')
+    finally:
+        committer.unlock()
+
+
+def create_example_merge(test_case):
+    """Create a merge proposal with conflicts and updates."""
+    bmp = test_case.factory.makeBranchMergeProposal()
+    # Make the branches of the merge proposal look good as far as the
+    # model is concerned.
+    test_case.factory.makeRevisionsForBranch(bmp.source_branch)
+    test_case.factory.makeRevisionsForBranch(bmp.target_branch)
+    bzr_target = test_case.createBzrBranch(bmp.target_branch)
+    commit_file(bmp.target_branch, 'foo', 'a\n')
+    test_case.createBzrBranch(bmp.source_branch, bzr_target)
+    source_rev_id = commit_file(bmp.source_branch, 'foo', 'd\na\nb\n')
+    target_rev_id = commit_file(bmp.target_branch, 'foo', 'c\na\n')
+    return bmp, source_rev_id, target_rev_id
+
+
 class DiffTestCase(TestCaseWithFactory):
 
-    @staticmethod
-    def commitFile(branch, path, contents, merge_parents=None):
-        """Create a commit that updates a file to specified contents.
-
-        This will create or modify the file, as needed.
-        """
-        committer = DirectBranchCommit(
-            removeSecurityProxy(branch), no_race_check=True,
-            merge_parents=merge_parents)
-        committer.writeFile(path, contents)
-        try:
-            return committer.commit('committing')
-        finally:
-            committer.unlock()
-
     def createExampleMerge(self):
-        """Create a merge proposal with conflicts and updates."""
         self.useBzrBranches(direct_database=True)
-        bmp = self.factory.makeBranchMergeProposal()
-        # Make the branches of the merge proposal look good as far as the
-        # model is concerned.
-        self.factory.makeRevisionsForBranch(bmp.source_branch)
-        self.factory.makeRevisionsForBranch(bmp.target_branch)
-        bzr_target = self.createBzrBranch(bmp.target_branch)
-        self.commitFile(bmp.target_branch, 'foo', 'a\n')
-        self.createBzrBranch(bmp.source_branch, bzr_target)
-        source_rev_id = self.commitFile(bmp.source_branch, 'foo', 'd\na\nb\n')
-        target_rev_id = self.commitFile(bmp.target_branch, 'foo', 'c\na\n')
-        return bmp, source_rev_id, target_rev_id
+        return create_example_merge(self)
 
     def checkExampleMerge(self, diff_text):
         """Ensure the diff text matches the values for ExampleMerge."""
@@ -113,12 +117,12 @@
             source = bmp.source_branch
             prerequisite = bmp.prerequisite_branch
         target_bzr = self.createBzrBranch(target)
-        self.commitFile(target, 'file', 'target text\n')
+        commit_file(target, 'file', 'target text\n')
         prerequisite_bzr = self.createBzrBranch(prerequisite, target_bzr)
-        self.commitFile(
+        commit_file(
             prerequisite, 'file', 'target text\nprerequisite text\n')
         source_bzr = self.createBzrBranch(source, prerequisite_bzr)
-        source_rev_id = self.commitFile(
+        source_rev_id = commit_file(
             source, 'file',
             'target text\nprerequisite text\nsource text\n')
         return (source_bzr, source_rev_id, target_bzr, prerequisite_bzr,
@@ -245,7 +249,7 @@
         # affect the diff.
         (source_bzr, source_rev_id, target_bzr, prerequisite_bzr,
          prerequisite) = self.preparePrerequisiteMerge()
-        self.commitFile(
+        commit_file(
             prerequisite, 'file', 'prerequisite text2\n')
         diff, conflicts = Diff.mergePreviewFromBranches(
             source_bzr, source_rev_id, target_bzr, prerequisite_bzr)
@@ -461,10 +465,10 @@
         self.useBzrBranches(direct_database=True)
         bmp = self.factory.makeBranchMergeProposal()
         bzr_target = self.createBzrBranch(bmp.target_branch)
-        self.commitFile(bmp.target_branch, 'foo', 'a\n')
+        commit_file(bmp.target_branch, 'foo', 'a\n')
         self.createBzrBranch(bmp.source_branch, bzr_target)
-        source_rev_id = self.commitFile(bmp.source_branch, 'foo', 'a\nb\n')
-        target_rev_id = self.commitFile(bmp.target_branch, 'foo', 'c\na\n')
+        source_rev_id = commit_file(bmp.source_branch, 'foo', 'a\nb\n')
+        target_rev_id = commit_file(bmp.target_branch, 'foo', 'c\na\n')
         diff = PreviewDiff.fromBranchMergeProposal(bmp)
         self.assertEqual('', diff.conflicts)
         self.assertFalse(diff.has_conflicts)
@@ -558,26 +562,22 @@
         bmp = self.factory.makeBranchMergeProposal(
             prerequisite_branch=prerequisite_branch)
         target_branch = self.createBzrBranch(bmp.target_branch)
-        old_revision_id = self.commitFile(
-            bmp.target_branch, 'foo', 'a\nb\ne\n')
+        old_revision_id = commit_file(bmp.target_branch, 'foo', 'a\nb\ne\n')
         old_revision = self.factory.makeRevision(rev_id=old_revision_id)
         source_branch = self.createBzrBranch(
             bmp.source_branch, target_branch)
-        self.commitFile(
-            bmp.source_branch, 'foo', 'a\nc\ne\n')
+        commit_file(bmp.source_branch, 'foo', 'a\nc\ne\n')
         prerequisite = self.createBzrBranch(
             bmp.prerequisite_branch, target_branch)
-        prerequisite_revision = self.commitFile(bmp.prerequisite_branch,
-            'foo', 'd\na\nb\ne\n')
-        merge_parent = self.commitFile(bmp.target_branch, 'foo',
-            'a\nb\ne\nf\n')
+        prerequisite_revision = commit_file(
+            bmp.prerequisite_branch, 'foo', 'd\na\nb\ne\n')
+        merge_parent = commit_file(bmp.target_branch, 'foo', 'a\nb\ne\nf\n')
         source_branch.repository.fetch(target_branch.repository,
             revision_id=merge_parent)
-        self.commitFile(
-            bmp.source_branch, 'foo', 'a\nc\ne\nf\n', [merge_parent])
+        commit_file(bmp.source_branch, 'foo', 'a\nc\ne\nf\n', [merge_parent])
         source_branch.repository.fetch(prerequisite.repository,
             revision_id=prerequisite_revision)
-        new_revision_id = self.commitFile(
+        new_revision_id = commit_file(
             bmp.source_branch, 'foo', 'd\na\nc\ne\nf\n',
             [prerequisite_revision])
         new_revision = self.factory.makeRevision(rev_id=new_revision_id)

=== modified file 'lib/lp/code/scripts/tests/test_merge_proposal_jobs.py'
--- lib/lp/code/scripts/tests/test_merge_proposal_jobs.py	2010-08-20 20:31:18 +0000
+++ lib/lp/code/scripts/tests/test_merge_proposal_jobs.py	2010-09-28 15:36:01 +0000
@@ -7,8 +7,14 @@
 
 import unittest
 
+import transaction
+
 from canonical.launchpad.scripts.tests import run_script
 from canonical.testing import ZopelessAppServerLayer
+from lp.code.model.tests.test_branchmergeproposaljobs import (
+    make_runnable_incremental_diff_job
+)
+from lp.services.job.interfaces.job import JobStatus
 from lp.testing import TestCaseWithFactory
 
 
@@ -18,6 +24,8 @@
 
     def test_script_runs(self):
         """Ensure merge-proposal-jobs script runs."""
+        job = make_runnable_incremental_diff_job(self)
+        transaction.commit()
         retcode, stdout, stderr = run_script(
             'cronscripts/merge-proposal-jobs.py', [])
         self.assertEqual(0, retcode)
@@ -25,7 +33,10 @@
         self.assertEqual(
             'INFO    Creating lockfile:'
             ' /var/lock/launchpad-merge-proposal-jobs.lock\n'
-            'INFO    Running through Twisted.\n', stderr)
+            'INFO    Running through Twisted.\n'
+            'INFO    Ran 1 GenerateIncrementalDiffJob jobs.\n', stderr)
+        self.assertEqual(JobStatus.COMPLETED, job.status)
+
 
 def test_suite():
     return unittest.TestLoader().loadTestsFromName(__name__)

=== modified file 'lib/lp/codehosting/scanner/tests/test_bzrsync.py'
--- lib/lp/codehosting/scanner/tests/test_bzrsync.py	2010-09-13 04:56:29 +0000
+++ lib/lp/codehosting/scanner/tests/test_bzrsync.py	2010-09-28 15:36:01 +0000
@@ -33,11 +33,14 @@
 from lp.code.interfaces.branchlookup import IBranchLookup
 from lp.code.interfaces.revision import IRevisionSet
 from lp.code.model.branchrevision import BranchRevision
+from lp.code.model.branchmergeproposaljob import (
+    BranchMergeProposalJobSource, BranchMergeProposalJobType)
 from lp.code.model.revision import (
     Revision,
     RevisionAuthor,
     RevisionParent,
     )
+from lp.code.model.tests.test_diff import commit_file
 from lp.codehosting.scanner.bzrsync import BzrSync
 from lp.services.osutils import override_environ
 from lp.testing import TestCaseWithFactory
@@ -606,6 +609,34 @@
         self.assertIsNot(None, bmp.next_preview_diff_job)
 
 
+class TestGenerateIncrementalDiffJob(BzrSyncTestCase):
+    """Test the scheduling of GenerateIncrementalDiffJobs."""
+
+    def getPending(self):
+        return list(
+            BranchMergeProposalJobSource.iterReady(
+                BranchMergeProposalJobType.GENERATE_INCREMENTAL_DIFF
+                )
+            )
+
+    @run_as_db_user(config.launchpad.dbuser)
+    def test_create_on_new_revision(self):
+        """When branch tip changes, a job is created."""
+        bmp = self.factory.makeBranchMergeProposal(
+            source_branch=self.db_branch,
+            date_created=self.factory.getUniqueDate())
+        parent_id = commit_file(self.db_branch, 'foo', 'bar')
+        revision_id = commit_file(self.db_branch, 'foo', 'baz')
+        removeSecurityProxy(bmp).target_branch.last_scanned_id = 'rev'
+        self.assertEqual([], self.getPending())
+        transaction.commit()
+        LaunchpadZopelessLayer.switchDbUser(config.branchscanner.dbuser)
+        self.makeBzrSync(self.db_branch).syncBranchAndClose()
+        (job,) = self.getPending()
+        self.assertEqual(revision_id, job.new_revision_id)
+        self.assertEqual(parent_id, job.old_revision_id)
+
+
 class TestSetRecipeStale(BzrSyncTestCase):
     """Test recipes associated with the branch are marked stale."""