~launchpad-pqm/launchpad/devel

8687.15.22 by Karl Fogel
Add the copyright header block to the remaining .py files.
1
# Copyright 2009 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
3
4
"""Tests for the scanner's merge detection."""
5
6
__metaclass__ = type
7
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
8
import logging
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
9
10
from bzrlib.revision import NULL_REVISION
11
import transaction
12
from zope.component import getUtility
10391.5.7 by Aaron Bentley
Ensure merge detection is subscribed to ScanCompleted
13
from zope.event import notify
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
14
7675.624.65 by Tim Penhey
Fix the broken test.
15
from canonical.config import config
14560.2.29 by Curtis Hovey
Restored lpstorm module name because it lp engineers know that name.
16
from lp.services.database.lpstorm import IStore
14604.1.1 by Curtis Hovey
Separate test-authoring classes from test-running classes.
17
from lp.testing.layers import LaunchpadZopelessLayer
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
18
from lp.code.enums import (
19
    BranchLifecycleStatus,
20
    BranchMergeProposalStatus,
21
    )
22
from lp.code.interfaces.branchlookup import IBranchLookup
23
from lp.code.model.branchmergeproposaljob import (
24
    BranchMergeProposalJob,
25
    BranchMergeProposalJobFactory,
26
    BranchMergeProposalJobType,
27
    )
28
from lp.codehosting.scanner import (
29
    events,
30
    mergedetection,
31
    )
8426.6.1 by Michael Hudson
bzr ls --versioned --recursive --kind=file | xargs sed -i -e 's,from canonical.codehosting,from lp.codehosting,'
32
from lp.codehosting.scanner.tests.test_bzrsync import (
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
33
    BzrSyncTestCase,
34
    run_as_db_user,
35
    )
11040.1.14 by Aaron Bentley
Fix more test failures.
36
from lp.services.osutils import override_environ
11403.1.4 by Henning Eggers
Reformatted imports using format-imports script r32.
37
from lp.testing import (
38
    TestCase,
39
    TestCaseWithFactory,
40
    )
9042.1.3 by Aaron Bentley
Notify when branch scanner detects a merge
41
from lp.testing.mail_helpers import pop_notifications
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
42
43
44
class TestAutoMergeDetectionForMergeProposals(BzrSyncTestCase):
45
    """Test the scanner's ability to mark merge proposals as merged."""
46
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
47
    def setUp(self):
48
        BzrSyncTestCase.setUp(self)
49
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
50
    @run_as_db_user(config.launchpad.dbuser)
51
    def createProposal(self, source, target):
52
        # The scanner doesn't have insert rights, so do it here.
7675.624.66 by Tim Penhey
Much lint cleanup.
53
        source.addLandingTarget(source.owner, target)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
54
        transaction.commit()
55
56
    def _createBranchesAndProposal(self):
57
        # Create two branches where the trunk has the branch as a merge.  Also
58
        # create a merge proposal from the branch to the trunk.
59
        (db_trunk, trunk_tree), (db_branch, branch_tree) = (
60
            self.makeBranchWithMerge('base', 'trunk', 'branch', 'merge'))
61
        trunk_id = db_trunk.id
62
        branch_id = db_branch.id
63
        self.createProposal(db_branch, db_trunk)
64
        # Reget the objects due to transaction boundary.
7940.2.9 by Jonathan Lange
Fix up a bunch of tests. Make IBranchSet.__getitem__ delegate to IBranchLookup.
65
        branch_lookup = getUtility(IBranchLookup)
66
        db_trunk = branch_lookup.get(trunk_id)
67
        db_branch = branch_lookup.get(branch_id)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
68
        proposal = list(db_branch.landing_targets)[0]
69
        return proposal, db_trunk, db_branch, branch_tree
70
71
    def _scanTheBranches(self, branch1, branch2):
72
        for branch in (branch1, branch2):
73
            scanner = self.makeBzrSync(branch)
74
            scanner.syncBranchAndClose()
75
7960.4.54 by Jonathan Lange
Move the methods that detect merges into the mergedetection module.
76
    def test_auto_merge_proposals_real_merge(self):
77
        # If there is a merge proposal where the tip of the source is in the
78
        # ancestry of the target, mark it as merged.
79
        proposal, db_trunk, db_branch, branch_tree = (
80
            self._createBranchesAndProposal())
81
82
        self._scanTheBranches(db_branch, db_trunk)
83
        # The proposal should now be merged.
84
        self.assertEqual(
85
            BranchMergeProposalStatus.MERGED,
86
            proposal.queue_status)
10652.2.4 by Tim Penhey
Make sure that the event method works properly.
87
        self.assertEqual(3, proposal.merged_revno)
7960.4.54 by Jonathan Lange
Move the methods that detect merges into the mergedetection module.
88
89
    def test_auto_merge_proposals_real_merge_target_scanned_first(self):
90
        # If there is a merge proposal where the tip of the source is in the
91
        # ancestry of the target, mark it as merged.
92
        proposal, db_trunk, db_branch, branch_tree = (
93
            self._createBranchesAndProposal())
94
95
        self._scanTheBranches(db_trunk, db_branch)
96
        # The proposal should now be merged.
97
        self.assertEqual(
98
            BranchMergeProposalStatus.MERGED,
99
            proposal.queue_status)
100
101
    def test_auto_merge_proposals_rejected_proposal(self):
102
        # If there is a merge proposal where the tip of the source is in the
103
        # ancestry of the target but the proposal is in a final state the
104
        # proposal is not marked as merged.
105
106
        proposal, db_trunk, db_branch, branch_tree = (
107
            self._createBranchesAndProposal())
108
109
        proposal.rejectBranch(db_trunk.owner, 'branch')
110
111
        self._scanTheBranches(db_branch, db_trunk)
112
113
        # The proposal should stay rejected..
114
        self.assertEqual(
115
            BranchMergeProposalStatus.REJECTED,
116
            proposal.queue_status)
117
11768.1.3 by Curtis Hovey
Hushed lint
118
    def test_auto_merge_proposals_rejected_proposal_target_scanned_first(
119
                                                                        self):
7960.4.54 by Jonathan Lange
Move the methods that detect merges into the mergedetection module.
120
        # If there is a merge proposal where the tip of the source is in the
121
        # ancestry of the target but the proposal is in a final state the
122
        # proposal is not marked as merged.
123
124
        proposal, db_trunk, db_branch, branch_tree = (
125
            self._createBranchesAndProposal())
126
127
        proposal.rejectBranch(db_trunk.owner, 'branch')
128
129
        self._scanTheBranches(db_trunk, db_branch)
130
131
        # The proposal should stay rejected..
132
        self.assertEqual(
133
            BranchMergeProposalStatus.REJECTED,
134
            proposal.queue_status)
135
136
    def test_auto_merge_proposals_not_merged_proposal(self):
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
137
        # If there is a merge proposal where the tip of the source is not in
138
        # the ancestry of the target it is not marked as merged.
139
140
        proposal, db_trunk, db_branch, branch_tree = (
141
            self._createBranchesAndProposal())
142
11040.1.14 by Aaron Bentley
Fix more test failures.
143
        # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
144
        # required to generate the revision-id.
145
        with override_environ(BZR_EMAIL='me@example.com'):
146
            branch_tree.commit(u'another revision', rev_id='another-rev')
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
147
        current_proposal_status = proposal.queue_status
148
        self.assertNotEqual(
149
            current_proposal_status,
150
            BranchMergeProposalStatus.MERGED)
151
152
        self._scanTheBranches(db_branch, db_trunk)
153
154
        # The proposal should stay in the same state.
155
        self.assertEqual(current_proposal_status, proposal.queue_status)
156
7960.4.54 by Jonathan Lange
Move the methods that detect merges into the mergedetection module.
157
    def test_auto_merge_proposals_not_merged_with_updated_source(self):
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
158
        # If there is a merge proposal where the tip of the source is not in
159
        # the ancestry of the target it is not marked as merged.
160
161
        proposal, db_trunk, db_branch, branch_tree = (
162
            self._createBranchesAndProposal())
163
11040.1.14 by Aaron Bentley
Fix more test failures.
164
        # XXX: AaronBentley 2010-08-06 bug=614404: a bzr username is
165
        # required to generate the revision-id.
166
        with override_environ(BZR_EMAIL='me@example.com'):
167
            branch_tree.commit(u'another revision', rev_id='another-rev')
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
168
        current_proposal_status = proposal.queue_status
169
        self.assertNotEqual(
170
            current_proposal_status,
171
            BranchMergeProposalStatus.MERGED)
172
173
        self._scanTheBranches(db_trunk, db_branch)
174
175
        # The proposal should stay in the same state.
176
        self.assertEqual(current_proposal_status, proposal.queue_status)
177
178
179
class TestMergeDetection(TestCaseWithFactory):
180
    """Test that the merges are detected, and the handler called."""
181
182
    layer = LaunchpadZopelessLayer
183
184
    def setUp(self):
185
        TestCaseWithFactory.setUp(self)
186
        self.product = self.factory.makeProduct()
187
        self.db_branch = self.factory.makeProductBranch(product=self.product)
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
188
        # Replace the built-in merge_detected with our test stub.
189
        self._original_merge_detected = mergedetection.merge_detected
190
        mergedetection.merge_detected = self.mergeDetected
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
191
        # Reset the recorded branches.
192
        self.merges = []
193
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
194
    def tearDown(self):
195
        mergedetection.merge_detected = self._original_merge_detected
196
        TestCaseWithFactory.tearDown(self)
197
11681.1.1 by Aaron Bentley
Switch to only new ancestry in ScanCompleted.
198
    def autoMergeBranches(self, db_branch, new_ancestry):
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
199
        mergedetection.auto_merge_branches(
200
            events.ScanCompleted(
7960.4.72 by Jonathan Lange
Fix up the merge detection tests.
201
                db_branch=db_branch, bzr_branch=None,
11681.1.1 by Aaron Bentley
Switch to only new ancestry in ScanCompleted.
202
                logger=None, new_ancestry=new_ancestry))
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
203
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
204
    def mergeDetected(self, logger, source, target):
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
205
        # Record the merged branches
206
        self.merges.append((source, target))
207
208
    def test_own_branch_not_emitted(self):
209
        # A merge is never emitted with the source branch being the same as
210
        # the target branch.
211
        self.db_branch.last_scanned_id = 'revid'
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
212
        self.autoMergeBranches(self.db_branch, ['revid'])
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
213
        self.assertEqual([], self.merges)
214
215
    def test_branch_tip_in_ancestry(self):
216
        # If there is another branch with their tip revision id in the
217
        # ancestry passed in, the merge detection is emitted.
218
        source = self.factory.makeProductBranch(product=self.product)
219
        source.last_scanned_id = 'revid'
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
220
        self.autoMergeBranches(self.db_branch, ['revid'])
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
221
        self.assertEqual([(source, self.db_branch)], self.merges)
222
223
    def test_branch_tip_in_ancestry_status_merged(self):
224
        # Branches that are already merged do emit events.
225
        source = self.factory.makeProductBranch(
226
            product=self.product,
227
            lifecycle_status=BranchLifecycleStatus.MERGED)
228
        source.last_scanned_id = 'revid'
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
229
        self.autoMergeBranches(self.db_branch, ['revid'])
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
230
        self.assertEqual([], self.merges)
231
232
    def test_other_branch_with_no_last_scanned_id(self):
233
        # Other branches for the product are checked, but if the tip revision
234
        # of the branch is not yet been set no merge event is emitted for that
235
        # branch.
7675.624.66 by Tim Penhey
Much lint cleanup.
236
        self.factory.makeProductBranch(product=self.product)
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
237
        self.autoMergeBranches(self.db_branch, ['revid'])
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
238
        self.assertEqual([], self.merges)
239
240
    def test_other_branch_with_NULL_REVISION_last_scanned_id(self):
241
        # Other branches for the product are checked, but if the tip revision
242
        # of the branch is the NULL_REVISION no merge event is emitted for
243
        # that branch.
244
        source = self.factory.makeProductBranch(product=self.product)
245
        source.last_scanned_id = NULL_REVISION
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
246
        self.autoMergeBranches(self.db_branch, ['revid'])
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
247
        self.assertEqual([], self.merges)
248
249
    def test_other_branch_same_tip_revision_not_emitted(self):
250
        # If two different branches have the same tip revision, then they are
251
        # conceptually the same branch, not one merged into the other.
252
        source = self.factory.makeProductBranch(product=self.product)
253
        source.last_scanned_id = 'revid'
254
        self.db_branch.last_scanned_id = 'revid'
7960.4.63 by Jonathan Lange
Make the merge detection code event based.
255
        self.autoMergeBranches(self.db_branch, ['revid'])
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
256
        self.assertEqual([], self.merges)
257
258
259
class TestBranchMergeDetectionHandler(TestCaseWithFactory):
7960.4.72 by Jonathan Lange
Fix up the merge detection tests.
260
    """Test the merge_detected handler."""
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
261
262
    layer = LaunchpadZopelessLayer
263
264
    def test_mergeProposalMergeDetected(self):
265
        # A merge proposal that is merged has the proposal itself marked as
266
        # merged, and the source branch lifecycle status set as merged.
267
        product = self.factory.makeProduct()
268
        proposal = self.factory.makeBranchMergeProposal(product=product)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
269
        product.development_focus.branch = proposal.target_branch
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
270
        self.assertNotEqual(
271
            BranchMergeProposalStatus.MERGED, proposal.queue_status)
272
        self.assertNotEqual(
273
            BranchLifecycleStatus.MERGED,
274
            proposal.source_branch.lifecycle_status)
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
275
        mergedetection.merge_detected(
276
            logging.getLogger(),
7960.4.60 by Jonathan Lange
Remove merge proposal merge.
277
            proposal.source_branch, proposal.target_branch, proposal)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
278
        self.assertEqual(
279
            BranchMergeProposalStatus.MERGED, proposal.queue_status)
280
        self.assertEqual(
281
            BranchLifecycleStatus.MERGED,
282
            proposal.source_branch.lifecycle_status)
7675.624.65 by Tim Penhey
Fix the broken test.
283
        job = IStore(proposal).find(
284
            BranchMergeProposalJob,
285
            BranchMergeProposalJob.branch_merge_proposal == proposal,
286
            BranchMergeProposalJob.job_type ==
287
            BranchMergeProposalJobType.MERGE_PROPOSAL_UPDATED).one()
288
        derived_job = BranchMergeProposalJobFactory.create(job)
289
        derived_job.run()
9042.1.3 by Aaron Bentley
Notify when branch scanner detects a merge
290
        notifications = pop_notifications()
291
        self.assertIn('Work in progress => Merged',
292
                      notifications[0].get_payload(decode=True))
293
        self.assertEqual(
294
            config.canonical.noreply_from_address, notifications[0]['From'])
295
        recipients = set(msg['x-envelope-to'] for msg in notifications)
296
        expected = set(
297
            [proposal.source_branch.registrant.preferredemail.email,
298
             proposal.target_branch.registrant.preferredemail.email])
299
        self.assertEqual(expected, recipients)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
300
301
    def test_mergeProposalMergeDetected_not_series(self):
302
        # If the target branch is not a series branch, then the merge proposal
303
        # is still marked as merged, but the lifecycle status of the source
304
        # branch is not updated.
305
        proposal = self.factory.makeBranchMergeProposal()
306
        self.assertNotEqual(
307
            BranchMergeProposalStatus.MERGED, proposal.queue_status)
308
        self.assertNotEqual(
309
            BranchLifecycleStatus.MERGED,
310
            proposal.source_branch.lifecycle_status)
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
311
        mergedetection.merge_detected(
312
            logging.getLogger(),
7960.4.60 by Jonathan Lange
Remove merge proposal merge.
313
            proposal.source_branch, proposal.target_branch, proposal)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
314
        self.assertEqual(
315
            BranchMergeProposalStatus.MERGED, proposal.queue_status)
316
        self.assertNotEqual(
317
            BranchLifecycleStatus.MERGED,
318
            proposal.source_branch.lifecycle_status)
319
320
    def test_mergeOfTwoBranches_target_not_dev_focus(self):
321
        # The target branch must be the development focus in order for the
322
        # lifecycle status of the source branch to be updated to merged.
323
        source = self.factory.makeProductBranch()
324
        target = self.factory.makeProductBranch()
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
325
        mergedetection.merge_detected(logging.getLogger(), source, target)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
326
        self.assertNotEqual(
327
            BranchLifecycleStatus.MERGED, source.lifecycle_status)
328
329
    def test_mergeOfTwoBranches_target_dev_focus(self):
330
        # If the target branch is the development focus branch of the product,
331
        # then the source branch gets its lifecycle status set to merged.
332
        product = self.factory.makeProduct()
333
        source = self.factory.makeProductBranch(product=product)
334
        target = self.factory.makeProductBranch(product=product)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
335
        product.development_focus.branch = target
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
336
        mergedetection.merge_detected(logging.getLogger(), source, target)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
337
        self.assertEqual(
338
            BranchLifecycleStatus.MERGED, source.lifecycle_status)
339
340
    def test_mergeOfTwoBranches_source_series_branch(self):
341
        # If the source branch is associated with a series, its lifecycle
342
        # status is not updated.
343
        product = self.factory.makeProduct()
344
        source = self.factory.makeProductBranch(product=product)
345
        target = self.factory.makeProductBranch(product=product)
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
346
        product.development_focus.branch = target
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
347
        series = product.newSeries(product.owner, 'new', '')
7675.85.2 by Jonathan Lange
Undo revision generated by step 2 of process.
348
        series.branch = source
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
349
7960.4.62 by Jonathan Lange
Make the merge detection handling code entirely stateless, thus paving
350
        mergedetection.merge_detected(logging.getLogger(), source, target)
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
351
        self.assertNotEqual(
352
            BranchLifecycleStatus.MERGED, source.lifecycle_status)
353
10391.5.7 by Aaron Bentley
Ensure merge detection is subscribed to ScanCompleted
354
    def test_auto_merge_branches_subscribed(self):
355
        """Auto merging is triggered by ScanCompleted."""
356
        source = self.factory.makeBranch()
357
        source.last_scanned_id = '23foo'
358
        target = self.factory.makeBranchTargetBranch(source.target)
359
        target.product.development_focus.branch = target
360
        logger = logging.getLogger('test')
11681.1.1 by Aaron Bentley
Switch to only new ancestry in ScanCompleted.
361
        notify(events.ScanCompleted(target, None, logger, ['23foo']))
10391.5.7 by Aaron Bentley
Ensure merge detection is subscribed to ScanCompleted
362
        self.assertEqual(
363
            BranchLifecycleStatus.MERGED, source.lifecycle_status)
364
7913.1.3 by Jonathan Lange
Move the merge detection tests into a separate module.
365
10652.2.3 by Tim Penhey
Record the merge revno.
366
class TestFindMergedRevno(TestCase):
367
    """Tests for find_merged_revno."""
368
369
    def get_merge_graph(self):
370
        # Create a fake merge graph.
371
        return [
372
            ('rev-3', 0, (3,), False),
373
            ('rev-3a', 1, (15, 4, 8), False),
374
            ('rev-3b', 1, (15, 4, 7), False),
375
            ('rev-3c', 1, (15, 4, 6), False),
376
            ('rev-2', 0, (2,), False),
377
            ('rev-2a', 1, (4, 4, 8), False),
378
            ('rev-2b', 1, (4, 4, 7), False),
379
            ('rev-2-1a', 2, (7, 2, 47), False),
380
            ('rev-2-1b', 2, (7, 2, 45), False),
381
            ('rev-2-1c', 2, (7, 2, 42), False),
382
            ('rev-2c', 1, (4, 4, 6), False),
383
            ('rev-1', 0, (1,), False),
384
            ]
385
386
    def assertFoundRevisionNumber(self, expected, rev_id):
387
        merge_sorted = self.get_merge_graph()
388
        revno = mergedetection.find_merged_revno(merge_sorted, rev_id)
389
        if expected is None:
390
            self.assertIs(None, revno)
391
        else:
392
            self.assertEqual(expected, revno)
393
394
    def test_not_found(self):
395
        # If the rev_id passed into the function isn't in the merge sorted
396
        # graph, None is returned.
397
        self.assertFoundRevisionNumber(None, 'not-there')
398
399
    def test_existing_revision(self):
400
        # If a revision is found, the last mainline revision is returned.
401
        self.assertFoundRevisionNumber(3, 'rev-3b')
402
        self.assertFoundRevisionNumber(2, 'rev-2-1c')
403
        self.assertFoundRevisionNumber(1, 'rev-1')