# Copyright 2012 Hewlett-Packard Development Company, L.P.
# Copyright 2018 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from tests.base import ZuulTestCase, skipIfMultiScheduler


class TestGerritToGithubCRD(ZuulTestCase):
    config_file = 'zuul-gerrit-github.conf'
    tenant_config_file = 'config/cross-source/main.yaml'

    def test_crd_gate(self):
        "Test cross-repo dependencies"
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'B')

        A.addApproval('Code-Review', 2)

        AM2 = self.fake_gerrit.addFakeChange('gerrit/project1', 'master',
                                             'AM2')
        AM1 = self.fake_gerrit.addFakeChange('gerrit/project1', 'master',
                                             'AM1')
        AM2.setMerged()
        AM1.setMerged()

        # A -> AM1 -> AM2
        # A Depends-On: B
        # M2 is here to make sure it is never queried.  If it is, it
        # means zuul is walking down the entire history of merged
        # changes.

        A.setDependsOn(AM1, 1)
        AM1.setDependsOn(AM2, 1)

        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)

        for connection in self.scheds.first.connections.connections.values():
            connection.maintainCache([], max_age=0)

        self.executor_server.hold_jobs_in_build = True
        B.addLabel('approved')
        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertEqual(AM2.queried, 0)
        self.assertEqual(A.data['status'], 'MERGED')
        self.assertTrue(B.is_merged)
        self.assertEqual(A.reported, 2)
        self.assertEqual(len(B.comments), 2)

        changes = self.getJobFromHistory(
            'project-merge', 'gerrit/project1').changes
        self.assertEqual(changes, '1,%s 1,1' % B.head_sha)

    def test_crd_branch(self):
        "Test cross-repo dependencies in multiple branches"

        self.create_branch('github/project2', 'mp')
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'B')
        C1 = self.fake_github.openFakePullRequest('github/project2', 'mp',
                                                  'C1')

        A.addApproval('Code-Review', 2)

        # A Depends-On: B+C1
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\nDepends-On: %s\n' % (
            A.subject, B.url, C1.url)

        self.executor_server.hold_jobs_in_build = True
        B.addLabel('approved')
        C1.addLabel('approved')
        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'MERGED')
        self.assertTrue(B.is_merged)
        self.assertTrue(C1.is_merged)
        self.assertEqual(A.reported, 2)
        self.assertEqual(len(B.comments), 2)
        self.assertEqual(len(C1.comments), 2)

        changes = self.getJobFromHistory(
            'project-merge', 'gerrit/project1').changes
        self.assertEqual(changes, '1,%s 2,%s 1,1' %
                         (B.head_sha, C1.head_sha))

    def test_crd_gate_reverse(self):
        "Test reverse cross-repo dependencies"
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'B')
        A.addApproval('Code-Review', 2)

        # A Depends-On: B

        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)

        self.executor_server.hold_jobs_in_build = True
        A.addApproval('Approved', 1)
        self.fake_github.emitEvent(B.addLabel('approved'))
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'MERGED')
        self.assertTrue(B.is_merged)
        self.assertEqual(A.reported, 2)
        self.assertEqual(len(B.comments), 2)

        changes = self.getJobFromHistory(
            'project-merge', 'gerrit/project1').changes
        self.assertEqual(changes, '1,%s 1,1' %
                         (B.head_sha,))

    def test_crd_cycle(self):
        "Test cross-repo dependency cycles"
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        msg = "Depends-On: %s" % (A.data['url'],)
        B = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'B', body=msg)
        A.addApproval('Code-Review', 2)
        B.addLabel('approved')

        # A -> B -> A (via commit-depends)

        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.assertEqual(A.reported, 1)
        self.assertEqual(
            A.messages[0],
            "Build failed.\n\n\nWarning:\n  "
            "Dependency cycle detected and project github/project2 "
            "doesn't allow circular dependencies\n")
        self.assertEqual(len(B.comments), 0)
        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)

    def test_crd_gate_unknown(self):
        "Test unknown projects in dependent pipeline"
        self.init_repo("github/unknown", tag='init')
        # We also need make the repo known to the fake github as a
        # side-effect of getting the project.
        self.fake_github.source.getProject("github/unknown")
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest('github/unknown', 'master',
                                                 'B')
        A.addApproval('Code-Review', 2)

        # A Depends-On: B
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        event = B.addLabel('approved')
        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        # Unknown projects cannot share a queue with any other since
        # they don't have common jobs with any other (they have no
        # jobs).  Changes which depend on unknown project changes
        # should only report an error.
        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)
        self.assertEqual(A.reported, 1)
        self.assertIn("does not share a change queue", A.messages[-1])
        self.assertEqual(len(B.comments), 0)
        self.assertEqual(len(self.history), 0)

        # Simulate change B being gated outside this layout Set the
        # change merged before submitting the event so that when the
        # event triggers a gerrit query to update the change, we get
        # the information that it was merged.
        B.setMerged('merged')
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 0)

        # Now that B is merged, A should be able to be enqueued and
        # merged.
        self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'MERGED')
        self.assertEqual(A.reported, 3)
        self.assertTrue(B.is_merged)
        self.assertEqual(len(B.comments), 0)

    def test_crd_check(self):
        "Test cross-repo dependencies in independent pipelines"
        self.executor_server.hold_jobs_in_build = True
        self.hold_jobs_in_queue = True
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest(
            'github/project2', 'master', 'B')

        # A Depends-On: B
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()

        self.hold_jobs_in_queue = False
        self.executor_api.release()
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()

        self.assertTrue(self.builds[0].hasChanges(A, B))

        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)
        self.assertEqual(A.reported, 1)
        self.assertEqual(len(B.comments), 0)

        changes = self.getJobFromHistory(
            'project-merge', 'gerrit/project1').changes
        self.assertEqual(changes, '1,%s 1,1' %
                         (B.head_sha,))

        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 0)

    def test_crd_check_duplicate(self):
        "Test duplicate check in independent pipelines"
        self.executor_server.hold_jobs_in_build = True
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest(
            'github/project2', 'master', 'B')
        self.waitUntilSettled()

        # A Depends-On: B
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        # Add two dependent changes...
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.getAllItems('tenant-one', 'check')), 2)

        # ...make sure the live one is not duplicated...
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.getAllItems('tenant-one', 'check')), 2)

        # ...but the non-live one is able to be.
        self.fake_github.emitEvent(B.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(len(self.getAllItems('tenant-one', 'check')), 3)

        # Release jobs in order to avoid races with change A jobs
        # finishing before change B jobs.
        self.orderedRelease()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)
        self.assertEqual(A.reported, 1)
        self.assertEqual(len(B.comments), 1)

        changes = self.getJobFromHistory(
            'project-merge', 'gerrit/project1').changes
        self.assertEqual(changes, '1,%s 1,1' %
                         (B.head_sha,))

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        self.assertEqual(changes, '1,%s' %
                         (B.head_sha,))
        self.assertEqual(len(self.getAllQueues('tenant-one', 'check')), 0)

        self.assertIn('Build succeeded', A.messages[0])

    def _test_crd_check_reconfiguration(self, project1, project2):
        "Test cross-repo dependencies re-enqueued in independent pipelines"

        self.hold_jobs_in_queue = True
        A = self.fake_gerrit.addFakeChange(project1, 'master', 'A')
        B = self.fake_github.openFakePullRequest(project2, 'master', 'B')

        # A Depends-On: B
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()

        self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
        self.waitUntilSettled()

        # Make sure the items still share a change queue, and the
        # first one is not live.
        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 1)
        queue = tenant.layout.pipeline_managers['check'].state.queues[0]
        first_item = queue.queue[0]
        for item in queue.queue:
            self.assertEqual(item.queue, first_item.queue)
        self.assertFalse(first_item.live)
        self.assertTrue(queue.queue[1].live)

        self.hold_jobs_in_queue = False
        self.executor_api.release()
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'NEW')
        self.assertFalse(B.is_merged)
        self.assertEqual(A.reported, 1)
        self.assertEqual(len(B.comments), 0)

        changes = self.getJobFromHistory(
            'project-merge', project1).changes
        self.assertEqual(changes, '1,%s 1,1' %
                         (B.head_sha,))
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 0)

    @skipIfMultiScheduler()
    def test_crd_check_reconfiguration(self):
        self._test_crd_check_reconfiguration(
            'gerrit/project1', 'github/project2')

    @skipIfMultiScheduler()
    def test_crd_undefined_project(self):
        """Test that undefined projects in dependencies are handled for
        independent pipelines"""
        # It's a hack for fake github,
        # as it implies repo creation upon the creation of any change
        self.init_repo("github/unknown", tag='init')
        # We also need make the repo known to the fake github as a
        # side-effect of getting the project.
        self.fake_github.source.getProject("github/unknown")
        self._test_crd_check_reconfiguration('gerrit/project1',
                                             'github/unknown')

    def test_crd_check_transitive(self):
        "Test transitive cross-repo dependencies"
        # Specifically, if A -> B -> C, and C gets a new patchset and
        # A gets a new patchset, ensure the test of A,2 includes B,1
        # and C,2 (not C,1 which would indicate stale data in the
        # cache for B).
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        C = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'C')
        # B Depends-On: C
        msg = "Depends-On: %s" % (C.data['url'],)
        B = self.fake_github.openFakePullRequest(
            'github/project2', 'master', 'B', body=msg)

        # A Depends-On: B
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,1 1,%s 1,1' %
                         (B.head_sha,))

        self.fake_github.emitEvent(B.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,1 1,%s' %
                         (B.head_sha,))

        self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,1')

        C.addPatchset()
        self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(2))
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,2')

        A.addPatchset()
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(2))
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,2 1,%s 1,2' %
                         (B.head_sha,))

    def test_crd_check_unknown(self):
        "Test unknown projects in independent pipeline"
        self.init_repo("github/unknown", tag='init')
        # We also need make the repo known to the fake github as a
        # side-effect of getting the project.
        self.fake_github.source.getProject("github/unknown")
        A = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'A')
        B = self.fake_github.openFakePullRequest(
            'github/unknown', 'master', 'B')
        # A Depends-On: B
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)

        # Make sure zuul has seen an event on B. This is necessary
        # in order to populate our fake github project db.
        self.fake_github.emitEvent(B.getPullRequestEditedEvent())
        # Note we wait until settled here as the event processing for
        # the next event may not have the updated db yet otherwise.
        self.waitUntilSettled()
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()

        self.assertEqual(A.data['status'], 'NEW')
        self.assertEqual(A.reported, 1)
        self.assertFalse(B.is_merged)
        self.assertEqual(len(B.comments), 0)

    def test_crd_cycle_join(self):
        "Test an updated change creates a cycle"
        A = self.fake_github.openFakePullRequest(
            'github/project2', 'master', 'A')

        self.fake_github.emitEvent(A.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(len(A.comments), 1)

        # Create B->A
        B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')
        B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            B.subject, A.url)
        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()

        # Dep is there so zuul should have reported on B
        self.assertEqual(B.reported, 1)

        # Update A to add A->B (a cycle).
        self.fake_github.emitEvent(
            A.editBody('Depends-On: %s\n' % (B.data['url'])))
        self.waitUntilSettled()

        # Dependency cycle injected so zuul should have reported again on A
        self.assertEqual(len(A.comments), 2)

        # Now if we update B to remove the depends-on, everything
        # should be okay.  B; A->B

        B.addPatchset()
        B.data['commitMessage'] = '%s\n' % (B.subject,)
        self.fake_github.emitEvent(A.getPullRequestEditedEvent())
        self.waitUntilSettled()

        # Cycle was removed so now zuul should have reported again on A
        self.assertEqual(len(A.comments), 3)

        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(2))
        self.waitUntilSettled()
        self.assertEqual(B.reported, 2)


