diff options
Diffstat (limited to 'tests')
30 files changed, 539 insertions, 220 deletions
diff --git a/tests/base.py b/tests/base.py index 2811db7ce..94146ebff 100644 --- a/tests/base.py +++ b/tests/base.py @@ -1628,6 +1628,12 @@ class FakeGitlabConnection(gitlabconnection.GitlabConnection): } return (name, data) + @contextmanager + def enable_community_edition(self): + self.gl_client.community_edition = True + yield + self.gl_client.community_edition = False + class FakeGitlabAPIClient(gitlabconnection.GitlabAPIClient): log = logging.getLogger("zuul.test.FakeGitlabAPIClient") @@ -1635,6 +1641,7 @@ class FakeGitlabAPIClient(gitlabconnection.GitlabAPIClient): def __init__(self, baseurl, api_token, merge_requests_db={}): super(FakeGitlabAPIClient, self).__init__(baseurl, api_token) self.merge_requests = merge_requests_db + self.community_edition = False def gen_error(self, verb): return { @@ -1661,10 +1668,19 @@ class FakeGitlabAPIClient(gitlabconnection.GitlabAPIClient): 'title': mr.subject, 'state': mr.state, 'description': mr.description, + 'author': { + 'name': 'Administrator', + 'username': 'admin' + }, 'updated_at': mr.updated_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'sha': mr.sha, 'labels': mr.labels, 'merged_at': mr.merged_at, + 'diff_refs': { + 'base_sha': 'c380d3acebd181f13629a25d2e2acca46ffe1e00', + 'head_sha': '2be7ddb704c7b6b83732fdd5b9f09d5a397b5f8f', + 'start_sha': 'c380d3acebd181f13629a25d2e2acca46ffe1e00' + }, 'merge_status': mr.merge_status, }, 200, "", "GET" @@ -1676,9 +1692,14 @@ class FakeGitlabAPIClient(gitlabconnection.GitlabAPIClient): r'.+/projects/(.+)/merge_requests/(\d+)/approvals$', url) if match: mr = self._get_mr(match) - return { - 'approvals_left': 0 if mr.approved else 1, - }, 200, "", "GET" + if not self.community_edition: + return { + 'approvals_left': 0 if mr.approved else 1, + }, 200, "", "GET" + else: + return { + 'approved': mr.approved, + }, 200, "", "GET" def post(self, url, params=None, zuul_event_id=None): @@ -1693,7 +1714,11 @@ class FakeGitlabAPIClient(gitlabconnection.GitlabAPIClient): match = re.match( r'.+/projects/(.+)/merge_requests/(\d+)/approve$', url) if match: + assert 'sha' in params mr = self._get_mr(match) + if params['sha'] != mr.sha: + return {'message': 'SHA does not match HEAD of source ' + 'branch: <new_sha>'}, 409, "", "POST" mr.approved = True match = re.match( @@ -1909,7 +1934,8 @@ class FakeGithubPullRequest(object): def __init__(self, github, number, project, branch, subject, upstream_root, files=[], number_of_commits=1, - writers=[], body=None, body_text=None, draft=False): + writers=[], body=None, body_text=None, draft=False, + base_sha=None): """Creates a new PR with several commits. Sends an event about opened PR.""" self.github = github @@ -1936,7 +1962,7 @@ class FakeGithubPullRequest(object): self.merge_message = None self.state = 'open' self.url = 'https://%s/%s/pull/%s' % (github.server, project, number) - self._createPRRef() + self._createPRRef(base_sha=base_sha) self._addCommitToRepo(files=files) self._updateTimeStamp() @@ -2103,10 +2129,11 @@ class FakeGithubPullRequest(object): repo_path = os.path.join(self.upstream_root, self.project) return git.Repo(repo_path) - def _createPRRef(self): + def _createPRRef(self, base_sha=None): + base_sha = base_sha or 'refs/tags/init' repo = self._getRepo() GithubChangeReference.create( - repo, self.getPRReference(), 'refs/tags/init') + repo, self.getPRReference(), base_sha) def _addCommitToRepo(self, files=[], reset=False): repo = self._getRepo() @@ -2359,11 +2386,13 @@ class FakeGithubConnection(githubconnection.GithubConnection): self.zuul_web_port = port def openFakePullRequest(self, project, branch, subject, files=[], - body=None, body_text=None, draft=False): + body=None, body_text=None, draft=False, + base_sha=None): self.pr_number += 1 pull_request = FakeGithubPullRequest( self, self.pr_number, project, branch, subject, self.upstream_root, - files=files, body=body, body_text=body_text, draft=draft) + files=files, body=body, body_text=body_text, draft=draft, + base_sha=base_sha) self.pull_requests[self.pr_number] = pull_request return pull_request diff --git a/tests/fake_graphql.py b/tests/fake_graphql.py index 9e86f81ad..6c7567910 100644 --- a/tests/fake_graphql.py +++ b/tests/fake_graphql.py @@ -72,9 +72,14 @@ class FakeBranchProtectionRules(ObjectType): return parent.values() +class FakeActor(ObjectType): + login = String() + + class FakeStatusContext(ObjectType): state = String() context = String() + creator = Field(FakeActor) def resolve_state(parent, info): state = parent.state.upper() @@ -83,6 +88,9 @@ class FakeStatusContext(ObjectType): def resolve_context(parent, info): return parent.context + def resolve_creator(parent, info): + return parent.creator + class FakeStatus(ObjectType): contexts = List(FakeStatusContext) @@ -99,7 +107,9 @@ class FakeCheckRun(ObjectType): return parent.name def resolve_conclusion(parent, info): - return parent.conclusion.upper() + if parent.conclusion: + return parent.conclusion.upper() + return None class FakeCheckRuns(ObjectType): @@ -109,11 +119,28 @@ class FakeCheckRuns(ObjectType): return parent +class FakeApp(ObjectType): + slug = String() + name = String() + + class FakeCheckSuite(ObjectType): + app = Field(FakeApp) checkRuns = Field(FakeCheckRuns, first=Int()) + def resolve_app(parent, info): + if not parent: + return None + return parent[0].app + def resolve_checkRuns(parent, info, first=None): - return parent + # We only want to return the latest result for a check run per app. + # Since the check runs are ordered from latest to oldest result we + # need to traverse the list in reverse order. + check_runs_by_name = { + "{}:{}".format(cr.app, cr.name): cr for cr in reversed(parent) + } + return check_runs_by_name.values() class FakeCheckSuites(ObjectType): diff --git a/tests/fakegithub.py b/tests/fakegithub.py index 89b3ec73a..8e2f275eb 100644 --- a/tests/fakegithub.py +++ b/tests/fakegithub.py @@ -65,13 +65,18 @@ class FakeBranch(object): } +class FakeCreator: + def __init__(self, login): + self.login = login + + class FakeStatus(object): def __init__(self, state, url, description, context, user): self.state = state self.context = context + self.creator = FakeCreator(user) self._url = url self._description = description - self._user = user def as_dict(self): return { @@ -80,11 +85,17 @@ class FakeStatus(object): 'description': self._description, 'context': self.context, 'creator': { - 'login': self._user + 'login': self.creator.login } } +class FakeApp: + def __init__(self, name, slug): + self.name = name + self.slug = slug + + class FakeCheckRun(object): def __init__(self, name, details_url, output, status, conclusion, completed_at, external_id, actions, app): @@ -98,7 +109,7 @@ class FakeCheckRun(object): self.completed_at = completed_at self.external_id = external_id self.actions = actions - self.app = app + self.app = FakeApp(name=app, slug=app) # Github automatically sets the status to "completed" if a conclusion # is provided. @@ -118,7 +129,8 @@ class FakeCheckRun(object): "external_id": self.external_id, "actions": self.actions, "app": { - "slug": self.app, + "slug": self.app.slug, + "name": self.app.name, }, } @@ -543,6 +555,9 @@ class FakePull(object): }, 'ref': pr.branch, }, + 'user': { + 'login': 'octocat' + }, 'draft': pr.draft, 'mergeable': True, 'state': pr.state, @@ -719,6 +734,13 @@ class FakeGithubClient(object): def pull_request(self, owner, project, number): fake_pr = self._data.pull_requests[int(number)] + repo = self.repository(owner, project) + # Ensure a commit for the head_sha exists so this can be resolved in + # graphql queries. + repo._commits.setdefault( + fake_pr.head_sha, + FakeCommit(fake_pr.head_sha) + ) return FakePull(fake_pr) def search_issues(self, query): @@ -735,9 +757,24 @@ class FakeGithubClient(object): return re.match(r'[a-z0-9]{40}', s) if query_is_sha(query): - return (FakeIssueSearchResult(FakeIssue(pr)) - for pr in self._data.pull_requests.values() - if pr.head_sha == query) + # Github returns all PR's that contain the sha in their history + result = [] + for pr in self._data.pull_requests.values(): + # Quick check if head sha matches + if pr.head_sha == query: + result.append(FakeIssueSearchResult(FakeIssue(pr))) + continue + + # If head sha doesn't match it still could be in the pr history + repo = pr._getRepo() + commits = repo.iter_commits( + '%s...%s' % (pr.branch, pr.head_sha)) + for commit in commits: + if commit.hexsha == query: + result.append(FakeIssueSearchResult(FakeIssue(pr))) + continue + + return result # Non-SHA queries are of the form: # diff --git a/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml b/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml index 488e41a5c..8cef613a4 100644 --- a/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml +++ b/tests/fixtures/config/ansible-versions/git/common-config/zuul.yaml @@ -34,15 +34,7 @@ parent: ansible-version vars: test_ansible_version_major: 2 - test_ansible_version_minor: 7 - -- job: - name: ansible-27 - parent: ansible-version - ansible-version: 2.7 - vars: - test_ansible_version_major: 2 - test_ansible_version_minor: 7 + test_ansible_version_minor: 8 - job: name: ansible-28 @@ -66,7 +58,6 @@ check: jobs: - ansible-default - - ansible-27 - ansible-28 - ansible-29 @@ -75,6 +66,5 @@ check: jobs: - ansible-default-zuul-conf - - ansible-27 - ansible-28 - ansible-29 diff --git a/tests/fixtures/config/intermediate/git/common-config/playbooks/base.yaml b/tests/fixtures/config/intermediate/git/common-config/playbooks/base.yaml new file mode 100644 index 000000000..f679dceae --- /dev/null +++ b/tests/fixtures/config/intermediate/git/common-config/playbooks/base.yaml @@ -0,0 +1,2 @@ +- hosts: all + tasks: [] diff --git a/tests/fixtures/config/intermediate/git/common-config/zuul.yaml b/tests/fixtures/config/intermediate/git/common-config/zuul.yaml new file mode 100644 index 000000000..6c787c91d --- /dev/null +++ b/tests/fixtures/config/intermediate/git/common-config/zuul.yaml @@ -0,0 +1,50 @@ +- pipeline: + name: check + manager: independent + trigger: + gerrit: + - event: patchset-created + success: + gerrit: + Verified: 1 + failure: + gerrit: + Verified: -1 + +- job: + name: base + parent: null + run: playbooks/base.yaml + +- job: + name: job-abstract-intermediate + abstract: true + intermediate: true + +- job: + name: job-abstract + abstract: true + parent: job-abstract-intermediate + +# an intermediate, with an intermediate parent also +- job: + name: job-another-intermediate + parent: job-abstract-intermediate + abstract: true + intermediate: true + +- job: + name: job-another-abstract + parent: job-another-intermediate + abstract: true + +- job: + name: job-actual + parent: job-another-abstract + run: playbooks/base.yaml + +- project: + name: org/project + check: + jobs: [] + diff --git a/tests/fixtures/config/intermediate/git/org_project/README b/tests/fixtures/config/intermediate/git/org_project/README new file mode 100644 index 000000000..9daeafb98 --- /dev/null +++ b/tests/fixtures/config/intermediate/git/org_project/README @@ -0,0 +1 @@ +test diff --git a/tests/fixtures/config/intermediate/main.yaml b/tests/fixtures/config/intermediate/main.yaml new file mode 100644 index 000000000..208e274b1 --- /dev/null +++ b/tests/fixtures/config/intermediate/main.yaml @@ -0,0 +1,8 @@ +- tenant: + name: tenant-one + source: + gerrit: + config-projects: + - common-config + untrusted-projects: + - org/project diff --git a/tests/fixtures/config/inventory/git/common-config/zuul.yaml b/tests/fixtures/config/inventory/git/common-config/zuul.yaml index 42c933201..32f64c592 100644 --- a/tests/fixtures/config/inventory/git/common-config/zuul.yaml +++ b/tests/fixtures/config/inventory/git/common-config/zuul.yaml @@ -100,15 +100,6 @@ run: playbooks/jinja2-message.yaml - job: - name: ansible-version27-inventory - nodeset: - nodes: - - name: ubuntu-xenial - label: ubuntu-xenial - ansible-version: '2.7' - run: playbooks/ansible-version.yaml - -- job: name: ansible-version28-inventory nodeset: nodes: diff --git a/tests/fixtures/config/inventory/git/org_project/.zuul.yaml b/tests/fixtures/config/inventory/git/org_project/.zuul.yaml index 83961d75a..fd615518d 100644 --- a/tests/fixtures/config/inventory/git/org_project/.zuul.yaml +++ b/tests/fixtures/config/inventory/git/org_project/.zuul.yaml @@ -7,5 +7,4 @@ - executor-only-inventory - group-inventory - hostvars-inventory - - ansible-version27-inventory - ansible-version28-inventory diff --git a/tests/fixtures/config/inventory/git/org_project3/.zuul.yaml b/tests/fixtures/config/inventory/git/org_project3/.zuul.yaml index ac09fd329..6cf9faf88 100644 --- a/tests/fixtures/config/inventory/git/org_project3/.zuul.yaml +++ b/tests/fixtures/config/inventory/git/org_project3/.zuul.yaml @@ -6,5 +6,4 @@ - executor-only-inventory - group-inventory - hostvars-inventory - - ansible-version27-inventory - ansible-version28-inventory diff --git a/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-dest.yaml b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-dest.yaml new file mode 100644 index 000000000..6c151842a --- /dev/null +++ b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-dest.yaml @@ -0,0 +1,6 @@ +- hosts: localhost + tasks: + - name: Request with bad src + uri: + url: https://zuul.opendev.org + dest: /etc/zuul-uri-output-testing diff --git a/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-src.yaml b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-src.yaml new file mode 100644 index 000000000..9a9460c9b --- /dev/null +++ b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-src.yaml @@ -0,0 +1,7 @@ +- hosts: localhost + tasks: + - name: Request with bad src + uri: + url: https://zuul.opendev.org + method: POST + src: /etc/resolv.conf diff --git a/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-url.yaml b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-url.yaml new file mode 100644 index 000000000..222f1925a --- /dev/null +++ b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-bad-url.yaml @@ -0,0 +1,14 @@ +- hosts: localhost + tasks: + - name: Request with bad url scheme + uri: + url: "file:///etc/resolv.conf" + dest: "{{ zuul.executor.log_root }}/resolv.conf" + - name: stat file that shouldnt exist + stat: + path: "{{ zuul.executor.log_root }}/resolv.conf" + register: test_stat + - name: Debug the stat + debug: + msg: "resolv.conf exists when it shouldn't" + when: test_stat.stat.exists diff --git a/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-good.yaml b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-good.yaml new file mode 100644 index 000000000..0416cef4e --- /dev/null +++ b/tests/fixtures/config/remote-action-modules/git/org_project/playbooks/uri-good.yaml @@ -0,0 +1,16 @@ +- hosts: localhost + tasks: + - name: Safe uri request from localhost + uri: + # We don't seem to have working ssl cert chains in + # the test bwrap context. Use http to workaround that + # and don't follow the redirect to https. + url: http://zuul.opendev.org + follow_redirects: none + return_content: yes + status_code: + - 301 + - 302 + - 303 + - 307 + - 308 diff --git a/tests/fixtures/config/split-config/git/org_project1/.zuul.d/playbooks/.zuul.ignore b/tests/fixtures/config/split-config/git/org_project1/.zuul.d/playbooks/.zuul.ignore new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/fixtures/config/split-config/git/org_project1/.zuul.d/playbooks/.zuul.ignore diff --git a/tests/fixtures/config/split-config/git/org_project1/.zuul.d/playbooks/main.yaml b/tests/fixtures/config/split-config/git/org_project1/.zuul.d/playbooks/main.yaml new file mode 100644 index 000000000..b136e8444 --- /dev/null +++ b/tests/fixtures/config/split-config/git/org_project1/.zuul.d/playbooks/main.yaml @@ -0,0 +1,8 @@ +# This is a playbook in the configuration directory which should be +# ignored because the directory has a .zuul.ignore +- hosts: all + tasks: + + - name: foo + debug: + msg: blah diff --git a/tests/fixtures/zuul-default-ansible-version.conf b/tests/fixtures/zuul-default-ansible-version.conf index caecb38ef..ff15a0fed 100644 --- a/tests/fixtures/zuul-default-ansible-version.conf +++ b/tests/fixtures/zuul-default-ansible-version.conf @@ -9,7 +9,7 @@ server=127.0.0.1 [scheduler] tenant_config=main.yaml relative_priority=true -default_ansible_version=2.7 +default_ansible_version=2.8 [merger] git_dir=/tmp/zuul-test/merger-git diff --git a/tests/remote/test_remote_action_modules.py b/tests/remote/test_remote_action_modules.py index 23833f174..a147a9e5d 100644 --- a/tests/remote/test_remote_action_modules.py +++ b/tests/remote/test_remote_action_modules.py @@ -22,6 +22,7 @@ ERROR_LOCAL_CODE = "Executing local code is prohibited" ERROR_SYNC_TO_OUTSIDE = "Syncing files to outside the working dir" ERROR_SYNC_FROM_OUTSIDE = "Syncing files from outside the working dir" ERROR_SYNC_RSH = "Using custom synchronize rsh is prohibited" +ERROR_SCHEME_INVALID = "file urls are not allowed from localhost." class FunctionalActionModulesMixIn: @@ -221,13 +222,11 @@ class FunctionalActionModulesMixIn: self._run_job('known-hosts-bad', 'FAILURE', ERROR_ACCESS_OUTSIDE) - -class TestActionModules27(AnsibleZuulTestCase, FunctionalActionModulesMixIn): - ansible_version = '2.7' - - def setUp(self): - super().setUp() - self._setUp() + def test_uri_module(self): + self._run_job('uri-good', 'SUCCESS') + self._run_job('uri-bad-src', 'FAILURE', ERROR_ACCESS_OUTSIDE) + self._run_job('uri-bad-dest', 'FAILURE', ERROR_ACCESS_OUTSIDE) + self._run_job('uri-bad-url', 'FAILURE', ERROR_SCHEME_INVALID) class TestActionModules28(AnsibleZuulTestCase, FunctionalActionModulesMixIn): diff --git a/tests/remote/test_remote_zuul_json.py b/tests/remote/test_remote_zuul_json.py index e93f3f119..688894190 100644 --- a/tests/remote/test_remote_zuul_json.py +++ b/tests/remote/test_remote_zuul_json.py @@ -91,8 +91,11 @@ class FunctionalZuulJSONMixIn: text = self._get_json_as_text(build) json_result = json.loads(text) tasks = json_result[0]['plays'][0]['tasks'] + # NOTE(pabelanger): In 2.8 gather_facts are now logged as an + # expected action. expected_actions = [ - 'debug', 'debug', 'debug', 'copy', 'find', 'stat', 'debug' + 'gather_facts', 'debug', 'debug', 'debug', 'copy', 'find', + 'stat', 'debug' ] for i, expected in enumerate(expected_actions): host_result = tasks[i]['hosts']['controller'] @@ -108,10 +111,12 @@ class FunctionalZuulJSONMixIn: self.assertIn('json-role', text) json_result = json.loads(text) - role_name = json_result[0]['plays'][0]['tasks'][0]['role']['name'] + # NOTE(pabelanger): In 2.8 gather_facts are now logged as the + # first task. + role_name = json_result[0]['plays'][0]['tasks'][1]['role']['name'] self.assertEqual('json-role', role_name) - role_path = json_result[0]['plays'][0]['tasks'][0]['role']['path'] + role_path = json_result[0]['plays'][0]['tasks'][1]['role']['path'] self.assertEqual('json-role', os.path.basename(role_path)) def test_json_time_log(self): @@ -143,58 +148,17 @@ class FunctionalZuulJSONMixIn: dateutil.parser.parse(play_end_time) -class TestZuulJSON27(AnsibleZuulTestCase, FunctionalZuulJSONMixIn): - ansible_version = '2.7' +class TestZuulJSON28(AnsibleZuulTestCase, FunctionalZuulJSONMixIn): + ansible_version = '2.8' def setUp(self): super().setUp() self._setUp() -class TestZuulJSON28(AnsibleZuulTestCase, FunctionalZuulJSONMixIn): - ansible_version = '2.8' +class TestZuulJSON29(AnsibleZuulTestCase, FunctionalZuulJSONMixIn): + ansible_version = '2.9' def setUp(self): super().setUp() self._setUp() - - def test_json_task_action(self): - job = self._run_job('no-log') - with self.jobLog(job): - build = self.history[-1] - self.assertEqual(build.result, 'SUCCESS') - - text = self._get_json_as_text(build) - json_result = json.loads(text) - tasks = json_result[0]['plays'][0]['tasks'] - # NOTE(pabelanger): In 2.8 gather_facts are now logged as an - # expected action. - expected_actions = [ - 'gather_facts', 'debug', 'debug', 'debug', 'copy', 'find', - 'stat', 'debug' - ] - for i, expected in enumerate(expected_actions): - host_result = tasks[i]['hosts']['controller'] - self.assertEquals(expected, host_result['action']) - - def test_json_role_log(self): - job = self._run_job('json-role') - with self.jobLog(job): - build = self.history[-1] - self.assertEqual(build.result, 'SUCCESS') - - text = self._get_json_as_text(build) - self.assertIn('json-role', text) - - json_result = json.loads(text) - # NOTE(pabelanger): In 2.8 gather_facts are now logged as the - # first task. - role_name = json_result[0]['plays'][0]['tasks'][1]['role']['name'] - self.assertEqual('json-role', role_name) - - role_path = json_result[0]['plays'][0]['tasks'][1]['role']['path'] - self.assertEqual('json-role', os.path.basename(role_path)) - - -class TestZuulJSON29(TestZuulJSON28): - ansible_version = '2.9' diff --git a/tests/remote/test_remote_zuul_stream.py b/tests/remote/test_remote_zuul_stream.py index 9fef472b5..aed500769 100644 --- a/tests/remote/test_remote_zuul_stream.py +++ b/tests/remote/test_remote_zuul_stream.py @@ -158,9 +158,11 @@ class FunctionalZuulStreamMixIn: self.assertLogLine( r'controller \| ok: Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text) self.assertLogLine('PLAY RECAP', text) + # NOTE(pabelanger): Ansible 2.8 added new stats + # skipped, rescued, ignored. self.assertLogLine( - r'controller \| ok: \d+ changed: \d+ unreachable: 0 failed: 1', - text) + r'controller \| ok: \d+ changed: \d+ unreachable: 0 failed: 0 ' + 'skipped: 0 rescued: 1 ignored: 0', text) self.assertLogLine( r'RUN END RESULT_NORMAL: \[untrusted : review.example.com/' r'org/project/playbooks/command.yaml@master]', text) @@ -210,103 +212,17 @@ class FunctionalZuulStreamMixIn: self.assertLogLine(regex, text) -class TestZuulStream27(AnsibleZuulTestCase, FunctionalZuulStreamMixIn): - ansible_version = '2.7' +class TestZuulStream28(AnsibleZuulTestCase, FunctionalZuulStreamMixIn): + ansible_version = '2.8' def setUp(self): super().setUp() self._setUp() -class TestZuulStream28(AnsibleZuulTestCase, FunctionalZuulStreamMixIn): - ansible_version = '2.8' +class TestZuulStream29(AnsibleZuulTestCase, FunctionalZuulStreamMixIn): + ansible_version = '2.9' def setUp(self): super().setUp() self._setUp() - - def test_command(self): - job = self._run_job('command') - with self.jobLog(job): - build = self.history[-1] - self.assertEqual(build.result, 'SUCCESS') - - text = self._get_job_output(build) - self.assertLogLine( - r'RUN START: \[untrusted : review.example.com/org/project/' - r'playbooks/command.yaml@master\]', text) - self.assertLogLine(r'PLAY \[all\]', text) - self.assertLogLine( - r'Ansible version={}'.format(self.ansible_version), text) - self.assertLogLine(r'TASK \[Show contents of first file\]', text) - self.assertLogLine(r'controller \| command test one', text) - self.assertLogLine( - r'controller \| ok: Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text) - self.assertLogLine(r'TASK \[Show contents of second file\]', text) - self.assertLogLine(r'compute1 \| command test two', text) - self.assertLogLine(r'controller \| command test two', text) - self.assertLogLine(r'compute1 \| This is a rescue task', text) - self.assertLogLine(r'controller \| This is a rescue task', text) - self.assertLogLine(r'compute1 \| This is an always task', text) - self.assertLogLine(r'controller \| This is an always task', text) - self.assertLogLine(r'compute1 \| This is a handler', text) - self.assertLogLine(r'controller \| This is a handler', text) - self.assertLogLine(r'controller \| First free task', text) - self.assertLogLine(r'controller \| Second free task', text) - self.assertLogLine(r'controller \| This is a shell task after an ' - 'included role', text) - self.assertLogLine(r'compute1 \| This is a shell task after an ' - 'included role', text) - self.assertLogLine(r'controller \| This is a command task after ' - 'an included role', text) - self.assertLogLine(r'compute1 \| This is a command task after an ' - 'included role', text) - self.assertLogLine(r'controller \| This is a shell task with ' - 'delegate compute1', text) - self.assertLogLine(r'controller \| This is a shell task with ' - 'delegate controller', text) - self.assertLogLine(r'compute1 \| item_in_loop1', text) - self.assertLogLine(r'compute1 \| ok: Item: item_in_loop1 ' - r'Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text) - self.assertLogLine(r'compute1 \| item_in_loop2', text) - self.assertLogLine(r'compute1 \| ok: Item: item_in_loop2 ' - r'Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text) - self.assertLogLine(r'compute1 \| failed_in_loop1', text) - self.assertLogLine(r'compute1 \| ok: Item: failed_in_loop1 ' - r'Result: 1', text) - self.assertLogLine(r'compute1 \| failed_in_loop2', text) - self.assertLogLine(r'compute1 \| ok: Item: failed_in_loop2 ' - r'Result: 1', text) - self.assertLogLine(r'compute1 \| .*No such file or directory: .*' - r'\'/remote-shelltask/somewhere/' - r'that/does/not/exist\'', text) - self.assertLogLine(r'controller \| .*No such file or directory: .*' - r'\'/remote-shelltask/somewhere/' - r'that/does/not/exist\'', text) - self.assertLogLine( - r'controller \| ok: Runtime: \d:\d\d:\d\d\.\d\d\d\d\d\d', text) - self.assertLogLine('PLAY RECAP', text) - # NOTE(pabelanger): Ansible 2.8 added new stats - # skipped, rescued, ignored. - self.assertLogLine( - r'controller \| ok: \d+ changed: \d+ unreachable: 0 failed: 0 ' - 'skipped: 0 rescued: 1 ignored: 0', text) - self.assertLogLine( - r'RUN END RESULT_NORMAL: \[untrusted : review.example.com/' - r'org/project/playbooks/command.yaml@master]', text) - - # Run a pre-defined job that is defined in a trusted repo to test - # localhost tasks. - job = self._run_job('command-localhost', create=False) - with self.jobLog(job): - build = self.history[-1] - self.assertEqual(build.result, 'SUCCESS') - - text = self._get_job_output(build) - self.assertLogLine(r'localhost \| .*No such file or directory: .*' - r'\'/local-shelltask/somewhere/' - r'that/does/not/exist\'', text) - - -class TestZuulStream29(TestZuulStream28): - ansible_version = '2.9' diff --git a/tests/unit/test_configloader.py b/tests/unit/test_configloader.py index cf156e156..9dbba9e60 100644 --- a/tests/unit/test_configloader.py +++ b/tests/unit/test_configloader.py @@ -397,6 +397,11 @@ class TestSplitConfig(ZuulTestCase): self.assertIn('project1-project2-integration', project1_config[0].pipelines['check'].job_list.jobs) + # This check ensures the .zuul.ignore flag file is working in + # the config directory. + self.assertEquals( + len(tenant.layout.loading_errors), 0) + def test_dynamic_split_config(self): in_repo_conf = textwrap.dedent( """ diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index f64de8c7c..ebabb6a0b 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -555,6 +555,8 @@ class TestMQTTConnection(ZuulTestCase): 'tenant-one/zuul_start/check/org/project/master') mqtt_payload = start_event['msg'] self.assertEquals(mqtt_payload['project'], 'org/project') + self.assertEqual(len(mqtt_payload['commit_id']), 40) + self.assertEquals(mqtt_payload['owner'], 'username') self.assertEquals(mqtt_payload['branch'], 'master') self.assertEquals(mqtt_payload['buildset']['result'], None) self.assertEquals(mqtt_payload['buildset']['builds'][0]['job_name'], diff --git a/tests/unit/test_github_requirements.py b/tests/unit/test_github_requirements.py index e542f80f9..bde2c5ca8 100644 --- a/tests/unit/test_github_requirements.py +++ b/tests/unit/test_github_requirements.py @@ -92,6 +92,11 @@ class TestGithubRequirements(ZuulTestCase): project = 'org/project2' A = self.fake_github.openFakePullRequest(project, 'master', 'A') + # Create second PR which contains the head of A in its history. Zuul + # should not get disturbed by the existence of this one. + self.fake_github.openFakePullRequest( + project, 'master', 'A', base_sha=A.head_sha) + # An error status should not cause it to be enqueued self.fake_github.setCommitStatus(project, A.head_sha, 'error', context='tenant-one/check') diff --git a/tests/unit/test_gitlab_driver.py b/tests/unit/test_gitlab_driver.py index 11edaf0c4..c9a08c56b 100644 --- a/tests/unit/test_gitlab_driver.py +++ b/tests/unit/test_gitlab_driver.py @@ -162,6 +162,27 @@ class TestGitlabDriver(ZuulTestCase): self.assertEqual('check-approval', zuulvars['pipeline']) @simple_layout('layouts/basic-gitlab.yaml', driver='gitlab') + def test_merge_request_updated_during_build(self): + + A = self.fake_gitlab.openFakeMergeRequest('org/project', 'master', 'A') + self.fake_gitlab.emitEvent(A.getMergeRequestOpenedEvent()) + old = A.sha + A.addCommit() + new = A.sha + self.assertNotEqual(old, new) + self.waitUntilSettled() + + self.assertEqual(2, len(self.history)) + # MR must not be approved: tested commit isn't current commit + self.assertFalse(A.approved) + + self.fake_gitlab.emitEvent(A.getMergeRequestUpdatedEvent()) + self.waitUntilSettled() + + self.assertEqual(4, len(self.history)) + self.assertTrue(A.approved) + + @simple_layout('layouts/basic-gitlab.yaml', driver='gitlab') def test_merge_request_labeled(self): A = self.fake_gitlab.openFakeMergeRequest('org/project', 'master', 'A') @@ -560,6 +581,29 @@ class TestGitlabDriver(ZuulTestCase): self.assertEqual(1, len(self.history)) @simple_layout('layouts/requirements-gitlab.yaml', driver='gitlab') + def test_approval_require_community_edition(self): + + with self.fake_gitlab.enable_community_edition(): + A = self.fake_gitlab.openFakeMergeRequest( + 'org/project2', 'master', 'A') + + self.fake_gitlab.emitEvent(A.getMergeRequestOpenedEvent()) + self.waitUntilSettled() + self.assertEqual(0, len(self.history)) + + A.approved = True + + self.fake_gitlab.emitEvent(A.getMergeRequestUpdatedEvent()) + self.waitUntilSettled() + self.assertEqual(1, len(self.history)) + + A.approved = False + + self.fake_gitlab.emitEvent(A.getMergeRequestUpdatedEvent()) + self.waitUntilSettled() + self.assertEqual(1, len(self.history)) + + @simple_layout('layouts/requirements-gitlab.yaml', driver='gitlab') def test_label_require(self): A = self.fake_gitlab.openFakeMergeRequest( diff --git a/tests/unit/test_inventory.py b/tests/unit/test_inventory.py index af45aebb7..024e438a4 100644 --- a/tests/unit/test_inventory.py +++ b/tests/unit/test_inventory.py @@ -167,30 +167,6 @@ class TestInventoryAutoPython(TestInventoryBase): self.executor_server.release() self.waitUntilSettled() - def test_auto_python_ansible27_inventory(self): - inventory = self._get_build_inventory('ansible-version27-inventory') - - all_nodes = ('ubuntu-xenial',) - self.assertIn('all', inventory) - self.assertIn('hosts', inventory['all']) - self.assertIn('vars', inventory['all']) - for node_name in all_nodes: - self.assertIn(node_name, inventory['all']['hosts']) - node_vars = inventory['all']['hosts'][node_name] - self.assertEqual( - '/usr/bin/python2', node_vars['ansible_python_interpreter']) - - self.assertIn('zuul', inventory['all']['vars']) - z_vars = inventory['all']['vars']['zuul'] - self.assertIn('executor', z_vars) - self.assertIn('src_root', z_vars['executor']) - self.assertIn('job', z_vars) - self.assertEqual(z_vars['job'], 'ansible-version27-inventory') - self.assertEqual(z_vars['message'], 'QQ==') - - self.executor_server.release() - self.waitUntilSettled() - class TestInventory(TestInventoryBase): diff --git a/tests/unit/test_merger_repo.py b/tests/unit/test_merger_repo.py index 28e4b4456..9a626ea56 100644 --- a/tests/unit/test_merger_repo.py +++ b/tests/unit/test_merger_repo.py @@ -731,3 +731,40 @@ class TestMerger(ZuulTestCase): ref_map[foo_zuul_ref].commit.hexsha, merge_state[("gerrit", "org/project", "foo")] ) + + def test_stale_index_lock_cleanup(self): + # Stop the running executor's merger. We needed it running to merge + # things during test boostrapping but now it is just in the way. + self.executor_server.merger_gearworker.stop() + self.executor_server.merger_gearworker.join() + # Start the merger and do a merge to populate the repo on disk + self._startMerger() + + A = self.fake_gerrit.addFakeChange('org/project1', 'master', 'A') + A.addApproval('Code-Review', 2) + self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) + self.waitUntilSettled() + self.assertEqual(A.data['status'], 'MERGED') + + # Stop the merger so we can modify the git repo + self.merge_server.stop() + self.merge_server.join() + + # Add an index.lock file + fpath = os.path.join(self.merger_src_root, 'review.example.com', + 'org', 'project1', '.git', 'index.lock') + with open(fpath, 'w'): + pass + self.assertTrue(os.path.exists(fpath)) + + # Start a new merger and check that we can still merge things + self._startMerger() + + # This will fail if git can't modify the repo due to a stale lock file. + B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') + B.addApproval('Code-Review', 2) + self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) + self.waitUntilSettled() + self.assertEqual(B.data['status'], 'MERGED') + + self.assertFalse(os.path.exists(fpath)) diff --git a/tests/unit/test_scheduler.py b/tests/unit/test_scheduler.py index f92206d55..3c9abbe56 100644 --- a/tests/unit/test_scheduler.py +++ b/tests/unit/test_scheduler.py @@ -8365,3 +8365,71 @@ class TestSchedulerSmartReconfiguration(ZuulTestCase): def test_smart_reconfiguration_command_socket(self): "Test that live reconfiguration works using command socket" self._test_smart_reconfiguration(command_socket=True) + + +class TestReconfigureBranch(ZuulTestCase): + + def _setupTenantReconfigureTime(self): + self.old = self.scheds.first.sched.tenant_last_reconfigured\ + .get('tenant-one', 0) + + def _createBranch(self): + self.create_branch('org/project1', 'stable') + self.fake_gerrit.addEvent( + self.fake_gerrit.getFakeBranchCreatedEvent( + 'org/project1', 'stable')) + self.waitUntilSettled() + + def _deleteBranch(self): + self.delete_branch('org/project1', 'stable') + self.fake_gerrit.addEvent( + self.fake_gerrit.getFakeBranchDeletedEvent( + 'org/project1', 'stable')) + self.waitUntilSettled() + + def _expectReconfigure(self, doReconfigure): + new = self.scheds.first.sched.tenant_last_reconfigured\ + .get('tenant-one', 0) + if doReconfigure: + self.assertLess(self.old, new) + else: + self.assertEqual(self.old, new) + self.old = new + + +class TestReconfigureBranchCreateDeleteSshHttp(TestReconfigureBranch): + tenant_config_file = 'config/single-tenant/main.yaml' + config_file = 'zuul-gerrit-web.conf' + + def test_reconfigure_cache_branch_create_delete(self): + "Test that cache is updated clear on branch creation/deletion" + self._setupTenantReconfigureTime() + self._createBranch() + self._expectReconfigure(True) + self._deleteBranch() + self._expectReconfigure(True) + + +class TestReconfigureBranchCreateDeleteSsh(TestReconfigureBranch): + tenant_config_file = 'config/single-tenant/main.yaml' + + def test_reconfigure_cache_branch_create_delete(self): + "Test that cache is updated clear on branch creation/deletion" + self._setupTenantReconfigureTime() + self._createBranch() + self._expectReconfigure(True) + self._deleteBranch() + self._expectReconfigure(True) + + +class TestReconfigureBranchCreateDeleteHttp(TestReconfigureBranch): + tenant_config_file = 'config/single-tenant/main.yaml' + config_file = 'zuul-gerrit-no-stream.conf' + + def test_reconfigure_cache_branch_create_delete(self): + "Test that cache is updated clear on branch creation/deletion" + self._setupTenantReconfigureTime() + self._createBranch() + self._expectReconfigure(True) + self._deleteBranch() + self._expectReconfigure(True) diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py index dc8e8f6a1..3f8315e40 100644 --- a/tests/unit/test_v3.py +++ b/tests/unit/test_v3.py @@ -16,9 +16,10 @@ import io import json import logging import os +import sys import textwrap import gc -from unittest import skip +from unittest import skip, skipIf import paramiko @@ -221,6 +222,81 @@ class TestAbstract(ZuulTestCase): self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '1') +class TestIntermediate(ZuulTestCase): + tenant_config_file = 'config/intermediate/main.yaml' + + def test_intermediate_fail(self): + # you can not instantiate from an intermediate job + in_repo_conf = textwrap.dedent( + """ + - job: + name: job-instantiate-intermediate + parent: job-abstract-intermediate + + - project: + check: + jobs: + - job-instantiate-intermediate + """) + + 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.reported, 1) + self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '-1') + self.assertIn('may only inherit to another abstract job', + A.messages[0]) + + def test_intermediate_config_fail(self): + # an intermediate job must also be abstract + in_repo_conf = textwrap.dedent( + """ + - job: + name: job-intermediate-but-not-abstract + intermediate: true + abstract: false + + - project: + check: + jobs: + - job-intermediate-but-not-abstract + """) + + 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.reported, 1) + self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '-1') + self.assertIn('An intermediate job must also be abstract', + A.messages[0]) + + def test_intermediate_several(self): + # test passing through several intermediate jobs + in_repo_conf = textwrap.dedent( + """ + - project: + name: org/project + check: + jobs: + - job-actual + """) + + 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.reported, 1) + self.assertEqual(A.patchsets[-1]['approvals'][0]['value'], '1') + + class TestFinal(ZuulTestCase): tenant_config_file = 'config/final/main.yaml' @@ -1457,11 +1533,13 @@ class TestInRepoConfig(ZuulTestCase): "A should report failure") self.assertIn('not a list', A.messages[0], "A should have a syntax error reported") + self.assertIn('job: foo', A.messages[0], + "A should display the failing list") def test_yaml_dict_error(self): in_repo_conf = textwrap.dedent( """ - - job + - job_not_a_dict """) file_dict = {'.zuul.yaml': in_repo_conf} @@ -1476,6 +1554,8 @@ class TestInRepoConfig(ZuulTestCase): "A should report failure") self.assertIn('not a dictionary', A.messages[0], "A should have a syntax error reported") + self.assertIn('job_not_a_dict', A.messages[0], + "A should list the bad key") def test_yaml_duplicate_key_error(self): in_repo_conf = textwrap.dedent( @@ -1517,6 +1597,41 @@ class TestInRepoConfig(ZuulTestCase): "A should report failure") self.assertIn('has more than one key', A.messages[0], "A should have a syntax error reported") + self.assertIn("job: null\n name: project-test2", A.messages[0], + "A should have the failing section displayed") + + # This is non-deterministic without default dict ordering, which + # happended with python 3.7. + @skipIf(sys.version_info < (3, 7), "non-deterministic on < 3.7") + def test_yaml_error_truncation_message(self): + in_repo_conf = textwrap.dedent( + """ + - job: + name: project-test2 + this: is + a: long + set: of + keys: that + should: be + truncated: ok + """) + + file_dict = {'.zuul.yaml': in_repo_conf} + A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A', + files=file_dict) + A.addApproval('Code-Review', 2) + self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) + self.waitUntilSettled() + + self.assertEqual(A.data['status'], 'NEW') + self.assertEqual(A.reported, 1, + "A should report failure") + self.assertIn('has more than one key', A.messages[0], + "A should have a syntax error reported") + self.assertIn("job: null\n name: project-test2", A.messages[0], + "A should have the failing section displayed") + self.assertIn("...", A.messages[0], + "A should have the failing section truncated") def test_yaml_unknown_error(self): in_repo_conf = textwrap.dedent( @@ -1537,6 +1652,8 @@ class TestInRepoConfig(ZuulTestCase): "A should report failure") self.assertIn('not recognized', A.messages[0], "A should have a syntax error reported") + self.assertIn('foobar:\n foo: bar', A.messages[0], + "A should report the bad keys") def test_invalid_job_secret_var_name(self): in_repo_conf = textwrap.dedent( @@ -2849,10 +2966,6 @@ class FunctionalAnsibleMixIn(object): self._test_plugins(plugin_tests) -class TestAnsible27(AnsibleZuulTestCase, FunctionalAnsibleMixIn): - ansible_version = '2.7' - - class TestAnsible28(AnsibleZuulTestCase, FunctionalAnsibleMixIn): ansible_version = '2.8' @@ -6597,7 +6710,6 @@ class TestAnsibleVersion(AnsibleZuulTestCase): self.assertHistory([ dict(name='ansible-default', result='SUCCESS', changes='1,1'), - dict(name='ansible-27', result='SUCCESS', changes='1,1'), dict(name='ansible-28', result='SUCCESS', changes='1,1'), dict(name='ansible-29', result='SUCCESS', changes='1,1'), ], ordered=False) @@ -6618,7 +6730,6 @@ class TestDefaultAnsibleVersion(AnsibleZuulTestCase): self.assertHistory([ dict(name='ansible-default-zuul-conf', result='SUCCESS', changes='1,1'), - dict(name='ansible-27', result='SUCCESS', changes='1,1'), dict(name='ansible-28', result='SUCCESS', changes='1,1'), dict(name='ansible-29', result='SUCCESS', changes='1,1'), ], ordered=False) diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 6a74a8a96..784d4ba3a 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -341,6 +341,7 @@ class TestWeb(BaseTestWeb): 'dependencies': [], 'description': None, 'files': [], + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'final': False, @@ -385,6 +386,7 @@ class TestWeb(BaseTestWeb): 'dependencies': [], 'description': None, 'files': [], + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'final': False, @@ -434,6 +436,7 @@ class TestWeb(BaseTestWeb): 'description': None, 'files': [], 'final': False, + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'name': 'test-job', @@ -555,6 +558,7 @@ class TestWeb(BaseTestWeb): 'description': None, 'files': [], 'final': False, + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'name': 'project-merge', @@ -592,6 +596,7 @@ class TestWeb(BaseTestWeb): 'description': None, 'files': [], 'final': False, + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'name': 'project-test1', @@ -629,6 +634,7 @@ class TestWeb(BaseTestWeb): 'description': None, 'files': [], 'final': False, + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'name': 'project-test2', @@ -666,6 +672,7 @@ class TestWeb(BaseTestWeb): 'description': None, 'files': [], 'final': False, + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'name': 'project1-project2-integration', @@ -723,6 +730,7 @@ class TestWeb(BaseTestWeb): 'description': None, 'files': [], 'final': False, + 'intermediate': False, 'irrelevant_files': [], 'match_on_config_updates': True, 'name': 'project-post', |