From 4c6290be82e6a8d1c33604d3312635d0c4ed9d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC=20=28bersace=29?= Date: Tue, 21 Feb 2017 09:20:11 +0100 Subject: [PATCH 1/2] Poll and cancel build without Jenkinsapi --- jenkins_epo/extensions/jenkins.py | 70 +++++++------- jenkins_epo/jenkins.py | 146 +++++++++++++++++++----------- jenkins_epo/repository.py | 32 +------ tests/extensions/test_jenkins.py | 35 ++++--- tests/extensions/test_poll.py | 22 ++--- tests/test_build.py | 124 +++++++++++++++++++++++++ tests/test_jenkins.py | 98 +------------------- tests/test_repositories.py | 26 ------ tests/test_rest.py | 48 ++++++++++ 9 files changed, 337 insertions(+), 264 deletions(-) create mode 100644 tests/test_build.py create mode 100644 tests/test_rest.py diff --git a/jenkins_epo/extensions/jenkins.py b/jenkins_epo/extensions/jenkins.py index ee76fb1c..de450a11 100644 --- a/jenkins_epo/extensions/jenkins.py +++ b/jenkins_epo/extensions/jenkins.py @@ -19,7 +19,7 @@ from jenkinsapi.custom_exceptions import UnknownJob from ..bot import Extension, Error, SkipHead -from ..jenkins import JENKINS +from ..jenkins import Build, JENKINS, NotOnJenkins from ..repository import Commit, CommitStatus from ..utils import match, switch_coro @@ -129,6 +129,30 @@ def aggregate_queues(self, cancel_queue, poll_queue): for commit, status in poll_queue: yield commit, status, False + @asyncio.coroutine + def poll_build(self, commit, status, cancel): + asyncio.Task.current_task().logging_id = self.current.head.sha[:4] + logger.debug("Query Jenkins %s status for %s.", status, commit) + try: + build = yield from Build.from_url(status['target_url']) + except NotOnJenkins as e: + logger.debug("%s not on this Jenkins", status['target_url']) + return + + if cancel and build.is_running: + if self.current.SETTINGS.DRY_RUN: + logger.warn("Would cancelling %s.", build) + else: + logger.warn("Cancelling %s.", build) + yield from build.stop() + new_status = status.__class__( + status, state='error', description='Cancelled after push.' + ) + else: + new_status = CommitStatus(status, **build.commit_status) + + commit.maybe_update_status(new_status) + @asyncio.coroutine def run(self): aggregated_queue = self.aggregate_queues( @@ -136,38 +160,12 @@ def run(self): ) logger.info("Polling job statuses on Jenkins.") - for commit, status, cancel in aggregated_queue: - if not str(status['target_url']).startswith(JENKINS.baseurl): - continue - - logger.debug("Query Jenkins %s status for %s.", status, commit) - try: - build = JENKINS.get_build_from_url(status['target_url']) - build.poll() - except Exception as e: - logger.debug( - "Failed to get pending build status for contexts: %s: %s", - e.__class__.__name__, e, - ) - build = None - - if not build: - new_status = status.__class__( - status, state='error', description="Build not on Jenkins." - ) - elif cancel and build.is_running(): - if self.current.SETTINGS.DRY_RUN: - logger.warn("Would cancelling %s.", build) - else: - logger.warn("Cancelling %s.", build) - build.stop() - new_status = status.__class__( - status, state='error', description='Cancelled after push.' - ) - else: - new_status = status.from_build(build) - - commit.maybe_update_status(new_status) + loop = asyncio.get_event_loop() + tasks = [ + loop.create_task(self.poll_build(*args)) + for args in aggregated_queue + ] + yield from asyncio.gather(*tasks) class CreateJobsExtension(JenkinsExtension): @@ -283,9 +281,9 @@ def iter_preset_statuses(self, contextes, build): status = self.current.statuses.get( context, default_status, ) - new_url = status.get('target_url') == build.baseurl + new_url = status.get('target_url') == build.url if status.is_queueable or new_url: - status = status.from_build(build) + status = CommitStatus(status, **build.commit_status) yield status @asyncio.coroutine @@ -321,7 +319,7 @@ def poll_job(self, spec): continue else: commit = Commit(self.current.head.repository, build.sha) - status = CommitStatus(context=job.name).from_build(build) + status = CommitStatus(context=job.name, **build.commit_status) logger.info("Queuing %s for cancel.", build) self.current.cancel_queue.append((commit, status)) logger.debug("Polling %s done.", spec.name) diff --git a/jenkins_epo/jenkins.py b/jenkins_epo/jenkins.py index dab259d7..a8cd458f 100644 --- a/jenkins_epo/jenkins.py +++ b/jenkins_epo/jenkins.py @@ -22,27 +22,29 @@ import aiohttp from jenkinsapi.jenkinsbase import JenkinsBase -from jenkinsapi.build import Build as JenkinsBuild from jenkinsapi.jenkins import Jenkins, Requester from jenkins_yml import Job as JobSpec -import requests import yaml from yarl import URL from .settings import SETTINGS -from .utils import match, parse_patterns, retry +from .utils import format_duration, match, parse_patterns, retry from .web import fullurl logger = logging.getLogger(__name__) +class NotOnJenkins(Exception): + pass + + class RESTClient(object): def __init__(self, path=''): self.path = path def __call__(self, arg): - return self.__class__(self.path + '/' + str(arg)) + return self.__class__(self.path.lstrip('/') + '/' + str(arg)) def __getattr__(self, name): return self(name) @@ -59,12 +61,28 @@ def afetch(self, **kw): payload = yield from response.read() finally: yield from session.close() + response.raise_for_status() return payload.decode('utf-8') def aget(self, **kw): payload = yield from self.api.python.afetch(**kw) return ast.literal_eval(payload) + @retry + def apost(self, **kw): + session = aiohttp.ClientSession() + url = URL(self.path) + if kw: + url = url.with_query(**kw) + logger.debug("POST %s", url) + try: + response = yield from session.post(url, timeout=10) + payload = yield from response.read() + finally: + yield from session.close() + response.raise_for_status() + return payload.decode('utf-8') + class VerboseRequester(Requester): def get_url(self, url, *a, **kw): @@ -77,7 +95,6 @@ def get_url(self, url, *a, **kw): class LazyJenkins(object): - build_url_re = re.compile(r'.*/job/(?P.*?)/.*(?P\d+)/?') queue_patterns = parse_patterns(SETTINGS.JENKINS_QUEUE) def __init__(self, instance=None): @@ -87,23 +104,6 @@ def __getattr__(self, name): self.load() return getattr(self._instance, name) - @retry - def get_build_from_url(self, url): - if not url.startswith(self.baseurl): - raise Exception("%s is not on this Jenkins" % url) - match = self.build_url_re.match(url) - if not match: - raise Exception("Failed to parse build URL %s" % url) - job_name = match.group('job') - job = self.get_job(job_name) - buildno = int(match.group('buildno')) - try: - return Build(url, buildno, job) - except requests.exceptions.HTTPError as e: - if 404 == e.response.status_code: - raise Exception("Build %s not found. Lost ?" % url) - raise - @retry def load(self): if not self._instance: @@ -195,31 +195,79 @@ def update_job(self, job_spec): class Build(object): - def __init__(self, job, payload, api_instance=None): + def __init__(self, job, payload): self.job = job self.payload = payload - self._instance = api_instance - self.params = self.process_params(payload) + self.actions = self.process_actions(payload) + self.params = self.process_params(self.actions) @staticmethod - def process_params(payload): + def process_actions(payload): + known_actions = {'lastBuiltRevision', 'parameters'} + actions = {} for action in payload.get('actions', []): - if 'parameters' in action: - break - else: - return {} + for known in known_actions: + if known in action: + actions[known] = action[known] + return actions + @staticmethod + def process_params(actions): return { p['name']: p['value'] - for p in action['parameters'] + for p in actions.get('parameters', []) if 'value' in p } + jenkins_tree = ( + "actions[" + ( + "parameters[name,value]," + "lastBuiltRevision[branch[name,SHA1]]" + ) + "]," + "building,displayName,duration,fullDisplayName," + "number,result,timestamp,url" + ) + + @classmethod + @asyncio.coroutine + def from_url(cls, url): + if not url.startswith(SETTINGS.JENKINS_URL): + raise NotOnJenkins("%s is not on this Jenkins." % url) + + payload = yield from RESTClient(url).aget(tree=cls.jenkins_tree) + return Build(None, payload) + def __getattr__(self, name): - return getattr(self._instance, name) + return self.payload[name] + + def __repr__(self): + return '<%s %s>' % ( + self.__class__.__name__, self.payload['fullDisplayName'] + ) def __str__(self): - return str(self._instance) + return str(self.payload['fullDisplayName']) + + _status_map = { + # Requeue an aborted job + 'ABORTED': ('error', 'Aborted!'), + 'FAILURE': ('failure', 'Build %(name)s failed in %(duration)s!'), + 'UNSTABLE': ('failure', 'Build %(name)s failed in %(duration)s!'), + 'SUCCESS': ('success', 'Build %(name)s succeeded in %(duration)s!'), + None: ('pending', 'Build %(name)s in progress...'), + } + + @property + def commit_status(self): + state, description = self._status_map[self.payload['result']] + description = description % dict( + name=self.payload['displayName'], + duration=format_duration(self.payload['duration']), + ) + return dict( + description=description, state=state, + target_url=self.payload['url'], + ) @property def is_outdated(self): @@ -244,8 +292,8 @@ def is_running(self): @property def ref(self): try: - fullref = self.payload['lastBuiltRevision']['branch']['name'] - except (TypeError, KeyError): + fullref = self.actions['lastBuiltRevision']['branch'][0]['name'] + except (IndexError, KeyError, TypeError): return self.params[self.job.revision_param][len('refs/heads/'):] else: match = self._ref_re.match(fullref) @@ -256,9 +304,14 @@ def ref(self): @property def sha(self): try: - return self.payload['lastBuiltRevision']['branch']['SHA1'] - except (TypeError, KeyError): - raise Exception("No SHA1 yet.") + return self.actions['lastBuiltRevision']['branch'][0]['SHA1'] + except (IndexError, KeyError, TypeError) as e: + raise Exception("No SHA1 yet.") from e + + @asyncio.coroutine + def stop(self): + payload = yield from RESTClient(self.payload['url']).stop.apost() + return payload class Job(object): @@ -355,26 +408,13 @@ def node_param(self): @asyncio.coroutine def fetch_builds(self): - tree = "builds[" + ( - "actions[" + ( - "parameters[name,value]," - "lastBuiltRevision[branch[name,SHA1]]" - ) + "]," - "building,duration,number,result,timestamp,url" - ) + "]" + tree = "builds[" + Build.jenkins_tree + "]" payload = yield from RESTClient(self.baseurl).aget(tree=tree) return payload['builds'] def process_builds(self, payload): payload = reversed(sorted(payload, key=lambda b: b['number'])) - for entry in payload: - api_instance = JenkinsBuild( - url=entry['url'], - buildno=entry['number'], - job=self._instance - ) - api_instance._data = entry - yield Build(self, entry, api_instance) + return (Build(self, entry) for entry in payload) class FreestyleJob(Job): diff --git a/jenkins_epo/repository.py b/jenkins_epo/repository.py index 8a7dd410..21711769 100644 --- a/jenkins_epo/repository.py +++ b/jenkins_epo/repository.py @@ -28,9 +28,7 @@ cached_arequest, cached_request, unpaginate, GITHUB, ApiNotFoundError ) from .settings import SETTINGS -from .utils import ( - Bunch, format_duration, match, parse_datetime, parse_patterns, retry, -) +from .utils import Bunch, match, parse_datetime, parse_patterns, retry logger = logging.getLogger(__name__) @@ -94,34 +92,6 @@ def is_rebuildable(self): return True return False - jenkins_status_map = { - # Requeue an aborted job - 'ABORTED': ('error', 'Aborted!'), - 'FAILURE': ('failure', 'Build %(name)s failed in %(duration)s!'), - 'UNSTABLE': ('failure', 'Build %(name)s failed in %(duration)s!'), - 'SUCCESS': ('success', 'Build %(name)s succeeded in %(duration)s!'), - None: ('pending', 'Build %(name)s in progress...'), - } - - def from_build(self, build=None): - # If no build found, this may be an old CI build, or any other - # unconfirmed build. Retrigger. - jenkins_status = build.get_status() if build else 'ABORTED' - if build and jenkins_status in self.jenkins_status_map: - state, description = self.jenkins_status_map[jenkins_status] - if description != 'Backed': - description = description % dict( - name=build._data['displayName'], - duration=format_duration(build._data['duration']), - ) - return self.__class__( - self, description=description, state=state, - target_url=build._data['url'], - ) - else: - # Don't touch - return self.__class__(self) - class RepositoriesRegistry(dict): def __init__(self, *a, **kw): diff --git a/tests/extensions/test_jenkins.py b/tests/extensions/test_jenkins.py index a8d307cd..c97e27a1 100644 --- a/tests/extensions/test_jenkins.py +++ b/tests/extensions/test_jenkins.py @@ -1,6 +1,7 @@ import asyncio from unittest.mock import Mock +from asynctest import CoroutineMock import pytest @@ -119,24 +120,28 @@ def test_build_failed(): @pytest.mark.asyncio @asyncio.coroutine def test_cancel_ignore_other(mocker): - JENKINS = mocker.patch('jenkins_epo.extensions.jenkins.JENKINS') - from jenkins_epo.extensions.jenkins import CancellerExtension, CommitStatus + Build = mocker.patch('jenkins_epo.extensions.jenkins.Build') + from jenkins_epo.extensions.jenkins import ( + CancellerExtension, CommitStatus, NotOnJenkins + ) - JENKINS.baseurl = 'jenkins://' + Build.from_url = CoroutineMock(side_effect=NotOnJenkins()) commit = Mock() ext = CancellerExtension('test', Mock()) ext.current = ext.bot.current + ext.current.head.sha = 'cafed0d0' ext.current.poll_queue = [] ext.current.cancel_queue = [ - (commit, CommitStatus(context='ci/...', target_url='circleci://1')), + (commit, CommitStatus( + context='ci/...', target_url='circleci://1', state='pending', + )), ] ext.current.last_commit.fetch_statuses.return_value = [] yield from ext.run() - assert not JENKINS.get_build_from_url.mock_calls assert not commit.maybe_update_status.mock_calls @@ -144,6 +149,7 @@ def test_cancel_ignore_other(mocker): @asyncio.coroutine def test_cancel_build_running(mocker): JENKINS = mocker.patch('jenkins_epo.extensions.jenkins.JENKINS') + Build = mocker.patch('jenkins_epo.extensions.jenkins.Build') from jenkins_epo.extensions.jenkins import CancellerExtension, CommitStatus JENKINS.baseurl = 'jenkins://' @@ -152,6 +158,7 @@ def test_cancel_build_running(mocker): ext = CancellerExtension('test', Mock()) ext.current = ext.bot.current + ext.current.head.sha = 'cafed0d0' ext.current.poll_queue = [] ext.current.cancel_queue = [ (commit, CommitStatus(context='job', target_url='jenkins://job/1')), @@ -159,8 +166,8 @@ def test_cancel_build_running(mocker): ext.current.SETTINGS.DRY_RUN = 0 ext.current.last_commit.fetch_statuses.return_value = [] - build = JENKINS.get_build_from_url.return_value - build.get_status.return_value = None + Build.from_url = CoroutineMock() + build = Build.from_url.return_value yield from ext.run() @@ -172,6 +179,7 @@ def test_cancel_build_running(mocker): @asyncio.coroutine def test_poll_build_running(mocker): JENKINS = mocker.patch('jenkins_epo.extensions.jenkins.JENKINS') + Build = mocker.patch('jenkins_epo.extensions.jenkins.Build') from jenkins_epo.extensions.jenkins import CancellerExtension, CommitStatus JENKINS.baseurl = 'jenkins://' @@ -180,17 +188,15 @@ def test_poll_build_running(mocker): ext = CancellerExtension('test', Mock()) ext.current = ext.bot.current + ext.current.head.sha = 'cafed0d0' ext.current.cancel_queue = [] ext.current.poll_queue = [ (commit, CommitStatus(context='job', target_url='jenkins://job/1')), ] ext.current.last_commit.fetch_statuses.return_value = [] - build = JENKINS.get_build_from_url.return_value - build.get_status.return_value = None - build._data = { - 'duration': 0, 'displayName': '#6', 'url': 'jenkins://job/1', - } + Build.from_url = CoroutineMock() + build = Build.from_url.return_value yield from ext.run() @@ -202,12 +208,14 @@ def test_poll_build_running(mocker): @asyncio.coroutine def test_poll_lost_build(mocker): JENKINS = mocker.patch('jenkins_epo.extensions.jenkins.JENKINS') + Build = mocker.patch('jenkins_epo.extensions.jenkins.Build') from jenkins_epo.extensions.jenkins import CancellerExtension, CommitStatus commit = Mock() ext = CancellerExtension('test', Mock()) ext.current = ext.bot.current + ext.current.head.sha = 'cafed0d0' ext.current.poll_queue = [] ext.current.cancel_queue = [ (commit, CommitStatus(context='job', target_url='jenkins://job/1')), @@ -215,8 +223,9 @@ def test_poll_lost_build(mocker): ext.current.last_commit.fetch_statuses.return_value = [] JENKINS.baseurl = 'jenkins://' - JENKINS.get_build_from_url.side_effect = Exception('POUET') + Build.from_url = CoroutineMock() yield from ext.run() + assert Build.from_url.mock_calls assert commit.maybe_update_status.mock_calls diff --git a/tests/extensions/test_poll.py b/tests/extensions/test_poll.py index 477ed37c..8f986026 100644 --- a/tests/extensions/test_poll.py +++ b/tests/extensions/test_poll.py @@ -7,7 +7,7 @@ @pytest.mark.asyncio @asyncio.coroutine -def test_jenkins_skip_outdated(): +def test_skip_outdated(): from jenkins_epo.extensions.jenkins import PollExtension ext = PollExtension('test', Mock()) @@ -31,7 +31,7 @@ def test_jenkins_skip_outdated(): @pytest.mark.asyncio @asyncio.coroutine -def test_jenkins_skip_build_not_running(): +def test_skip_build_not_running(): from jenkins_epo.extensions.jenkins import PollExtension ext = PollExtension('test', Mock()) @@ -55,7 +55,7 @@ def test_jenkins_skip_build_not_running(): @pytest.mark.asyncio @asyncio.coroutine -def test_jenkins_skip_other_branch(): +def test_skip_other_branch(): from jenkins_epo.extensions.jenkins import PollExtension ext = PollExtension('test', Mock()) @@ -81,7 +81,7 @@ def test_jenkins_skip_other_branch(): @pytest.mark.asyncio @asyncio.coroutine -def test_jenkins_skip_current_sha(mocker): +def test_skip_current_sha(mocker): from jenkins_epo.extensions.jenkins import PollExtension ext = PollExtension('test', Mock()) @@ -109,7 +109,7 @@ def test_jenkins_skip_current_sha(mocker): @pytest.mark.asyncio @asyncio.coroutine -def test_jenkins_preset_status_cloning(mocker): +def test_preset_status_cloning(mocker): # When Jenkins is cloning, the build is real but no status is reported, we # preset status on latest sha. from jenkins_epo.extensions.jenkins import PollExtension @@ -127,14 +127,14 @@ def test_jenkins_preset_status_cloning(mocker): job.list_contexts.return_value = ['job'] job.fetch_builds = CoroutineMock() job.process_builds.return_value = builds = [Mock(spec=[ - '_data', 'is_outdated', 'is_running', 'ref', 'baseurl', 'get_status', + 'is_outdated', 'is_running', 'ref', 'url' ])] build = builds[0] build.is_outdated = False build.is_running = True build.ref = 'branch' - build.get_status.return_value = None - build._data = dict(displayName='#1', duration=1, url='url://') + build.commit_status = dict() + build.url = 'url://' yield from ext.run() @@ -144,7 +144,7 @@ def test_jenkins_preset_status_cloning(mocker): @pytest.mark.asyncio @asyncio.coroutine -def test_jenkins_cancel(mocker): +def test_cancel(mocker): from jenkins_epo.extensions.jenkins import PollExtension ext = PollExtension('test', Mock()) @@ -163,8 +163,8 @@ def test_jenkins_cancel(mocker): build.is_running = True build.ref = 'branch' build.sha = '01d' - build.get_status.return_value = None - build._data = dict(displayName='#1', duration=1, url='url://') + build.url = 'url://' + build.commit_status = dict() yield from ext.run() diff --git a/tests/test_build.py b/tests/test_build.py new file mode 100644 index 00000000..4368a994 --- /dev/null +++ b/tests/test_build.py @@ -0,0 +1,124 @@ +import asyncio +from asynctest import CoroutineMock, Mock +from time import time + +import pytest + + +@pytest.mark.asyncio +@asyncio.coroutine +def test_from_url(SETTINGS, mocker): + SETTINGS.JENKINS_URL = 'jenkins://' + RESTClient = mocker.patch('jenkins_epo.jenkins.RESTClient') + + from jenkins_epo.jenkins import Build, NotOnJenkins + + with pytest.raises(NotOnJenkins): + yield from Build.from_url('circleci:///') + + RESTClient().aget = CoroutineMock(return_value=dict(number=1)) + + build = yield from Build.from_url('jenkins://job/1') + + assert 1 == build.number + + +def test_props(): + from jenkins_epo.jenkins import Build + + build = Build(job=Mock(), payload={ + 'timestamp': 1000 * (time() - 3600 * 4), + 'building': False, 'fullDisplayName': '#1' + }) + + assert build.is_outdated + assert not build.is_running + assert str(build) + assert repr(build) + + with pytest.raises(Exception): + build.sha + + build = Build(job=Mock(), payload=dict( + build.payload, + actions=[{'lastBuiltRevision': {'branch': [{'SHA1': 'cafed0d0'}]}}] + )) + + assert build.sha == 'cafed0d0' + + +def test_ref(): + from jenkins_epo.jenkins import Build + + build = Build(job=Mock(), payload={}) + + with pytest.raises(Exception): + build.ref + + build.job.revision_param = 'R' + build.params['R'] = 'refs/heads/master' + + assert 'master' == build.ref + + build.actions['lastBuiltRevision'] = { + 'branch': [{'name': 'otherremote/master'}] + } + + with pytest.raises(Exception): + build.ref + + build.actions['lastBuiltRevision'] = { + 'branch': [{'name': 'refs/remote/origin/master'}] + } + + assert 'master' == build.ref + + +def test_params(): + from jenkins_epo.jenkins import Build + + assert 0 == len(Build.process_params({})) + assert 0 == len(Build.process_params({'actions': [{'parameters': []}]})) + assert 0 == len(Build.process_params({ + 'actions': [{'parameters': [{'name': 'value'}]}] + })) + + +def test_future(): + from jenkins_epo.jenkins import Build + + build = Build(job=Mock(), payload={'timestamp': 1000 * (time() + 300)}) + + assert not build.is_outdated + + +def test_commit_status(): + from jenkins_epo.jenkins import Build + + payload = dict(displayName='#3 on master', duration=0, url='url://job/1') + build = Build(Mock(), payload=dict(payload, result=None)) + assert 'pending' == build.commit_status['state'] + + build = Build(Mock(), payload=dict(payload, result='ABORTED')) + assert 'error' == build.commit_status['state'] + + build = Build(Mock(), payload=dict(payload, result='SUCCESS')) + assert 'success' == build.commit_status['state'] + + build = Build(Mock(), payload=dict(payload, result='FAILURE')) + assert 'failure' == build.commit_status['state'] + + +@pytest.mark.asyncio +@asyncio.coroutine +def test_stop(SETTINGS, mocker): + RESTClient = mocker.patch('jenkins_epo.jenkins.RESTClient') + + from jenkins_epo.jenkins import Build + + build = Build(Mock(), payload=dict(url='jenkins://')) + RESTClient().stop.apost = CoroutineMock() + + yield from build.stop() + + assert RESTClient().stop.apost.mock_calls diff --git a/tests/test_jenkins.py b/tests/test_jenkins.py index 56b98dd2..0232d47e 100644 --- a/tests/test_jenkins.py +++ b/tests/test_jenkins.py @@ -1,32 +1,9 @@ import asyncio from asynctest import patch, CoroutineMock, Mock -from time import time import pytest -@pytest.mark.asyncio -@asyncio.coroutine -def test_rest_client(mocker): - ClientSession = mocker.patch('jenkins_epo.jenkins.aiohttp.ClientSession') - from jenkins_epo.jenkins import RESTClient - - client = RESTClient() - client = client('http://jenkins/path').subpath - - session = ClientSession.return_value - - response = Mock(name='response') - session.get = CoroutineMock(return_value=response) - response.read = CoroutineMock( - return_value=repr(dict(unittest=True)).encode('utf-8') - ) - - payload = yield from client.aget(param=1) - - assert payload['unittest'] - - def test_lazy_load(mocker): Jenkins = mocker.patch('jenkins_epo.jenkins.Jenkins') from jenkins_epo.jenkins import JENKINS @@ -73,72 +50,8 @@ def test_process_builds(): ])) assert 2 == len(builds) - assert 2 == builds[0].buildno - assert 1 == builds[1].buildno - - -def test_build_props(): - from jenkins_epo.jenkins import Build - - build = Build(job=Mock(), payload={ - 'timestamp': 1000 * (time() - 3600 * 4), - 'building': False, - }, api_instance=Mock()) - - assert build.is_outdated - assert not build.is_running - assert str(build) - - with pytest.raises(Exception): - build.sha - - build.payload['lastBuiltRevision'] = {'branch': {'SHA1': 'cafed0d0'}} - assert build.sha == 'cafed0d0' - - -def test_build_ref(): - from jenkins_epo.jenkins import Build - - build = Build(job=Mock(), payload={}, api_instance=Mock()) - - with pytest.raises(Exception): - build.ref - - build.job.revision_param = 'R' - build.params['R'] = 'refs/heads/master' - - assert 'master' == build.ref - - build.payload['lastBuiltRevision'] = { - 'branch': {'name': 'otherremote/master'} - } - - with pytest.raises(Exception): - build.ref - - build.payload['lastBuiltRevision'] = { - 'branch': {'name': 'refs/remote/origin/master'} - } - - assert 'master' == build.ref - - -def test_build_params(): - from jenkins_epo.jenkins import Build - - assert 0 == len(Build.process_params({})) - assert 0 == len(Build.process_params({'actions': [{'parameters': []}]})) - assert 0 == len(Build.process_params({ - 'actions': [{'parameters': [{'name': 'value'}]}] - })) - - -def test_build_future(): - from jenkins_epo.jenkins import Build - - build = Build(job=Mock(), payload={'timestamp': 1000 * (time() + 300)}) - - assert not build.is_outdated + assert 2 == builds[0].number + assert 1 == builds[1].number def test_freestyle_build(SETTINGS): @@ -362,8 +275,7 @@ def test_matrix_build(SETTINGS): assert api_instance.invoke.mock_calls -@patch('jenkins_epo.jenkins.requests.post') -def test_matrix_build_dry(post, SETTINGS): +def test_matrix_build_dry(mocker, SETTINGS): from jenkins_epo.jenkins import MatrixJob, JobSpec SETTINGS.DRY_RUN = 1 @@ -382,11 +294,9 @@ def test_matrix_build_dry(post, SETTINGS): job._node_axis = job._revision_param = None job._combination_param = 'C' - post.return_value.status_code = 200 - job.build(Mock(url='url://'), spec, 'matrix') - assert not post.mock_calls + assert not job.api_instance.invoke.mock_calls @patch('jenkins_epo.jenkins.Job.factory') diff --git a/tests/test_repositories.py b/tests/test_repositories.py index 7dd14b07..18bcee8f 100644 --- a/tests/test_repositories.py +++ b/tests/test_repositories.py @@ -448,32 +448,6 @@ def test_fetch_combined(cached_request): assert ret == cached_request.return_value -def test_commit_status_from_build(): - from jenkins_epo.repository import CommitStatus - - status = CommitStatus(context='job', state='pending') - build = Mock(_data=dict(duration=5, displayName='job', url='X')) - - build.get_status.return_value = None - new_status = status.from_build(build) - - build.get_status.return_value = 'ABORTED' - new_status = status.from_build(build) - assert 'error' == new_status['state'] - - build.get_status.return_value = 'SUCCESS' - new_status = status.from_build(build) - assert 'success' == new_status['state'] - - build.get_status.return_value = 'FAILURE' - new_status = status.from_build(build) - assert 'failure' == new_status['state'] - - build.get_status.return_value = 'UNKNOWN' - new_status = status.from_build(build) - assert status == new_status - - @patch('jenkins_epo.repository.cached_request') def test_commit_date(cached_request): from jenkins_epo.repository import Commit diff --git a/tests/test_rest.py b/tests/test_rest.py new file mode 100644 index 00000000..8c57441e --- /dev/null +++ b/tests/test_rest.py @@ -0,0 +1,48 @@ +import asyncio +from asynctest import CoroutineMock, Mock + +import pytest + + +@pytest.mark.asyncio +@asyncio.coroutine +def test_get(mocker): + ClientSession = mocker.patch('jenkins_epo.jenkins.aiohttp.ClientSession') + from jenkins_epo.jenkins import RESTClient + + client = RESTClient() + client = client('http://jenkins/path').subpath + + session = ClientSession.return_value + + response = Mock(name='response') + session.get = CoroutineMock(return_value=response) + response.read = CoroutineMock( + return_value=repr(dict(unittest=True)).encode('utf-8') + ) + + payload = yield from client.aget(param=1) + + assert payload['unittest'] + + +@pytest.mark.asyncio +@asyncio.coroutine +def test_post(mocker): + ClientSession = mocker.patch('jenkins_epo.jenkins.aiohttp.ClientSession') + from jenkins_epo.jenkins import RESTClient + + client = RESTClient() + client = client('http://jenkins/path').subpath + + session = ClientSession.return_value + + response = Mock(name='response') + session.post = CoroutineMock(return_value=response) + response.read = CoroutineMock( + return_value=repr(dict(unittest=True)).encode('utf-8') + ) + + payload = yield from client.apost(param=1) + + assert ': True' in payload From 3b0733762f412680dcdf9243553e4d839ec09bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20BERSAC=20=28bersace=29?= Date: Tue, 21 Feb 2017 10:11:15 +0100 Subject: [PATCH 2/2] Log lint --- jenkins_epo/extensions/jenkins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jenkins_epo/extensions/jenkins.py b/jenkins_epo/extensions/jenkins.py index de450a11..2a0a5b99 100644 --- a/jenkins_epo/extensions/jenkins.py +++ b/jenkins_epo/extensions/jenkins.py @@ -80,7 +80,7 @@ def run(self): yield from switch_coro() toqueue_contexts = [] for context in not_built: - logger.debug("Computing new status for %s.", spec) + logger.debug("Computing next state for %s.", spec) new_status = self.current.last_commit.maybe_update_status( self.status_for_new_context(job, context, queue_empty), )