class TestGithubToGerritCRD(ZuulTestCase):
    config_file = 'zuul-gerrit-github.conf'
    tenant_config_file = 'config/cross-source/main.yaml'

    def test_crd_gate(self):
        "Test cross-repo dependencies"
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')

        B.addApproval('Code-Review', 2)

        # A Depends-On: B
        A.editBody('Depends-On: %s\n' % (B.data['url']))

        event = A.addLabel('approved')
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')

        for connection in self.scheds.first.connections.connections.values():
            connection.maintainCache([], max_age=0)

        self.executor_server.hold_jobs_in_build = True
        B.addApproval('Approved', 1)
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertTrue(A.is_merged)
        self.assertEqual(B.data['status'], 'MERGED')
        self.assertEqual(len(A.comments), 2)
        self.assertEqual(B.reported, 2)

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        self.assertEqual(changes, '1,1 1,%s' % A.head_sha)

    def test_crd_branch(self):
        "Test cross-repo dependencies in multiple branches"

        self.create_branch('gerrit/project1', 'mp')
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')
        C1 = self.fake_gerrit.addFakeChange('gerrit/project1', 'mp', 'C1')

        B.addApproval('Code-Review', 2)
        C1.addApproval('Code-Review', 2)

        # A Depends-On: B+C1
        A.editBody('Depends-On: %s\nDepends-On: %s\n' % (
            B.data['url'], C1.data['url']))

        self.executor_server.hold_jobs_in_build = True
        B.addApproval('Approved', 1)
        C1.addApproval('Approved', 1)
        self.fake_github.emitEvent(A.addLabel('approved'))
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()
        self.assertTrue(A.is_merged)
        self.assertEqual(B.data['status'], 'MERGED')
        self.assertEqual(C1.data['status'], 'MERGED')
        self.assertEqual(len(A.comments), 2)
        self.assertEqual(B.reported, 2)
        self.assertEqual(C1.reported, 2)

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        self.assertEqual(changes, '1,1 2,1 1,%s' %
                         (A.head_sha,))

    def test_crd_gate_reverse(self):
        "Test reverse cross-repo dependencies"
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')
        B.addApproval('Code-Review', 2)

        # A Depends-On: B
        A.editBody('Depends-On: %s\n' % (B.data['url'],))

        self.fake_github.emitEvent(A.addLabel('approved'))
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')

        self.executor_server.hold_jobs_in_build = True
        A.addLabel('approved')
        self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.release('.*-merge')
        self.waitUntilSettled()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertTrue(A.is_merged)
        self.assertEqual(B.data['status'], 'MERGED')
        self.assertEqual(len(A.comments), 2)
        self.assertEqual(B.reported, 2)

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        self.assertEqual(changes, '1,1 1,%s' %
                         (A.head_sha,))

    def test_crd_cycle(self):
        "Test cross-repo dependency cycles"
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')
        B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            B.subject, A.url)

        B.addApproval('Code-Review', 2)
        B.addApproval('Approved', 1)

        # A -> B -> A (via commit-depends)
        A.editBody('Depends-On: %s\n' % (B.data['url'],))

        self.fake_github.emitEvent(A.addLabel('approved'))
        self.waitUntilSettled()

        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.reported, 0)
        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')

    def test_crd_gate_unknown(self):
        "Test unknown projects in dependent pipeline"
        self.init_repo("gerrit/unknown", tag='init')
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/unknown', 'master', 'B')
        B.addApproval('Code-Review', 2)

        # A Depends-On: B
        A.editBody('Depends-On: %s\n' % (B.data['url'],))

        B.addApproval('Approved', 1)
        event = A.addLabel('approved')
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()

        # Unknown projects cannot share a queue with any other
        # since they don't have common jobs with any other (they have no jobs).
        # Changes which depend on unknown project changes
        # should not be processed in dependent pipeline
        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')
        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.reported, 0)
        self.assertEqual(len(self.history), 0)

        # Simulate change B being gated outside this layout Set the
        # change merged before submitting the event so that when the
        # event triggers a gerrit query to update the change, we get
        # the information that it was merged.
        B.setMerged()
        self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
        self.waitUntilSettled()
        self.assertEqual(len(self.history), 0)

        # Now that B is merged, A should be able to be enqueued and
        # merged.
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()

        self.assertTrue(A.is_merged)
        self.assertEqual(len(A.comments), 3)
        self.assertEqual(B.data['status'], 'MERGED')
        self.assertEqual(B.reported, 0)

    def test_crd_check(self):
        "Test cross-repo dependencies in independent pipelines"
        self.executor_server.hold_jobs_in_build = True
        self.hold_jobs_in_queue = True
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange(
            'gerrit/project1', 'master', 'B')

        # A Depends-On: B
        self.fake_github.emitEvent(
            A.editBody('Depends-On: %s\n' % (B.data['url'],)))
        self.waitUntilSettled()

        self.hold_jobs_in_queue = False
        self.executor_api.release()
        self.waitUntilSettled()

        self.executor_server.release('.*-merge')
        self.waitUntilSettled()

        self.assertTrue(self.builds[0].hasChanges(A, B))

        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')
        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.reported, 0)

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        self.assertEqual(changes, '1,1 1,%s' %
                         (A.head_sha,))

        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 0)

    def test_crd_check_duplicate(self):
        "Test duplicate check in independent pipelines"
        self.executor_server.hold_jobs_in_build = True
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange(
            'gerrit/project1', 'master', 'B')

        # A Depends-On: B
        event = A.editBody('Depends-On: %s\n' % (B.data['url'],))
        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')

        # Add two dependent changes...
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()
        self.assertEqual(len(self.getAllItems('tenant-one', 'check')), 2)

        # ...make sure the live one is not duplicated...
        self.fake_github.emitEvent(A.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(len(self.getAllItems('tenant-one', 'check')), 2)

        # ...but the non-live one is able to be.
        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(len(self.getAllItems('tenant-one', 'check')), 3)

        # Release jobs in order to avoid races with change A jobs
        # finishing before change B jobs.
        self.orderedRelease()
        self.executor_server.hold_jobs_in_build = False
        self.executor_server.release()
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')
        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.reported, 1)

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        self.assertEqual(changes, '1,1 1,%s' %
                         (A.head_sha,))

        changes = self.getJobFromHistory(
            'project-merge', 'gerrit/project1').changes
        self.assertEqual(changes, '1,1')
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 0)

        self.assertIn('Build succeeded', A.comments[0])

    def _test_crd_check_reconfiguration(self, project1, project2):
        "Test cross-repo dependencies re-enqueued in independent pipelines"

        self.hold_jobs_in_queue = True
        A = self.fake_github.openFakePullRequest(project1, 'master', 'A')
        B = self.fake_gerrit.addFakeChange(project2, 'master', 'B')

        # A Depends-On: B
        self.fake_github.emitEvent(
            A.editBody('Depends-On: %s\n' % (B.data['url'],)))
        self.waitUntilSettled()

        self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
        self.waitUntilSettled()

        # Make sure the items still share a change queue, and the
        # first one is not live.
        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 1)
        queue = tenant.layout.pipeline_managers['check'].state.queues[0]
        first_item = queue.queue[0]
        for item in queue.queue:
            self.assertEqual(item.queue, first_item.queue)
        self.assertFalse(first_item.live)
        self.assertTrue(queue.queue[1].live)

        self.hold_jobs_in_queue = False
        self.executor_api.release()
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')
        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.reported, 0)

        changes = self.getJobFromHistory(
            'project-merge', project1).changes
        self.assertEqual(changes, '1,1 1,%s' %
                         (A.head_sha,))
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 0)

    @skipIfMultiScheduler()
    def test_crd_check_reconfiguration(self):
        self._test_crd_check_reconfiguration(
            'github/project2', 'gerrit/project1')

    @skipIfMultiScheduler()
    def test_crd_undefined_project(self):
        """Test that undefined projects in dependencies are handled for
        independent pipelines"""
        # It's a hack for fake gerrit,
        # as it implies repo creation upon the creation of any change
        self.init_repo("gerrit/unknown", tag='init')
        self._test_crd_check_reconfiguration('github/project2',
                                             'gerrit/unknown')

    def test_crd_other_tenant_project(self):
        """Test that undefined projects in dependencies are handled for
        independent pipelines"""
        self.hold_jobs_in_queue = True
        A = self.fake_github.openFakePullRequest('github/project2',
                                                 'master', 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/project2', 'master', 'B')

        # A Depends-On: B
        gh_event = A.editBody('Depends-On: %s\n' % (B.data['url'],))
        self.fake_github.emitEvent(gh_event)
        self.waitUntilSettled()

        self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
        self.waitUntilSettled()

        # Make sure the items still share a change queue, and the
        # first one is not live.
        tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 1)
        queue = tenant.layout.pipeline_managers['check'].state.queues[0]
        first_item = queue.queue[0]
        for item in queue.queue:
            self.assertEqual(item.queue, first_item.queue)
        self.assertFalse(first_item.live)
        self.assertTrue(queue.queue[1].live)

        self.hold_jobs_in_queue = False
        self.executor_api.release()
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(B.data['status'], 'NEW')
        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.reported, 0)

        changes = self.getJobFromHistory(
            'project-merge', 'github/project2').changes
        expected_changes = f'1,1 1,{A.head_sha}'
        self.assertEqual(changes, expected_changes)
        self.assertEqual(
            len(tenant.layout.pipeline_managers['check'].state.queues), 0)

        # Re-check the same PR again
        self.fake_github.emitEvent(gh_event)
        self.waitUntilSettled()

        self.assertHistory([
            dict(name='project-merge', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project-test1', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project-test2', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project1-project2-integration', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project-merge', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project-test1', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project1-project2-integration', result='SUCCESS',
                 changes=expected_changes),
            dict(name='project-test2', result='SUCCESS',
                 changes=expected_changes),
        ], ordered=False)

    def test_crd_check_transitive(self):
        "Test transitive cross-repo dependencies"
        # Specifically, if A -> B -> C, and C gets a new patchset and
        # A gets a new patchset, ensure the test of A,2 includes B,1
        # and C,2 (not C,1 which would indicate stale data in the
        # cache for B).
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange('gerrit/project1', 'master', 'B')
        C = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'C')

        # B Depends-On: C
        B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            B.subject, C.url)

        # A Depends-On: B
        self.fake_github.emitEvent(
            A.editBody('Depends-On: %s\n' % (B.data['url'],)))
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,%s 1,1 1,%s' %
                         (C.head_sha, A.head_sha))

        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,%s 1,1' %
                         (C.head_sha,))

        self.fake_github.emitEvent(C.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,%s' %
                         (C.head_sha,))

        new_c_head = C.head_sha
        C.addCommit()
        old_c_head = C.head_sha
        self.assertNotEqual(old_c_head, new_c_head)
        self.fake_github.emitEvent(C.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,%s' %
                         (C.head_sha,))

        new_a_head = A.head_sha
        A.addCommit()
        old_a_head = A.head_sha
        self.assertNotEqual(old_a_head, new_a_head)
        self.fake_github.emitEvent(A.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(self.history[-1].changes, '2,%s 1,1 1,%s' %
                         (C.head_sha, A.head_sha,))

    def test_crd_check_unknown(self):
        "Test unknown projects in independent pipeline"
        self.init_repo("gerrit/unknown", tag='init')
        A = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'A')
        B = self.fake_gerrit.addFakeChange(
            'gerrit/unknown', 'master', 'B')

        # A Depends-On: B
        event = A.editBody('Depends-On: %s\n' % (B.data['url'],))

        # Make sure zuul has seen an event on B.
        self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
        # Note we wait until settled here as the event processing for
        # the next event may not have the updated db yet otherwise.
        self.waitUntilSettled()
        self.fake_github.emitEvent(event)
        self.waitUntilSettled()

        self.assertFalse(A.is_merged)
        self.assertEqual(len(A.comments), 1)
        self.assertEqual(B.data['status'], 'NEW')
        self.assertEqual(B.reported, 0)

    def test_crd_cycle_join(self):
        "Test an updated change creates a cycle"
        A = self.fake_gerrit.addFakeChange(
            'gerrit/project1', 'master', 'A')

        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()
        self.assertEqual(A.reported, 1)

        # Create B->A
        B = self.fake_github.openFakePullRequest('github/project2', 'master',
                                                 'B')
        self.fake_github.emitEvent(
            B.editBody('Depends-On: %s\n' % (A.data['url'],)))
        self.waitUntilSettled()

        # Dep is there so zuul should have reported on B
        self.assertEqual(len(B.comments), 1)

        # Update A to add A->B (a cycle).
        A.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % (
            A.subject, B.url)
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()

        # Dependency cycle injected so zuul should have reported again on A
        self.assertEqual(A.reported, 2)

        # Now if we update B to remove the depends-on, everything
        # should be okay.  B; A->B

        B.addCommit()
        B.editBody('')
        self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
        self.waitUntilSettled()

        # Cycle was removed so now zuul should have reported again on A
        self.assertEqual(A.reported, 3)

        self.fake_github.emitEvent(B.getPullRequestEditedEvent())
        self.waitUntilSettled()
        self.assertEqual(len(B.comments), 2)
