From nobody Tue May 13 11:26:44 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 15269722894591010.1123947706388; Mon, 21 May 2018 23:58:09 -0700 (PDT) Received: from smtp.corp.redhat.com (int-mx11.intmail.prod.int.phx2.redhat.com [10.5.11.26]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 8AFD55A1F7; Tue, 22 May 2018 06:58:08 +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 76DD130BF205; Tue, 22 May 2018 06:58:08 +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 623E81801250; Tue, 22 May 2018 06:58:08 +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 w4M6w7LE028227 for ; Tue, 22 May 2018 02:58:07 -0400 Received: by smtp.corp.redhat.com (Postfix) id 789AE30001EB; Tue, 22 May 2018 06:58:07 +0000 (UTC) Received: from mx1.redhat.com (ext-mx05.extmail.prod.ext.phx2.redhat.com [10.5.110.29]) by smtp.corp.redhat.com (Postfix) with ESMTPS id 7062830012C8 for ; Tue, 22 May 2018 06:58:07 +0000 (UTC) Received: from mail-pl0-f43.google.com (mail-pl0-f43.google.com [209.85.160.43]) (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 15C90385A57 for ; Tue, 22 May 2018 06:57:55 +0000 (UTC) Received: by mail-pl0-f43.google.com with SMTP id c19-v6so10307804pls.6 for ; Mon, 21 May 2018 23:57:55 -0700 (PDT) Received: from localhost.localdomain ([208.181.63.202]) by smtp.gmail.com with ESMTPSA id y2-v6sm19818676pgp.92.2018.05.21.23.57.52 for (version=TLS1_2 cipher=ECDHE-RSA-CHACHA20-POLY1305 bits=256/256); Mon, 21 May 2018 23:57:52 -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=ecGiz2zEHzXwiajA82j3aQUE+pfBota3UBgnDtJqS1k=; b=BGePZIvQXE+DU8vRz/Vh9S5fvYQJDrOP+yPvR1VEQ326pkCTZDTOY0QjI/zT8RiclH F1xtAVI2H3Aoy1LZmIXMHIME3w1v+i/SKL1s0/08hLt2wQTTJgfsNsB/rafB57eOr1Nu lp6xH7HoQft9YL7UPlUAsy9LdXA1a9wxF5AWvrEczRT4ptqiMOVSmJqi6+6yuzC1t5kj zk+1u2z/ny5Mb/0xv4RckLMkJzcKORxfOe7+2G3XD1Mr33EyqOBiBLcEWz0OY9iKgus9 cdMWy2h2owmsd7jrOKTrwzsvhq7yXLcf/CGbrkcvmHZbudeHW83AszUaUs1UpPYLzEfU 9c1A== 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=ecGiz2zEHzXwiajA82j3aQUE+pfBota3UBgnDtJqS1k=; b=q2Lwy52ubgSqUU2gxJQ+OPQIzcu9wP+x7V+heh725yg63/oRk8X+UBo+VRlx+2A/gb gyceOVk5tzXH+y6LeVaxj+l1jBLq5dz+nH53YD1xmr7Wx+0/2Wx7F/cmulTehv9ZjSfd byBlZ+rbh//R0QYxTVYUFgydd5AqlsV2Bjg0SNDibvoqEoz6yySyeS41XNBXMzGbEa2a 9/XA3JRU+2Oe7NtGJ9Z3QHCgpL7JLlkBSuZRXu7wPmk2979JphZPYZZKYUEMavk6lPLB tE1w7ZUW3epDsyxxUM2WmPwV3oMXVrTOgJTMMOaeq6xAbix41NolwxP06i6fNY/9REe2 hgSQ== X-Gm-Message-State: ALKqPwflEvGeajbtLbrceerH9D6syXxzaVLUgGrQL7JiVkAHWTLAMCcT ypuE3m8xCB0MO3xbCciI5vgYTGtM X-Google-Smtp-Source: AB8JxZp2T35WqwsylrnAthPHCt9ouowpY4NFlwaJfT2S0svPalqZ/200JxqT60sk56Y9PpU9m4ytVg== X-Received: by 2002:a17:902:24e:: with SMTP id 72-v6mr23110518plc.87.1526972273778; Mon, 21 May 2018 23:57:53 -0700 (PDT) From: Paolo Bonzini To: patchew-devel@redhat.com Date: Tue, 22 May 2018 08:57:37 +0200 Message-Id: <20180522065740.9710-10-pbonzini@redhat.com> In-Reply-To: <20180522065740.9710-1-pbonzini@redhat.com> References: <20180522065740.9710-1-pbonzini@redhat.com> X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.29]); Tue, 22 May 2018 06:57:55 +0000 (UTC) X-Greylist: inspected by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.29]); Tue, 22 May 2018 06:57:55 +0000 (UTC) for IP:'209.85.160.43' DOMAIN:'mail-pl0-f43.google.com' HELO:'mail-pl0-f43.google.com' FROM:'paolo.bonzini@gmail.com' RCPT:'' X-RedHat-Spam-Score: -1.2 (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.160.43 mail-pl0-f43.google.com 209.85.160.43 mail-pl0-f43.google.com X-RedHat-Possible-Forgery: Paolo Bonzini X-Scanned-By: MIMEDefang 2.78 on 10.5.110.29 X-Scanned-By: MIMEDefang 2.84 on 10.5.11.24 X-loop: patchew-devel@redhat.com Subject: [Patchew-devel] [PATCH 09/12] 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.26 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.28]); Tue, 22 May 2018 06:58:08 +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 | 205 ++++++++++++++++++ mods/testing.py | 182 +++++++--------- tests/test_testing.py | 57 +++-- 3 files changed, 321 insertions(+), 123 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..0fa91ea --- /dev/null +++ b/api/migrations/0029_populate_testing_results.py @@ -0,0 +1,205 @@ +# -*- 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.Result =3D apps.get_model('api', 'Result') + + @abc.abstractmethod + def objects_filter_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 + + 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.objects_filter_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.Result() + 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: + r.log_xz =3D lzma.compress(log.encode("utf-8")) + 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.Result() + 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.objects_filter_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_xz: + log =3D lzma.decompress(r.log_xz).decode("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 objects_filter_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= () + +class MessageConverter(Converter): + def objects_filter_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 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 64952fb..ed40a92 100644 --- a/mods/testing.py +++ b/mods/testing.py @@ -11,6 +11,7 @@ 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 @@ -42,10 +43,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 +121,44 @@ 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() + obj.results.remove(r) 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: + r =3D Result(name=3D'testing.' + tn, status=3DResult.P= ENDING) + r.save() + obj.results.add(r) 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() + obj.results.remove(r) self.recalc_pending_tests(obj) =20 def www_view_testing_reset(self, request, project_or_series): @@ -202,24 +214,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 +266,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 +276,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(obj, ~Q(status=3DResult.PENDI= NG)).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 +404,56 @@ 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"): + def _find_applicable_test(self, user, po, tester, capabilities, **kwar= gs): + q =3D Result.objects.filter(status__in=3D(Result.PENDING, Result.R= UNNING), + name__startswith=3D'testing.', **kwargs) + 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 - if _instance.is_testing_done(obj, tn): - 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(request.user, po, tester= , capabilities, + project=3Dpo) + 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) + candidates =3D self._find_applicable_test(request.user, po, tester= , capabilities, + 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 + earliest_start_time =3D None + for r, test in candidates: + s =3D r.message_set.first() + if r.status =3D=3D Result.PENDING: + candidate =3D r, 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 + r.last_update < earliest_start_time: + candidate =3D r, s, test + earliest_start_time =3D r.last_update if not candidate: return None - return candidate[0], \ - self._generate_series_test_data(candidate[0], candidate[1]) + + td =3D self._generate_series_test_data(candidate[1], candidate[2]) + return candidate[0], candidate[1], td =20 def handle(self, request, project, tester, capabilities): # Try project head test first @@ -486,9 +464,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..b5314cf 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,28 @@ 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): + need_add =3D False + try: + r =3D obj.results.get(name=3D'testing.a') + except: + r =3D Result(name=3D'testing.a') + need_add =3D True + 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() + if need_add: + obj.results.add(r) + + 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 +95,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 +112,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 +121,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 +166,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 +179,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 +198,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