# Copyright 2015 BMW Car IT GmbH # Copyright 2023 Acme Gating, LLC # # 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. import os import threading import time import textwrap from unittest import mock import zuul.model import tests.base from tests.base import ( AnsibleZuulTestCase, BaseTestCase, ZuulTestCase, gerrit_config, iterate_timeout, okay_tracebacks, simple_layout, skipIfMultiScheduler, ) from zuul.lib import strings from zuul.driver.gerrit import GerritDriver from zuul.driver.gerrit.gerritconnection import ( ChangeNetworkConflict, GerritConnection, PeekQueue, ) import git import paramiko import testtools FIXTURE_DIR = os.path.join(tests.base.FIXTURE_DIR, 'gerrit') def read_fixture(file): with open('%s/%s' % (FIXTURE_DIR, file), 'r') as fixturefile: lines = fixturefile.readlines() command = lines[0].replace('\n', '') value = ''.join(lines[1:]) return command, value def read_fixtures(files): calls = [] values = [] for fixture_file in files: command, value = read_fixture(fixture_file) calls.append(mock.call(command)) values.append([value, '']) return calls, values class TestGerrit(BaseTestCase): @mock.patch('zuul.driver.gerrit.gerritconnection.GerritConnection._ssh') def run_query(self, files, expected_patches, _ssh_mock): gerrit_config = { 'user': 'gerrit', 'server': 'localhost', } driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) calls, values = read_fixtures(files) _ssh_mock.side_effect = values result = gerrit.simpleQuery('project:zuul/zuul') _ssh_mock.assert_has_calls(calls) self.assertEqual(len(calls), _ssh_mock.call_count, '_ssh should be called %d times' % len(calls)) self.assertIsNotNone(result, 'Result is not none') self.assertEqual(len(result), expected_patches, 'There must be %d patches.' % expected_patches) def test_simple_query_pagination_new(self): files = ['simple_query_pagination_new_1', 'simple_query_pagination_new_2'] expected_patches = 5 self.run_query(files, expected_patches) def test_simple_query_pagination_old(self): files = ['simple_query_pagination_old_1', 'simple_query_pagination_old_2', 'simple_query_pagination_old_3'] expected_patches = 5 self.run_query(files, expected_patches) def test_ref_name_check_rules(self): # See man git-check-ref-format for the rules referenced here test_strings = [ ('refs/heads/normal', True), ('refs/heads/.bad', False), # rule 1 ('refs/heads/bad.lock', False), # rule 1 ('refs/heads/good.locked', True), ('refs/heads/go.od', True), ('refs/heads//bad', False), # rule 6 ('refs/heads/b?d', False), # rule 5 ('refs/heads/b[d', False), # rule 5 ('refs/heads/b..ad', False), # rule 3 ('bad', False), # rule 2 ('refs/heads/\nbad', False), # rule 4 ('/refs/heads/bad', False), # rule 6 ('refs/heads/bad/', False), # rule 6 ('refs/heads/bad.', False), # rule 7 ('.refs/heads/bad', False), # rule 1 ('refs/he@{ads/bad', False), # rule 8 ('@', False), # rule 9 ('refs\\heads/bad', False) # rule 10 ] for ref, accepted in test_strings: self.assertEqual( accepted, GerritConnection._checkRefFormat(ref), ref + ' shall be ' + ('accepted' if accepted else 'rejected')) def test_getGitURL(self): gerrit_config = { 'user': 'gerrit', 'server': 'localhost', 'password': '1/badpassword', } # The 1/ in the password ensures we test the url encoding # path; this is the format of password we get from # googlesource.com. driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) project = gerrit.source.getProject('org/project') url = gerrit.source.getGitUrl(project) self.assertEqual( 'https://gerrit:1%2Fbadpassword@localhost/a/org/project', url) def test_git_over_ssh_getGitURL(self): gerrit_config = { 'user': 'gerrit', 'server': 'localhost', 'password': '1/badpassword', 'git_over_ssh': 'true', } # The 1/ in the password ensures we test the url encoding # path; this is the format of password we get from # googlesource.com. driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) project = gerrit.source.getProject('org/project') url = gerrit.source.getGitUrl(project) self.assertEqual( 'ssh://gerrit@localhost:29418/org/project', url) def test_ssh_server_getGitURL(self): gerrit_config = { 'user': 'gerrit', 'server': 'otherserver', 'password': '1/badpassword', 'ssh_server': 'localhost', 'git_over_ssh': 'true', } # The 1/ in the password ensures we test the url encoding # path; this is the format of password we get from # googlesource.com. driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) project = gerrit.source.getProject('org/project') url = gerrit.source.getGitUrl(project) self.assertEqual( 'ssh://gerrit@localhost:29418/org/project', url) def test_ssh_exec_timeout(self): with mock.patch('paramiko.SSHClient') as mock_ssh_client: mock_client = mock.Mock() mock_ssh_client.return_value = mock_client stdin = mock.Mock() stdout = mock.Mock() stdout.channel = mock.Mock() stdout.channel.recv_exit_status.return_value = 0 stderr = mock.Mock() mock_client.exec_command.return_value = (stdin, stdout, stderr) gerrit_config = { 'user': 'gerrit', 'server': 'localhost', } driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) gerrit._ssh("echo hi") expected = [ mock.call('echo hi', timeout=30), ] self.assertEqual(expected, mock_client.exec_command.mock_calls) with mock.patch('paramiko.SSHClient') as mock_ssh_client: mock_client = mock.Mock() mock_ssh_client.return_value = mock_client mock_client.exec_command.side_effect =\ paramiko.buffered_pipe.PipeTimeout() gerrit_config = { 'user': 'gerrit', 'server': 'localhost', } driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) with testtools.ExpectedException( paramiko.buffered_pipe.PipeTimeout): gerrit._ssh("echo hi") expected = [ mock.call('echo hi', timeout=30), mock.call('echo hi', timeout=30), ] self.assertEqual(expected, mock_client.exec_command.mock_calls) class TestGerritWeb(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' tenant_config_file = 'config/single-tenant/main.yaml' def test_jobs_executed(self): "Test that jobs are executed and a change is merged" A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertEqual(self.getJobFromHistory('project-merge').result, 'SUCCESS') self.assertEqual(self.getJobFromHistory('project-test1').result, 'SUCCESS') self.assertEqual(self.getJobFromHistory('project-test2').result, 'SUCCESS') self.assertEqual(A.data['status'], 'MERGED') self.assertEqual(A.reported, 2) self.assertEqual(self.getJobFromHistory('project-test1').node, 'label1') self.assertEqual(self.getJobFromHistory('project-test2').node, 'label1') def test_dynamic_line_comment(self): in_repo_conf = textwrap.dedent( """ - job: name: garbage-job garbage: True """) file_dict = {'.zuul.yaml': in_repo_conf} A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', files=file_dict) self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1") self.assertEqual(A.patchsets[0]['approvals'][0]['__tag'], "autogenerated:zuul:check") self.assertIn('Zuul encountered a syntax error', A.messages[0]) comments = sorted(A.comments, key=lambda x: x['line']) self.assertEqual(comments[0], {'file': '.zuul.yaml', 'line': 4, 'message': "extra keys not allowed @ " "data['garbage']", 'range': {'end_character': 0, 'end_line': 4, 'start_character': 2, 'start_line': 2}, 'reviewer': {'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins'}} ) def test_message_too_long(self): in_repo_conf = textwrap.dedent( """ - job: name: garbage-job description: %s garbage: True """ ) % ('x' * 16384) file_dict = {'.zuul.yaml': in_repo_conf} A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', files=file_dict) self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertEqual(A.patchsets[0]['approvals'][0]['value'], "-1") self.assertEqual(A.patchsets[0]['approvals'][0]['__tag'], "autogenerated:zuul:check") self.assertIn('... (truncated)', A.messages[0]) def test_dependent_dynamic_line_comment(self): in_repo_conf = textwrap.dedent( """ - job: name: garbage-job garbage: True """) file_dict = {'.zuul.yaml': in_repo_conf} A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', files=file_dict) B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') B.data['commitMessage'] = '%s\n\nDepends-On: %s\n' % ( B.subject, A.data['id']) self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertEqual(B.patchsets[0]['approvals'][0]['value'], "-1") self.assertIn('This change depends on a change ' 'with an invalid configuration', B.messages[0]) self.assertEqual(B.comments, []) @simple_layout('layouts/single-file-matcher.yaml') def test_single_file(self): # HTTP requests don't return a commit_msg entry in the files # list, but the rest of zuul always expects one. This test # returns a single file to exercise the single-file code path # in the files matcher. files = {'README': 'please!\n'} change = self.fake_gerrit.addFakeChange('org/project', 'master', 'test irrelevant-files', files=files) self.fake_gerrit.addEvent(change.getPatchsetCreatedEvent(1)) self.waitUntilSettled() tested_change_ids = [x.changes[0] for x in self.history if x.name == 'project-test-irrelevant-files'] self.assertEqual([], tested_change_ids) def test_recheck(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertEqual(3, len(self.history)) self.fake_gerrit.addEvent(A.getChangeCommentEvent(1)) self.waitUntilSettled() self.assertEqual(3, len(self.history)) self.fake_gerrit.addEvent(A.getChangeCommentEvent(1, 'recheck')) self.waitUntilSettled() self.assertEqual(6, len(self.history)) self.fake_gerrit.addEvent(A.getChangeCommentEvent(1, patchsetcomment='recheck')) self.waitUntilSettled() self.assertEqual(9, len(self.history)) self.fake_gerrit.addEvent(A.getChangeCommentEvent(1, patchsetcomment='do not recheck')) self.waitUntilSettled() self.assertEqual(9, len(self.history)) def test_submitted_together_git(self): # This tests that the circular dependency handling for submit # whole topic doesn't activate for changes which are only in a # git dependency. A = self.fake_gerrit.addFakeChange('org/project1', "master", "A") B = self.fake_gerrit.addFakeChange('org/project1', "master", "B") C = self.fake_gerrit.addFakeChange('org/project1', "master", "C") D = self.fake_gerrit.addFakeChange('org/project1', "master", "D") E = self.fake_gerrit.addFakeChange('org/project1', "master", "E") F = self.fake_gerrit.addFakeChange('org/project1', "master", "F") G = self.fake_gerrit.addFakeChange('org/project1', "master", "G") G.setDependsOn(F, 1) F.setDependsOn(E, 1) E.setDependsOn(D, 1) D.setDependsOn(C, 1) C.setDependsOn(B, 1) B.setDependsOn(A, 1) self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertEqual(len(C.patchsets[-1]["approvals"]), 1) self.assertEqual(C.patchsets[-1]["approvals"][0]["type"], "Verified") self.assertEqual(C.patchsets[-1]["approvals"][0]["value"], "1") self.assertEqual(A.queried, 1) self.assertEqual(B.queried, 1) self.assertEqual(C.queried, 1) self.assertEqual(D.queried, 1) self.assertEqual(E.queried, 1) self.assertEqual(F.queried, 1) self.assertEqual(G.queried, 1) self.assertHistory([ dict(name="project-merge", result="SUCCESS", changes="1,1 2,1 3,1"), dict(name="project-test1", result="SUCCESS", changes="1,1 2,1 3,1"), dict(name="project-test2", result="SUCCESS", changes="1,1 2,1 3,1"), dict(name="project1-project2-integration", result="SUCCESS", changes="1,1 2,1 3,1"), ], ordered=False) def test_submit_failure(self): # Test that we log the reason for a submit failure (403 error) self.fake_gerrit._fake_submit_permission = False A = self.fake_gerrit.addFakeChange('org/project1', "master", "A") A.addApproval('Code-Review', 2) with self.assertLogs('zuul.test.FakeGerritConnection', level='INFO' ) as full_logs: self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.log.debug("Full logs:") for x in full_logs.output: self.log.debug(x) self.assertRegexInList( r'Error submitting data to gerrit on attempt 3: ' 'Received response 403: submit not permitted', full_logs.output) self.assertEqual(A.data['status'], 'NEW') @okay_tracebacks('_handleEvent') def test_event_failure(self): # Test that a failure to process a Gerrit event is handled # correctly A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') event = A.getPatchsetCreatedEvent(1) self.fake_gerrit._fake_return_api_error = True self.fake_gerrit.addEvent(event) # If the event is not processed, then we will never settle and # the test will time-out. self.waitUntilSettled() class TestFileComments(AnsibleZuulTestCase): config_file = 'zuul-gerrit-web.conf' tenant_config_file = 'config/gerrit-file-comments/main.yaml' def test_file_comments(self): A = self.fake_gerrit.addFakeChange( 'org/project', 'master', 'A', files={'path/to/file.py': 'test1', 'otherfile.txt': 'test2', }) A.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertEqual(self.getJobFromHistory('file-comments').result, 'SUCCESS') self.assertEqual(self.getJobFromHistory('file-comments-error').result, 'SUCCESS') self.assertEqual(len(A.comments), 7) comments = sorted(A.comments, key=lambda x: (x['file'], x['line'])) self.assertEqual( comments[0], { 'file': '/COMMIT_MSG', 'line': 1, 'message': 'commit message comment', 'reviewer': { 'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins' }, }, ) self.assertEqual( comments[1], { 'file': 'otherfile.txt', 'line': 21, 'message': 'This is a much longer message.\n\n' 'With multiple paragraphs.\n', 'reviewer': { 'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins' }, }, ) self.assertEqual( comments[2], { "file": "path/to/file.py", "line": 2, "message": "levels are ignored by gerrit", "reviewer": { "email": "zuul@example.com", "name": "Zuul", "username": "jenkins", }, }, ) self.assertEqual( comments[3], { "file": "path/to/file.py", "line": 21, "message": ( "A second zuul return value using the same file should not" "\noverride the first result, but both should be merged.\n" ), "reviewer": { "email": "zuul@example.com", "name": "Zuul", "username": "jenkins", }, }, ) self.assertEqual( comments[4], { 'file': 'path/to/file.py', 'line': 42, 'message': 'line too long', 'reviewer': { 'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins' }, }, ) self.assertEqual( comments[5], { "file": "path/to/file.py", "line": 42, "message": ( "A second comment applied to the same line in the same " "file\nshould also be added to the result.\n" ), "reviewer": { "email": "zuul@example.com", "name": "Zuul", "username": "jenkins", } } ) self.assertEqual(comments[6], {'file': 'path/to/file.py', 'line': 82, 'message': 'line too short', 'reviewer': {'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins'}} ) self.assertIn('expected a dictionary', A.messages[0], "A should have a validation error reported") self.assertIn('invalid file missingfile.txt', A.messages[0], "A should have file error reported") def test_merge_commit_file_comments(self): self.create_branch('org/project', 'stable/queens') self.fake_gerrit.addEvent( self.fake_gerrit.getFakeBranchCreatedEvent( 'org/project', 'stable/queens')) self.waitUntilSettled() A = self.fake_gerrit.addFakeChange('org/project', 'stable/queens', 'A', files={'path/to/file.py': 'test1', 'otherfile.txt': 'test2', }) A.setMerged() self.fake_gerrit.addEvent(A.getChangeMergedEvent()) self.waitUntilSettled() upstream_path = os.path.join(self.upstream_root, 'org/project') upstream_repo = git.Repo(upstream_path) master_sha = upstream_repo.heads.master.commit.hexsha B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B', merge_parents=[ master_sha, A.patchsets[-1]['revision'], ], files={'path/to/file.py': 'test1', 'otherfile.txt': 'test2', }) self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='file-comments', result='SUCCESS', changes='2,1'), dict(name='file-comments-error', result='SUCCESS', changes='2,1'), ], ordered=False) self.assertEqual(len(B.comments), 1) comments = sorted(B.comments, key=lambda x: (x['file'], x['line'])) self.assertEqual( comments[0], { 'file': '/COMMIT_MSG', 'line': 1, 'message': 'commit message comment', 'reviewer': { 'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins' }, }, ) self.assertIn('invalid file missingfile.txt', B.messages[0], "A should have file error reported") class TestChecksApi(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' @simple_layout('layouts/gerrit-checks.yaml') def test_check_pipeline(self): B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setDependsOn(B, 1) A.setCheck('zuul:check', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.assertEqual(A.checks_history[0]['zuul:check']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[1]['zuul:check']['state'], 'SCHEDULED') self.assertEqual( A.checks_history[1]['zuul:check']['url'], 'http://zuul.example.com/t/tenant-one/status/change/2,1') self.assertEqual(A.checks_history[2]['zuul:check']['state'], 'RUNNING') self.assertEqual( A.checks_history[2]['zuul:check']['url'], 'http://zuul.example.com/t/tenant-one/status/change/2,1') self.assertEqual(A.checks_history[3]['zuul:check']['state'], 'SUCCESSFUL') self.assertTrue( A.checks_history[3]['zuul:check']['url'].startswith( 'http://zuul.example.com/t/tenant-one/buildset/')) self.assertEqual(len(A.checks_history), 4) self.assertTrue(isinstance( A.checks_history[3]['zuul:check']['started'], str)) self.assertTrue(isinstance( A.checks_history[3]['zuul:check']['finished'], str)) self.assertTrue( A.checks_history[3]['zuul:check']['finished'] > A.checks_history[3]['zuul:check']['started']) self.assertEqual(A.checks_history[3]['zuul:check']['message'], 'Change passed all voting jobs') self.assertHistory([ dict(name='test-job', result='SUCCESS', changes='1,1 2,1')]) self.assertEqual(A.reported, 0, "no messages should be reported") self.assertEqual(A.messages, [], "no messages should be reported") # Make sure B was never updated self.assertEqual(len(B.checks_history), 0) @simple_layout('layouts/gerrit-checks.yaml') def test_gate_pipeline(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) A.addApproval('Approved', 1) A.setCheck('zuul:gate', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.assertEqual(A.checks_history[0]['zuul:gate']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[1]['zuul:gate']['state'], 'SCHEDULED') self.assertEqual(A.checks_history[2]['zuul:gate']['state'], 'RUNNING') self.assertEqual(A.checks_history[3]['zuul:gate']['state'], 'SUCCESSFUL') self.assertEqual(len(A.checks_history), 4) self.assertHistory([ dict(name='test-job', result='SUCCESS', changes='1,1')]) self.assertEqual(A.data['status'], 'MERGED') self.assertEqual(A.reported, 2, "start and success messages should be reported") @simple_layout('layouts/gerrit-checks-scheme.yaml') @skipIfMultiScheduler() # This is the only gerrit checks API test which is failing because # it uses a check scheme rather than an UUID. The scheme must first # be evaluated and mapped to an UUID. # This shouldn't be a problem in production as the evaluation takes # place on the gerrit webserver. However, in the tests we get a # dedicated (fake) gerrit webserver for each fake gerrrit # connection. Since each scheduler gets a new connection, only one # of those webservers will be aware of the check. If any other # webserver tries to evaluate the check it will fail with # "Unable to find matching checker". def test_check_pipeline_scheme(self): self.fake_gerrit.addFakeChecker(uuid='zuul_check:abcd', repository='org/project', status='ENABLED') self.scheds.execute(lambda app: app.sched.reconfigure(app.config)) self.waitUntilSettled() A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setCheck('zuul_check:abcd', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.assertEqual(A.checks_history[0]['zuul_check:abcd']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[1]['zuul_check:abcd']['state'], 'SCHEDULED') self.assertEqual(A.checks_history[2]['zuul_check:abcd']['state'], 'RUNNING') self.assertEqual(A.checks_history[3]['zuul_check:abcd']['state'], 'SUCCESSFUL') self.assertEqual(len(A.checks_history), 4) self.assertHistory([ dict(name='test-job', result='SUCCESS', changes='1,1')]) @simple_layout('layouts/gerrit-checks-nojobs.yaml') def test_no_jobs(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setCheck('zuul:check', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.assertEqual(A.checks_history[0]['zuul:check']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[1]['zuul:check']['state'], 'SCHEDULED') self.assertEqual(A.checks_history[2]['zuul:check']['state'], 'NOT_RELEVANT') self.assertEqual(len(A.checks_history), 3) self.assertEqual(A.data['status'], 'NEW') @simple_layout('layouts/gerrit-checks.yaml') def test_config_error(self): # Test that line comments are reported on config errors in_repo_conf = textwrap.dedent( """ - project: check: jobs: - bad-job """) file_dict = {'.zuul.yaml': in_repo_conf} A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', files=file_dict) A.setCheck('zuul:check', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.assertEqual(A.checks_history[0]['zuul:check']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[1]['zuul:check']['state'], 'SCHEDULED') self.assertEqual(A.checks_history[2]['zuul:check']['state'], 'FAILED') self.assertEqual(len(A.checks_history), 3) comments = sorted(A.comments, key=lambda x: x['line']) self.assertEqual(comments[0], {'file': '.zuul.yaml', 'line': 5, 'message': 'Job bad-job not defined', 'range': {'end_character': 0, 'end_line': 5, 'start_character': 2, 'start_line': 2}, 'reviewer': {'email': 'zuul@example.com', 'name': 'Zuul', 'username': 'jenkins'}} ) self.assertEqual(A.reported, 0, "no messages should be reported") self.assertEqual(A.messages, [], "no messages should be reported") @simple_layout('layouts/gerrit-checks.yaml') def test_new_patchset(self): self.executor_server.hold_jobs_in_build = True A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setCheck('zuul:check', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.assertEqual(A.checks_history[0]['zuul:check']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[1]['zuul:check']['state'], 'SCHEDULED') self.assertEqual(A.checks_history[2]['zuul:check']['state'], 'RUNNING') self.assertEqual(len(A.checks_history), 3) A.addPatchset() A.setCheck('zuul:check', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled() self.executor_server.hold_jobs_in_build = False self.executor_server.release() self.waitUntilSettled() self.log.info(A.checks_history) self.assertEqual(A.checks_history[3]['zuul:check']['state'], 'NOT_STARTED') self.assertEqual(A.checks_history[4]['zuul:check']['state'], 'SCHEDULED') self.assertEqual(A.checks_history[5]['zuul:check']['state'], 'RUNNING') self.assertEqual(A.checks_history[6]['zuul:check']['state'], 'SUCCESSFUL') self.assertEqual(len(A.checks_history), 7) self.assertHistory([ dict(name='test-job', result='ABORTED', changes='1,1'), dict(name='test-job', result='SUCCESS', changes='1,2'), ], ordered=False) class TestPolling(ZuulTestCase): config_file = 'zuul-gerrit-no-stream.conf' @simple_layout('layouts/gerrit-checks.yaml') def test_config_update(self): # Test that the config is updated via polling when a change # merges without stream-events enabled. in_repo_conf = textwrap.dedent( """ - job: name: test-job2 parent: test-job - project: check: jobs: - test-job2 """) file_dict = {'.zuul.yaml': in_repo_conf} A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', files=file_dict) A.setMerged() self.waitForPoll('gerrit') self.waitUntilSettled('poll 1') B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.setCheck('zuul:check', reset=True) self.waitForPoll('gerrit') self.waitUntilSettled('poll 2') self.assertEqual(B.checks_history[0]['zuul:check']['state'], 'NOT_STARTED') self.assertEqual(B.checks_history[1]['zuul:check']['state'], 'SCHEDULED') self.assertEqual(B.checks_history[2]['zuul:check']['state'], 'RUNNING') self.assertEqual(B.checks_history[3]['zuul:check']['state'], 'SUCCESSFUL') self.assertEqual(len(B.checks_history), 4) self.assertHistory([ dict(name='test-job', result='SUCCESS', changes='2,1'), dict(name='test-job2', result='SUCCESS', changes='2,1'), ], ordered=False) # There may be an extra change-merged event because we poll # too often during the tests. self.waitForPoll('gerrit') self.waitUntilSettled('poll 3') @simple_layout('layouts/gerrit-poll-post.yaml') def test_post(self): # Test that ref-updated events trigger post jobs. self.waitUntilSettled() # Wait for an initial poll to get the original sha. self.waitForPoll('gerrit-ref') # Merge a change. self.create_commit('org/project') # Wait for the job to run. self.waitForPoll('gerrit-ref') self.waitUntilSettled() self.assertHistory([ dict(name='post-job', result='SUCCESS'), ]) @simple_layout('layouts/gerrit-poll-post.yaml') def test_tag(self): # Test that ref-updated events trigger post jobs. self.waitUntilSettled() # Wait for an initial poll to get the original sha. self.waitForPoll('gerrit-ref') # Merge a change. self.fake_gerrit.addFakeTag('org/project', 'master', 'foo') # Wait for the job to run. self.waitForPoll('gerrit-ref') self.waitUntilSettled() self.assertHistory([ dict(name='tag-job', result='SUCCESS'), ]) class TestWrongConnection(ZuulTestCase): config_file = 'zuul-connections-multiple-gerrits.conf' tenant_config_file = 'config/wrong-connection-in-pipeline/main.yaml' def test_wrong_connection(self): # Test if the wrong connection is configured in a gate pipeline # Our system has two gerrits, and we have configured a gate # pipeline to trigger on the "review_gerrit" connection, but # report (and merge) via "another_gerrit". A = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) self.fake_review_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() B = self.fake_review_gerrit.addFakeChange('org/project', 'master', 'B') # Let's try this as if the change was merged (say, via another tenant). B.setMerged() B.addApproval('Code-Review', 2) self.fake_review_gerrit.addEvent(B.addApproval('Approved', 1)) self.waitUntilSettled() self.assertEqual(A.reported, 0) self.assertEqual(B.reported, 0) self.assertHistory([ dict(name='test-job', result='SUCCESS', changes='1,1'), dict(name='test-job', result='SUCCESS', changes='2,1'), ], ordered=False) class TestGerritFake(ZuulTestCase): config_file = "zuul-gerrit-github.conf" tenant_config_file = "config/circular-dependencies/main.yaml" def _make_tuple(self, data): ret = [] for c in data: dep_change = c['number'] dep_ps = c['currentPatchSet']['number'] ret.append((int(dep_change), int(dep_ps))) return sorted(ret) def _get_tuple(self, change_number): ret = [] data = self.fake_gerrit.get( f'changes/{change_number}/submitted_together') for c in data: dep_change = c['_number'] dep_ps = c['revisions'][c['current_revision']]['_number'] ret.append((dep_change, dep_ps)) return sorted(ret) def test_submitted_together_normal(self): # Test that the fake submitted together endpoint returns # expected data # This test verifies behavior with submitWholeTopic=False # A single change A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') data = self._get_tuple(1) self.assertEqual(data, []) # A dependent series (B->A) B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.setDependsOn(A, 1) data = self._get_tuple(2) self.assertEqual(data, [(1, 1), (2, 1)]) # A topic cycle self.fake_gerrit.addFakeChange('org/project', 'master', 'C1', topic='test-topic') self.fake_gerrit.addFakeChange('org/project', 'master', 'C2', topic='test-topic') data = self._get_tuple(3) self.assertEqual(data, []) @gerrit_config(submit_whole_topic=True) def test_submitted_together_whole_topic(self): # Test that the fake submitted together endpoint returns # expected data # This test verifies behavior with submitWholeTopic=True # A single change A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') data = self._get_tuple(1) self.assertEqual(data, []) # A dependent series (B->A) B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.setDependsOn(A, 1) data = self._get_tuple(2) self.assertEqual(data, [(1, 1), (2, 1)]) # The Gerrit connection method filters out the queried change # A topic cycle self.fake_gerrit.addFakeChange('org/project', 'master', 'C1', topic='test-topic') self.fake_gerrit.addFakeChange('org/project', 'master', 'C2', topic='test-topic') data = self._get_tuple(3) self.assertEqual(data, [(3, 1), (4, 1)]) # The Gerrit connection method filters out the queried change # Test also the query used by the GerritConnection: ret = self.fake_gerrit._simpleQuery('status:open topic:test-topic') ret = self._make_tuple(ret) self.assertEqual(ret, [(3, 1), (4, 1)]) class TestGerritConnection(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' tenant_config_file = 'config/single-tenant/main.yaml' def test_zuul_query_ltime(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.setDependsOn(A, 1) # We want the three events below to proceed so that all three # events are processed simultaneously by three threads. We # will allow the first event we submit to complete its change # network and therefore perform all the queries. The other # two events should receive ChangeNetworkConflict errors and # eventually use the results of the first network. To # accomplish that, we: Allow the network to proceed if it # includes change A (this will be our first network) * Wait # for two ChangeNetworkConflict errors to occur permission_barrier = threading.Barrier(2) permission_event = threading.Event() cn_manager = self.fake_gerrit.change_network_manager orig_permission = cn_manager.permissionToProceed def permissionToProceed(future): if permission_event.is_set(): # We're past the critical section, revert to normal # behavior. return orig_permission(future) try: changes = [int(c.stable_id) for c in future.changes] if 1 in changes: # This change network includes change A; this is a # characteristic of the first event, so we let it # through. return orig_permission(future) for _ in iterate_timeout(30, "queries to occur"): # Wait until the first change network has expanded # to include both changes so that it will # conflict. for f in cn_manager.futures: if len(f.changes) > 1: return orig_permission(future) except ChangeNetworkConflict: # The second and third events will conflict; once the # barrier tells us that they have both gotten to this # point, we can allow the first network to finish and # these two threads to resume normal operation (they # will use the results of their failed networks to try # again. permission_barrier.wait() permission_event.set() raise self.patch(self.fake_gerrit.change_network_manager, 'permissionToProceed', permissionToProceed) orig_complete = cn_manager.setComplete def setComplete(future): # The first change network must wait here until the second # and third have seen conflict errors from it, so that it # is not removed from the manager before they see the # conflict. permission_event.wait() return orig_complete(future) self.patch(self.fake_gerrit.change_network_manager, 'setComplete', setComplete) # These are the three events with critical ordering described # above. self.fake_gerrit.addEvent(A.addApproval('Code-Review', 2)) self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) self.fake_gerrit.addEvent(B.addApproval('Code-Review', 2)) self.waitUntilSettled() self.assertHistory([]) # One query for each change in the above cluster of events. self.assertEqual(A.queried, 1) self.assertEqual(B.queried, 1) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name="project-merge", result="SUCCESS", changes="1,1"), dict(name="project-test1", result="SUCCESS", changes="1,1"), dict(name="project-test2", result="SUCCESS", changes="1,1"), dict(name="project-merge", result="SUCCESS", changes="1,1 2,1"), dict(name="project-test1", result="SUCCESS", changes="1,1 2,1"), dict(name="project-test2", result="SUCCESS", changes="1,1 2,1"), ], ordered=False) # One query due to the event on change A, followed by a query # to verify the merge. self.assertEqual(A.queried, 3) # No query for change B necessary since our cache is up to # date with respect for the triggering event. One query to # verify the merge. self.assertEqual(B.queried, 2) self.assertEqual(A.data['status'], 'MERGED') self.assertEqual(B.data['status'], 'MERGED') def test_submit_requirements(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.addApproval('Code-Review', 2) # Set an unsatisfied submit requirement A.setSubmitRequirements([ { "name": "Code-Review", "description": "Disallow self-review", "status": "UNSATISFIED", "is_legacy": False, "submittability_expression_result": { "expression": "label:Code-Review=MAX,user=non_uploader " "AND -label:Code-Review=MIN", "fulfilled": False, "passing_atoms": [], "failing_atoms": [ "label:Code-Review=MAX,user=non_uploader", "label:Code-Review=MIN" ] } }, { "name": "Verified", "status": "UNSATISFIED", "is_legacy": True, "submittability_expression_result": { "expression": "label:Verified=MAX -label:Verified=MIN", "fulfilled": False, "passing_atoms": [], "failing_atoms": [ "label:Verified=MAX", "-label:Verified=MIN" ] } }, ]) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([]) self.assertEqual(A.queried, 1) self.assertEqual(A.data['status'], 'NEW') # Mark the requirement satisfied A.setSubmitRequirements([ { "name": "Code-Review", "description": "Disallow self-review", "status": "SATISFIED", "is_legacy": False, "submittability_expression_result": { "expression": "label:Code-Review=MAX,user=non_uploader " "AND -label:Code-Review=MIN", "fulfilled": False, "passing_atoms": [ "label:Code-Review=MAX,user=non_uploader", ], "failing_atoms": [ "label:Code-Review=MIN" ] } }, { "name": "Verified", "status": "UNSATISFIED", "is_legacy": True, "submittability_expression_result": { "expression": "label:Verified=MAX -label:Verified=MIN", "fulfilled": False, "passing_atoms": [], "failing_atoms": [ "label:Verified=MAX", "-label:Verified=MIN" ] } }, ]) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name="project-merge", result="SUCCESS", changes="1,1"), dict(name="project-test1", result="SUCCESS", changes="1,1"), dict(name="project-test2", result="SUCCESS", changes="1,1"), ], ordered=False) self.assertEqual(A.queried, 3) self.assertEqual(A.data['status'], 'MERGED') def test_submit_missing_labels(self): # This test checks that missing labels will not allow a change to # enqueued. Specifically in this case the Code-Review and Verified is # missing (but Verified is ignored because Zuul will score it) A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([]) # Even adding the Verified score already provided by Zuul will not # cause the change to gate self.fake_gerrit.addEvent(A.addApproval('Verified', 2)) # Reapply this score to trigger gate, which shouldn't happen because of # the still missing Code-Review label self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([]) def test_ref_updated_reconfig(self): # Gerrit emits change-merged events after ref-updated events for the # change; make sure that job configuration changes take effect # for post pipelines that trigger off of ref-updated. in_repo_conf = textwrap.dedent( """ - job: name: new-post-job parent: project-post - project: post: jobs: - new-post-job """) file_dict = {'zuul.yaml': in_repo_conf} A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', files=file_dict) A.setMerged() self.fake_gerrit.addEvent(A.getRefUpdatedEvent()) self.fake_gerrit.addEvent(A.getChangeMergedEvent()) self.waitUntilSettled() self.assertHistory([ dict(name='project-post', result='SUCCESS'), dict(name='new-post-job', result='SUCCESS'), dict(name='project-promote', result='SUCCESS'), ], ordered=False) class TestGerritConnectionPreFilter(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' tenant_config_file = 'config/pre-filter/main.yaml' def test_ignored_events(self): A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A') event_queue = self.fake_gerrit.gerrit_event_connector.event_queue # Reconfigure the second tenant to ensure that it doesn't # delete the pre-filters for the first tenant. tenant = self.scheds.first.sched.abide.tenants.get('tenant-two') (trusted, project1) = tenant.getProject('org/project2') event = zuul.model.TriggerEvent() event.zuul_event_ltime = -1 self.scheds.first.sched.reconfigureTenant( self.scheds.first.sched.abide.tenants['tenant-two'], project1, event) self.waitUntilSettled() # For efficiency, ensure that the total filter length is the # number of filters in the first tenant plus 1 (there is one # unique filter in the second tenant). self.assertEqual(6, len(self.fake_gerrit.watched_event_filters)) # Stop the event connector so it does not pull any events from # the queue self.fake_gerrit.stopEventConnector() # This is ignored unconditionally self.fake_gerrit.addEvent({ "type": "cache-eviction", }) self.assertEqual(0, len(event_queue._listEvents())) # This is ignored for meta refs only self.fake_gerrit.addEvent({ "type": "ref-updated", "submitter": { "name": "User Name", }, "refUpdate": { "oldRev": '0', "newRev": '0', "refName": 'refs/changes/01/1/meta', "project": 'org/project1', } }) self.assertEqual(0, len(event_queue._listEvents())) # This is ignored because the ref doesn't match the pre-filter self.fake_gerrit.addEvent({ "type": "ref-updated", "submitter": { "name": "User Name", }, "refUpdate": { "oldRev": '0', "newRev": '0', "refName": 'refs/something/else', "project": 'org/project1', } }) self.assertEqual(0, len(event_queue._listEvents())) # This is not ignored self.fake_gerrit.addEvent({ "type": "ref-updated", "submitter": { "name": "User Name", }, "refUpdate": { "oldRev": '0', "newRev": 'abcd', "refName": 'refs/tags/1234', "project": 'org/project1', } }) self.assertEqual(1, len(event_queue._listEvents())) # This is not ignored A.setMerged() self.fake_gerrit.addEvent(A.getRefUpdatedEvent()) self.assertEqual(2, len(event_queue._listEvents())) self.fake_gerrit.startEventConnector() self.waitUntilSettled() class TestGerritConnectionReplication(ZuulTestCase): config_file = 'zuul-gerrit-replication.conf' tenant_config_file = 'config/single-tenant/main.yaml' def test_replication_new_patchset(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') self.fake_gerrit.addEvent(A.getPatchsetReplicationStartedEvent(1)) self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) old_wait_timeout = self.wait_timeout self.wait_timeout = 10 with testtools.ExpectedException(Exception, "Timeout waiting for Zuul to settle"): # We expect this exception because ZK queues won't empty until # we either timeout waiting for the replication event or get # the replication event which happens below. self.waitUntilSettled() # In addition to checking that we have inflight items above we # also ensure that we haven't run any jobs yet. self.assertHistory([]) self.wait_timeout = old_wait_timeout # Add a second patchset created event to ensure that we're not going # to wait for that one to replicate. self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1)) # Finally complete replication and ensure everything settles out. self.fake_gerrit.addEvent(A.getPatchsetReplicatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='project-merge', result='SUCCESS', changes='1,1'), dict(name='project-test1', result='SUCCESS', changes='1,1'), dict(name='project-test2', result='SUCCESS', changes='1,1'), dict(name='project-merge', result='SUCCESS', changes='2,1'), dict(name='project-test1', result='SUCCESS', changes='2,1'), dict(name='project-test2', result='SUCCESS', changes='2,1'), dict(name='project1-project2-integration', result='SUCCESS', changes='2,1'), ], ordered=False) def test_replication_change_merged(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setMerged() B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') B.setMerged() self.fake_gerrit.addEvent(A.getChangeMergedReplicationStartedEvent()) self.fake_gerrit.addEvent(A.getChangeMergedEvent()) old_wait_timeout = self.wait_timeout self.wait_timeout = 10 with testtools.ExpectedException(Exception, "Timeout waiting for Zuul to settle"): # We expect this exception because ZK queues won't empty until # we either timeout waiting for the replication event or get # the replication event which happens below. self.waitUntilSettled() # In addition to checking that we have inflight items above we # also ensure that we haven't run any jobs yet. self.assertHistory([]) self.wait_timeout = old_wait_timeout # Add a second merged change event to ensure that we're not going # to wait for that one to replicate self.fake_gerrit.addEvent(B.getChangeMergedEvent()) # Finally complete replication and ensure everything settles out. self.fake_gerrit.addEvent(A.getChangeMergedReplicatedEvent()) self.waitUntilSettled() self.assertHistory([ dict(name='project-promote', result='SUCCESS', changes='1,1'), dict(name='project-promote', result='SUCCESS', changes='2,1'), ], ordered=False) def test_replication_ref_updated(self): # This test is admittedly a bit contrived but is a good exercise of # the ref updated without change merged event combo alongside our # wait for replication to complete. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setMerged() B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') B.setMerged() self.fake_gerrit.addEvent(A.getChangeMergedReplicationStartedEvent()) # Change merged and ref updated events share the same replication # events. self.fake_gerrit.addEvent(A.getRefUpdatedEvent()) old_wait_timeout = self.wait_timeout self.wait_timeout = 10 with testtools.ExpectedException(Exception, "Timeout waiting for Zuul to settle"): # We expect this exception because ZK queues won't empty until # we either timeout waiting for the replication event or get # the replication event which happens below. self.waitUntilSettled() # In addition to checking that we have inflight items above we # also ensure that we haven't run any jobs yet. self.assertHistory([]) self.wait_timeout = old_wait_timeout # Add a second ref updated event to ensure that we're not going # to wait for that one to replicate self.fake_gerrit.addEvent(B.getRefUpdatedEvent()) # Finally complete replication and ensure everything settles out. self.fake_gerrit.addEvent(A.getChangeMergedReplicatedEvent()) self.waitUntilSettled() self.assertHistory([ dict(name='project-post', result='SUCCESS', ref='refs/heads/master'), dict(name='project-post', result='SUCCESS', ref='refs/heads/master'), ], ordered=False) def test_replication_change_merged_combo(self): # This test tests the combo of ref updated and change merged events # which occurs when gerrit merges changes. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.setMerged() B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') B.setMerged() self.fake_gerrit.addEvent(A.getChangeMergedReplicationStartedEvent()) # Change merged and ref updated events share the same replication # events. self.fake_gerrit.addEvent(A.getRefUpdatedEvent()) self.fake_gerrit.addEvent(A.getChangeMergedEvent()) old_wait_timeout = self.wait_timeout self.wait_timeout = 10 with testtools.ExpectedException(Exception, "Timeout waiting for Zuul to settle"): # We expect this exception because ZK queues won't empty until # we either timeout waiting for the replication event or get # the replication event which happens below. self.waitUntilSettled() # In addition to checking that we have inflight items above we # also ensure that we haven't run any jobs yet. self.assertHistory([]) self.wait_timeout = old_wait_timeout # Add a second ref updated event to ensure that we're not going # to wait for that one to replicate self.fake_gerrit.addEvent(B.getRefUpdatedEvent()) self.fake_gerrit.addEvent(B.getChangeMergedEvent()) # Finally complete replication and ensure everything settles out. self.fake_gerrit.addEvent(A.getChangeMergedReplicatedEvent()) self.waitUntilSettled() self.assertHistory([ dict(name='project-promote', result='SUCCESS', changes='1,1'), dict(name='project-post', result='SUCCESS', ref='refs/heads/master'), dict(name='project-promote', result='SUCCESS', changes='2,1'), dict(name='project-post', result='SUCCESS', ref='refs/heads/master'), ], ordered=False) class TestGerritConnectionReplicationTimeout(ZuulTestCase): config_file = 'zuul-gerrit-replication-timeout.conf' tenant_config_file = 'config/single-tenant/main.yaml' def test_new_patchset_replication_timeout(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') self.fake_gerrit.addEvent(A.getPatchsetReplicationStartedEvent(1)) self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1)) self.waitUntilSettled() # We configure a 10 second timeout. Without replication completion # events we know that we timed out if the jobs run. self.assertHistory([ dict(name='project-merge', result='SUCCESS', changes='1,1'), dict(name='project-test1', result='SUCCESS', changes='1,1'), dict(name='project-test2', result='SUCCESS', changes='1,1'), dict(name='project-merge', result='SUCCESS', changes='2,1'), dict(name='project-test1', result='SUCCESS', changes='2,1'), dict(name='project-test2', result='SUCCESS', changes='2,1'), dict(name='project1-project2-integration', result='SUCCESS', changes='2,1'), ], ordered=False) class TestGerritUnicodeRefs(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' tenant_config_file = 'config/single-tenant/main.yaml' upload_pack_data = (b'014452944ee370db5c87691e62e0f9079b6281319b4e HEAD' b'\x00multi_ack thin-pack side-band side-band-64k ' b'ofs-delta shallow deepen-since deepen-not ' b'deepen-relative no-progress include-tag ' b'multi_ack_detailed allow-tip-sha1-in-want ' b'allow-reachable-sha1-in-want ' b'symref=HEAD:refs/heads/faster filter ' b'object-format=sha1 agent=git/2.37.1.gl1\n' b'003d5f42665d737b3fd4ec22ca0209e6191859f09fd6 ' b'refs/for/faster\n' b'004952944ee370db5c87691e62e0f9079b6281319b4e ' b'refs/heads/foo/\xf0\x9f\x94\xa5\xf0\x9f\x94\xa5' b'\xf0\x9f\x94\xa5\n' b'003f52944ee370db5c87691e62e0f9079b6281319b4e ' b'refs/heads/faster\n0000').decode("utf-8") def test_mb_unicode_refs(self): gerrit_config = { 'user': 'gerrit', 'server': 'localhost', } driver = GerritDriver() gerrit = GerritConnection(driver, 'review_gerrit', gerrit_config) def _uploadPack(project): return self.upload_pack_data self.patch(gerrit, '_uploadPack', _uploadPack) project = gerrit.source.getProject('org/project') refs = gerrit.getInfoRefs(project) self.assertEqual(refs, {'refs/for/faster': '5f42665d737b3fd4ec22ca0209e6191859f09fd6', 'refs/heads/foo/🔥🔥🔥': '52944ee370db5c87691e62e0f9079b6281319b4e', 'refs/heads/faster': '52944ee370db5c87691e62e0f9079b6281319b4e'}) class TestGerritDefaultBranch(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' tenant_config_file = 'config/single-tenant/main.yaml' scheduler_count = 1 def test_default_branch_changed(self): """Test the project-head-updated event""" self.waitUntilSettled() layout = self.scheds.first.sched.abide.tenants.get('tenant-one').layout md = layout.getProjectMetadata('review.example.com/org/project1') self.assertEqual('master', md.default_branch) prev_layout = layout.uuid self.fake_gerrit._fake_project_default_branch[ 'org/project1'] = 'foobar' self.fake_gerrit.addEvent(self.fake_gerrit.getProjectHeadUpdatedEvent( project='org/project1', old='master', new='foobar' )) self.waitUntilSettled() layout = self.scheds.first.sched.abide.tenants.get('tenant-one').layout md = layout.getProjectMetadata('review.example.com/org/project1') self.assertEqual('foobar', md.default_branch) new_layout = layout.uuid self.assertNotEqual(new_layout, prev_layout) class TestGerritMaxDeps(ZuulTestCase): config_file = 'zuul-gerrit-max-deps.conf' @simple_layout('layouts/simple.yaml') def test_max_deps(self): # max_dependencies for the connection is 1, so this is okay A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) # So is this B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.setDependsOn(A, 1) self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), dict(name='check-job', result='SUCCESS', changes='1,1 2,1'), ]) # This is not C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C') C.data['commitMessage'] = '%s\n\nDepends-On: %s\nDepends-On: %s\n' % ( C.subject, A.data['id'], B.data['id']) self.fake_gerrit.addEvent(C.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), dict(name='check-job', result='SUCCESS', changes='1,1 2,1'), ]) class TestGerritDriver(ZuulTestCase): # Most of the Zuul test suite tests the Gerrit driver, to some # extent. The other classes in this file test specific methods of # Zuul interacting with Gerrit. But the other drivers also test # some basic driver functionality that, if tested for Gerrit at # all, is spread out in random tests. This class adds some # (potentially duplicative) testing to validate parity with the # other drivers. @simple_layout('layouts/simple.yaml') def test_change_event(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', topic='test-topic') self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) job = self.getJobFromHistory('check-job') zuulvars = job.parameters['zuul'] self.assertEqual(str(A.number), zuulvars['change']) self.assertEqual('1', zuulvars['patchset']) self.assertEqual(str(A.patchsets[-1]['revision']), zuulvars['commit_id']) self.assertEqual('master', zuulvars['branch']) self.assertEquals('https://review.example.com/1', zuulvars['items'][0]['change_url']) self.assertEqual(zuulvars["message"], strings.b64encode('A')) self.assertEqual(1, len(self.history)) self.assertEqual(1, len(A.messages)) self.assertEqual('test-topic', zuulvars['topic']) @simple_layout('layouts/simple.yaml') def test_tag_event(self): event = self.fake_gerrit.addFakeTag('org/project', 'master', 'foo') tagsha = event['refUpdate']['newRev'] self.fake_gerrit.addEvent(event) self.waitUntilSettled() self.assertHistory([ dict(name='tag-job', result='SUCCESS', ref='refs/tags/foo'), ]) job = self.getJobFromHistory('tag-job') zuulvars = job.parameters['zuul'] zuulvars = job.parameters['zuul'] self.assertEqual('refs/tags/foo', zuulvars['ref']) self.assertEqual('tag', zuulvars['pipeline']) self.assertEqual('tag-job', zuulvars['job']) self.assertEqual(tagsha, zuulvars['newrev']) self.assertEqual(tagsha, zuulvars['commit_id']) @simple_layout('layouts/gerrit-hashtags.yaml') def test_hashtags_event(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.data['hashtags'] = ['check'] self.fake_gerrit.addEvent(A.getHashtagsChangedEvent(added=['check'])) self.waitUntilSettled() # Does not meet pipeline requirement self.assertHistory([]) A.data['hashtags'] = ['okay', 'check'] self.fake_gerrit.addEvent(A.getHashtagsChangedEvent(added=['check'])) self.waitUntilSettled() # This should work self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) # Matches reject A.data['hashtags'] = ['okay', 'check', 'nope'] self.fake_gerrit.addEvent(A.getHashtagsChangedEvent( removed=['nocheck'])) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) @simple_layout('layouts/gerrit-approval-change.yaml') def test_approval_change(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.data['hashtags'] = ['check'] self.fake_gerrit.addEvent(A.addApproval('Code-Review', 2)) self.waitUntilSettled() # Does not meet pipeline requirements, because old value is not present self.assertHistory([]) # Still not sufficient because old value matches new value self.fake_gerrit.addEvent(A.addApproval('Code-Review', 2, old_value=2)) self.waitUntilSettled() self.assertHistory([]) # Old value present, but new value is insufficient self.fake_gerrit.addEvent(A.addApproval('Code-Review', 0, old_value=2)) self.waitUntilSettled() self.assertHistory([]) # This should work now self.fake_gerrit.addEvent(A.addApproval('Code-Review', 2, old_value=0)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) @simple_layout('layouts/gerrit-notify.yaml') def test_notify(self): A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS'), ]) self.assertEqual(A.notify, 'NONE') class TestGerritCherryPickSSH(ZuulTestCase): @simple_layout('layouts/cherry-pick.yaml') def test_gerrit_cherry_pick_ssh(self): # Test the simple case case where we approve A then B. # Because the gerrit driver doesn't store the new patchset # that is created on a cherry-pick at the time of submission, # this test actually works because we intentionally # erroneously mark the old revision as merged. So when we # enqueue B, it sees its dependency of 1,1 as merged (even # though 1,2 was what was actually created-and-merged. This # test is here to ensure that works. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.cherry_pick = True B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.cherry_pick = True B.setDependsOn(A, 1) A.addApproval('Code-Review', 2) B.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), dict(name='check-job', result='SUCCESS', changes='2,1'), ]) # Unlike the web test below, we are unable to handle a case where # we add a patchset to change A before enqueing B, because the ssh # query does not tell us the current patchset of dependencies, so # there is no corresponding test for the ssh code path. class TestGerritCherryPickWeb(ZuulTestCase): config_file = 'zuul-gerrit-web.conf' @simple_layout('layouts/cherry-pick.yaml') def test_gerrit_cherry_pick_web(self): # Test the simple case case where we approve A then B. # Because the gerrit driver doesn't store the new patchset # that is created on a cherry-pick at the time of submission, # this test actually works because we intentionally # erroneously mark the old revision as merged. So when we # enqueue B, it sees its dependency of 1,1 as merged (even # though 1,2 was what was actually created-and-merged. This # test is here to ensure that works. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.cherry_pick = True B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.cherry_pick = True B.setDependsOn(A, 1) A.addApproval('Code-Review', 2) B.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), ]) self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,1'), dict(name='check-job', result='SUCCESS', changes='2,1'), ]) @simple_layout('layouts/cherry-pick.yaml') def test_gerrit_cherry_pick_web_with_update(self): # Test updating A behind B's back then approving B. # Unlike the test above, this test forces the issue of # patchset updates by having the user create a second patchset # on A before anything is enqueued. But B still depends on # the first patchset of A. So when the gerrit driver # intentionally erroneously marks 1,2 as merged, that won't # help B since it points at 1,1. However, the web query code # path will update B's dependency to point at 1,2 in the case # of a cherry-pick, and we will query 1,2 and discover that it # is merged, so B can proceed. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.cherry_pick = True B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.cherry_pick = True # Set the dependency on the first patchset of A B.setDependsOn(A, 1) # Then add a new patchset to A, so that B points to an old # version. A.addPatchset() A.addApproval('Code-Review', 2) B.addApproval('Code-Review', 2) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,2'), ]) self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,2'), dict(name='check-job', result='SUCCESS', changes='2,1'), ]) @simple_layout('layouts/cherry-pick.yaml') def test_gerrit_cherry_pick_web_with_update_queue(self): # Test that two cherry-pick changes enqued simultaneously work # This test should exercise the code path where the second # change remains blissfully unaware that there ever was a # 1,3 patchset. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') A.cherry_pick = True B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') B.cherry_pick = True # Set the dependency on the first patchset of A B.setDependsOn(A, 1) # Then add a new patchset to A, so that B points to an old # version. A.addPatchset() A.addApproval('Code-Review', 2) B.addApproval('Code-Review', 2) B.addApproval('Approved', 1) self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) self.waitUntilSettled() self.assertHistory([ dict(name='check-job', result='SUCCESS', changes='1,2'), dict(name='check-job', result='SUCCESS', changes='1,2 2,1'), ]) class TestGerritPeekQueue(BaseTestCase): def make_ref_updated_event(self, ref, newrev, ltime): e = zuul.model.ConnectionEvent({ "timestamp": time.time(), "payload": { "refUpdate": { "oldRev": "old", "newRev": newrev, "refName": ref, "project": "org/project" }, "type": "ref-updated", } }) e.zuul_event_ltime = ltime return e def make_change_merged_event(self, number, newrev, ltime): e = zuul.model.ConnectionEvent({ "timestamp": time.time(), "payload": { "newRev": newrev, "patchSet": { "number": 1, }, "change": { "project": "org/project", "branch": "master", "number": number, }, "refName": "ref/heads/master", "type": "change-merged", } }) e.zuul_event_ltime = ltime return e def make_replication_scheduled_event(self, ref, ltime): e = zuul.model.ConnectionEvent({ "timestamp": time.time(), "payload": { "type": "ref-replication-scheduled", "project": "org/project", "ref": ref, "refStatus": "OK", "status": "succeeded", "targetNode": "git@gitserver:22", "targetUri": "ssh://git@gitserver:22/org/project", "eventCreatedOn": int(time.time()), } }) e.zuul_event_ltime = ltime return e def make_replicated_event(self, ref, ltime): e = zuul.model.ConnectionEvent({ "timestamp": time.time(), "payload": { "type": "ref-replicated", "project": "org/project", "ref": ref, "refStatus": "OK", "status": "succeeded", "targetNode": "git@gitserver:22", "targetUri": "ssh://git@gitserver:22/org/project", "eventCreatedOn": int(time.time()), } }) e.zuul_event_ltime = ltime return e def test_peek_queue(self): handled = [] def handler(x): handled.append(x) q = PeekQueue(handler, 0) # Check noop q.run() self.assertEqual([], handled) # Check all at once (even though this shouldn't happen orig = [ self.make_ref_updated_event('refs/heads/master', 'new1', 1), self.make_ref_updated_event('refs/meta/foo', 'foo', 2), self.make_change_merged_event(1, 'new1', 3), ] [q.append(x) for x in orig] q.run() expected = [orig[2], orig[0], orig[1]] self.assertEqual(expected, handled) self.assertEqual(1, handled[0].zuul_event_ltime) self.assertEqual(1, handled[1].zuul_event_ltime) self.assertEqual(2, handled[2].zuul_event_ltime) # Check one at a time (typical case) handled.clear() orig = [ self.make_ref_updated_event('refs/heads/master', 'new1', 1), self.make_ref_updated_event('refs/meta/foo', 'foo', 2), self.make_change_merged_event(1, 'new1', 3), ] q.append(orig[0]) q.run() self.assertEqual([], handled) q.append(orig[1]) q.run() self.assertEqual([], handled) q.append(orig[2]) q.run() expected = [orig[2], orig[0], orig[1]] self.assertEqual(expected, handled) self.assertEqual(1, handled[0].zuul_event_ltime) self.assertEqual(1, handled[1].zuul_event_ltime) self.assertEqual(2, handled[2].zuul_event_ltime) # Check that other events arrive immediately handled.clear() q.append(orig[1]) q.run() expected = [orig[1]] self.assertEqual(expected, handled) # Check missing change-merged event handled.clear() orig = [ self.make_ref_updated_event('refs/heads/master', 'new1', 1), self.make_ref_updated_event('refs/meta/foo', 'foo', 2), ] q.append(orig[0]) q.run() self.assertEqual([], handled) q.append(orig[1]) q.run() self.assertEqual([], handled) # This is what the loop will acutally do q.timeout = 0 delay = q.run() self.assertEqual(delay, None) q.run() expected = [orig[0], orig[1]] self.assertEqual(expected, handled) self.assertEqual(1, handled[0].zuul_event_ltime) self.assertEqual(2, handled[1].zuul_event_ltime) q.timeout = 10 # Check if Gerrit changes the event order; it doesn't do this # today, but we want to defend against that. handled.clear() orig = [ self.make_change_merged_event(1, 'new1', 1), self.make_ref_updated_event('refs/heads/master', 'new1', 2), self.make_ref_updated_event('refs/meta/foo', 'foo', 3), ] q.append(orig[0]) q.run() expected = [orig[0]] self.assertEqual(expected, handled) q.append(orig[1]) q.run() expected = [orig[0], orig[1]] self.assertEqual(expected, handled) q.append(orig[2]) q.run() expected = [orig[0], orig[1], orig[2]] self.assertEqual(expected, handled) self.assertEqual(1, handled[0].zuul_event_ltime) self.assertEqual(2, handled[1].zuul_event_ltime) self.assertEqual(3, handled[2].zuul_event_ltime) def test_peek_queue_with_replication(self): handled = [] def handler(x): handled.append(x) q = PeekQueue(handler, 0) q.replication_timeout = 5 # Check noop q.run() self.assertEqual([], handled) # Check one at a time (typical case) handled.clear() orig = [ self.make_replication_scheduled_event('refs/heads/master', 1), self.make_ref_updated_event('refs/heads/master', 'new1', 2), self.make_change_merged_event(1, 'new1', 3), self.make_replicated_event('refs/heads/master', 4), ] q.append(orig[0]) q.run() self.assertEqual([], handled) q.append(orig[1]) q.run() self.assertEqual([], handled) q.append(orig[2]) q.run() self.assertEqual([], handled) q.append(orig[3]) q.run() expected = [orig[0], orig[2], orig[1], orig[3]] self.assertEqual(expected, handled) self.assertEqual(1, handled[0].zuul_event_ltime) self.assertEqual(2, handled[1].zuul_event_ltime) self.assertEqual(2, handled[2].zuul_event_ltime) self.assertEqual(4, handled[3].zuul_event_ltime) # Check with replication timeout handled.clear() q.replication_timeout = 1 orig = [ self.make_replication_scheduled_event('refs/heads/master', 1), self.make_ref_updated_event('refs/heads/master', 'new1', 2), self.make_change_merged_event(1, 'new1', 3), ] q.append(orig[0]) q.run() self.assertEqual([], handled) q.append(orig[1]) q.run() self.assertEqual([], handled) q.append(orig[2]) q.run() self.assertEqual([], handled) # Longer than our timeout time.sleep(2) q.run() expected = [orig[0], orig[2], orig[1]] self.assertEqual(expected, handled) self.assertEqual(1, handled[0].zuul_event_ltime) self.assertEqual(2, handled[1].zuul_event_ltime) self.assertEqual(2, handled[2].zuul_event_ltime) def test_peek_queue_busy_timeout(self): # Our last line of defense on a busy system is the extra 10 # second timeout -- if we see an event that's 10 seconds later # than the event we're waiting for, then we'll give up. If we # exhaust the event queue in zookeeper though, we'll give up # earlier; that's tested in the normal test_peek_queue test. handled = [] def handler(x): handled.append(x) q = PeekQueue(handler, 0) orig = [ self.make_ref_updated_event('refs/heads/master', 'new1', 1), ] [q.append(x) for x in orig] q.run() expected = [] self.assertEqual(expected, handled) q.timeout = 0 q.run() expected = [orig[0]] self.assertEqual(expected, handled)