diff options
author | Tobias Henkel <tobias.henkel@bmw.de> | 2020-02-25 21:19:54 +0100 |
---|---|---|
committer | Tobias Henkel <tobias.henkel@bmw.de> | 2020-02-28 09:43:56 +0100 |
commit | 4c972f00bdca8526fc2c21a68022a17e6cbdbfac (patch) | |
tree | 5e2b1ae026d0d8a6632a03a14ead7c782eb1b0ad /tests | |
parent | 1ed1c7f53d829246a85aa65dea2887bfb61be588 (diff) | |
download | zuul-4c972f00bdca8526fc2c21a68022a17e6cbdbfac.tar.gz |
Optimize canMerge using graphql
The canMerge check is executed whenever zuul tests if a change can
enter a gate pipeline. This is part of the critical path in the event
handling of the scheduler and therefore must be as fast as
possible. Currently this takes five requests for doing its work and
also transfers large amounts of data that is unneeded:
* get pull request
* get branch protection settings
* get commits
* get status of latest commit
* get check runs of latest commit
Especially when Github is busy this can slow down zuul's event
processing considerably. This can be optimized using graphql to only
query the data we need with a single request. This reduces requests
and load on Github and speeds up event processing in the scheduler.
Since this is the first usage of graphql this also sets up needed
testing infrastructure using graphene to mock the github api with real
test data.
Change-Id: I77be4f16cf7eb5c8035ce0312f792f4e8d4c3e10
Diffstat (limited to 'tests')
-rw-r--r-- | tests/fake_graphql.py | 166 | ||||
-rw-r--r-- | tests/fakegithub.py | 18 |
2 files changed, 184 insertions, 0 deletions
diff --git a/tests/fake_graphql.py b/tests/fake_graphql.py new file mode 100644 index 000000000..66bbdfad6 --- /dev/null +++ b/tests/fake_graphql.py @@ -0,0 +1,166 @@ +# Copyright 2019 BMW Group +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from graphene import Boolean, Field, Int, List, ObjectType, String + + +class FakePageInfo(ObjectType): + end_cursor = String() + has_next_page = Boolean() + + def resolve_end_cursor(parent, info): + return 'testcursor' + + def resolve_has_next_page(parent, info): + return False + + +class FakeBranchProtectionRule(ObjectType): + pattern = String() + requiredStatusCheckContexts = List(String) + requiresApprovingReviews = Boolean() + requiresCodeOwnerReviews = Boolean() + + def resolve_pattern(parent, info): + return parent.pattern + + def resolve_requiredStatusCheckContexts(parent, info): + return parent.required_contexts + + def resolve_requiresApprovingReviews(parent, info): + return parent.require_reviews + + def resolve_requiresCodeOwnerReviews(parent, info): + return parent.require_codeowners_review + + +class FakeBranchProtectionRules(ObjectType): + nodes = List(FakeBranchProtectionRule) + + def resolve_nodes(parent, info): + return parent.values() + + +class FakeStatusContext(ObjectType): + state = String() + context = String() + + def resolve_state(parent, info): + state = parent.state.upper() + return state + + def resolve_context(parent, info): + return parent.context + + +class FakeStatus(ObjectType): + contexts = List(FakeStatusContext) + + def resolve_contexts(parent, info): + return parent + + +class FakeCheckRun(ObjectType): + name = String() + conclusion = String() + + def resolve_name(parent, info): + return parent.name + + def resolve_conclusion(parent, info): + return parent.conclusion.upper() + + +class FakeCheckRuns(ObjectType): + nodes = List(FakeCheckRun) + + def resolve_nodes(parent, info): + return parent + + +class FakeCheckSuite(ObjectType): + checkRuns = Field(FakeCheckRuns, first=Int()) + + def resolve_checkRuns(parent, info, first=None): + return parent + + +class FakeCheckSuites(ObjectType): + + nodes = List(FakeCheckSuite) + + def resolve_nodes(parent, info): + # Note: we only use a single check suite in the tests so return a + # single item to keep it simple. + return [parent] + + +class FakeCommit(ObjectType): + + class Meta: + # Graphql object type that defaults to the class name, but we require + # 'Commit'. + name = 'Commit' + + status = Field(FakeStatus) + checkSuites = Field(FakeCheckSuites, first=Int()) + + def resolve_status(parent, info): + seen = set() + result = [] + for status in parent._statuses: + if status.context not in seen: + seen.add(status.context) + result.append(status) + # Github returns None if there are no results + return result or None + + def resolve_checkSuites(parent, info, first=None): + # Tests only utilize one check suite so return all runs for that. + return parent._check_runs + + +class FakePullRequest(ObjectType): + isDraft = Boolean() + + def resolve_isDraft(parent, info): + return parent.draft + + +class FakeRepository(ObjectType): + name = String() + branchProtectionRules = Field(FakeBranchProtectionRules, first=Int()) + pullRequest = Field(FakePullRequest, number=Int(required=True)) + object = Field(FakeCommit, expression=String(required=True)) + + def resolve_name(parent, info): + org, name = parent.name.split('/') + return name + + def resolve_branchProtectionRules(parent, info, first): + return parent._branch_protection_rules + + def resolve_pullRequest(parent, info, number): + return parent.data.pull_requests.get(number) + + def resolve_object(parent, info, expression): + return parent._commits.get(expression) + + +class FakeGithubQuery(ObjectType): + repository = Field(FakeRepository, owner=String(required=True), + name=String(required=True)) + + def resolve_repository(root, info, owner, name): + return info.context.repos.get((owner, name)) diff --git a/tests/fakegithub.py b/tests/fakegithub.py index b16a8e410..366f503d2 100644 --- a/tests/fakegithub.py +++ b/tests/fakegithub.py @@ -19,9 +19,12 @@ import github3.exceptions import re import time +import graphene from requests import HTTPError from requests.structures import CaseInsensitiveDict +from tests.fake_graphql import FakeGithubQuery + FAKE_BASE_URL = 'https://example.com/api/v3/' @@ -536,6 +539,8 @@ class FakeGithubSession(object): def __init__(self, data): self._data = data self.headers = CaseInsensitiveDict() + self._base_url = None + self.schema = graphene.Schema(query=FakeGithubQuery) def build_url(self, *args): fakepath = '/'.join(args) @@ -554,6 +559,17 @@ class FakeGithubSession(object): # unknown entity to process return None + def post(self, url, data=None, headers=None, params=None, json=None): + + if json and json.get('query'): + query = json.get('query') + variables = json.get('variables') + result = self.schema.execute( + query, variables=variables, context=self._data) + return FakeResponse({'data': result.data}, 200) + + return FakeResponse(None, 404) + def get_repo(self, request, params=None): org, project, request = request.split('/', 2) project_name = '{}/{}'.format(org, project) @@ -569,6 +585,8 @@ class FakeBranchProtectionRule: def __init__(self): self.pattern = None self.required_contexts = [] + self.require_reviews = False + self.require_codeowners_review = False class FakeGithubData(object): |