From 3b8a61c68fa61eadebf7b19329e8d3bffde9e6b4 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Wed, 27 May 2015 09:56:36 +0800 Subject: Add patch tag infrastructure This change add patch 'tags', eg 'Acked-by' / 'Reviewed-by', etc., to patchwork. Tag parsing is implemented in the patch parser's extract_tags function, which returns a Counter object of the tags in a comment. These are stored in the PatchTag (keyed to Tag) objects associated with each patch. We need to ensure that the main patch lists do not cause per-patch queries on the Patch.tags ManyToManyField (this would result in ~500 queries per page), so we introduce a new QuerySet (and Manager) for Patch, adding a with_tag_counts() method to populate the tag counts in a single query. As users may be migrating from previous patchwork versions (ie, with no tag counts in the database), we add a 'retag' management command. Signed-off-by: Jeremy Kerr --- patchwork/tests/__init__.py | 1 + patchwork/tests/test_patchparser.py | 27 +++++ patchwork/tests/test_tags.py | 217 ++++++++++++++++++++++++++++++++++++ 3 files changed, 245 insertions(+) create mode 100644 patchwork/tests/test_tags.py (limited to 'patchwork/tests') diff --git a/patchwork/tests/__init__.py b/patchwork/tests/__init__.py index 85200bd..662386a 100644 --- a/patchwork/tests/__init__.py +++ b/patchwork/tests/__init__.py @@ -18,6 +18,7 @@ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from patchwork.tests.test_patchparser import * +from patchwork.tests.test_tags import * from patchwork.tests.test_encodings import * from patchwork.tests.test_bundles import * from patchwork.tests.test_mboxviews import * diff --git a/patchwork/tests/test_patchparser.py b/patchwork/tests/test_patchparser.py index 119936a..5eefeb5 100644 --- a/patchwork/tests/test_patchparser.py +++ b/patchwork/tests/test_patchparser.py @@ -552,3 +552,30 @@ class InitialPatchStateTest(TestCase): def tearDown(self): self.p1.delete() self.user.delete() + +class ParseInitialTagsTest(PatchTest): + patch_filename = '0001-add-line.patch' + test_comment = ('test comment\n\n' + + 'Tested-by: Test User \n' + + 'Reviewed-by: Test User \n') + fixtures = ['default_tags'] + + def setUp(self): + project = defaults.project + project.listid = 'test.example.com' + project.save() + self.orig_patch = read_patch(self.patch_filename) + email = create_email(self.test_comment + '\n' + self.orig_patch, + project = project) + email['Message-Id'] = '<1@example.com>' + parse_mail(email) + + def testTags(self): + self.assertEquals(Patch.objects.count(), 1) + patch = Patch.objects.all()[0] + self.assertEquals(patch.patchtag_set.filter( + tag__name='Acked-by').count(), 0) + self.assertEquals(patch.patchtag_set.get( + tag__name='Reviewed-by').count, 1) + self.assertEquals(patch.patchtag_set.get( + tag__name='Tested-by').count, 1) diff --git a/patchwork/tests/test_tags.py b/patchwork/tests/test_tags.py new file mode 100644 index 0000000..f1196e7 --- /dev/null +++ b/patchwork/tests/test_tags.py @@ -0,0 +1,217 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2014 Jeremy Kerr +# +# 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 + +import unittest +import datetime +from django.test import TestCase, TransactionTestCase +from patchwork.models import Project, Patch, Comment, Tag, PatchTag +from patchwork.tests.utils import defaults +from patchwork.parser import extract_tags + +from django.conf import settings +from django.db import connection + +class ExtractTagsTest(TestCase): + + email = 'test@exmaple.com' + name_email = 'test name <' + email + '>' + fixtures = ['default_tags'] + + def assertTagsEqual(self, str, acks, reviews, tests): + counts = extract_tags(str, Tag.objects.all()) + self.assertEquals((acks, reviews, tests), + (counts[Tag.objects.get(name='Acked-by')], + counts[Tag.objects.get(name='Reviewed-by')], + counts[Tag.objects.get(name='Tested-by')])) + + def testEmpty(self): + self.assertTagsEqual("", 0, 0, 0) + + def testNoTag(self): + self.assertTagsEqual("foo", 0, 0, 0) + + def testAck(self): + self.assertTagsEqual("Acked-by: %s" % self.name_email, 1, 0, 0) + + def testAckEmailOnly(self): + self.assertTagsEqual("Acked-by: %s" % self.email, 1, 0, 0) + + def testReviewed(self): + self.assertTagsEqual("Reviewed-by: %s" % self.name_email, 0, 1, 0) + + def testTested(self): + self.assertTagsEqual("Tested-by: %s" % self.name_email, 0, 0, 1) + + def testAckAfterNewline(self): + self.assertTagsEqual("\nAcked-by: %s" % self.name_email, 1, 0, 0) + + def testMultipleAcks(self): + str = "Acked-by: %s\nAcked-by: %s\n" % ((self.name_email,) * 2) + self.assertTagsEqual(str, 2, 0, 0) + + def testMultipleTypes(self): + str = "Acked-by: %s\nAcked-by: %s\nReviewed-by: %s\n" % ( + (self.name_email,) * 3) + self.assertTagsEqual(str, 2, 1, 0) + + def testLower(self): + self.assertTagsEqual("acked-by: %s" % self.name_email, 1, 0, 0) + + def testUpper(self): + self.assertTagsEqual("ACKED-BY: %s" % self.name_email, 1, 0, 0) + + def testAckInReply(self): + self.assertTagsEqual("> Acked-by: %s\n" % self.name_email, 0, 0, 0) + +class PatchTagsTest(TransactionTestCase): + ACK = 1 + REVIEW = 2 + TEST = 3 + fixtures = ['default_tags'] + + def assertTagsEqual(self, patch, acks, reviews, tests): + patch = Patch.objects.get(pk=patch.pk) + + def count(name): + try: + return patch.patchtag_set.get(tag__name=name).count + except PatchTag.DoesNotExist: + return 0 + + counts = ( + count(name='Acked-by'), + count(name='Reviewed-by'), + count(name='Tested-by'), + ) + + self.assertEqual(counts, (acks, reviews, tests)) + + def create_tag(self, tagtype = None): + tags = { + self.ACK: 'Acked', + self.REVIEW: 'Reviewed', + self.TEST: 'Tested' + } + if tagtype not in tags: + return '' + return '%s-by: %s\n' % (tags[tagtype], self.tagger) + + def create_tag_comment(self, patch, tagtype = None): + comment = Comment(patch=patch, msgid=str(datetime.datetime.now()), + submitter=defaults.patch_author_person, + content=self.create_tag(tagtype)) + comment.save() + return comment + + def setUp(self): + settings.DEBUG = True + project = Project(linkname='test-project', name='Test Project', + use_tags=True) + 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.tagger = 'Test Tagger ' + + def tearDown(self): + self.patch.delete() + + def testNoComments(self): + self.assertTagsEqual(self.patch, 0, 0, 0) + + def testNoTagComment(self): + self.create_tag_comment(self.patch, None) + self.assertTagsEqual(self.patch, 0, 0, 0) + + def testSingleComment(self): + self.create_tag_comment(self.patch, self.ACK) + self.assertTagsEqual(self.patch, 1, 0, 0) + + def testMultipleComments(self): + self.create_tag_comment(self.patch, self.ACK) + self.create_tag_comment(self.patch, self.ACK) + self.assertTagsEqual(self.patch, 2, 0, 0) + + def testMultipleCommentTypes(self): + self.create_tag_comment(self.patch, self.ACK) + self.create_tag_comment(self.patch, self.REVIEW) + self.create_tag_comment(self.patch, self.TEST) + self.assertTagsEqual(self.patch, 1, 1, 1) + + def testCommentAdd(self): + self.create_tag_comment(self.patch, self.ACK) + self.assertTagsEqual(self.patch, 1, 0, 0) + self.create_tag_comment(self.patch, self.ACK) + self.assertTagsEqual(self.patch, 2, 0, 0) + + def testCommentUpdate(self): + comment = self.create_tag_comment(self.patch, self.ACK) + self.assertTagsEqual(self.patch, 1, 0, 0) + + comment.content += self.create_tag(self.ACK) + comment.save() + self.assertTagsEqual(self.patch, 2, 0, 0) + + def testCommentDelete(self): + comment = self.create_tag_comment(self.patch, self.ACK) + self.assertTagsEqual(self.patch, 1, 0, 0) + comment.delete() + self.assertTagsEqual(self.patch, 0, 0, 0) + + def testSingleCommentMultipleTags(self): + comment = self.create_tag_comment(self.patch, self.ACK) + comment.content += self.create_tag(self.REVIEW) + comment.save() + self.assertTagsEqual(self.patch, 1, 1, 0) + + def testMultipleCommentsMultipleTags(self): + c1 = self.create_tag_comment(self.patch, self.ACK) + c1.content += self.create_tag(self.REVIEW) + c1.save() + self.assertTagsEqual(self.patch, 1, 1, 0) + +class PatchTagManagerTest(PatchTagsTest): + + def assertTagsEqual(self, patch, acks, reviews, tests): + + tagattrs = {} + for tag in Tag.objects.all(): + tagattrs[tag.name] = tag.attr_name + + # force project.tags to be queried outside of the assertNumQueries + patch.project.tags + + # we should be able to do this with two queries: one for + # the patch table lookup, and the prefetch_related for the + # projects table. + with self.assertNumQueries(2): + patch = Patch.objects.with_tag_counts(project=patch.project) \ + .get(pk = patch.pk) + + counts = ( + getattr(patch, tagattrs['Acked-by']), + getattr(patch, tagattrs['Reviewed-by']), + getattr(patch, tagattrs['Tested-by']), + ) + + self.assertEqual(counts, (acks, reviews, tests)) + -- cgit v1.2.3