From nobody Tue May 13 08:33:27 2025 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 1527761047352903.0280739773514; Thu, 31 May 2018 03:04:07 -0700 (PDT) Received: from smtp.corp.redhat.com (int-mx09.intmail.prod.int.phx2.redhat.com [10.5.11.24]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 74E9730A7606; Thu, 31 May 2018 10:04:06 +0000 (UTC) Received: from colo-mx.corp.redhat.com (colo-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.20]) by smtp.corp.redhat.com (Postfix) with ESMTPS id 61BE1308BDA6; Thu, 31 May 2018 10:04:06 +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 5275718005D1; Thu, 31 May 2018 10:04:06 +0000 (UTC) Received: from smtp.corp.redhat.com (int-mx09.intmail.prod.int.phx2.redhat.com [10.5.11.24]) by lists01.pubmisc.prod.ext.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id w4VA445u024995 for ; Thu, 31 May 2018 06:04:04 -0400 Received: by smtp.corp.redhat.com (Postfix) id 8858F308BDA8; Thu, 31 May 2018 10:04:04 +0000 (UTC) Received: from mx1.redhat.com (ext-mx08.extmail.prod.ext.phx2.redhat.com [10.5.110.32]) by smtp.corp.redhat.com (Postfix) with ESMTPS id 796F0308BDA6 for ; Thu, 31 May 2018 10:04:04 +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 91109C059B6D for ; Thu, 31 May 2018 10:03:51 +0000 (UTC) Received: by mail-wr0-f176.google.com with SMTP id k16-v6so5154800wro.0 for ; Thu, 31 May 2018 03:03:51 -0700 (PDT) Received: from donizetti.lan (dynamic-adsl-78-12-189-60.clienti.tiscali.it. [78.12.189.60]) by smtp.gmail.com with ESMTPSA id g16-v6sm3520664wro.86.2018.05.31.03.03.47 for (version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256); Thu, 31 May 2018 03:03:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20161025; h=sender:from:to:subject:date:message-id:in-reply-to:references; bh=Tl8C3GA7d5Fz2/b+sxGOuC/WmDYLNBWsUt45iYV1CKs=; b=t2T3dWX+YUOQF/XCzMGc/x+f+mFQSQLr5F21AtnXWYzmXJXOYQhqpVMiig1p6XBpwP oaG6bcdRGNknlLrUEfIlPfwEePShZS+cGuuEMBoPdr5lMuTsrtrNVorQfySVIeXeQQ7Z mWhXMlpsEx9QbiOfX15fZw7ocPk0VZVA6sMolKtKGccljXK7Dx9SYWvJyaolx0zrNGFS aghu2tauhvL4n1xL8aoNJB5pvx7z7yFcra1u2NSf+IFFcFkMgCggcVIWKLSenMESfwDf S9S7t+B0gWK/Sqfb9DXiRCLxjwYPe5qe1EI67t4nezI/lobKDUNrDx7ddhaA3l5sioPf sBYA== 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:subject:date:message-id :in-reply-to:references; bh=Tl8C3GA7d5Fz2/b+sxGOuC/WmDYLNBWsUt45iYV1CKs=; b=lDHL/HNQ5jbmtwFlSoZmrBm4Q2TnW8Ioiecx4QRU2AMHLyVspPoE3fQ2nyPWOrqyQ1 /rnOaba8WJ4kVRtlfdQAlHbtCtdLDPbHWjeex5MaWf5lUrdjrAEGo6hHKI7AhD6ReyUR LatPpsfG9UcJuwY17gICsiOl2/P0FFSuFPaGrBD+hoOS5I06aIBqrEemB1EhTB55sh4s 0MY0vuHxKjYzpjWJrrYSdfds9ekjT8c2spZQpXSmS+yfhxQtdAE+SiXOlQX5dkD0Ayju bi7YosLBGz7r8x6A+2wvRPX6Kkl7mrgG+wM0QtmzIE51AYk8YunY5fV5cfs/O7gXQU0C 5wDA== X-Gm-Message-State: ALKqPwfjAnogdWQ9E2/s4bWme7zwLv5UlrIjSsPJNvoI6ajNZHqFbDvI 12X/WHPQzKF36ZW3KyriZYXydjjC X-Google-Smtp-Source: ADUXVKIYZ8D1B/iFLhoHp05RURx4fwy1BCV2fXs0T469oNbj310wVVB0PIXnACh28epfRVi3e+KSwA== X-Received: by 2002:adf:f2cb:: with SMTP id d11-v6mr4550143wrp.263.1527761029619; Thu, 31 May 2018 03:03:49 -0700 (PDT) From: Paolo Bonzini To: patchew-devel@redhat.com Date: Thu, 31 May 2018 12:03:31 +0200 Message-Id: <20180531100334.2249-8-pbonzini@redhat.com> In-Reply-To: <20180531100334.2249-1-pbonzini@redhat.com> References: <20180531100334.2249-1-pbonzini@redhat.com> X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.32]); Thu, 31 May 2018 10:03:52 +0000 (UTC) X-Greylist: inspected by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.32]); Thu, 31 May 2018 10:03:52 +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: -1.1 (DKIM_SIGNED, FREEMAIL_FORGED_FROMDOMAIN, FREEMAIL_FROM, HEADER_FROM_DIFFERENT_DOMAINS, RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H2, SPF_PASS, T_DKIM_INVALID) 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.32 X-Scanned-By: MIMEDefang 2.84 on 10.5.11.24 X-loop: patchew-devel@redhat.com Subject: [Patchew-devel] [PATCH 07/10] testing: switch to Result model 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.84 on 10.5.11.24 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.44]); Thu, 31 May 2018 10:04:06 +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" --- .../0029_populate_testing_results.py | 220 ++++++++++++++++++ mods/testing.py | 182 ++++++--------- tests/test_testing.py | 53 +++-- 3 files changed, 325 insertions(+), 130 deletions(-) create mode 100644 api/migrations/0029_populate_testing_results.py diff --git a/api/migrations/0029_populate_testing_results.py b/api/migratio= ns/0029_populate_testing_results.py new file mode 100644 index 0000000..0607936 --- /dev/null +++ b/api/migrations/0029_populate_testing_results.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations +from django.db.models import Count +from api.migrations import (get_property, get_property_raw, + load_property, set_property, delete_property_blob) + +import abc +from collections import defaultdict +from copy import copy +import datetime +import lzma + +# For Result's constant status values +import api.models + +class Converter(object, metaclass=3Dabc.ABCMeta): + def __init__(self, apps, schema_editor): + # We can't import the models directly as they may be a newer + # version than this migration expects. We use the historical versi= on. + self.Project =3D apps.get_model('api', 'Project') + self.Message =3D apps.get_model('api', 'Message') + self.MessageProperty =3D apps.get_model('api', 'MessageProperty') + self.ProjectProperty =3D apps.get_model('api', 'ProjectProperty') + self.MessageResult =3D apps.get_model('api', 'MessageResult') + self.ProjectResult =3D apps.get_model('api', 'ProjectResult') + self.LogEntry =3D apps.get_model('api', 'LogEntry') + + @abc.abstractmethod + def get_objects_for_project(self, po): + pass + + @abc.abstractmethod + def set_property(self, obj, name, value): + pass + + @abc.abstractmethod + def get_property(self, obj, name): + pass + + @abc.abstractmethod + def get_properties(self, obj, **kwargs): + pass + + @abc.abstractmethod + def delete_property(self, obj, name): + pass + + @abc.abstractmethod + def create_result(self, obj, **kwargs): + pass + + def get_projects_with_tests(self): + return self.Project.objects.filter( + projectproperty__name__startswith=3D'testing.').distinct() + + def get_tests(self, po): + # Based on TestingModule.get_tests + ret =3D {} + props =3D self.ProjectProperty.objects.filter(name__startswith=3D'= testing.tests.', + project=3Dpo) + for p in props: + k =3D p.name + v =3D load_property(p) + tn =3D k[len("testing.tests."):] + if "." not in tn: + continue + an =3D tn[tn.find(".") + 1:] + tn =3D tn[:tn.find(".")] + ret.setdefault(tn, {}) + ret[tn][an] =3D v + return ret + + def do_result_from_properties(self): + for po in self.Project.objects.all(): + tests =3D self.get_tests(po) + for obj in self.get_objects_for_project(po): + pending_status =3D api.models.Result.RUNNING \ + if self.get_property(obj, "testing.started") \ + else api.models.Result.PENDING + done_tests =3D set() + results =3D [] + for prop in self.get_properties(obj, name__startswith=3D't= esting.report.'): + tn =3D prop.name[len('testing.report.'):] + done_tests.add(tn) + r =3D self.create_result(obj) + report =3D load_property(prop) + passed =3D report["passed"] + del report["passed"] + log =3D self.get_property(obj, "testing.log." + tn) + r.name =3D 'testing.' + tn + r.status =3D api.models.Result.SUCCESS if passed else = api.models.Result.FAILURE + r.last_update =3D datetime.datetime.utcnow() + r.data =3D report + if log: + log_xz =3D lzma.compress(log.encode("utf-8")) + log_entry =3D self.LogEntry(data_xz=3Dlog_xz) + log_entry.save() + r.log_entry =3D log_entry + self.delete_property(obj, "testing.log." + tn) + r.save() + results.append(r) + self.delete_property(obj, "testing.report." + tn) + if self.get_property(obj, "testing.ready"): + for tn, test in tests.items(): + if tn in done_tests: + continue + r =3D self.create_result(obj) + r.name =3D 'testing.' + tn + r.status =3D pending_status + r.last_update =3D datetime.datetime.utcnow() + r.save() + results.append(r) + self.delete_property(obj, "testing.ready") + #print(obj, len(done_tests), len(tests) - len(done_tests)) + obj.results.add(*results) + try: + self.delete_property(obj, "testing.started") + self.delete_property(obj, "testing.failed") + self.delete_property(obj, "testing.start-time") + except: + pass + + def do_result_to_properties(self): + for po in self.Project.objects.all(): + for obj in self.get_objects_for_project(po): + by_status =3D defaultdict(lambda: 0) + start_time =3D datetime.datetime.utcnow() + for r in obj.results.filter(name__startswith=3D'testing.'): + by_status[r.status] +=3D 1 + if r.status in (api.models.Result.SUCCESS, api.models.= Result.FAILURE): + tn =3D r.name[len('testing.'):] + report =3D copy(r.data) + report['passed'] =3D (r.status =3D=3D api.models.R= esult.SUCCESS) + self.set_property(obj, "testing.report." + tn, rep= ort) + if r.log_entry: + log =3D lzma.decompress(r.log_entry.data_xz).d= ecode("utf-8") + self.set_property(obj, "testing.log." + tn, lo= g) + else: + started =3D started or r.status =3D=3D api.models.= Result.RUNNING + if r.last_update < start_time: + start_time =3D r.last_update + #print(obj, dict(by_status)) + if by_status[api.models.Result.FAILURE]: + self.set_property(obj, "testing.failed", True) + if by_status[api.models.Result.RUNNING]: + self.set_property(obj, "testing.started", True) + self.set_property(obj, "testing.start-time", d.timesta= mp()) + if by_status[api.models.Result.RUNNING] + by_status[api.mo= dels.Result.PENDING]: + self.set_property(obj, "testing.ready", 1) + else: + self.set_property(obj, "testing.done", True) + obj.results.filter(name__startswith=3D'testing.').delete() + +class ProjectConverter(Converter): + def get_objects_for_project(self, po): + yield po + + def set_property(self, obj, name, value): + set_property(self.ProjectProperty, name, value, project=3Dobj) + + def get_property(self, obj, name): + try: + return get_property(self.ProjectProperty, name, project=3Dobj) + except: + return None + + def get_properties(self, obj, **kwargs): + return self.ProjectProperty.objects.filter(project=3Dobj, **kwargs) + + def delete_property(self, obj, name): + delete_property_blob(self.ProjectProperty, name, project=3Dobj) + get_property_raw(self.ProjectProperty, name, project=3Dobj).delete= () + + def create_result(self, obj, **kwargs): + return self.ProjectResult(project=3Dobj, **kwargs) + +class MessageConverter(Converter): + def get_objects_for_project(self, po): + yield from self.Message.objects.filter(is_series_head=3DTrue, proj= ect=3Dpo) + + def set_property(self, obj, name, value): + set_property(self.MessageProperty, name, value, message=3Dobj) + + def get_property(self, obj, name): + try: + return get_property(self.MessageProperty, name, message=3Dobj) + except: + return None + + def get_properties(self, obj, **kwargs): + return self.MessageProperty.objects.filter(message=3Dobj, **kwargs) + + def delete_property(self, obj, name): + delete_property_blob(self.MessageProperty, name, message=3Dobj) + get_property_raw(self.MessageProperty, name, message=3Dobj).delete= () + + def create_result(self, obj, **kwargs): + return self.MessageResult(message=3Dobj, **kwargs) + +def result_from_properties(apps, schema_editor): + ProjectConverter(apps, schema_editor).do_result_from_properties() + MessageConverter(apps, schema_editor).do_result_from_properties() + +def result_to_properties(apps, schema_editor): + ProjectConverter(apps, schema_editor).do_result_to_properties() + MessageConverter(apps, schema_editor).do_result_to_properties() + +class Migration(migrations.Migration): + + dependencies =3D [ + ('api', '0028_populate_git_results'), + ] + + operations =3D [ + migrations.RunPython(result_from_properties, + reverse_code=3Dresult_to_properties), + ] diff --git a/mods/testing.py b/mods/testing.py index 8328a7c..9c053a7 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -11,13 +11,15 @@ from django.conf.urls import url from django.http import HttpResponseForbidden, Http404, HttpResponseRedire= ct from django.core.exceptions import PermissionDenied +from django.db.models import Q from django.urls import reverse from django.utils.html import format_html from mod import PatchewModule import time import math from api.views import APILoginRequiredView -from api.models import Message, MessageProperty, Project, Result, ResultTu= ple +from api.models import (Message, MessageProperty, MessageResult, + Project, ProjectResult, Result, ResultTuple) from api.search import SearchEngine from event import emit_event, declare_event, register_handler from patchew.logviewer import LogView @@ -42,10 +44,8 @@ class TestingLogViewer(LogView): obj =3D Message.objects.find_series(project_or_series) if not obj: raise Http404("Object not found: " + project_or_series) - log =3D obj.get_property("testing.log." + testing_name) - if log is None: - raise Http404("Testing log not found: " + testing_name) - return log + r =3D _instance.get_testing_result(obj, testing_name) + return r.log =20 =20 class TestingModule(PatchewModule): @@ -122,31 +122,40 @@ class TestingModule(PatchewModule): and old_value !=3D value: self.recalc_pending_tests(obj) =20 - def is_testing_done(self, obj, tn): - return obj.get_property("testing.report." + tn) + def get_testing_results(self, obj, *args, **kwargs): + return obj.results.filter(name__startswith=3D'testing.', *args, **= kwargs) + + def get_testing_result(self, obj, name): + try: + return obj.results.get(name=3D'testing.' + name) + except: + raise Http404("Test doesn't exist") + + def get_test_name(self, result): + return result.name[len('testing.'):] =20 def recalc_pending_tests(self, obj): test_dict =3D self.get_tests(obj) all_tests =3D set((k for k, v in test_dict.items() if v.get("enabl= ed", False))) + for r in self.get_testing_results(obj, status=3DResult.PENDING): + r.delete() if len(all_tests): - done_tests =3D set((tn for tn in all_tests if self.is_testing_= done(obj, tn))) + done_tests =3D [self.get_test_name(r) for r in self.get_testin= g_results(obj)] + for tn in all_tests: + if not tn in done_tests: + obj.create_result(name=3D'testing.' + tn, status=3DRes= ult.PENDING).save() if len(done_tests) < len(all_tests): obj.set_property("testing.done", None) - obj.set_property("testing.ready", 1) return obj.set_property("testing.done", True) - obj.set_property("testing.ready", None) =20 def clear_and_start_testing(self, obj, test=3D""): for k in list(obj.get_properties().keys()): - if (not test and k =3D=3D "testing.started") or \ - (not test and k =3D=3D "testing.start-time") or \ - (not test and k =3D=3D "testing.failed") or \ - k =3D=3D "testing.done" or \ - k =3D=3D "testing.tested-head" or \ - k.startswith("testing.report." + test) or \ - k.startswith("testing.log." + test): + if k =3D=3D "testing.done" or \ + k =3D=3D "testing.tested-head": obj.set_property(k, None) + for r in self.get_testing_results(obj): + r.delete() self.recalc_pending_tests(obj) =20 def www_view_testing_reset(self, request, project_or_series): @@ -202,24 +211,21 @@ class TestingModule(PatchewModule): raise Exception("Series doesn't exist") project =3D obj.project.name user =3D request.user - log_url =3D self.reverse_testing_log(obj, test, request=3Drequest) - html_log_url =3D self.reverse_testing_log(obj, test, request=3Dreq= uest, html=3DTrue) - obj.set_property("testing.report." + test, - {"passed": passed, - "is_timeout": is_timeout, - "user": user.username, - "tester": tester or user.username, - }) - obj.set_property("testing.log." + test, log) - if not passed: - obj.set_property("testing.failed", True) - reports =3D [x for x in obj.get_properties() if x.startswith("test= ing.report.")] - done_tests =3D set([x[len("testing.report."):] for x in reports]) - all_tests =3D set([k for k, v in self.get_tests(obj).items() if v[= "enabled"]]) - if all_tests.issubset(done_tests): + + r =3D self.get_testing_result(obj, test) + r.data =3D {"is_timeout": is_timeout, + "user": user.username, + "tester": tester or user.username} + r.log =3D log + r.status =3D Result.SUCCESS if passed else Result.FAILURE + r.save() + if not self.get_testing_results(obj, + status__in=3D(Result.PENDING, Result.RUNNING)).exis= ts(): obj.set_property("testing.done", True) - obj.set_property("testing.ready", None) obj.set_property("testing.tested-head", head) + + log_url =3D self.reverse_testing_log(obj, test, request=3Drequest) + html_log_url =3D self.reverse_testing_log(obj, test, request=3Dreq= uest, html=3DTrue) emit_event("TestingReport", tester=3Dtester, user=3Duser.username, obj=3Dobj, passed=3Dpassed, test=3Dtest, log=3Dlog, lo= g_url=3Dlog_url, html_log_url=3Dhtml_log_url, is_timeout=3Dis_timeout) @@ -257,11 +263,8 @@ class TestingModule(PatchewModule): "class": "warning", "icon": "refresh", }] - for pn, p in obj.get_properties().items(): - if not pn.startswith("testing.report."): - continue - tn =3D pn[len("testing.report."):] - failed =3D not p["passed"] + for r in self.get_testing_results(obj, ~Q(status=3DResult.PENDING)= ): + tn =3D self.get_test_name(r) ret.append({"url": url + "&test=3D" + tn, "title": format_html("Reset {} testing stat= e", tn), "class": "warning", @@ -270,39 +273,16 @@ class TestingModule(PatchewModule): return ret =20 def rest_results_hook(self, obj, results, detailed=3DFalse): - all_tests =3D set([k for k, v in _instance.get_tests(obj).items() = if v["enabled"]]) - for pn, p in obj.get_properties().items(): - if not pn.startswith("testing.report."): - continue - tn =3D pn[len("testing.report."):] - try: - all_tests.remove(tn) - except: - pass - failed =3D not p["passed"] - passed_str =3D Result.FAILURE if failed else Result.SUCCESS - if detailed: - log =3D obj.get_property("testing.log." + tn) - else: - log =3D None - - data =3D p.copy() - del data['passed'] - results.append(ResultTuple(name=3D'testing.' + tn, obj=3Dobj, = status=3Dpassed_str, - log=3Dlog, data=3Ddata, renderer=3Dself)) - - if obj.get_property("testing.ready"): - for tn in all_tests: - results.append(ResultTuple(name=3D'testing.' + tn, obj=3Do= bj, status=3D'pending')) + Result.get_result_tuples(obj, "testing", results) =20 def prepare_message_hook(self, request, message, detailed): if not message.is_series_head: return if message.project.maintained_by(request.user) \ - and message.get_property("testing.started"): + and self.get_testing_results(message, ~Q(status=3DResult.P= ENDING)).exists(): message.extra_ops +=3D self._build_reset_ops(message) =20 - if message.get_property("testing.failed"): + if self.get_testing_results(message, status=3DResult.FAILURE).exis= ts(): message.status_tags.append({ "title": "Testing failed", "url": reverse("series_detail", @@ -421,61 +401,45 @@ class TestingGetView(APILoginRequiredView): }, test=3Dtest) =20 - def _find_applicable_test(self, user, project, tester, capabilities, o= bj): - for tn, t in _instance.get_tests(project).items(): - if not t.get("enabled"): - continue - if _instance.is_testing_done(obj, tn): + def _find_applicable_test(self, queryset, user, po, tester, capabiliti= es): + # Prefer non-running tests, or tests that started the earliest + q =3D queryset.filter(status__in=3D(Result.PENDING, Result.RUNNING= ), + name__startswith=3D'testing.').order_by('statu= s', 'last_update') + tests =3D _instance.get_tests(po) + for r in q: + tn =3D _instance.get_test_name(r) + t =3D tests.get(tn, None) + # Shouldn't happen, but let's protect against it + if not t: continue - # TODO: group? - ok =3D True reqs =3D t.get("requirements", "") for r in [x.strip() for x in reqs.split(",") if x]: if r not in capabilities: - ok =3D False break - if not ok: - continue - return t + else: + yield r, t =20 def _find_project_test(self, request, po, tester, capabilities): - if not po.get_property("testing.ready"): - return head =3D po.get_property("git.head") repo =3D po.git tested =3D po.get_property("testing.tested-head") if not head or not repo: - return - test =3D self._find_applicable_test(request.user, po, - tester, capabilities, po) - if not test: - return - td =3D self._generate_project_test_data(po.name, repo, head, teste= d, test) - return po, td + return None + candidates =3D self._find_applicable_test(ProjectResult.objects.fi= lter(project=3Dpo), + request.user, po, tester, = capabilities) + for r, test in candidates: + td =3D self._generate_project_test_data(po.name, repo, head, t= ested, test) + return r, po, td + return None =20 def _find_series_test(self, request, po, tester, capabilities): - q =3D MessageProperty.objects.filter(name=3D"testing.ready", - value=3D1, - message__project=3Dpo) - candidate =3D None - for prop in q: - s =3D prop.message - test =3D self._find_applicable_test(request.user, po, - tester, capabilities, s) - if not test: - continue - if not s.get_property("testing.started"): - candidate =3D s, test - break - # Pick one series that started test the earliest - if not candidate or \ - s.get_property("testing.start-time") < \ - candidate[0].get_property("testing.start-time"): - candidate =3D s, test - if not candidate: - return None - return candidate[0], \ - self._generate_series_test_data(candidate[0], candidate[1]) + candidates =3D self._find_applicable_test(MessageResult.objects.fi= lter(message__project=3Dpo), + request.user, po, tester, = capabilities) + for r, test in candidates: + s =3D r.message + td =3D self._generate_series_test_data(s, test) + return r, s, td + return None =20 def handle(self, request, project, tester, capabilities): # Try project head test first @@ -486,9 +450,9 @@ class TestingGetView(APILoginRequiredView): candidate =3D self._find_series_test(request, po, tester, capa= bilities) if not candidate: return - obj, test_data =3D candidate - obj.set_property("testing.started", True) - obj.set_property("testing.start-time", time.time()) + r, obj, test_data =3D candidate + r.status =3D Result.RUNNING + r.save() return test_data =20 class TestingReportView(APILoginRequiredView): diff --git a/tests/test_testing.py b/tests/test_testing.py index 8f831a1..9e61a45 100755 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -14,7 +14,7 @@ import os import subprocess sys.path.append(os.path.dirname(__file__)) from patchewtest import PatchewTestCase, main -from api.models import Message +from api.models import Message, Result =20 def create_test(project, name): prefix =3D "testing.tests." + name + "." @@ -35,14 +35,24 @@ class TestingTestCase(PatchewTestCase, metaclass=3Dabc.= ABCMeta): =20 create_test(self.p, "a") =20 - def _do_testing_done(self, obj, log, report): - if not 'passed' in report: - report['passed'] =3D True - obj.set_property("testing.report.tests", report) - if log is not None: - obj.set_property("testing.log.tests", log) + def modify_test_result(self, obj, **kwargs): + try: + r =3D obj.results.get(name=3D'testing.a') + except: + r =3D obj.create_result(name=3D'testing.a') + if not 'status' in kwargs: + kwargs['status'] =3D Result.PENDING + + if len(kwargs): + for k, v in kwargs.items(): + setattr(r, k, v) + r.save() + + def _do_testing_done(self, obj, **kwargs): + if not 'status' in kwargs: + kwargs['status'] =3D Result.SUCCESS + self.modify_test_result(obj, **kwargs) obj.set_property("testing.done", True) - obj.set_property("testing.ready", None) =20 def do_testing_report(self, **report): self.api_login() @@ -81,6 +91,8 @@ class TestingTestCase(PatchewTestCase, metaclass=3Dabc.AB= CMeta): tester=3D"dummy tester", capabilities=3D[]) self.assertIn("head", td) + resp =3D self.get_test_result('a') + self.assertEquals(resp.data['status'], 'running') =20 def test_done(self): self.do_testing_done() @@ -96,8 +108,8 @@ class TestingTestCase(PatchewTestCase, metaclass=3Dabc.A= BCMeta): self.assertEquals(resp.data['status'], 'pending') =20 def test_rest_done_success(self): - self.do_testing_done(log=3D'everything good!', passed=3DTrue) - resp =3D self.get_test_result('tests') + self.do_testing_done(log=3D'everything good!', status=3DResult.SUC= CESS) + resp =3D self.get_test_result('a') self.assertEquals(resp.data['status'], 'success') self.assertEquals(resp.data['log'], 'everything good!') log =3D self.client.get(resp.data['log_url']) @@ -105,10 +117,9 @@ class TestingTestCase(PatchewTestCase, metaclass=3Dabc= .ABCMeta): self.assertEquals(log.content, b'everything good!') =20 def test_rest_done_failure(self): - self.do_testing_done(log=3D'sorry no good', passed=3DFalse, random= _stuff=3D'xyz') - resp =3D self.get_test_result('tests') + self.do_testing_done(log=3D'sorry no good', status=3DResult.FAILUR= E) + resp =3D self.get_test_result('a') self.assertEquals(resp.data['status'], 'failure') - self.assertEquals(resp.data['data']['random_stuff'], 'xyz') self.assertEquals(resp.data['log'], 'sorry no good') log =3D self.client.get(resp.data['log_url']) self.assertEquals(log.status_code, 200) @@ -151,8 +162,8 @@ class MessageTestingTest(TestingTestCase): self.msg.set_property("git.tag", "dummy tag") self.msg.set_property("git.base", "dummy base") =20 - def do_testing_done(self, log=3DNone, **report): - self._do_testing_done(self.msg, log, report) + def do_testing_done(self, **kwargs): + self._do_testing_done(self.msg, **kwargs) =20 def do_testing_report(self, **report): r =3D super(MessageTestingTest, self).do_testing_report(**report) @@ -164,17 +175,18 @@ class MessageTestingTest(TestingTestCase): self.PROJECT_BASE, self.msg.message= _id, test_name)) =20 def test_testing_ready(self): - self.assertTrue(self.msg.get_property("testing.ready")) # Set property through series_heads elements must be handled the s= ame self.msg.set_property("git.repo", None) self.msg.set_property("git.tag", None) - self.msg.set_property("testing.ready", None) + self.assertEqual(self.msg.results.filter(name=3D'testing.a').first= ().status, + Result.PENDING) msg =3D Message.objects.series_heads()[0] self.assertEqual(self.msg.message_id, msg.message_id) msg.set_property("git.repo", "dummy repo") msg.set_property("git.tag", "dummy tag") msg.set_property("git.base", "dummy base") - self.assertTrue(msg.get_property("testing.ready")) + self.assertEqual(self.msg.results.filter(name=3D'testing.a').first= ().status, + Result.PENDING) =20 class ProjectTestingTest(TestingTestCase): =20 @@ -182,10 +194,9 @@ class ProjectTestingTest(TestingTestCase): super(ProjectTestingTest, self).setUp() self.p.set_property("git.head", "5678") self.p.set_property("testing.tested-head", "1234") - self.p.set_property("testing.ready", 1) =20 - def do_testing_done(self, log=3DNone, **report): - self._do_testing_done(self.p, log, report) + def do_testing_done(self, **kwargs): + self._do_testing_done(self.p, **kwargs) =20 def do_testing_report(self, **report): r =3D super(ProjectTestingTest, self).do_testing_report(**report) --=20 2.17.0 _______________________________________________ Patchew-devel mailing list Patchew-devel@redhat.com https://www.redhat.com/mailman/listinfo/patchew-devel