From nobody Fri Oct 25 19:25:54 2024 Delivered-To: importer@patchew.org Received-SPF: pass (zoho.com: domain of redhat.com designates 209.132.183.28 as permitted sender) client-ip=209.132.183.28; envelope-from=patchew-devel-bounces@redhat.com; helo=mx1.redhat.com; Authentication-Results: mx.zohomail.com; dkim=fail; spf=pass (zoho.com: domain of redhat.com designates 209.132.183.28 as permitted sender) smtp.mailfrom=patchew-devel-bounces@redhat.com; dmarc=pass(p=none dis=none) header.from=redhat.com Return-Path: Received: from mx1.redhat.com (mx1.redhat.com [209.132.183.28]) by mx.zohomail.com with SMTPS id 1521298055748648.3978602307159; Sat, 17 Mar 2018 07:47:35 -0700 (PDT) Received: from smtp.corp.redhat.com (int-mx06.intmail.prod.int.phx2.redhat.com [10.5.11.16]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id ADBD04E4C6; Sat, 17 Mar 2018 14:47:34 +0000 (UTC) Received: from colo-mx.corp.redhat.com (colo-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.21]) by smtp.corp.redhat.com (Postfix) with ESMTPS id A12B27EE12; Sat, 17 Mar 2018 14:47:34 +0000 (UTC) Received: from lists01.pubmisc.prod.ext.phx2.redhat.com (lists01.pubmisc.prod.ext.phx2.redhat.com [10.5.19.33]) by colo-mx.corp.redhat.com (Postfix) with ESMTP id 959244CA98; Sat, 17 Mar 2018 14:47:34 +0000 (UTC) Received: from smtp.corp.redhat.com (int-mx04.intmail.prod.int.phx2.redhat.com [10.5.11.14]) by lists01.pubmisc.prod.ext.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id w2HElXqg007100 for ; Sat, 17 Mar 2018 10:47:33 -0400 Received: by smtp.corp.redhat.com (Postfix) id 6AC3E76133; Sat, 17 Mar 2018 14:47:33 +0000 (UTC) Received: from mx1.redhat.com (ext-mx03.extmail.prod.ext.phx2.redhat.com [10.5.110.27]) by smtp.corp.redhat.com (Postfix) with ESMTPS id 63C2F5DD7B for ; Sat, 17 Mar 2018 14:47:33 +0000 (UTC) Received: from mail-wr0-f176.google.com (mail-wr0-f176.google.com [209.85.128.176]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 03FF283F45 for ; Sat, 17 Mar 2018 14:47:25 +0000 (UTC) Received: by mail-wr0-f176.google.com with SMTP id n12so14364454wra.2 for ; Sat, 17 Mar 2018 07:47:24 -0700 (PDT) Received: from donizetti.lan (94-36-191-219.adsl-ull.clienti.tiscali.it. [94.36.191.219]) by smtp.gmail.com with ESMTPSA id p12sm7724855wre.89.2018.03.17.07.47.21 (version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256); Sat, 17 Mar 2018 07:47:22 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=sender:from:to:cc:subject:date:message-id:in-reply-to:references; bh=LfafLmNA2Uc2JfD7JK6LYz2tEdoV7Z2bvxfJVY9Jpaw=; b=Ij9Nh72D2rTlHN33zoK9UEe8FYXXfLa1cw7VL6Z8Yr/AmPjJDtiieIQJt43Q/hAyOv X3/2J+Uj8W8SzWwGrIkCZXcX16O/A7qx7f9L9T5T2doRPSwV7QfGAS/j1gPH07kc3V0Q h/3Ko/TWjc1zLYxtFQWd+jaJe4rwU0fpkaVIl2WZnVyHpLgoaphTSeTlV932Ybq1OIjU eojheTHwj3sF3yuzdPJLIs/vvyrRijSoaui4ZYIg/Brmyz3/WSZZdEjj0hU+P8DttRUp 6rsNQecoXj9y7N9JFaNfIaTt2HorDjCJqDZc8BDOumc9LDN9vd96nElEvIrIQb4+gMTu f1Uw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20161025; h=x-gm-message-state:sender:from:to:cc:subject:date:message-id :in-reply-to:references; bh=LfafLmNA2Uc2JfD7JK6LYz2tEdoV7Z2bvxfJVY9Jpaw=; b=jUOvryL9blUW7wM8Qorc6HuUOdsJwp1Vs+dq4yvBOpSnbJS//bHKwqHdI1ExDKO/gH doJkM2f0QwZIx52x3lwjksQnsnEHcJbAQ0BF1Ap2GcOMto8oXEX7t3R4wndmhM6ZaKX+ XN8W6GdonJvkgWeJRWMnU0HqPDn+N+82Tgv19g6HXUlH1OuGrd3jOuF6mmmwOlyP/qzF /VIYIhdIzW6+sM+K9IDSQOkgb+D+Z1tigiGMrcR3oBaSJYML2wdEr3rAFtY2OCdiulPi 32VgeN83HAIBE13LsK6bdV0EbiTOYoJ5n+SH6pnDR7+V+YsZitV1+jsah/7IfPvPQU8G Pyqw== X-Gm-Message-State: AElRT7G0qc5hApMpeEUgaBdihypHpMGX3AvPyGRthxzqdFuuj9x3KeKe yssqyV23I9Hr4LErTmhvGpOGM5zo X-Google-Smtp-Source: AG47ELubbIwBnGm91uqT/NZX/YfXqNV/tMbTbBtEh/QBDRKNQhkqB+BmrhpmoarzJ2HSYjbyTNI86A== X-Received: by 10.223.141.143 with SMTP id o15mr4923710wrb.272.1521298043049; Sat, 17 Mar 2018 07:47:23 -0700 (PDT) From: Paolo Bonzini To: patchew-devel@redhat.com Date: Sat, 17 Mar 2018 15:47:13 +0100 Message-Id: <20180317144714.18268-5-pbonzini@redhat.com> In-Reply-To: <20180317144714.18268-1-pbonzini@redhat.com> References: <20180317144714.18268-1-pbonzini@redhat.com> X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.27]); Sat, 17 Mar 2018 14:47:25 +0000 (UTC) X-Greylist: inspected by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.27]); Sat, 17 Mar 2018 14:47:25 +0000 (UTC) for IP:'209.85.128.176' DOMAIN:'mail-wr0-f176.google.com' HELO:'mail-wr0-f176.google.com' FROM:'paolo.bonzini@gmail.com' RCPT:'' X-RedHat-Spam-Score: 0.497 (DKIM_SIGNED, DKIM_VALID, FREEMAIL_FORGED_FROMDOMAIN, FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS, RCVD_IN_MSPIKE_H2, SPF_PASS) 209.85.128.176 mail-wr0-f176.google.com 209.85.128.176 mail-wr0-f176.google.com X-RedHat-Possible-Forgery: Paolo Bonzini X-Scanned-By: MIMEDefang 2.78 on 10.5.110.27 X-Scanned-By: MIMEDefang 2.79 on 10.5.11.14 X-loop: patchew-devel@redhat.com Cc: Shubham Jain Subject: [Patchew-devel] [PATCH 4/5] rest: create separate results endpoint X-BeenThere: patchew-devel@redhat.com X-Mailman-Version: 2.1.12 Precedence: junk List-Id: Patchew development and discussion list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Sender: patchew-devel-bounces@redhat.com Errors-To: patchew-devel-bounces@redhat.com X-Scanned-By: MIMEDefang 2.79 on 10.5.11.16 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.38]); Sat, 17 Mar 2018 14:47:34 +0000 (UTC) X-ZohoMail-DKIM: fail (Header signature does not verify) X-ZohoMail: RDKM_2 RSF_0 Z_629925259 SPT_0 Content-Type: text/plain; charset="utf-8" In the REST API, each series can have one or more results. Right now results are synthesized from message properties which are set by the applier-report and tester-report API. In order to simplify the conversion to REST of those APIs (e.g. using POST, PUT or PATCH requests), move the results out of the SeriesSerializer and into their own collection. For now, the ViewSet is not model-based, but I am including paginator-like fields so that it should be possible to change it to a ModelViewSet in the future. Signed-off-by: Paolo Bonzini --- api/rest.py | 75 +++++++++++++++++++++++++++++++++++++++++------= ---- api/urls.py | 5 ++++ mods/git.py | 4 +-- mods/testing.py | 4 +-- tests/test_git.py | 40 +++++++++++++++------------ tests/test_rest.py | 6 +++++ tests/test_testing.py | 28 +++++++++---------- 7 files changed, 111 insertions(+), 51 deletions(-) diff --git a/api/rest.py b/api/rest.py index 7df7618..3d22de8 100644 --- a/api/rest.py +++ b/api/rest.py @@ -8,19 +8,22 @@ # This work is licensed under the MIT License. Please see the LICENSE fil= e or # http://opensource.org/licenses/MIT. =20 -from collections import namedtuple +from collections import namedtuple, OrderedDict =20 from django.contrib.auth.models import User +from django.http import Http404 from django.template import loader =20 from mod import dispatch_module_hook from .models import Project, Message from .search import SearchEngine -from rest_framework import permissions, serializers, viewsets, filters, mi= xins, renderers +from rest_framework import (permissions, serializers, viewsets, filters, + mixins, generics, renderers) from rest_framework.decorators import detail_route -from rest_framework.fields import SerializerMethodField +from rest_framework.fields import SerializerMethodField, CharField, JSONFi= eld from rest_framework.relations import HyperlinkedIdentityField from rest_framework.response import Response +import rest_framework =20 SEARCH_PARAM =3D 'q' =20 @@ -143,16 +146,16 @@ class ProjectMessagesViewSetMixin(mixins.RetrieveMode= lMixin): def get_queryset(self): return self.queryset.filter(project=3Dself.kwargs['projects_pk']) =20 -class Result(namedtuple("Result", "name status log_url data")): +class Result(namedtuple("Result", "name status log_url message data")): __slots__ =3D () =20 - def __new__(cls, name, status, log_url=3DNone, data=3DNone, request=3D= None): + def __new__(cls, name, status, message, log_url=3DNone, data=3DNone, r= equest=3DNone): if log_url is not None and request is not None: log_url =3D request.build_absolute_uri(log_url) if status not in ('pending', 'success', 'failure'): raise ValueError("invalid value '%s' for status field" % statu= s) return super(cls, Result).__new__(cls, status=3Dstatus, log_url=3D= log_url, - data=3Ddata, name=3Dname) + message=3Dmessage, data=3Ddata, = name=3Dname) =20 # Series =20 @@ -176,8 +179,8 @@ class SeriesSerializer(BaseMessageSerializer): =20 resource_uri =3D HyperlinkedMessageField(view_name=3D'series-detail') message =3D HyperlinkedMessageField(view_name=3D'messages-detail') + results =3D HyperlinkedMessageField(view_name=3D'results-list', lookup= _field=3D'series_message_id') total_patches =3D SerializerMethodField() - results =3D SerializerMethodField() =20 def __init__(self, *args, **kwargs): self.detailed =3D kwargs.pop('detailed', False) @@ -190,13 +193,6 @@ class SeriesSerializer(BaseMessageSerializer): fields=3Dfields, detailed=3Dself.detailed) return fields =20 - def get_results(self, message): - results =3D [] - request =3D self.context['request'] - dispatch_module_hook("rest_results_hook", request=3Drequest, - message=3Dmessage, results=3Dresults) - return {x.name: x._asdict() for x in results} - def get_total_patches(self, obj): return obj.get_total_patches() =20 @@ -329,3 +325,54 @@ class MessagesViewSet(ProjectMessagesViewSetMixin, serializer =3D BaseMessageSerializer(page, many=3DTrue, context=3Dself.get_serializer_c= ontext()) return self.get_paginated_response(serializer.data) + +# Results + +class HyperlinkedResultField(HyperlinkedIdentityField): + def get_url(self, obj, view_name, request, format): + message =3D obj.message + kwargs =3D {'projects_pk': message.project_id, 'series_message_id'= : message.message_id, + 'name': obj.name} + return self.reverse(view_name, kwargs=3Dkwargs, request=3Drequest,= format=3Dformat) + +class ResultSerializer(serializers.Serializer): + resource_uri =3D HyperlinkedResultField(view_name=3D'results-detail') + name =3D CharField() + status =3D CharField() # one of 'failure', 'success', 'pending' + log_url =3D CharField(required=3DFalse) + data =3D JSONField(required=3DFalse) + +class SeriesResultsViewSet(viewsets.ViewSet, generics.GenericAPIView): + serializer_class =3D ResultSerializer + lookup_field =3D 'name' + lookup_value_regex =3D '[^/]+' + + def get_queryset(self): + return Message.objects.filter(project=3Dself.kwargs['projects_pk'], + message_id=3Dself.kwargs['series_mes= sage_id']) + + def get_results(self): + message =3D self.get_queryset()[0] + results =3D [] + dispatch_module_hook("rest_results_hook", request=3Dself.request, + message=3Dmessage, results=3Dresults) + return {x.name: x for x in results} + + def list(self, request, *args, **kwargs): + results =3D self.get_results().values() + serializer =3D self.get_serializer(results, many=3DTrue) + # Fake paginator response for forwards-compatibility, in case + # this ViewSet becomes model-based + return Response(OrderedDict([ + ('count', len(results)), + ('results', serializer.data) + ])) + + def retrieve(self, request, name, *args, **kwargs): + results =3D self.get_results() + try: + result =3D results[name] + except KeyError: + raise Http404 + serializer =3D self.get_serializer(result) + return Response(serializer.data) diff --git a/api/urls.py b/api/urls.py index 0a447f0..becabeb 100644 --- a/api/urls.py +++ b/api/urls.py @@ -37,11 +37,16 @@ projects_router.include_format_suffixes =3D False projects_router.register('series', rest.ProjectSeriesViewSet, base_name=3D= 'series') projects_router.register('messages', rest.MessagesViewSet, base_name=3D'me= ssages') =20 +results_router =3D NestedDefaultRouter(projects_router, 'series', lookup= =3D'series', trailing_slash=3DTrue) +results_router.include_format_suffixes =3D False +results_router.register('results', rest.SeriesResultsViewSet, base_name=3D= 'results') + schema_view =3D get_schema_view(title=3D'API schema') =20 urlpatterns =3D _build_urls() + [ url(r"v1/", include(router.urls)), url(r"v1/", include(projects_router.urls)), + url(r"v1/", include(results_router.urls)), url(r'^v1/schema/$', schema_view), # Use the base class's handler by default url(r".*", views.APIView.as_view()) diff --git a/mods/git.py b/mods/git.py index fc71174..8164854 100644 --- a/mods/git.py +++ b/mods/git.py @@ -129,8 +129,8 @@ class GitModule(PatchewModule): else: status =3D 'pending' log_url =3D None - results.append(Result(name=3D'git', status=3Dstatus, log_url=3Dlog= _url, data=3Ddata, - request=3Drequest)) + results.append(Result(name=3D'git', message=3Dmessage, status=3Dst= atus, + log_url=3Dlog_url, data=3Ddata, request=3Dre= quest)) =20 def prepare_message_hook(self, request, message, detailed): if not message.is_series_head: diff --git a/mods/testing.py b/mods/testing.py index 970256d..6f7d1ae 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -290,8 +290,8 @@ class TestingModule(PatchewModule): failed =3D not p["passed"] log_url =3D self.reverse_testing_log(message, tn, request=3Dre= quest, html=3DFalse) passed_str =3D "failure" if failed else "success" - results.append(Result(name=3D'testing.' + tn, status=3Dpassed_= str, log_url=3Dlog_url, - request=3Drequest)) + results.append(Result(name=3D'testing.' + tn, message=3Dmessag= e, status=3Dpassed_str, + log_url=3Dlog_url, request=3Drequest)) =20 def prepare_message_hook(self, request, message, detailed): if not message.is_series_head: diff --git a/tests/test_git.py b/tests/test_git.py index 4744ef4..fdf6b67 100755 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -103,31 +103,37 @@ class GitTest(PatchewTestCase): self.repo + " patchew/20160628014747.20971-2-famz= @redhat.com") =20 def test_rest_need_apply(self): - resp =3D self.apply_and_retrieve("0013-foo-patch.mbox.gz", self.p.= id, - "20160628014747.20971-1-famz@redhat= .com") - self.assertEqual(resp.data['results']['git']['status'], 'pending') + self.cli_import("0013-foo-patch.mbox.gz") + MESSAGE_ID =3D '20160628014747.20971-1-famz@redhat.com' + resp =3D self.api_client.get('%sseries/%s/results/git/' % (self.PR= OJECT_BASE, MESSAGE_ID)) + self.assertEqual(resp.data['status'], 'pending') + self.assertEqual(resp.data['log_url'], None) =20 def test_rest_apply_failure(self): self.cli_import("0014-bar-patch.mbox.gz") self.do_apply() - resp =3D self.api_client.get(self.REST_BASE + 'series/?q=3Dproject= :QEMU') - self.assertEqual(resp.data['results'][0]['is_complete'], True) - self.assertEqual(resp.data['results'][0]['results']['git']['status= '], 'failure') - self.assertEqual('repo' in resp.data['results'][0]['results']['git= '], False) - self.assertEqual('tag' in resp.data['results'][0]['results']['git'= ], False) - log =3D self.client.get(resp.data['results'][0]['results']['git'][= 'log_url']) - self.assertEquals(log.status_code, 200) + MESSAGE_ID =3D '20160628014747.20971-2-famz@redhat.com' + resp =3D self.api_client.get('%sseries/%s/' % (self.PROJECT_BASE, = MESSAGE_ID)) + self.assertEqual(resp.data['is_complete'], True) + resp =3D self.api_client.get('%sseries/%s/results/git/' % (self.PR= OJECT_BASE, MESSAGE_ID)) + self.assertEqual(resp.data['status'], 'failure') + self.assertEqual('repo' in resp.data, False) + self.assertEqual('tag' in resp.data, False) + log =3D self.client.get(resp.data['log_url']) + self.assertEqual(log.status_code, 200) =20 def test_rest_apply_success(self): self.cli_import("0013-foo-patch.mbox.gz") self.do_apply() - resp =3D self.api_client.get(self.REST_BASE + 'series/?q=3Dproject= :QEMU') - self.assertEqual(resp.data['results'][0]['is_complete'], True) - self.assertEqual(resp.data['results'][0]['results']['git']['status= '], 'success') - self.assertEqual(resp.data['results'][0]['results']['git']['data']= ['repo'], self.repo) - self.assertEqual(resp.data['results'][0]['results']['git']['data']= ['tag'], "refs/tags/patchew/20160628014747.20971-1-famz@redhat.com") - log =3D self.client.get(resp.data['results'][0]['results']['git'][= 'log_url']) - self.assertEquals(log.status_code, 200) + MESSAGE_ID =3D '20160628014747.20971-1-famz@redhat.com' + resp =3D self.api_client.get('%sseries/%s/' % (self.PROJECT_BASE, = MESSAGE_ID)) + self.assertEqual(resp.data['is_complete'], True) + resp =3D self.api_client.get('%sseries/%s/results/git/' % (self.PR= OJECT_BASE, MESSAGE_ID)) + self.assertEqual(resp.data['status'], 'success') + self.assertEqual(resp.data['data']['repo'], self.repo) + self.assertEqual(resp.data['data']['tag'], "refs/tags/patchew/2016= 0628014747.20971-1-famz@redhat.com") + log =3D self.client.get(resp.data['log_url']) + self.assertEqual(log.status_code, 200) =20 if __name__ =3D=3D '__main__': main() diff --git a/tests/test_rest.py b/tests/test_rest.py index 1a7285b..38fd6bf 100755 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -193,6 +193,12 @@ class RestTest(PatchewTestCase): resp =3D self.api_client.get(self.REST_BASE + 'projects/12345/seri= es/') self.assertEqual(resp.data['count'], 0) =20 + def test_series_results_list(self): + resp1 =3D self.apply_and_retrieve('0001-simple-patch.mbox.gz', + self.p.id, '20160628014747.20971-1-= famz@redhat.com') + resp =3D self.api_client.get(resp1.data['results']) + self.assertEqual(resp.data['count'], len(resp.data['results'])) + def test_series_search(self): resp1 =3D self.apply_and_retrieve('0004-multiple-patch-reviewed.mb= ox.gz', self.p.id, '1469192015-16487-1-git= -send-email-berrange@redhat.com') diff --git a/tests/test_testing.py b/tests/test_testing.py index 0d7e316..9bc03ab 100755 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -94,41 +94,37 @@ class TestingTest(PatchewTestCase): capabilities=3D[]) self.assertFalse(td) =20 - def test_rest_basic(self): - resp =3D self.api_client.get(self.PROJECT_BASE + 'series/' + self.= msg.message_id + '/') - self.assertEquals('testing.tests' in resp.data['results'], False) - def test_rest_done_success(self): self.msg_testing_done(log=3D'everything good!', passed=3DTrue) - resp =3D self.api_client.get(self.PROJECT_BASE + 'series/' + self.= msg.message_id + '/') - self.assertEquals(resp.data['results']['testing.tests']['status'],= 'success') - log =3D self.client.get(resp.data['results']['testing.tests']['log= _url']) + resp =3D self.api_client.get('%sseries/%s/results/testing.tests/' = % (self.PROJECT_BASE, self.msg.message_id)) + self.assertEquals(resp.data['status'], 'success') + log =3D self.client.get(resp.data['log_url']) self.assertEquals(log.status_code, 200) self.assertEquals(log.content, b'everything good!') =20 def test_rest_done_failure(self): self.msg_testing_done(log=3D'sorry no good', passed=3DFalse) - resp =3D self.api_client.get(self.PROJECT_BASE + 'series/' + self.= msg.message_id + '/') - self.assertEquals(resp.data['results']['testing.tests']['status'],= 'failure') - log =3D self.client.get(resp.data['results']['testing.tests']['log= _url']) + resp =3D self.api_client.get('%sseries/%s/results/testing.tests/' = % (self.PROJECT_BASE, self.msg.message_id)) + self.assertEquals(resp.data['status'], 'failure') + log =3D self.client.get(resp.data['log_url']) self.assertEquals(log.status_code, 200) self.assertEquals(log.content, b'sorry no good') =20 def test_api_report_success(self): self.api_login() msgid =3D self.msg_testing_report(log=3D'everything good!', passed= =3DTrue) - resp =3D self.api_client.get(self.PROJECT_BASE + 'series/' + self.= msg.message_id + '/') - self.assertEquals(resp.data['results']['testing.a']['status'], 'su= ccess') - log =3D self.client.get(resp.data['results']['testing.a']['log_url= ']) + resp =3D self.api_client.get('%sseries/%s/results/testing.a/' % (s= elf.PROJECT_BASE, self.msg.message_id)) + self.assertEquals(resp.data['status'], 'success') + log =3D self.client.get(resp.data['log_url']) self.assertEquals(log.status_code, 200) self.assertEquals(log.content, b'everything good!') =20 def test_api_report_failure(self): self.api_login() msgid =3D self.msg_testing_report(log=3D'sorry no good', passed=3D= False) - resp =3D self.api_client.get(self.PROJECT_BASE + 'series/' + self.= msg.message_id + '/') - self.assertEquals(resp.data['results']['testing.a']['status'], 'fa= ilure') - log =3D self.client.get(resp.data['results']['testing.a']['log_url= ']) + resp =3D self.api_client.get('%sseries/%s/results/testing.a/' % (s= elf.PROJECT_BASE, self.msg.message_id)) + self.assertEquals(resp.data['status'], 'failure') + log =3D self.client.get(resp.data['log_url']) self.assertEquals(log.status_code, 200) self.assertEquals(log.content, b'sorry no good') =20 --=20 2.16.2 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel