Add properties for 'Patch' to get the unique statuses associated with a patch, the total number of each type of status and the combined state of the statuses. These will be necessary to display this information to the user.
Signed-off-by: Stephen Finucane <stephen.finuc...@intel.com> --- patchwork/models.py | 71 +++++++++++++++++ patchwork/tests/test_statuses.py | 162 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 patchwork/tests/test_statuses.py diff --git a/patchwork/models.py b/patchwork/models.py index ef5f13c..45cd1b5 100644 --- a/patchwork/models.py +++ b/patchwork/models.py @@ -313,6 +313,77 @@ class Patch(models.Model): str = fname_re.sub('-', self.name) return str.strip('-') + '.patch' + @property + def combined_status_state(self): + """Return the combined state for all statuses. + + Generate the combined status' state for this patch. This status + is one of the following, based on the value of each unique + status: + + * failure, if a context's latest statuses reports as failure + * warning, if a context's latest statuses reports as warning + * pending, if there are no statuses, or a context's latest + status reports as pending + * success, if latest statuses for all contexts reports as + success + """ + states = [status.state for status in self.statuses] + + if not states: + return Status.STATE_PENDING + + for state in [status.STATE_FAIL, status.STATE_WARNING, + status.STATE_PENDING]: # order sensitive + if state in states: + return state + + return Status.STATE_SUCCESS + + @property + def statuses(self): + """Return the list of unique statuses. + + Generate a list of statuses associated with this patch for each + type of status. Only "unique" statuses are considered, + identified by their 'context' field. This means, given n + statuses with the same 'context', the newest status is the only + one counted regardless of its value. The end result will be a + association of types to number of unique statuses for said + type. + """ + unique = {} + + for status in self.status_set.all(): + ctx = status.context + + # recheck condition - ignore the older result + if ctx in unique and unique[ctx].date > status.date: + continue + + unique[ctx] = status + + return unique.values() + + @property + def status_count(self): + """Generate a list of unique statuses for each patch. + + Compile a list of statuses associated with this patch for each + type of status . Only "unique" statuses are considered, + identified by their 'context' field. This means, given n + statuses with the same 'context', the newest status is the only + one counted regardless of its value. The end result will be a + association of types to number of unique statuses for said + type. + """ + counts = {key: 0 for key, _ in Status.STATE_CHOICES} + + for status in self.statuses: + counts[status.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_statuses.py b/patchwork/tests/test_statuses.py new file mode 100644 index 0000000..2ccde33 --- /dev/null +++ b/patchwork/tests/test_statuses.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, Status +from patchwork.tests.utils import defaults, create_user + + +class PatchStatusesTest(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_status(self, **kwargs): + status_values = { + 'patch': self.patch, + 'user': self.user, + 'date': dt.now(), + 'state': Status.STATE_SUCCESS, + 'target_url': 'http://example.com/', + 'description': '', + 'context': 'intel/jenkins-ci', + } + + for key in status_values: + if key in kwargs: + status_values[key] = kwargs[key] + + status = Status(**status_values) + status.save() + return status + + def assertStatusEqual(self, patch, status_state): + self.assertEqual(self.patch.combined_status_state, status_state) + + def assertStatusesEqual(self, patch, statuses=None): + if not statuses: + statuses = [] + + self.assertEqual(len(self.patch.statuses), len(statuses)) + self.assertEqual( + sorted(self.patch.statuses, key=lambda status: status.id), + sorted(statuses, key=lambda status: status.id)) + + def assertStatusCountEqual(self, patch, total, state_counts=None): + if not state_counts: + state_counts = {} + + counts = self.patch.status_count + + self.assertEqual(self.patch.status_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 Status.STATE_CHOICES: + if state not in state_counts: + self.assertEqual(counts[state], 0) + + def tearDown(self): + self.patch.delete() + + def test_statuses__no_statuses(self): + self.assertStatusesEqual(self.patch, []) + + def test_statuses__single_status(self): + status = self.create_status() + self.assertStatusesEqual(self.patch, [status]) + + def test_statuses__multiple_statuses(self): + status_a = self.create_status() + status_b = self.create_status(context='new-context/test1') + self.assertStatusesEqual(self.patch, [status_a, status_b]) + + def test_statuses__duplicate_statuses(self): + status_a = self.create_status(date=(dt.now() - timedelta(days=1))) + status_b = self.create_status() + # 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 + status_c = self.create_status(date=(dt.now() - timedelta(days=2))) + self.assertStatusesEqual(self.patch, [status_b]) + + def test_status_count__no_statuses(self): + self.assertStatusCountEqual(self.patch, 0) + + def test_status_count__single_status(self): + self.create_status() + self.assertStatusCountEqual(self.patch, 1, {Status.STATE_SUCCESS: 1}) + + def test_status_count__multiple_statuses(self): + self.create_status(date=(dt.now() - timedelta(days=1))) + self.create_status(context='new/test1') + self.assertStatusCountEqual(self.patch, 2, {Status.STATE_SUCCESS: 2}) + + def test_status_count__duplicate_status_same_state(self): + self.create_status(date=(dt.now() - timedelta(days=1))) + self.assertStatusCountEqual(self.patch, 1, {Status.STATE_SUCCESS: 1}) + + self.create_status() + self.assertStatusCountEqual(self.patch, 2, {Status.STATE_SUCCESS: 1}) + + def test_status_count__duplicate_status_new_state(self): + self.create_status(date=(dt.now() - timedelta(days=1))) + self.assertStatusCountEqual(self.patch, 1, {Status.STATE_SUCCESS: 1}) + + self.create_status(state=Status.STATE_FAIL) + self.assertStatusCountEqual(self.patch, 2, {Status.STATE_FAIL: 1}) + + def test_status__no_statuses(self): + self.assertStatusEqual(self.patch, Status.STATE_PENDING) + + def test_status__single_status(self): + self.create_status() + self.assertStatusEqual(self.patch, Status.STATE_SUCCESS) + + def test_status__failure_status(self): + self.create_status() + self.create_status(context='new/test1', state=Status.STATE_FAIL) + self.assertStatusEqual(self.patch, Status.STATE_FAIL) + + def test_status__warning_status(self): + self.create_status() + self.create_status(context='new/test1', state=Status.STATE_WARNING) + self.assertStatusEqual(self.patch, Status.STATE_WARNING) + + def test_status__success_status(self): + self.create_status() + self.create_status(context='new/test1') + self.assertStatusEqual(self.patch, Status.STATE_SUCCESS) + -- 2.0.0 _______________________________________________ Patchwork mailing list Patchwork@lists.ozlabs.org https://lists.ozlabs.org/listinfo/patchwork