Merge ~andrey-fedoseev/launchpad:bug-presense into launchpad:master

Proposed by Andrey Fedoseev
Status: Needs review
Proposed branch: ~andrey-fedoseev/launchpad:bug-presense
Merge into: launchpad:master
Diff against target: 769 lines (+691/-0)
7 files modified
lib/lp/bugs/configure.zcml (+25/-0)
lib/lp/bugs/interfaces/bugpresence.py (+103/-0)
lib/lp/bugs/model/bugpresence.py (+150/-0)
lib/lp/bugs/security.py (+33/-0)
lib/lp/bugs/tests/test_bugpresence.py (+337/-0)
lib/lp/code/model/gitrepository.py (+10/-0)
lib/lp/testing/factory.py (+33/-0)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+431710@code.launchpad.net

Commit message

Add `BugPresence` model

Description of the change

It represents a range of versions or git commits in which the bug was present.

To post a comment you must log in.

Unmerged commits

460e925... by Andrey Fedoseev

Add `BugPresence` model

It represents a range of versions or git commits in which the bug was present.

Succeeded
[SUCCEEDED] docs:0 (build)
[SUCCEEDED] lint:0 (build)
[SUCCEEDED] mypy:0 (build)
13 of 3 results

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
2index 6eaa58c..52977cd 100644
3--- a/lib/lp/bugs/configure.zcml
4+++ b/lib/lp/bugs/configure.zcml
5@@ -1113,4 +1113,29 @@
6 >
7 <allow interface="zope.schema.interfaces.IVocabularyFactory"/>
8 </securedutility>
9+
10+ <!-- BugPresence -->
11+ <class
12+ class="lp.bugs.model.bugpresence.BugPresence">
13+ <require
14+ permission="launchpad.View"
15+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceView"/>
16+ <require
17+ permission="launchpad.Edit"
18+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceEdit" />
19+ </class>
20+
21+ <!-- BugPresenceSet -->
22+ <class
23+ class="lp.bugs.model.bugpresence.BugPresenceSet">
24+ <allow
25+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/>
26+ </class>
27+ <securedutility
28+ class="lp.bugs.model.bugpresence.BugPresenceSet"
29+ provides="lp.bugs.interfaces.bugpresence.IBugPresenceSet">
30+ <allow
31+ interface="lp.bugs.interfaces.bugpresence.IBugPresenceSet"/>
32+ </securedutility>
33+
34 </configure>
35diff --git a/lib/lp/bugs/interfaces/bugpresence.py b/lib/lp/bugs/interfaces/bugpresence.py
36new file mode 100644
37index 0000000..4a34970
38--- /dev/null
39+++ b/lib/lp/bugs/interfaces/bugpresence.py
40@@ -0,0 +1,103 @@
41+from lazr.restful.fields import Reference
42+from zope.interface import Attribute, Interface
43+from zope.schema import Choice, Int, TextLine
44+
45+from lp import _
46+from lp.services.fields import BugField
47+
48+__all__ = ["IBugPresence", "IBugPresenceSet"]
49+
50+
51+class IBugPresenceView(Interface):
52+ """IBugPresence attributes that require launchpad.View permission."""
53+
54+ id = Int()
55+ bug = BugField(title=_("Bug"), readonly=True)
56+ bug_id = Int()
57+
58+ project = Choice(title=_("Project"), required=False, vocabulary="Product")
59+ project_id = Attribute("The product ID")
60+ source_package_name = Choice(
61+ title=_("Package"), required=False, vocabulary="SourcePackageName"
62+ )
63+ source_package_name_id = Attribute("The source_package_name ID")
64+ distribution = Choice(
65+ title=_("Distribution"), required=False, vocabulary="Distribution"
66+ )
67+ distribution_id = Attribute("The distribution ID")
68+
69+ broken_version = TextLine(
70+ required=False, title=_("Version that introduced the bug")
71+ )
72+ fixed_version = TextLine(
73+ required=False, title=_("Version that fixed the bug")
74+ )
75+
76+ git_repository = Choice(
77+ title=_("Git repository"), required=False, vocabulary="GitRepository"
78+ )
79+ git_repository_id = Attribute("The git repository ID")
80+ broken_git_commit_sha1 = TextLine(
81+ required=False,
82+ title=_("Git commit that introduced the bug"),
83+ max_length=40,
84+ )
85+ fixed_git_commit_sha1 = TextLine(
86+ required=False, title=_("Git commit that fixed the bug"), max_length=40
87+ )
88+
89+ target = Reference(
90+ title=_("Target"),
91+ required=True,
92+ schema=Interface, # IBugTarget|IGitRepository
93+ )
94+
95+
96+class IBugPresenceEdit(Interface):
97+ """IBugPresence attributes that require launchpad.Edit permission."""
98+
99+ def destroySelf():
100+ """Delete the specified bug presence."""
101+
102+
103+class IBugPresence(IBugPresenceView, IBugPresenceEdit):
104+ """
105+ Represents a range of versions or git commits in which the bug was present.
106+ """
107+
108+
109+class IBugPresenceSet(Interface):
110+ def new(
111+ bug,
112+ target,
113+ broken_version,
114+ fixed_version,
115+ broken_git_commit_sha1,
116+ fixed_git_commit_sha1,
117+ ):
118+ """Create new BugPresence instance.
119+ :param bug: a bug to create a bug presence for
120+ :param target: a project, a distribution, a distribution package or
121+ a git repository
122+ :param broken_version: version in which the bug was introduced
123+ :param fixed_version: version in which the bug was fixed
124+ :param broken_git_commit_sha1: git commit in which the bug
125+ was introduced (for git repository)
126+ :param fixed_git_commit_sha1: git commit in which the bug
127+ was fixed (for git repository)
128+ """
129+ pass
130+
131+ def getByBug(bug):
132+ """Get all BugPresence instances for the given bug.
133+ :param bug: a bug to get the bug presence instances from
134+ :return: a collection of BugPresence instances
135+ """
136+ pass
137+
138+ def getByTarget(target):
139+ """Get all BugPresence instances for the given target.
140+ :param target: a target to get the bug presence instances for
141+ :return: a collection of BugPresence instances
142+ """
143+ pass
144diff --git a/lib/lp/bugs/model/bugpresence.py b/lib/lp/bugs/model/bugpresence.py
145new file mode 100644
146index 0000000..b21371e
147--- /dev/null
148+++ b/lib/lp/bugs/model/bugpresence.py
149@@ -0,0 +1,150 @@
150+from storm.properties import Int, Unicode
151+from storm.references import Reference
152+from storm.store import Store
153+from zope.interface import implementer
154+
155+from lp.bugs.interfaces.bugpresence import IBugPresence, IBugPresenceSet
156+from lp.code.interfaces.gitrepository import IGitRepository
157+from lp.registry.interfaces.distributionsourcepackage import (
158+ IDistributionSourcePackage,
159+)
160+from lp.registry.interfaces.product import IProduct
161+from lp.services.database.interfaces import IStore
162+from lp.services.database.stormbase import StormBase
163+
164+__all__ = ["BugPresence", "BugPresenceSet"]
165+
166+
167+@implementer(IBugPresence)
168+class BugPresence(StormBase):
169+ """See `IBugPresence`."""
170+
171+ __storm_table__ = "BugPresence"
172+
173+ id = Int(primary=True)
174+
175+ bug_id = Int(name="bug", allow_none=False)
176+ bug = Reference(bug_id, "Bug.id")
177+
178+ project_id = Int(name="project", allow_none=True)
179+ project = Reference(project_id, "Product.id")
180+
181+ source_package_name_id = Int(name="source_package_name", allow_none=True)
182+ source_package_name = Reference(
183+ source_package_name_id, "SourcePackageName.id"
184+ )
185+
186+ distribution_id = Int(name="distribution", allow_none=True)
187+ distribution = Reference(distribution_id, "Distribution.id")
188+
189+ broken_version = Unicode(name="broken_version", allow_none=True)
190+ fixed_version = Unicode(name="fixed_version", allow_none=True)
191+
192+ git_repository_id = Int(name="git_repository", allow_none=True)
193+ git_repository = Reference(git_repository_id, "GitRepository.id")
194+
195+ broken_git_commit_sha1 = Unicode(
196+ name="broken_git_commit_sha1", allow_none=True
197+ )
198+ fixed_git_commit_sha1 = Unicode(
199+ name="fixed_git_commit_sha1", allow_none=True
200+ )
201+
202+ def __init__(
203+ self,
204+ bug,
205+ target,
206+ broken_version=None,
207+ fixed_version=None,
208+ broken_git_commit_sha1=None,
209+ fixed_git_commit_sha1=None,
210+ ):
211+ self.bug = bug
212+ self.target = target
213+ self.broken_version = broken_version
214+ self.fixed_version = fixed_version
215+ self.broken_git_commit_sha1 = broken_git_commit_sha1
216+ self.fixed_git_commit_sha1 = fixed_git_commit_sha1
217+
218+ @property
219+ def target(self):
220+ if self.project:
221+ return self.project
222+ elif self.source_package_name:
223+ return self.distribution.getSourcePackage(self.source_package_name)
224+ elif self.git_repository:
225+ return self.git_repository
226+ else:
227+ raise AssertionError("Could not determine BugPresence target")
228+
229+ @target.setter
230+ def target(self, target):
231+ if IProduct.providedBy(target):
232+ self.project = target
233+ elif IDistributionSourcePackage.providedBy(target):
234+ self.distribution = target.distribution
235+ self.source_package_name = target.sourcepackagename
236+ elif IGitRepository.providedBy(target):
237+ self.git_repository = target
238+ else:
239+ raise AssertionError("Invalid BugPresence target.")
240+
241+ def destroySelf(self):
242+ Store.of(self).remove(self)
243+
244+
245+@implementer(IBugPresenceSet)
246+class BugPresenceSet:
247+ """See `IBugPresenceSet`."""
248+
249+ def new(
250+ self,
251+ bug,
252+ target,
253+ broken_version=None,
254+ fixed_version=None,
255+ broken_git_commit_sha1=None,
256+ fixed_git_commit_sha1=None,
257+ ):
258+
259+ is_version = bool(broken_version or fixed_version)
260+ is_git_range = bool(broken_git_commit_sha1 or fixed_git_commit_sha1)
261+
262+ if is_version == is_git_range:
263+ raise ValueError(
264+ "Either broken_version/fixed_version or "
265+ "broken_git_commit_sha1/fixed_git_commit_sha1 "
266+ "must be specified"
267+ )
268+
269+ if target is None:
270+ raise ValueError("target must be specified")
271+
272+ if is_git_range and not IGitRepository.providedBy(target):
273+ raise ValueError("target must be a git repository")
274+
275+ return BugPresence(
276+ bug=bug,
277+ target=target,
278+ broken_version=broken_version,
279+ fixed_version=fixed_version,
280+ broken_git_commit_sha1=broken_git_commit_sha1,
281+ fixed_git_commit_sha1=fixed_git_commit_sha1,
282+ )
283+
284+ def getByBug(self, bug):
285+ return IStore(BugPresence).find(BugPresence, bug=bug)
286+
287+ def getByTarget(self, target):
288+ conditions = {}
289+ if IProduct.providedBy(target):
290+ conditions["project"] = target
291+ elif IDistributionSourcePackage.providedBy(target):
292+ conditions["distribution"] = target.distribution
293+ conditions["source_package_name"] = target.sourcepackagename
294+ elif IGitRepository.providedBy(target):
295+ conditions["git_repository"] = target
296+ else:
297+ raise AssertionError("Invalid BugPresence target.")
298+
299+ return IStore(BugPresence).find(BugPresence, **conditions)
300diff --git a/lib/lp/bugs/security.py b/lib/lp/bugs/security.py
301index 11d74e9..15ae315 100644
302--- a/lib/lp/bugs/security.py
303+++ b/lib/lp/bugs/security.py
304@@ -17,6 +17,7 @@ from lp.bugs.interfaces.bug import IBug
305 from lp.bugs.interfaces.bugactivity import IBugActivity
306 from lp.bugs.interfaces.bugattachment import IBugAttachment
307 from lp.bugs.interfaces.bugnomination import IBugNomination
308+from lp.bugs.interfaces.bugpresence import IBugPresence
309 from lp.bugs.interfaces.bugsubscription import IBugSubscription
310 from lp.bugs.interfaces.bugsubscriptionfilter import IBugSubscriptionFilter
311 from lp.bugs.interfaces.bugsupervisor import IHasBugSupervisor
312@@ -455,3 +456,35 @@ class BugTargetOwnerOrBugSupervisorOrAdmins(AuthorizationBase):
313 or user.inTeam(self.obj.owner)
314 or user.in_admin
315 )
316+
317+
318+class ViewBugPresence(DelegatedAuthorization):
319+ """
320+ A person that can view a Bug can also view a related BugPresence.
321+ """
322+
323+ permission = "launchpad.View"
324+ usedfor = IBugPresence
325+
326+ def __init__(self, obj):
327+ super().__init__(obj, obj.bug, "launchpad.View")
328+
329+ def checkAuthenticated(self, user):
330+ r = super().checkAuthenticated(user)
331+ return r
332+
333+ def checkUnauthenticated(self):
334+ r = super().checkUnauthenticated()
335+ return r
336+
337+
338+class EditBugPresence(DelegatedAuthorization):
339+ """
340+ A person that can edit a Bug can also edit a related BugPresence.
341+ """
342+
343+ permission = "launchpad.Edit"
344+ usedfor = IBugPresence
345+
346+ def __init__(self, obj):
347+ super().__init__(obj, obj.bug, "launchpad.Edit")
348diff --git a/lib/lp/bugs/tests/test_bugpresence.py b/lib/lp/bugs/tests/test_bugpresence.py
349new file mode 100644
350index 0000000..e328dff
351--- /dev/null
352+++ b/lib/lp/bugs/tests/test_bugpresence.py
353@@ -0,0 +1,337 @@
354+import transaction
355+from psycopg2.errors import CheckViolation
356+from zope.component import getUtility
357+from zope.security import checkPermission
358+
359+from lp.app.enums import InformationType
360+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
361+from lp.bugs.model.bugpresence import BugPresence
362+from lp.registry.model.distributionsourcepackage import (
363+ DistributionSourcePackage,
364+)
365+from lp.services.database.interfaces import IStore
366+from lp.testing import (
367+ TestCaseWithFactory,
368+ anonymous_logged_in,
369+ person_logged_in,
370+)
371+from lp.testing.layers import DatabaseFunctionalLayer, ZopelessDatabaseLayer
372+
373+
374+class TestBugPresence(TestCaseWithFactory):
375+
376+ layer = ZopelessDatabaseLayer
377+
378+ def test_target(self):
379+ project = self.factory.makeProduct()
380+ distribution = self.factory.makeDistribution()
381+ spn = self.factory.makeSourcePackageName()
382+
383+ for target, attrs in (
384+ (project, {"project": project}),
385+ (
386+ DistributionSourcePackage(distribution, spn),
387+ {"distribution": distribution, "source_package_name": spn},
388+ ),
389+ ):
390+ bug_presence = BugPresence(
391+ bug=self.factory.makeBug(),
392+ target=target,
393+ broken_version="1",
394+ )
395+ self.assertEqual(target, bug_presence.target)
396+ for attr_name, value in attrs.items():
397+ self.assertEqual(value, getattr(bug_presence, attr_name))
398+
399+ git_repository = self.factory.makeGitRepository()
400+ bug_presence = BugPresence(
401+ bug=self.factory.makeBug(),
402+ target=git_repository,
403+ broken_git_commit_sha1="1" * 40,
404+ )
405+ self.assertEqual(git_repository, bug_presence.target)
406+ self.assertEqual(git_repository, bug_presence.git_repository)
407+
408+ def test_constraints(self):
409+ project = self.factory.makeProduct()
410+ distro_package = self.factory.makeDistributionSourcePackage()
411+ git_repository = self.factory.makeGitRepository()
412+
413+ store = IStore(BugPresence)
414+ store.commit()
415+
416+ # no version range, nor git commit range
417+ for target in project, distro_package, git_repository:
418+ BugPresence(bug=self.factory.makeBug(), target=target)
419+ self.assertRaises(CheckViolation, store.flush)
420+ store.rollback()
421+
422+ # invalid version target
423+ BugPresence(
424+ bug=self.factory.makeBug(),
425+ target=git_repository,
426+ broken_version="1",
427+ )
428+ self.assertRaises(CheckViolation, store.flush)
429+ store.rollback()
430+
431+ # invalid git range target
432+ for target in project, distro_package:
433+ BugPresence(
434+ bug=self.factory.makeBug(),
435+ target=target,
436+ broken_git_commit_sha1="1" * 40,
437+ )
438+ self.assertRaises(CheckViolation, store.flush)
439+ store.rollback()
440+
441+ # valid version range
442+ for broken_version, fixed_version in (
443+ ("1", None),
444+ (None, "2"),
445+ ("1", "2"),
446+ ):
447+ for target in project, distro_package:
448+ bp = BugPresence(
449+ bug=self.factory.makeBug(),
450+ target=target,
451+ broken_version=broken_version,
452+ fixed_version=fixed_version,
453+ )
454+ store.commit()
455+ bp = store.get(BugPresence, bp.id)
456+ self.assertEqual(broken_version, bp.broken_version)
457+ self.assertEqual(fixed_version, bp.fixed_version)
458+
459+ # valid git commit range
460+ for broken_git_commit_sha1, fixed_git_commit_sha1 in (
461+ ("1" * 40, None),
462+ (None, "2" * 40),
463+ ("1" * 40, "2" * 40),
464+ ):
465+ bp = BugPresence(
466+ bug=self.factory.makeBug(),
467+ target=git_repository,
468+ broken_git_commit_sha1=broken_git_commit_sha1,
469+ fixed_git_commit_sha1=fixed_git_commit_sha1,
470+ )
471+ store.commit()
472+ bp = store.get(BugPresence, bp.id)
473+ self.assertEqual(broken_git_commit_sha1, bp.broken_git_commit_sha1)
474+ self.assertEqual(fixed_git_commit_sha1, bp.fixed_git_commit_sha1)
475+
476+ def test_referenced_git_repository_can_be_deleted(self):
477+ git_repository = self.factory.makeGitRepository()
478+ bug = self.factory.makeBug()
479+ bug_presence_to_delete = self.factory.makeBugPresence(
480+ bug=bug,
481+ target=git_repository,
482+ broken_git_commit_sha1="1" * 40,
483+ )
484+ bug_presence_to_keep = self.factory.makeBugPresence(bug=bug)
485+ git_repository.destroySelf(break_references=True)
486+ transaction.commit()
487+ bug_presences = list(getUtility(IBugPresenceSet).getByBug(bug))
488+ self.assertIn(bug_presence_to_keep, bug_presences)
489+ self.assertNotIn(bug_presence_to_delete, bug_presences)
490+
491+
492+class TestBugPresenceSecurity(TestCaseWithFactory):
493+
494+ layer = DatabaseFunctionalLayer
495+
496+ def test_view_permission(self):
497+ project = self.factory.makeProduct()
498+
499+ # Anyone can view a public bug presence
500+ public_bug = self.factory.makeBug(
501+ target=project, information_type=InformationType.PUBLIC
502+ )
503+ bp = BugPresence(bug=public_bug, target=project, broken_version="1")
504+ with anonymous_logged_in():
505+ self.assertTrue(checkPermission("launchpad.View", bp))
506+
507+ # Anonymous and random users can't see a private bug presence
508+ owner = self.factory.makePerson()
509+ project = self.factory.makeProduct(
510+ information_type=InformationType.PROPRIETARY, owner=owner
511+ )
512+ with person_logged_in(owner):
513+ private_bug = self.factory.makeBug(
514+ target=project,
515+ information_type=InformationType.PROPRIETARY,
516+ owner=owner,
517+ )
518+ bp = BugPresence(bug=private_bug, target=project, broken_version="1")
519+ with anonymous_logged_in():
520+ self.assertFalse(checkPermission("launchpad.View", bp))
521+ with person_logged_in(self.factory.makePerson()):
522+ self.assertFalse(checkPermission("launchpad.View", bp))
523+
524+ # Private bug owners can see a private bug presence
525+ with person_logged_in(owner):
526+ self.assertTrue(checkPermission("launchpad.View", bp))
527+
528+ def test_edit_permission(self):
529+ project = self.factory.makeProduct()
530+
531+ public_bug = self.factory.makeBug(
532+ target=project, information_type=InformationType.PUBLIC
533+ )
534+ bp = BugPresence(bug=public_bug, target=project, broken_version="1")
535+ # Anonymous can't edit a public bug presence
536+ with anonymous_logged_in():
537+ self.assertFalse(checkPermission("launchpad.Edit", bp))
538+
539+ # Any authenticated user can edit a public bug presence
540+ with person_logged_in(self.factory.makePerson()):
541+ self.assertTrue(checkPermission("launchpad.Edit", bp))
542+
543+ # Anonymous and random users can't edit a private bug presence
544+ owner = self.factory.makePerson()
545+ project = self.factory.makeProduct(
546+ information_type=InformationType.PROPRIETARY, owner=owner
547+ )
548+ with person_logged_in(owner):
549+ private_bug = self.factory.makeBug(
550+ target=project,
551+ information_type=InformationType.PROPRIETARY,
552+ owner=owner,
553+ )
554+ bp = BugPresence(bug=private_bug, target=project, broken_version="1")
555+ with anonymous_logged_in():
556+ self.assertFalse(checkPermission("launchpad.Edit", bp))
557+ with person_logged_in(self.factory.makePerson()):
558+ self.assertFalse(checkPermission("launchpad.Edit", bp))
559+
560+ # Private bug owners can edit a private bug presence
561+ with person_logged_in(owner):
562+ self.assertTrue(checkPermission("launchpad.Edit", bp))
563+
564+
565+class TestBugPresenceSet(TestCaseWithFactory):
566+
567+ layer = DatabaseFunctionalLayer
568+
569+ def test_new__version_range(self):
570+ project = self.factory.makeProduct()
571+ distro_package = self.factory.makeDistributionSourcePackage()
572+
573+ bug_presence_set = getUtility(IBugPresenceSet)
574+
575+ # version range
576+ for broken_version, fixed_version in (
577+ ("1", None),
578+ (None, "2"),
579+ ("1", "2"),
580+ ):
581+ for target in project, distro_package:
582+ bug = self.factory.makeBug()
583+ bp = bug_presence_set.new(
584+ bug=bug,
585+ target=target,
586+ broken_version=broken_version,
587+ fixed_version=fixed_version,
588+ )
589+ self.assertEqual(bug, bp.bug)
590+ self.assertEqual(target, bp.target)
591+ self.assertEqual(broken_version, bp.broken_version)
592+ self.assertEqual(fixed_version, bp.fixed_version)
593+
594+ def test_new__git_commit_range(self):
595+ git_repository = self.factory.makeGitRepository()
596+
597+ bug_presence_set = getUtility(IBugPresenceSet)
598+
599+ # version range
600+ for broken_git_commit_sha1, fixed_git_commit_sha1 in (
601+ ("1" * 40, None),
602+ (None, "2" * 40),
603+ ("1" * 40, "2" * 40),
604+ ):
605+ bug = self.factory.makeBug()
606+ bp = bug_presence_set.new(
607+ bug=bug,
608+ target=git_repository,
609+ broken_git_commit_sha1=broken_git_commit_sha1,
610+ fixed_git_commit_sha1=fixed_git_commit_sha1,
611+ )
612+ self.assertEqual(bug, bp.bug)
613+ self.assertEqual(git_repository, bp.target)
614+ self.assertEqual(broken_git_commit_sha1, bp.broken_git_commit_sha1)
615+ self.assertEqual(fixed_git_commit_sha1, bp.fixed_git_commit_sha1)
616+
617+ def test_new__invalid_arguments(self):
618+ project = self.factory.makeProduct()
619+ bug = self.factory.makeBug()
620+ bug_presence_set = getUtility(IBugPresenceSet)
621+
622+ # no version range nor git commit range is specified
623+ self.assertRaises(
624+ ValueError, bug_presence_set.new, bug=bug, target=project
625+ )
626+
627+ # both version range and commit range is specified
628+ self.assertRaises(
629+ ValueError,
630+ bug_presence_set.new,
631+ bug=bug,
632+ target=project,
633+ broken_version="1",
634+ fixed_git_commit_sha1="2",
635+ )
636+
637+ # target is not a git repository for git commit range
638+ self.assertRaises(
639+ ValueError,
640+ bug_presence_set.new,
641+ bug=bug,
642+ target=project,
643+ fixed_git_commit_sha1="2",
644+ )
645+
646+ # target is missing
647+ self.assertRaises(
648+ ValueError,
649+ bug_presence_set.new,
650+ bug=bug,
651+ target=None,
652+ broken_version="1",
653+ )
654+
655+ def test_getByBug(self):
656+ project = self.factory.makeProduct()
657+ bug = self.factory.makeBug()
658+ another_bug = self.factory.makeBug()
659+ bug_presence_set = getUtility(IBugPresenceSet)
660+
661+ self.assertEqual([], list(bug_presence_set.getByBug(bug)))
662+ self.assertEqual([], list(bug_presence_set.getByBug(another_bug)))
663+
664+ bp_1 = bug_presence_set.new(
665+ bug=bug, target=project, broken_version="1"
666+ )
667+ bp_2 = bug_presence_set.new(bug=bug, target=project, fixed_version="2")
668+ bp_3 = bug_presence_set.new(
669+ bug=another_bug, target=project, broken_version="3"
670+ )
671+ self.assertEqual({bp_1, bp_2}, set(bug_presence_set.getByBug(bug)))
672+ self.assertEqual({bp_3}, set(bug_presence_set.getByBug(another_bug)))
673+
674+
675+class TestBugPresenceFactory(TestCaseWithFactory):
676+
677+ layer = ZopelessDatabaseLayer
678+
679+ def test_makeBugPresence__no_arguments(self):
680+ bp = self.factory.makeBugPresence()
681+ self.assertIsNotNone(bp.target)
682+ self.assertIsNotNone(bp.broken_version)
683+ self.assertIsNotNone(bp.fixed_version)
684+
685+ def test_makeBugPresence__git_repository(self):
686+ git_repository = self.factory.makeGitRepository()
687+ bp = self.factory.makeBugPresence(target=git_repository)
688+ self.assertEqual(git_repository, bp.git_repository)
689+ self.assertIsNotNone(bp.broken_git_commit_sha1)
690+ self.assertIsNotNone(bp.fixed_git_commit_sha1)
691diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
692index bd09864..dee8312 100644
693--- a/lib/lp/code/model/gitrepository.py
694+++ b/lib/lp/code/model/gitrepository.py
695@@ -48,6 +48,7 @@ from lp.app.errors import (
696 from lp.app.interfaces.informationtype import IInformationType
697 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
698 from lp.app.interfaces.services import IService
699+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
700 from lp.charms.interfaces.charmrecipe import ICharmRecipeSet
701 from lp.code.adapters.branch import BranchMergeProposalNoPreviewDiffDelta
702 from lp.code.enums import (
703@@ -1955,6 +1956,15 @@ class GitRepository(
704 )
705 for recipe in recipes
706 )
707+ bug_presences = getUtility(IBugPresenceSet).getByTarget(self)
708+ deletion_operations.extend(
709+ DeletionCallable(
710+ bug_presence,
711+ msg("This bug presence refers to this repository."),
712+ bug_presence.destroySelf,
713+ )
714+ for bug_presence in bug_presences
715+ )
716 if not getUtility(ISnapSet).findByGitRepository(self).is_empty():
717 alteration_operations.append(
718 DeletionCallable(
719diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
720index b95c374..e43d9fc 100644
721--- a/lib/lp/testing/factory.py
722+++ b/lib/lp/testing/factory.py
723@@ -65,6 +65,7 @@ from lp.blueprints.interfaces.specification import ISpecificationSet
724 from lp.blueprints.interfaces.sprint import ISprintSet
725 from lp.bugs.interfaces.apportjob import IProcessApportBlobJobSource
726 from lp.bugs.interfaces.bug import CreateBugParams, IBugSet
727+from lp.bugs.interfaces.bugpresence import IBugPresenceSet
728 from lp.bugs.interfaces.bugtask import (
729 BugTaskImportance,
730 BugTaskStatus,
731@@ -2652,6 +2653,38 @@ class LaunchpadObjectFactory(ObjectFactory):
732 )
733 )
734
735+ def makeBugPresence(
736+ self,
737+ bug=None,
738+ target=None,
739+ broken_version=None,
740+ fixed_version=None,
741+ broken_git_commit_sha1=None,
742+ fixed_git_commit_sha1=None,
743+ ):
744+ if bug is None:
745+ bug = self.makeBug()
746+ if target is None:
747+ target = self.makeProduct()
748+
749+ if IGitRepository.providedBy(target):
750+ if not broken_git_commit_sha1 and not fixed_git_commit_sha1:
751+ broken_git_commit_sha1 = self.getUniqueHexString(40)
752+ fixed_git_commit_sha1 = self.getUniqueHexString(40)
753+
754+ elif not broken_version and not fixed_version:
755+ broken_version = self.getUniqueString("version")
756+ fixed_version = self.getUniqueString("version")
757+
758+ return getUtility(IBugPresenceSet).new(
759+ bug=bug,
760+ target=target,
761+ broken_version=broken_version,
762+ fixed_version=fixed_version,
763+ broken_git_commit_sha1=broken_git_commit_sha1,
764+ fixed_git_commit_sha1=fixed_git_commit_sha1,
765+ )
766+
767 def makeSignedMessage(
768 self,
769 msgid=None,

Subscribers

People subscribed via source and target branches

to status/vote changes: