---
.../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/migrations/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=abc.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 version.
+ self.Project = apps.get_model('api', 'Project')
+ self.Message = apps.get_model('api', 'Message')
+ self.MessageProperty = apps.get_model('api', 'MessageProperty')
+ self.ProjectProperty = apps.get_model('api', 'ProjectProperty')
+ self.MessageResult = apps.get_model('api', 'MessageResult')
+ self.ProjectResult = apps.get_model('api', 'ProjectResult')
+ self.LogEntry = 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='testing.').distinct()
+
+ def get_tests(self, po):
+ # Based on TestingModule.get_tests
+ ret = {}
+ props = self.ProjectProperty.objects.filter(name__startswith='testing.tests.',
+ project=po)
+ for p in props:
+ k = p.name
+ v = load_property(p)
+ tn = k[len("testing.tests."):]
+ if "." not in tn:
+ continue
+ an = tn[tn.find(".") + 1:]
+ tn = tn[:tn.find(".")]
+ ret.setdefault(tn, {})
+ ret[tn][an] = v
+ return ret
+
+ def do_result_from_properties(self):
+ for po in self.Project.objects.all():
+ tests = self.get_tests(po)
+ for obj in self.get_objects_for_project(po):
+ pending_status = api.models.Result.RUNNING \
+ if self.get_property(obj, "testing.started") \
+ else api.models.Result.PENDING
+ done_tests = set()
+ results = []
+ for prop in self.get_properties(obj, name__startswith='testing.report.'):
+ tn = prop.name[len('testing.report.'):]
+ done_tests.add(tn)
+ r = self.create_result(obj)
+ report = load_property(prop)
+ passed = report["passed"]
+ del report["passed"]
+ log = self.get_property(obj, "testing.log." + tn)
+ r.name = 'testing.' + tn
+ r.status = api.models.Result.SUCCESS if passed else api.models.Result.FAILURE
+ r.last_update = datetime.datetime.utcnow()
+ r.data = report
+ if log:
+ log_xz = lzma.compress(log.encode("utf-8"))
+ log_entry = self.LogEntry(data_xz=log_xz)
+ log_entry.save()
+ r.log_entry = 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 = self.create_result(obj)
+ r.name = 'testing.' + tn
+ r.status = pending_status
+ r.last_update = 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 = defaultdict(lambda: 0)
+ start_time = datetime.datetime.utcnow()
+ for r in obj.results.filter(name__startswith='testing.'):
+ by_status[r.status] += 1
+ if r.status in (api.models.Result.SUCCESS, api.models.Result.FAILURE):
+ tn = r.name[len('testing.'):]
+ report = copy(r.data)
+ report['passed'] = (r.status == api.models.Result.SUCCESS)
+ self.set_property(obj, "testing.report." + tn, report)
+ if r.log_entry:
+ log = lzma.decompress(r.log_entry.data_xz).decode("utf-8")
+ self.set_property(obj, "testing.log." + tn, log)
+ else:
+ started = started or r.status == api.models.Result.RUNNING
+ if r.last_update < start_time:
+ start_time = 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.timestamp())
+ if by_status[api.models.Result.RUNNING] + by_status[api.models.Result.PENDING]:
+ self.set_property(obj, "testing.ready", 1)
+ else:
+ self.set_property(obj, "testing.done", True)
+ obj.results.filter(name__startswith='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=obj)
+
+ def get_property(self, obj, name):
+ try:
+ return get_property(self.ProjectProperty, name, project=obj)
+ except:
+ return None
+
+ def get_properties(self, obj, **kwargs):
+ return self.ProjectProperty.objects.filter(project=obj, **kwargs)
+
+ def delete_property(self, obj, name):
+ delete_property_blob(self.ProjectProperty, name, project=obj)
+ get_property_raw(self.ProjectProperty, name, project=obj).delete()
+
+ def create_result(self, obj, **kwargs):
+ return self.ProjectResult(project=obj, **kwargs)
+
+class MessageConverter(Converter):
+ def get_objects_for_project(self, po):
+ yield from self.Message.objects.filter(is_series_head=True, project=po)
+
+ def set_property(self, obj, name, value):
+ set_property(self.MessageProperty, name, value, message=obj)
+
+ def get_property(self, obj, name):
+ try:
+ return get_property(self.MessageProperty, name, message=obj)
+ except:
+ return None
+
+ def get_properties(self, obj, **kwargs):
+ return self.MessageProperty.objects.filter(message=obj, **kwargs)
+
+ def delete_property(self, obj, name):
+ delete_property_blob(self.MessageProperty, name, message=obj)
+ get_property_raw(self.MessageProperty, name, message=obj).delete()
+
+ def create_result(self, obj, **kwargs):
+ return self.MessageResult(message=obj, **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 = [
+ ('api', '0028_populate_git_results'),
+ ]
+
+ operations = [
+ migrations.RunPython(result_from_properties,
+ reverse_code=result_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, HttpResponseRedirect
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, ResultTuple
+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 = Message.objects.find_series(project_or_series)
if not obj:
raise Http404("Object not found: " + project_or_series)
- log = obj.get_property("testing.log." + testing_name)
- if log is None:
- raise Http404("Testing log not found: " + testing_name)
- return log
+ r = _instance.get_testing_result(obj, testing_name)
+ return r.log
class TestingModule(PatchewModule):
@@ -122,31 +122,40 @@ class TestingModule(PatchewModule):
and old_value != value:
self.recalc_pending_tests(obj)
- 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='testing.', *args, **kwargs)
+
+ def get_testing_result(self, obj, name):
+ try:
+ return obj.results.get(name='testing.' + name)
+ except:
+ raise Http404("Test doesn't exist")
+
+ def get_test_name(self, result):
+ return result.name[len('testing.'):]
def recalc_pending_tests(self, obj):
test_dict = self.get_tests(obj)
all_tests = set((k for k, v in test_dict.items() if v.get("enabled", False)))
+ for r in self.get_testing_results(obj, status=Result.PENDING):
+ r.delete()
if len(all_tests):
- done_tests = set((tn for tn in all_tests if self.is_testing_done(obj, tn)))
+ done_tests = [self.get_test_name(r) for r in self.get_testing_results(obj)]
+ for tn in all_tests:
+ if not tn in done_tests:
+ obj.create_result(name='testing.' + tn, status=Result.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)
def clear_and_start_testing(self, obj, test=""):
for k in list(obj.get_properties().keys()):
- if (not test and k == "testing.started") or \
- (not test and k == "testing.start-time") or \
- (not test and k == "testing.failed") or \
- k == "testing.done" or \
- k == "testing.tested-head" or \
- k.startswith("testing.report." + test) or \
- k.startswith("testing.log." + test):
+ if k == "testing.done" or \
+ k == "testing.tested-head":
obj.set_property(k, None)
+ for r in self.get_testing_results(obj):
+ r.delete()
self.recalc_pending_tests(obj)
def www_view_testing_reset(self, request, project_or_series):
@@ -202,24 +211,21 @@ class TestingModule(PatchewModule):
raise Exception("Series doesn't exist")
project = obj.project.name
user = request.user
- log_url = self.reverse_testing_log(obj, test, request=request)
- html_log_url = self.reverse_testing_log(obj, test, request=request, html=True)
- 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 = [x for x in obj.get_properties() if x.startswith("testing.report.")]
- done_tests = set([x[len("testing.report."):] for x in reports])
- all_tests = set([k for k, v in self.get_tests(obj).items() if v["enabled"]])
- if all_tests.issubset(done_tests):
+
+ r = self.get_testing_result(obj, test)
+ r.data = {"is_timeout": is_timeout,
+ "user": user.username,
+ "tester": tester or user.username}
+ r.log = log
+ r.status = Result.SUCCESS if passed else Result.FAILURE
+ r.save()
+ if not self.get_testing_results(obj,
+ status__in=(Result.PENDING, Result.RUNNING)).exists():
obj.set_property("testing.done", True)
- obj.set_property("testing.ready", None)
obj.set_property("testing.tested-head", head)
+
+ log_url = self.reverse_testing_log(obj, test, request=request)
+ html_log_url = self.reverse_testing_log(obj, test, request=request, html=True)
emit_event("TestingReport", tester=tester, user=user.username,
obj=obj, passed=passed, test=test, log=log, log_url=log_url,
html_log_url=html_log_url, is_timeout=is_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 = pn[len("testing.report."):]
- failed = not p["passed"]
+ for r in self.get_testing_results(obj, ~Q(status=Result.PENDING)):
+ tn = self.get_test_name(r)
ret.append({"url": url + "&test=" + tn,
"title": format_html("Reset <b>{}</b> testing state", tn),
"class": "warning",
@@ -270,39 +273,16 @@ class TestingModule(PatchewModule):
return ret
def rest_results_hook(self, obj, results, detailed=False):
- all_tests = 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 = pn[len("testing.report."):]
- try:
- all_tests.remove(tn)
- except:
- pass
- failed = not p["passed"]
- passed_str = Result.FAILURE if failed else Result.SUCCESS
- if detailed:
- log = obj.get_property("testing.log." + tn)
- else:
- log = None
-
- data = p.copy()
- del data['passed']
- results.append(ResultTuple(name='testing.' + tn, obj=obj, status=passed_str,
- log=log, data=data, renderer=self))
-
- if obj.get_property("testing.ready"):
- for tn in all_tests:
- results.append(ResultTuple(name='testing.' + tn, obj=obj, status='pending'))
+ Result.get_result_tuples(obj, "testing", results)
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=Result.PENDING)).exists():
message.extra_ops += self._build_reset_ops(message)
- if message.get_property("testing.failed"):
+ if self.get_testing_results(message, status=Result.FAILURE).exists():
message.status_tags.append({
"title": "Testing failed",
"url": reverse("series_detail",
@@ -421,61 +401,45 @@ class TestingGetView(APILoginRequiredView):
},
test=test)
- def _find_applicable_test(self, user, project, tester, capabilities, obj):
- 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, capabilities):
+ # Prefer non-running tests, or tests that started the earliest
+ q = queryset.filter(status__in=(Result.PENDING, Result.RUNNING),
+ name__startswith='testing.').order_by('status', 'last_update')
+ tests = _instance.get_tests(po)
+ for r in q:
+ tn = _instance.get_test_name(r)
+ t = tests.get(tn, None)
+ # Shouldn't happen, but let's protect against it
+ if not t:
continue
- # TODO: group?
- ok = True
reqs = t.get("requirements", "")
for r in [x.strip() for x in reqs.split(",") if x]:
if r not in capabilities:
- ok = False
break
- if not ok:
- continue
- return t
+ else:
+ yield r, t
def _find_project_test(self, request, po, tester, capabilities):
- if not po.get_property("testing.ready"):
- return
head = po.get_property("git.head")
repo = po.git
tested = po.get_property("testing.tested-head")
if not head or not repo:
- return
- test = self._find_applicable_test(request.user, po,
- tester, capabilities, po)
- if not test:
- return
- td = self._generate_project_test_data(po.name, repo, head, tested, test)
- return po, td
+ return None
+ candidates = self._find_applicable_test(ProjectResult.objects.filter(project=po),
+ request.user, po, tester, capabilities)
+ for r, test in candidates:
+ td = self._generate_project_test_data(po.name, repo, head, tested, test)
+ return r, po, td
+ return None
def _find_series_test(self, request, po, tester, capabilities):
- q = MessageProperty.objects.filter(name="testing.ready",
- value=1,
- message__project=po)
- candidate = None
- for prop in q:
- s = prop.message
- test = self._find_applicable_test(request.user, po,
- tester, capabilities, s)
- if not test:
- continue
- if not s.get_property("testing.started"):
- candidate = 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 = s, test
- if not candidate:
- return None
- return candidate[0], \
- self._generate_series_test_data(candidate[0], candidate[1])
+ candidates = self._find_applicable_test(MessageResult.objects.filter(message__project=po),
+ request.user, po, tester, capabilities)
+ for r, test in candidates:
+ s = r.message
+ td = self._generate_series_test_data(s, test)
+ return r, s, td
+ return None
def handle(self, request, project, tester, capabilities):
# Try project head test first
@@ -486,9 +450,9 @@ class TestingGetView(APILoginRequiredView):
candidate = self._find_series_test(request, po, tester, capabilities)
if not candidate:
return
- obj, test_data = candidate
- obj.set_property("testing.started", True)
- obj.set_property("testing.start-time", time.time())
+ r, obj, test_data = candidate
+ r.status = Result.RUNNING
+ r.save()
return test_data
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
def create_test(project, name):
prefix = "testing.tests." + name + "."
@@ -35,14 +35,24 @@ class TestingTestCase(PatchewTestCase, metaclass=abc.ABCMeta):
create_test(self.p, "a")
- def _do_testing_done(self, obj, log, report):
- if not 'passed' in report:
- report['passed'] = 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 = obj.results.get(name='testing.a')
+ except:
+ r = obj.create_result(name='testing.a')
+ if not 'status' in kwargs:
+ kwargs['status'] = 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'] = Result.SUCCESS
+ self.modify_test_result(obj, **kwargs)
obj.set_property("testing.done", True)
- obj.set_property("testing.ready", None)
def do_testing_report(self, **report):
self.api_login()
@@ -81,6 +91,8 @@ class TestingTestCase(PatchewTestCase, metaclass=abc.ABCMeta):
tester="dummy tester",
capabilities=[])
self.assertIn("head", td)
+ resp = self.get_test_result('a')
+ self.assertEquals(resp.data['status'], 'running')
def test_done(self):
self.do_testing_done()
@@ -96,8 +108,8 @@ class TestingTestCase(PatchewTestCase, metaclass=abc.ABCMeta):
self.assertEquals(resp.data['status'], 'pending')
def test_rest_done_success(self):
- self.do_testing_done(log='everything good!', passed=True)
- resp = self.get_test_result('tests')
+ self.do_testing_done(log='everything good!', status=Result.SUCCESS)
+ resp = self.get_test_result('a')
self.assertEquals(resp.data['status'], 'success')
self.assertEquals(resp.data['log'], 'everything good!')
log = self.client.get(resp.data['log_url'])
@@ -105,10 +117,9 @@ class TestingTestCase(PatchewTestCase, metaclass=abc.ABCMeta):
self.assertEquals(log.content, b'everything good!')
def test_rest_done_failure(self):
- self.do_testing_done(log='sorry no good', passed=False, random_stuff='xyz')
- resp = self.get_test_result('tests')
+ self.do_testing_done(log='sorry no good', status=Result.FAILURE)
+ resp = 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 = 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")
- def do_testing_done(self, log=None, **report):
- self._do_testing_done(self.msg, log, report)
+ def do_testing_done(self, **kwargs):
+ self._do_testing_done(self.msg, **kwargs)
def do_testing_report(self, **report):
r = super(MessageTestingTest, self).do_testing_report(**report)
@@ -164,17 +175,18 @@ class MessageTestingTest(TestingTestCase):
self.PROJECT_BASE, self.msg.message_id, test_name))
def test_testing_ready(self):
- self.assertTrue(self.msg.get_property("testing.ready"))
# Set property through series_heads elements must be handled the same
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='testing.a').first().status,
+ Result.PENDING)
msg = 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='testing.a').first().status,
+ Result.PENDING)
class ProjectTestingTest(TestingTestCase):
@@ -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)
- def do_testing_done(self, log=None, **report):
- self._do_testing_done(self.p, log, report)
+ def do_testing_done(self, **kwargs):
+ self._do_testing_done(self.p, **kwargs)
def do_testing_report(self, **report):
r = super(ProjectTestingTest, self).do_testing_report(**report)
--
2.17.0
_______________________________________________
Patchew-devel mailing list
Patchew-devel@redhat.com
https://www.redhat.com/mailman/listinfo/patchew-devel
© 2016 - 2025 Red Hat, Inc.