From patchwork Thu Nov 5 17:13:19 2015 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Stephen Finucane X-Patchwork-Id: 540534 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 3267F14018C for ; Fri, 6 Nov 2015 04:14:11 +1100 (AEDT) Received: from lists.ozlabs.org (lists.ozlabs.org [IPv6:2401:3900:2:1::3]) by lists.ozlabs.org (Postfix) with ESMTP id 1766B1A0C0E for ; Fri, 6 Nov 2015 04:14:11 +1100 (AEDT) X-Original-To: patchwork@lists.ozlabs.org Delivered-To: patchwork@lists.ozlabs.org Received: from mga14.intel.com (mga14.intel.com [192.55.52.115]) by lists.ozlabs.org (Postfix) with ESMTP id 8FF921A0D6A for ; Fri, 6 Nov 2015 04:13:39 +1100 (AEDT) Received: from orsmga003.jf.intel.com ([10.7.209.27]) by fmsmga103.fm.intel.com with ESMTP; 05 Nov 2015 09:13:38 -0800 X-ExtLoop1: 1 X-IronPort-AV: E=Sophos;i="5.20,248,1444719600"; d="scan'208";a="679058279" Received: from irvmail001.ir.intel.com ([163.33.26.43]) by orsmga003.jf.intel.com with ESMTP; 05 Nov 2015 09:13:36 -0800 Received: from sivswdev01.ir.intel.com (sivswdev01.ir.intel.com [10.237.217.45]) by irvmail001.ir.intel.com (8.14.3/8.13.6/MailSET/Hub) with ESMTP id tA5HDaGh023477; Thu, 5 Nov 2015 17:13:36 GMT Received: from sivswdev01.ir.intel.com (localhost [127.0.0.1]) by sivswdev01.ir.intel.com with ESMTP id tA5HDaO1032216; Thu, 5 Nov 2015 17:13:36 GMT Received: (from sfinucan@localhost) by sivswdev01.ir.intel.com with id tA5HDavo032212; Thu, 5 Nov 2015 17:13:36 GMT From: Stephen Finucane To: patchwork@lists.ozlabs.org Subject: [PATCH v3 4/8] models: Add properties related to checks Date: Thu, 5 Nov 2015 17:13:19 +0000 Message-Id: <1446743603-31517-5-git-send-email-stephen.finucane@intel.com> X-Mailer: git-send-email 2.0.0 In-Reply-To: <1446743603-31517-1-git-send-email-stephen.finucane@intel.com> References: <1446743603-31517-1-git-send-email-stephen.finucane@intel.com> X-BeenThere: patchwork@lists.ozlabs.org X-Mailman-Version: 2.1.20 Precedence: list List-Id: Patchwork development List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Errors-To: patchwork-bounces+incoming=patchwork.ozlabs.org@lists.ozlabs.org Sender: "Patchwork" Add properties for 'Patch' to get the unique checks associated with a patch, the total number of each type of check and the combined state of the check. These will be necessary to display this information to the user. Signed-off-by: Stephen Finucane --- patchwork/models.py | 70 ++++++++++++++++++ patchwork/tests/test_checks.py | 162 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 232 insertions(+) create mode 100644 patchwork/tests/test_checks.py diff --git a/patchwork/models.py b/patchwork/models.py index 80f1eff..a2b9498 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -329,6 +329,76 @@ class Patch(models.Model): str = fname_re.sub('-', self.name) return str.strip('-') + '.patch' + @property + def combined_check_state(self): + """Return the combined state for all checks. + + Generate the combined check's state for this patch. This check + is one of the following, based on the value of each unique + check: + + * failure, if any context's latest check reports as failure + * warning, if any context's latest check reports as warning + * pending, if there are no checks, or a context's latest + Check reports as pending + * success, if latest checks for all contexts reports as + success + """ + states = [check.state for check in self.checks] + + if not states: + return Check.STATE_PENDING + + for state in [Check.STATE_FAIL, Check.STATE_WARNING, + Check.STATE_PENDING]: # order sensitive + if state in states: + return state + + return Check.STATE_SUCCESS + + @property + def checks(self): + """Return the list of unique checks. + + Generate a list of checks associated with this patch for each + type of Check. Only "unique" checks are considered, + identified by their 'context' field. This means, given n + checks with the same 'context', the newest check is the only + one counted regardless of its value. The end result will be a + association of types to number of unique checks for said + type. + """ + unique = {} + + for check in self.check_set.all(): + ctx = check.context + + # recheck condition - ignore the older result + if ctx in unique and unique[ctx].date > check.date: + continue + + unique[ctx] = check + + return unique.values() + + @property + def check_count(self): + """Generate a list of unique checks for each patch. + + Compile a list of checks associated with this patch for each + type of check. Only "unique" checks are considered, identified + by their 'context' field. This means, given n checks with the + same 'context', the newest check is the only one counted + regardless of its value. The end result will be a association + of types to number of unique checks for said type. + """ + counts = {key: 0 for key, _ in Check.STATE_CHOICES} + + for check in self.checks: + counts[check.state] += 1 + + return counts + @models.permalink def get_absolute_url(self): return ('patchwork.views.patch.patch', (), {'patch_id': self.id}) diff --git a/patchwork/tests/test_checks.py b/patchwork/tests/test_checks.py new file mode 100644 index 0000000..6d4f7e3 --- /dev/null +++ b/patchwork/tests/test_checks.py @@ -0,0 +1,162 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2015 Intel Corporation +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from datetime import datetime as dt, timedelta + +from django.conf import settings +from django.db import connection +from django.test import TransactionTestCase + +from patchwork.models import Patch, Check +from patchwork.tests.utils import defaults, create_user + + +class PatchChecksTest(TransactionTestCase): + fixtures = ['default_tags', 'default_states'] + + def setUp(self): + project = defaults.project + defaults.project.save() + defaults.patch_author_person.save() + self.patch = Patch(project=project, + msgid='x', name=defaults.patch_name, + submitter=defaults.patch_author_person, + content='') + self.patch.save() + self.user = create_user() + + def create_check(self, **kwargs): + check_values = { + 'patch': self.patch, + 'user': self.user, + 'date': dt.now(), + 'state': Check.STATE_SUCCESS, + 'target_url': 'http://example.com/', + 'description': '', + 'context': 'intel/jenkins-ci', + } + + for key in check_values: + if key in kwargs: + check_values[key] = kwargs[key] + + check = Check(**check_values) + check.save() + return check + + def assertCheckEqual(self, patch, check_state): + self.assertEqual(self.patch.combined_check_state, check_state) + + def assertChecksEqual(self, patch, checks=None): + if not checks: + checks = [] + + self.assertEqual(len(self.patch.checks), len(checks)) + self.assertEqual( + sorted(self.patch.checks, key=lambda check: check.id), + sorted(checks, key=lambda check: check.id)) + + def assertCheckCountEqual(self, patch, total, state_counts=None): + if not state_counts: + state_counts = {} + + counts = self.patch.check_count + + self.assertEqual(self.patch.check_set.count(), total) + + for state in state_counts.keys(): + self.assertEqual(counts[state], state_counts[state]) + + # also check the ones we didn't explicitly state + for state, _ in Check.STATE_CHOICES: + if state not in state_counts: + self.assertEqual(counts[state], 0) + + def tearDown(self): + self.patch.delete() + + def test_checks__no_checks(self): + self.assertChecksEqual(self.patch, []) + + def test_checks__single_check(self): + check = self.create_check() + self.assertChecksEqual(self.patch, [check]) + + def test_checks__multiple_checks(self): + check_a = self.create_check() + check_b = self.create_check(context='new-context/test1') + self.assertChecksEqual(self.patch, [check_a, check_b]) + + def test_checks__duplicate_checks(self): + check_a = self.create_check(date=(dt.now() - timedelta(days=1))) + check_b = self.create_check() + # this isn't a realistic scenario (dates shouldn't be set by user so + # they will always increment), but it's useful to verify the removal + # of older duplicates by the function + check_c = self.create_check(date=(dt.now() - timedelta(days=2))) + self.assertChecksEqual(self.patch, [check_b]) + + def test_check_count__no_checks(self): + self.assertCheckCountEqual(self.patch, 0) + + def test_check_count__single_check(self): + self.create_check() + self.assertCheckCountEqual(self.patch, 1, {Check.STATE_SUCCESS: 1}) + + def test_check_count__multiple_checks(self): + self.create_check(date=(dt.now() - timedelta(days=1))) + self.create_check(context='new/test1') + self.assertCheckCountEqual(self.patch, 2, {Check.STATE_SUCCESS: 2}) + + def test_check_count__duplicate_check_same_state(self): + self.create_check(date=(dt.now() - timedelta(days=1))) + self.assertCheckCountEqual(self.patch, 1, {Check.STATE_SUCCESS: 1}) + + self.create_check() + self.assertCheckCountEqual(self.patch, 2, {Check.STATE_SUCCESS: 1}) + + def test_check_count__duplicate_check_new_state(self): + self.create_check(date=(dt.now() - timedelta(days=1))) + self.assertCheckCountEqual(self.patch, 1, {Check.STATE_SUCCESS: 1}) + + self.create_check(state=Check.STATE_FAIL) + self.assertCheckCountEqual(self.patch, 2, {Check.STATE_FAIL: 1}) + + def test_check__no_checks(self): + self.assertCheckEqual(self.patch, Check.STATE_PENDING) + + def test_check__single_check(self): + self.create_check() + self.assertCheckEqual(self.patch, Check.STATE_SUCCESS) + + def test_check__failure_check(self): + self.create_check() + self.create_check(context='new/test1', state=Check.STATE_FAIL) + self.assertCheckEqual(self.patch, Check.STATE_FAIL) + + def test_check__warning_check(self): + self.create_check() + self.create_check(context='new/test1', state=Check.STATE_WARNING) + self.assertCheckEqual(self.patch, Check.STATE_WARNING) + + def test_check__success_check(self): + self.create_check() + self.create_check(context='new/test1') + self.assertCheckEqual(self.patch, Check.STATE_SUCCESS) +