Merge ~andrey-fedoseev/launchpad:bug-task-channel into launchpad:master

Proposed by Andrey Fedoseev
Status: Needs review
Proposed branch: ~andrey-fedoseev/launchpad:bug-task-channel
Merge into: launchpad:master
Diff against target: 1067 lines (+328/-71)
20 files modified
lib/lp/bugs/configure.zcml (+2/-0)
lib/lp/bugs/interfaces/bugsummary.py (+3/-1)
lib/lp/bugs/interfaces/bugtask.py (+1/-0)
lib/lp/bugs/model/bugsummary.py (+12/-1)
lib/lp/bugs/model/bugtask.py (+27/-1)
lib/lp/bugs/model/bugtaskflat.py (+2/-1)
lib/lp/bugs/model/tests/test_bugtask.py (+24/-0)
lib/lp/bugs/scripts/bugsummaryrebuild.py (+36/-20)
lib/lp/bugs/scripts/bugtasktargetnamecaches.py (+2/-2)
lib/lp/registry/interfaces/distroseries.py (+1/-1)
lib/lp/registry/interfaces/sourcepackage.py (+11/-1)
lib/lp/registry/model/distroseries.py (+3/-2)
lib/lp/registry/model/sourcepackage.py (+64/-16)
lib/lp/registry/stories/webservice/xx-source-package.rst (+1/-0)
lib/lp/registry/tests/test_distributionsourcepackage.py (+1/-1)
lib/lp/registry/tests/test_distroseries.py (+6/-0)
lib/lp/registry/tests/test_sourcepackage.py (+60/-0)
lib/lp/soyuz/tests/test_binarypackagebuild.py (+9/-5)
lib/lp/soyuz/tests/test_hasbuildrecords.py (+50/-17)
lib/lp/testing/factory.py (+13/-2)
Reviewer Review Type Date Requested Status
Launchpad code reviewers Pending
Review via email: mp+434693@code.launchpad.net

Commit message

WIP: Add `channel` field to `BugTask` and `SourcePackage`

This should also make a `SourcePackage` with a channel a valid target for a `BugTask`, and some work has been done in that direction, but it may not be 100% complete.

Description of the change

Currently, it is confirmed to pass all tests in `lp.{bugs,soyuz,registry}` (other packages hasn't been checked)

To post a comment you must log in.

Unmerged commits

c6e1a93... by Andrey Fedoseev

WIP: Add `channel` field to `BugTask` and `SourcePackage`

This should also make a `SourcePackage` with a channel a valid target for a `BugTask`, and some work has been done in that direction, but it may not be 100% complete.

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
diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
index 6eaa58c..12bfdd7 100644
--- a/lib/lp/bugs/configure.zcml
+++ b/lib/lp/bugs/configure.zcml
@@ -211,6 +211,8 @@
211 distribution211 distribution
212 distroseries212 distroseries
213 milestone213 milestone
214 _channel
215 channel
214 _status216 _status
215 status217 status
216 status_explanation218 status_explanation
diff --git a/lib/lp/bugs/interfaces/bugsummary.py b/lib/lp/bugs/interfaces/bugsummary.py
index 7cb55ef..be83775 100644
--- a/lib/lp/bugs/interfaces/bugsummary.py
+++ b/lib/lp/bugs/interfaces/bugsummary.py
@@ -9,7 +9,7 @@ __all__ = [
9]9]
1010
11from zope.interface import Interface11from zope.interface import Interface
12from zope.schema import Bool, Choice, Int, Object, Text12from zope.schema import Bool, Choice, Int, Object, Text, TextLine
1313
14from lp import _14from lp import _
15from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatusSearch15from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatusSearch
@@ -51,6 +51,8 @@ class IBugSummary(Interface):
51 ociproject_id = Int(readonly=True)51 ociproject_id = Int(readonly=True)
52 ociproject = Object(IOCIProject, readonly=True)52 ociproject = Object(IOCIProject, readonly=True)
5353
54 channel = TextLine(readonly=True)
55
54 milestone_id = Int(readonly=True)56 milestone_id = Int(readonly=True)
55 milestone = Object(IMilestone, readonly=True)57 milestone = Object(IMilestone, readonly=True)
5658
diff --git a/lib/lp/bugs/interfaces/bugtask.py b/lib/lp/bugs/interfaces/bugtask.py
index a8c4e45..d2586f7 100644
--- a/lib/lp/bugs/interfaces/bugtask.py
+++ b/lib/lp/bugs/interfaces/bugtask.py
@@ -475,6 +475,7 @@ class IBugTask(IHasBug, IBugTaskDelete):
475 title=_("Series"), required=False, vocabulary="DistroSeries"475 title=_("Series"), required=False, vocabulary="DistroSeries"
476 )476 )
477 distroseries_id = Attribute("The distroseries ID")477 distroseries_id = Attribute("The distroseries ID")
478 channel = TextLine(title=_("Channel"), required=False)
478 milestone = exported(479 milestone = exported(
479 ReferenceChoice(480 ReferenceChoice(
480 title=_("Milestone"),481 title=_("Milestone"),
diff --git a/lib/lp/bugs/model/bugsummary.py b/lib/lp/bugs/model/bugsummary.py
index 9084e0e..2caabf6 100644
--- a/lib/lp/bugs/model/bugsummary.py
+++ b/lib/lp/bugs/model/bugsummary.py
@@ -9,6 +9,8 @@ __all__ = [
9 "get_bugsummary_filter_for_user",9 "get_bugsummary_filter_for_user",
10]10]
1111
12from typing import Optional
13
12from storm.base import Storm14from storm.base import Storm
13from storm.expr import SQL, And, Or, Select15from storm.expr import SQL, And, Or, Select
14from storm.properties import Bool, Int, Unicode16from storm.properties import Bool, Int, Unicode
@@ -32,9 +34,10 @@ from lp.registry.model.product import Product
32from lp.registry.model.productseries import ProductSeries34from lp.registry.model.productseries import ProductSeries
33from lp.registry.model.sourcepackagename import SourcePackageName35from lp.registry.model.sourcepackagename import SourcePackageName
34from lp.registry.model.teammembership import TeamParticipation36from lp.registry.model.teammembership import TeamParticipation
37from lp.services.channels import channel_list_to_string
35from lp.services.database.enumcol import DBEnum38from lp.services.database.enumcol import DBEnum
36from lp.services.database.interfaces import IStore39from lp.services.database.interfaces import IStore
37from lp.services.database.stormexpr import WithMaterialized40from lp.services.database.stormexpr import ImmutablePgJSON, WithMaterialized
3841
3942
40@implementer(IBugSummary)43@implementer(IBugSummary)
@@ -64,6 +67,8 @@ class BugSummary(Storm):
64 ociproject_id = Int(name="ociproject")67 ociproject_id = Int(name="ociproject")
65 ociproject = Reference(ociproject_id, "OCIProject.id")68 ociproject = Reference(ociproject_id, "OCIProject.id")
6669
70 _channel = ImmutablePgJSON(name="channel")
71
67 milestone_id = Int(name="milestone")72 milestone_id = Int(name="milestone")
68 milestone = Reference(milestone_id, Milestone.id)73 milestone = Reference(milestone_id, Milestone.id)
6974
@@ -80,6 +85,12 @@ class BugSummary(Storm):
8085
81 has_patch = Bool()86 has_patch = Bool()
8287
88 @property
89 def channel(self) -> Optional[str]:
90 if self._channel is None:
91 return None
92 return channel_list_to_string(*self._channel)
93
8394
84@implementer(IBugSummaryDimension)95@implementer(IBugSummaryDimension)
85class CombineBugSummaryConstraint:96class CombineBugSummaryConstraint:
diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py
index c006618..8955720 100644
--- a/lib/lp/bugs/model/bugtask.py
+++ b/lib/lp/bugs/model/bugtask.py
@@ -21,6 +21,7 @@ import re
21from collections import defaultdict21from collections import defaultdict
22from itertools import chain, repeat22from itertools import chain, repeat
23from operator import attrgetter, itemgetter23from operator import attrgetter, itemgetter
24from typing import Optional
2425
25import pytz26import pytz
26from lazr.lifecycle.event import ObjectDeletedEvent27from lazr.lifecycle.event import ObjectDeletedEvent
@@ -98,6 +99,7 @@ from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
98from lp.registry.model.pillar import pillar_sort_key99from lp.registry.model.pillar import pillar_sort_key
99from lp.registry.model.sourcepackagename import SourcePackageName100from lp.registry.model.sourcepackagename import SourcePackageName
100from lp.services import features101from lp.services import features
102from lp.services.channels import channel_list_to_string, channel_string_to_list
101from lp.services.database.bulk import create, load, load_related103from lp.services.database.bulk import create, load, load_related
102from lp.services.database.constants import UTC_NOW104from lp.services.database.constants import UTC_NOW
103from lp.services.database.decoratedresultset import DecoratedResultSet105from lp.services.database.decoratedresultset import DecoratedResultSet
@@ -111,6 +113,7 @@ from lp.services.database.sqlbase import (
111 sqlvalues,113 sqlvalues,
112)114)
113from lp.services.database.stormbase import StormBase115from lp.services.database.stormbase import StormBase
116from lp.services.database.stormexpr import ImmutablePgJSON
114from lp.services.helpers import shortlist117from lp.services.helpers import shortlist
115from lp.services.propertycache import get_property_cache118from lp.services.propertycache import get_property_cache
116from lp.services.searchbuilder import any119from lp.services.searchbuilder import any
@@ -171,6 +174,7 @@ def bug_target_from_key(
171 distroseries,174 distroseries,
172 sourcepackagename,175 sourcepackagename,
173 ociproject,176 ociproject,
177 channel,
174):178):
175 """Returns the IBugTarget defined by the given DB column values."""179 """Returns the IBugTarget defined by the given DB column values."""
176 if ociproject:180 if ociproject:
@@ -189,7 +193,7 @@ def bug_target_from_key(
189 return distribution193 return distribution
190 elif distroseries:194 elif distroseries:
191 if sourcepackagename:195 if sourcepackagename:
192 return distroseries.getSourcePackage(sourcepackagename)196 return distroseries.getSourcePackage(sourcepackagename, channel)
193 else:197 else:
194 return distroseries198 return distroseries
195 else:199 else:
@@ -205,6 +209,7 @@ def bug_target_to_key(target):
205 distroseries=None,209 distroseries=None,
206 sourcepackagename=None,210 sourcepackagename=None,
207 ociproject=None,211 ociproject=None,
212 channel=None,
208 )213 )
209 if IProduct.providedBy(target):214 if IProduct.providedBy(target):
210 values["product"] = target215 values["product"] = target
@@ -220,6 +225,7 @@ def bug_target_to_key(target):
220 elif ISourcePackage.providedBy(target):225 elif ISourcePackage.providedBy(target):
221 values["distroseries"] = target.distroseries226 values["distroseries"] = target.distroseries
222 values["sourcepackagename"] = target.sourcepackagename227 values["sourcepackagename"] = target.sourcepackagename
228 values["channel"] = target.channel
223 elif IOCIProject.providedBy(target):229 elif IOCIProject.providedBy(target):
224 # De-normalize the ociproject, including also the ociproject's230 # De-normalize the ociproject, including also the ociproject's
225 # pillar (distribution or product).231 # pillar (distribution or product).
@@ -499,6 +505,8 @@ class BugTask(StormBase):
499 distroseries_id = Int(name="distroseries", allow_none=True)505 distroseries_id = Int(name="distroseries", allow_none=True)
500 distroseries = Reference(distroseries_id, "DistroSeries.id")506 distroseries = Reference(distroseries_id, "DistroSeries.id")
501507
508 _channel = ImmutablePgJSON(name="channel", allow_none=True)
509
502 milestone_id = Int(510 milestone_id = Int(
503 name="milestone",511 name="milestone",
504 allow_none=True,512 allow_none=True,
@@ -613,6 +621,19 @@ class BugTask(StormBase):
613 )621 )
614622
615 @property623 @property
624 def channel(self) -> Optional[str]:
625 if self._channel is None:
626 return None
627 return channel_list_to_string(*self._channel)
628
629 @channel.setter
630 def channel(self, value: str) -> None:
631 if value is None:
632 self._channel = None
633 else:
634 self._channel = channel_string_to_list(value)
635
636 @property
616 def status(self):637 def status(self):
617 if self._status in DB_INCOMPLETE_BUGTASK_STATUSES:638 if self._status in DB_INCOMPLETE_BUGTASK_STATUSES:
618 return BugTaskStatus.INCOMPLETE639 return BugTaskStatus.INCOMPLETE
@@ -652,6 +673,7 @@ class BugTask(StormBase):
652 self.distroseries,673 self.distroseries,
653 self.sourcepackagename,674 self.sourcepackagename,
654 self.ociproject,675 self.ociproject,
676 self.channel,
655 )677 )
656678
657 @property679 @property
@@ -1863,6 +1885,9 @@ class BugTaskSet:
1863 key["distroseries"],1885 key["distroseries"],
1864 key["sourcepackagename"],1886 key["sourcepackagename"],
1865 key["ociproject"],1887 key["ociproject"],
1888 channel_string_to_list(key["channel"])
1889 if key["channel"]
1890 else None,
1866 status,1891 status,
1867 importance,1892 importance,
1868 assignee,1893 assignee,
@@ -1880,6 +1905,7 @@ class BugTaskSet:
1880 BugTask.distroseries,1905 BugTask.distroseries,
1881 BugTask.sourcepackagename,1906 BugTask.sourcepackagename,
1882 BugTask.ociproject,1907 BugTask.ociproject,
1908 BugTask._channel,
1883 BugTask._status,1909 BugTask._status,
1884 BugTask.importance,1910 BugTask.importance,
1885 BugTask.assignee,1911 BugTask.assignee,
diff --git a/lib/lp/bugs/model/bugtaskflat.py b/lib/lp/bugs/model/bugtaskflat.py
index b25a440..b636ecf 100644
--- a/lib/lp/bugs/model/bugtaskflat.py
+++ b/lib/lp/bugs/model/bugtaskflat.py
@@ -1,6 +1,5 @@
1# Copyright 2012-2020 Canonical Ltd. This software is licensed under the1# Copyright 2012-2020 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4from storm.locals import Bool, DateTime, Int, List, Reference, Storm3from storm.locals import Bool, DateTime, Int, List, Reference, Storm
54
6from lp.app.enums import InformationType5from lp.app.enums import InformationType
@@ -10,6 +9,7 @@ from lp.bugs.interfaces.bugtask import (
10 BugTaskStatusSearch,9 BugTaskStatusSearch,
11)10)
12from lp.services.database.enumcol import DBEnum11from lp.services.database.enumcol import DBEnum
12from lp.services.database.stormexpr import ImmutablePgJSON
1313
1414
15class BugTaskFlat(Storm):15class BugTaskFlat(Storm):
@@ -42,6 +42,7 @@ class BugTaskFlat(Storm):
42 sourcepackagename = Reference(sourcepackagename_id, "SourcePackageName.id")42 sourcepackagename = Reference(sourcepackagename_id, "SourcePackageName.id")
43 ociproject_id = Int(name="ociproject")43 ociproject_id = Int(name="ociproject")
44 ociproject = Reference(ociproject_id, "OCIProject.id")44 ociproject = Reference(ociproject_id, "OCIProject.id")
45 channel = ImmutablePgJSON()
45 status = DBEnum(enum=(BugTaskStatus, BugTaskStatusSearch))46 status = DBEnum(enum=(BugTaskStatus, BugTaskStatusSearch))
46 importance = DBEnum(enum=BugTaskImportance)47 importance = DBEnum(enum=BugTaskImportance)
47 assignee_id = Int(name="assignee")48 assignee_id = Int(name="assignee")
diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
index c3eb248..ecb258f 100644
--- a/lib/lp/bugs/model/tests/test_bugtask.py
+++ b/lib/lp/bugs/model/tests/test_bugtask.py
@@ -3145,6 +3145,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3145 distroseries=None,3145 distroseries=None,
3146 sourcepackagename=None,3146 sourcepackagename=None,
3147 ociproject=None,3147 ociproject=None,
3148 channel=None,
3148 ),3149 ),
3149 )3150 )
31503151
@@ -3159,6 +3160,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3159 distroseries=None,3160 distroseries=None,
3160 sourcepackagename=None,3161 sourcepackagename=None,
3161 ociproject=None,3162 ociproject=None,
3163 channel=None,
3162 ),3164 ),
3163 )3165 )
31643166
@@ -3173,6 +3175,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3173 distroseries=None,3175 distroseries=None,
3174 sourcepackagename=None,3176 sourcepackagename=None,
3175 ociproject=None,3177 ociproject=None,
3178 channel=None,
3176 ),3179 ),
3177 )3180 )
31783181
@@ -3187,6 +3190,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3187 distroseries=distroseries,3190 distroseries=distroseries,
3188 sourcepackagename=None,3191 sourcepackagename=None,
3189 ociproject=None,3192 ociproject=None,
3193 channel=None,
3190 ),3194 ),
3191 )3195 )
31923196
@@ -3201,6 +3205,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3201 distroseries=None,3205 distroseries=None,
3202 sourcepackagename=dsp.sourcepackagename,3206 sourcepackagename=dsp.sourcepackagename,
3203 ociproject=None,3207 ociproject=None,
3208 channel=None,
3204 ),3209 ),
3205 )3210 )
32063211
@@ -3215,6 +3220,22 @@ class TestBugTargetKeys(TestCaseWithFactory):
3215 distroseries=sp.distroseries,3220 distroseries=sp.distroseries,
3216 sourcepackagename=sp.sourcepackagename,3221 sourcepackagename=sp.sourcepackagename,
3217 ociproject=None,3222 ociproject=None,
3223 channel=None,
3224 ),
3225 )
3226
3227 def test_sourcepackage_with_channel(self):
3228 sp = self.factory.makeSourcePackage(channel="stable")
3229 self.assertTargetKeyWorks(
3230 sp,
3231 dict(
3232 product=None,
3233 productseries=None,
3234 distribution=None,
3235 distroseries=sp.distroseries,
3236 sourcepackagename=sp.sourcepackagename,
3237 ociproject=None,
3238 channel="stable",
3218 ),3239 ),
3219 )3240 )
32203241
@@ -3230,6 +3251,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3230 distroseries=None,3251 distroseries=None,
3231 sourcepackagename=None,3252 sourcepackagename=None,
3232 ociproject=ociproject,3253 ociproject=ociproject,
3254 channel=None,
3233 ),3255 ),
3234 )3256 )
32353257
@@ -3245,6 +3267,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3245 distroseries=None,3267 distroseries=None,
3246 sourcepackagename=None,3268 sourcepackagename=None,
3247 ociproject=ociproject,3269 ociproject=ociproject,
3270 channel=None,
3248 ),3271 ),
3249 )3272 )
32503273
@@ -3263,6 +3286,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
3263 None,3286 None,
3264 None,3287 None,
3265 None,3288 None,
3289 None,
3266 )3290 )
32673291
32683292
diff --git a/lib/lp/bugs/scripts/bugsummaryrebuild.py b/lib/lp/bugs/scripts/bugsummaryrebuild.py
index 21003b4..cf9ae8a 100644
--- a/lib/lp/bugs/scripts/bugsummaryrebuild.py
+++ b/lib/lp/bugs/scripts/bugsummaryrebuild.py
@@ -24,6 +24,7 @@ from lp.registry.model.ociproject import OCIProject
24from lp.registry.model.product import Product24from lp.registry.model.product import Product
25from lp.registry.model.productseries import ProductSeries25from lp.registry.model.productseries import ProductSeries
26from lp.registry.model.sourcepackagename import SourcePackageName26from lp.registry.model.sourcepackagename import SourcePackageName
27from lp.services.channels import channel_string_to_list
27from lp.services.database.bulk import create28from lp.services.database.bulk import create
28from lp.services.database.interfaces import IStore29from lp.services.database.interfaces import IStore
29from lp.services.database.stormexpr import Unnest30from lp.services.database.stormexpr import Unnest
@@ -109,7 +110,7 @@ def load_target(pid, psid, did, dsid, spnid, ociproject_id):
109 (pid, psid, did, dsid, spnid, ociproject_id),110 (pid, psid, did, dsid, spnid, ociproject_id),
110 ),111 ),
111 )112 )
112 return bug_target_from_key(p, ps, d, ds, spn, ociproject)113 return bug_target_from_key(p, ps, d, ds, spn, ociproject, None)
113114
114115
115def format_target(target):116def format_target(target):
@@ -130,7 +131,15 @@ def format_target(target):
130def _get_bugsummary_constraint_bits(target):131def _get_bugsummary_constraint_bits(target):
131 raw_key = bug_target_to_key(target)132 raw_key = bug_target_to_key(target)
132 # Map to ID columns to work around Storm bug #682989.133 # Map to ID columns to work around Storm bug #682989.
133 return {"%s_id" % k: v.id if v else None for (k, v) in raw_key.items()}134 constraint_bits = {}
135 for name, value in raw_key.items():
136 if name == "channel":
137 constraint_bits["_channel"] = (
138 channel_string_to_list(value) if value else None
139 )
140 else:
141 constraint_bits["{}_id".format(name)] = value.id if value else None
142 return constraint_bits
134143
135144
136def get_bugsummary_constraint(target, cls=RawBugSummary):145def get_bugsummary_constraint(target, cls=RawBugSummary):
@@ -154,10 +163,19 @@ def get_bugtaskflat_constraint(target):
154 if IProduct.providedBy(target):163 if IProduct.providedBy(target):
155 del raw_key["ociproject"]164 del raw_key["ociproject"]
156 # Map to ID columns to work around Storm bug #682989.165 # Map to ID columns to work around Storm bug #682989.
157 return [166 constraint_bits = []
158 getattr(BugTaskFlat, "%s_id" % k) == (v.id if v else None)167 for name, value in raw_key.items():
159 for (k, v) in raw_key.items()168 if name == "channel":
160 ]169 constraint_bits.append(
170 getattr(BugTaskFlat, name)
171 == (channel_string_to_list(value) if value else None)
172 )
173 else:
174 constraint_bits.append(
175 getattr(BugTaskFlat, "{}_id".format(name))
176 == (value.id if value else None)
177 )
178 return constraint_bits
161179
162180
163def get_bugsummary_rows(target):181def get_bugsummary_rows(target):
@@ -167,6 +185,7 @@ def get_bugsummary_rows(target):
167 with BugSummary which is actually combinedbugsummary, a view over185 with BugSummary which is actually combinedbugsummary, a view over
168 bugsummary and bugsummaryjournal.186 bugsummary and bugsummaryjournal.
169 """187 """
188 constraint = get_bugsummary_constraint(target)
170 return IStore(RawBugSummary).find(189 return IStore(RawBugSummary).find(
171 (190 (
172 RawBugSummary.status,191 RawBugSummary.status,
@@ -178,7 +197,7 @@ def get_bugsummary_rows(target):
178 RawBugSummary.access_policy_id,197 RawBugSummary.access_policy_id,
179 RawBugSummary.count,198 RawBugSummary.count,
180 ),199 ),
181 *get_bugsummary_constraint(target),200 *constraint,
182 )201 )
183202
184203
@@ -191,7 +210,7 @@ def get_bugsummaryjournal_rows(target):
191210
192211
193def calculate_bugsummary_changes(old, new):212def calculate_bugsummary_changes(old, new):
194 """Calculate the changes between between the new and old dicts.213 """Calculate the changes between the new and old dicts.
195214
196 Takes {key: int} dicts, returns items from the new dict that differ215 Takes {key: int} dicts, returns items from the new dict that differ
197 from the old one.216 from the old one.
@@ -219,18 +238,14 @@ def calculate_bugsummary_changes(old, new):
219def apply_bugsummary_changes(target, added, updated, removed):238def apply_bugsummary_changes(target, added, updated, removed):
220 """Apply a set of BugSummary changes to the DB."""239 """Apply a set of BugSummary changes to the DB."""
221 bits = _get_bugsummary_constraint_bits(target)240 bits = _get_bugsummary_constraint_bits(target)
222 target_key = tuple(241 target_key = (
223 map(242 bits["product_id"],
224 bits.get,243 bits["productseries_id"],
225 (244 bits["distribution_id"],
226 "product_id",245 bits["distroseries_id"],
227 "productseries_id",246 bits["sourcepackagename_id"],
228 "distribution_id",247 bits["ociproject_id"],
229 "distroseries_id",248 bits["_channel"],
230 "sourcepackagename_id",
231 "ociproject_id",
232 ),
233 )
234 )249 )
235 target_cols = (250 target_cols = (
236 RawBugSummary.product_id,251 RawBugSummary.product_id,
@@ -239,6 +254,7 @@ def apply_bugsummary_changes(target, added, updated, removed):
239 RawBugSummary.distroseries_id,254 RawBugSummary.distroseries_id,
240 RawBugSummary.sourcepackagename_id,255 RawBugSummary.sourcepackagename_id,
241 RawBugSummary.ociproject_id,256 RawBugSummary.ociproject_id,
257 RawBugSummary._channel,
242 )258 )
243 key_cols = (259 key_cols = (
244 RawBugSummary.status,260 RawBugSummary.status,
diff --git a/lib/lp/bugs/scripts/bugtasktargetnamecaches.py b/lib/lp/bugs/scripts/bugtasktargetnamecaches.py
index 70a82d4..dcbf424 100644
--- a/lib/lp/bugs/scripts/bugtasktargetnamecaches.py
+++ b/lib/lp/bugs/scripts/bugtasktargetnamecaches.py
@@ -103,10 +103,10 @@ class BugTaskTargetNameCachesTunableLoop:
103 self.offset += 1103 self.offset += 1
104 # Resolve the IDs to objects, and get the actual IBugTarget.104 # Resolve the IDs to objects, and get the actual IBugTarget.
105 # If the ID is None, don't even try to get an object.105 # If the ID is None, don't even try to get an object.
106 target_objects = (106 target_objects = [
107 (store.get(cls, id) if id is not None else None)107 (store.get(cls, id) if id is not None else None)
108 for cls, id in zip(target_classes, target_bits)108 for cls, id in zip(target_classes, target_bits)
109 )109 ] + [None]
110 target = bug_target_from_key(*target_objects)110 target = bug_target_from_key(*target_objects)
111 new_name = target.bugtargetdisplayname111 new_name = target.bugtargetdisplayname
112 cached_names.discard(new_name)112 cached_names.discard(new_name)
diff --git a/lib/lp/registry/interfaces/distroseries.py b/lib/lp/registry/interfaces/distroseries.py
index 03e6d68..5a718c2 100644
--- a/lib/lp/registry/interfaces/distroseries.py
+++ b/lib/lp/registry/interfaces/distroseries.py
@@ -667,7 +667,7 @@ class IDistroSeriesPublic(
667 @operation_returns_entry(ISourcePackage)667 @operation_returns_entry(ISourcePackage)
668 @export_read_operation()668 @export_read_operation()
669 @operation_for_version("beta")669 @operation_for_version("beta")
670 def getSourcePackage(name):670 def getSourcePackage(name, channel=None):
671 """Return a source package in this distro series by name.671 """Return a source package in this distro series by name.
672672
673 The name given may be a string or an ISourcePackageName-providing673 The name given may be a string or an ISourcePackageName-providing
diff --git a/lib/lp/registry/interfaces/sourcepackage.py b/lib/lp/registry/interfaces/sourcepackage.py
index d8f776e..a4505c0 100644
--- a/lib/lp/registry/interfaces/sourcepackage.py
+++ b/lib/lp/registry/interfaces/sourcepackage.py
@@ -130,6 +130,15 @@ class ISourcePackagePublic(
130130
131 sourcepackagename = Attribute("SourcePackageName")131 sourcepackagename = Attribute("SourcePackageName")
132132
133 channel = exported(
134 TextLine(
135 title=_("Channel"),
136 required=False,
137 readonly=True,
138 description=_("The channel for this source package."),
139 ),
140 )
141
133 # This is really a reference to an IProductSeries.142 # This is really a reference to an IProductSeries.
134 productseries = exported(143 productseries = exported(
135 ReferenceChoice(144 ReferenceChoice(
@@ -362,11 +371,12 @@ class ISourcePackage(ISourcePackagePublic, ISourcePackageEdit):
362class ISourcePackageFactory(Interface):371class ISourcePackageFactory(Interface):
363 """A creator of source packages."""372 """A creator of source packages."""
364373
365 def new(sourcepackagename, distroseries):374 def new(sourcepackagename, distroseries, channel=None):
366 """Create a new `ISourcePackage`.375 """Create a new `ISourcePackage`.
367376
368 :param sourcepackagename: An `ISourcePackageName`.377 :param sourcepackagename: An `ISourcePackageName`.
369 :param distroseries: An `IDistroSeries`.378 :param distroseries: An `IDistroSeries`.
379 :param channel: A channel name or None.
370 :return: `ISourcePackage`.380 :return: `ISourcePackage`.
371 """381 """
372382
diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py
index 7844249..a128ed5 100644
--- a/lib/lp/registry/model/distroseries.py
+++ b/lib/lp/registry/model/distroseries.py
@@ -13,6 +13,7 @@ __all__ = [
13import collections13import collections
14from io import BytesIO14from io import BytesIO
15from operator import itemgetter15from operator import itemgetter
16from typing import Optional
1617
17import apt_pkg18import apt_pkg
18from lazr.delegates import delegate_to19from lazr.delegates import delegate_to
@@ -1036,7 +1037,7 @@ class DistroSeries(
1036 self.messagecount = messagecount1037 self.messagecount = messagecount
1037 ztm.commit()1038 ztm.commit()
10381039
1039 def getSourcePackage(self, name):1040 def getSourcePackage(self, name, channel: Optional[str] = None):
1040 """See `IDistroSeries`."""1041 """See `IDistroSeries`."""
1041 if not ISourcePackageName.providedBy(name):1042 if not ISourcePackageName.providedBy(name):
1042 try:1043 try:
@@ -1044,7 +1045,7 @@ class DistroSeries(
1044 except SQLObjectNotFound:1045 except SQLObjectNotFound:
1045 return None1046 return None
1046 return getUtility(ISourcePackageFactory).new(1047 return getUtility(ISourcePackageFactory).new(
1047 sourcepackagename=name, distroseries=self1048 sourcepackagename=name, distroseries=self, channel=channel
1048 )1049 )
10491050
1050 def getBinaryPackage(self, name):1051 def getBinaryPackage(self, name):
diff --git a/lib/lp/registry/model/sourcepackage.py b/lib/lp/registry/model/sourcepackage.py
index 8a0280b..5aff937 100644
--- a/lib/lp/registry/model/sourcepackage.py
+++ b/lib/lp/registry/model/sourcepackage.py
@@ -8,7 +8,9 @@ __all__ = [
8 "SourcePackageQuestionTargetMixin",8 "SourcePackageQuestionTargetMixin",
9]9]
1010
11import json
11from operator import attrgetter, itemgetter12from operator import attrgetter, itemgetter
13from typing import Optional
1214
13from storm.locals import And, Desc, Join, Store15from storm.locals import And, Desc, Join, Store
14from zope.component import getUtility16from zope.component import getUtility
@@ -42,6 +44,7 @@ from lp.registry.interfaces.sourcepackage import (
42from lp.registry.model.hasdrivers import HasDriversMixin44from lp.registry.model.hasdrivers import HasDriversMixin
43from lp.registry.model.packaging import Packaging45from lp.registry.model.packaging import Packaging
44from lp.registry.model.suitesourcepackage import SuiteSourcePackage46from lp.registry.model.suitesourcepackage import SuiteSourcePackage
47from lp.services.channels import channel_string_to_list
45from lp.services.database.decoratedresultset import DecoratedResultSet48from lp.services.database.decoratedresultset import DecoratedResultSet
46from lp.services.database.interfaces import IStore49from lp.services.database.interfaces import IStore
47from lp.services.database.sqlbase import flush_database_updates, sqlvalues50from lp.services.database.sqlbase import flush_database_updates, sqlvalues
@@ -204,7 +207,9 @@ class SourcePackage(
204 to the relevant database objects.207 to the relevant database objects.
205 """208 """
206209
207 def __init__(self, sourcepackagename, distroseries):210 def __init__(
211 self, sourcepackagename, distroseries, channel: Optional[str] = None
212 ):
208 # We store the ID of the sourcepackagename and distroseries213 # We store the ID of the sourcepackagename and distroseries
209 # simply because Storm can break when accessing them214 # simply because Storm can break when accessing them
210 # with implicit flush is blocked (like in a permission check when215 # with implicit flush is blocked (like in a permission check when
@@ -213,11 +218,14 @@ class SourcePackage(
213 self.sourcepackagename = sourcepackagename218 self.sourcepackagename = sourcepackagename
214 self.distroseries = distroseries219 self.distroseries = distroseries
215 self.distroseriesID = distroseries.id220 self.distroseriesID = distroseries.id
221 self.channel = channel
216222
217 @classmethod223 @classmethod
218 def new(cls, sourcepackagename, distroseries):224 def new(
225 cls, sourcepackagename, distroseries, channel: Optional[str] = None
226 ):
219 """See `ISourcePackageFactory`."""227 """See `ISourcePackageFactory`."""
220 return cls(sourcepackagename, distroseries)228 return cls(sourcepackagename, distroseries, channel=channel)
221229
222 def __repr__(self):230 def __repr__(self):
223 return "<%s %r %r %r>" % (231 return "<%s %r %r %r>" % (
@@ -242,6 +250,12 @@ class SourcePackage(
242 == self.sourcepackagename,250 == self.sourcepackagename,
243 SourcePackagePublishingHistory.distroseries251 SourcePackagePublishingHistory.distroseries
244 == self.distroseries,252 == self.distroseries,
253 SourcePackagePublishingHistory._channel
254 == (
255 None
256 if self.channel is None
257 else channel_string_to_list(self.channel)
258 ),
245 SourcePackagePublishingHistory.archiveID.is_in(259 SourcePackagePublishingHistory.archiveID.is_in(
246 self.distribution.all_distro_archive_ids260 self.distribution.all_distro_archive_ids
247 ),261 ),
@@ -296,24 +310,27 @@ class SourcePackage(
296 )310 )
297311
298 @property312 @property
313 def series_name(self):
314 series_name = self.distroseries.fullseriesname
315 if self.channel is not None:
316 series_name = "%s, %s" % (series_name, self.channel)
317 return series_name
318
319 @property
299 def display_name(self):320 def display_name(self):
300 return "%s in %s %s" % (321 return "%s in %s" % (self.sourcepackagename.name, self.series_name)
301 self.sourcepackagename.name,
302 self.distribution.displayname,
303 self.distroseries.displayname,
304 )
305322
306 displayname = display_name323 displayname = display_name
307324
308 @property325 @property
309 def bugtargetdisplayname(self):326 def bugtargetdisplayname(self):
310 """See IBugTarget."""327 """See IBugTarget."""
311 return "%s (%s)" % (self.name, self.distroseries.fullseriesname)328 return "%s (%s)" % (self.name, self.series_name)
312329
313 @property330 @property
314 def bugtargetname(self):331 def bugtargetname(self):
315 """See `IBugTarget`."""332 """See `IBugTarget`."""
316 return "%s (%s)" % (self.name, self.distroseries.fullseriesname)333 return "%s (%s)" % (self.name, self.series_name)
317334
318 @property335 @property
319 def bugtarget_parent(self):336 def bugtarget_parent(self):
@@ -551,6 +568,12 @@ class SourcePackage(
551 return And(568 return And(
552 BugSummary.distroseries == self.distroseries,569 BugSummary.distroseries == self.distroseries,
553 BugSummary.sourcepackagename == self.sourcepackagename,570 BugSummary.sourcepackagename == self.sourcepackagename,
571 BugSummary._channel
572 == (
573 None
574 if self.channel is None
575 else channel_string_to_list(self.channel)
576 ),
554 )577 )
555578
556 def setPackaging(self, productseries, owner):579 def setPackaging(self, productseries, owner):
@@ -616,7 +639,11 @@ class SourcePackage(
616639
617 def __hash__(self):640 def __hash__(self):
618 """See `ISourcePackage`."""641 """See `ISourcePackage`."""
619 return hash(self.distroseriesID) ^ hash(self.sourcepackagenameID)642 return (
643 hash(self.distroseriesID)
644 ^ hash(self.sourcepackagenameID)
645 ^ hash(self.channel)
646 )
620647
621 def __eq__(self, other):648 def __eq__(self, other):
622 """See `ISourcePackage`."""649 """See `ISourcePackage`."""
@@ -624,6 +651,7 @@ class SourcePackage(
624 (ISourcePackage.providedBy(other))651 (ISourcePackage.providedBy(other))
625 and (self.distroseries.id == other.distroseries.id)652 and (self.distroseries.id == other.distroseries.id)
626 and (self.sourcepackagename.id == other.sourcepackagename.id)653 and (self.sourcepackagename.id == other.sourcepackagename.id)
654 and (self.channel == other.channel)
627 )655 )
628656
629 def __ne__(self, other):657 def __ne__(self, other):
@@ -672,6 +700,16 @@ class SourcePackage(
672 )700 )
673 ]701 ]
674702
703 if self.channel is None:
704 condition_clauses.append(
705 "SourcePackagePublishingHistory.channel IS NULL"
706 )
707 else:
708 condition_clauses.append(
709 "SourcePackagePublishingHistory.channel = '%s'::jsonb"
710 % json.dumps(channel_string_to_list(self.channel))
711 )
712
675 # We re-use the optional-parameter handling provided by BuildSet713 # We re-use the optional-parameter handling provided by BuildSet
676 # here, but pass None for the name argument as we've already714 # here, but pass None for the name argument as we've already
677 # matched on exact source package name.715 # matched on exact source package name.
@@ -882,16 +920,26 @@ class SourcePackage(
882920
883 def weight_function(bugtask):921 def weight_function(bugtask):
884 if bugtask.sourcepackagename_id == sourcepackagenameID:922 if bugtask.sourcepackagename_id == sourcepackagenameID:
885 if bugtask.distroseries_id == seriesID:923 if (
924 bugtask.distroseries_id == seriesID
925 and bugtask.channel == self.channel
926 ):
886 return OrderedBugTask(1, bugtask.id, bugtask)927 return OrderedBugTask(1, bugtask.id, bugtask)
887 elif bugtask.distribution_id == distributionID:928 elif bugtask.distroseries_id == seriesID:
888 return OrderedBugTask(2, bugtask.id, bugtask)929 return OrderedBugTask(2, bugtask.id, bugtask)
930 elif bugtask.distribution_id == distributionID:
931 return OrderedBugTask(3, bugtask.id, bugtask)
932 elif (
933 bugtask.distroseries_id == seriesID
934 and bugtask.channel == self.channel
935 ):
936 return OrderedBugTask(4, bugtask.id, bugtask)
889 elif bugtask.distroseries_id == seriesID:937 elif bugtask.distroseries_id == seriesID:
890 return OrderedBugTask(3, bugtask.id, bugtask)938 return OrderedBugTask(5, bugtask.id, bugtask)
891 elif bugtask.distribution_id == distributionID:939 elif bugtask.distribution_id == distributionID:
892 return OrderedBugTask(4, bugtask.id, bugtask)940 return OrderedBugTask(6, bugtask.id, bugtask)
893 # Catch the default case, and where there is a task for the same941 # Catch the default case, and where there is a task for the same
894 # sourcepackage on a different distro.942 # sourcepackage on a different distro.
895 return OrderedBugTask(5, bugtask.id, bugtask)943 return OrderedBugTask(7, bugtask.id, bugtask)
896944
897 return weight_function945 return weight_function
diff --git a/lib/lp/registry/stories/webservice/xx-source-package.rst b/lib/lp/registry/stories/webservice/xx-source-package.rst
index d09785d..b5c17bc 100644
--- a/lib/lp/registry/stories/webservice/xx-source-package.rst
+++ b/lib/lp/registry/stories/webservice/xx-source-package.rst
@@ -32,6 +32,7 @@ distribution series.
32 >>> pprint_entry(evolution)32 >>> pprint_entry(evolution)
33 bug_reported_acknowledgement: None33 bug_reported_acknowledgement: None
34 bug_reporting_guidelines: None34 bug_reporting_guidelines: None
35 channel: None
35 displayname: 'evolution in My-distro My-series'36 displayname: 'evolution in My-distro My-series'
36 distribution_link: 'http://.../my-distro'37 distribution_link: 'http://.../my-distro'
37 distroseries_link: 'http://.../my-distro/my-series'38 distroseries_link: 'http://.../my-distro/my-series'
diff --git a/lib/lp/registry/tests/test_distributionsourcepackage.py b/lib/lp/registry/tests/test_distributionsourcepackage.py
index d8dffd9..6262cb4 100644
--- a/lib/lp/registry/tests/test_distributionsourcepackage.py
+++ b/lib/lp/registry/tests/test_distributionsourcepackage.py
@@ -49,7 +49,7 @@ class TestDistributionSourcePackage(TestCaseWithFactory):
49 registrant=self.factory.makePerson(),49 registrant=self.factory.makePerson(),
50 )50 )
51 naked_distribution = removeSecurityProxy(distribution)51 naked_distribution = removeSecurityProxy(distribution)
52 self.factory.makeSourcePackage(distroseries=distribution)52 self.factory.makeDistributionSourcePackage(distribution=distribution)
53 dsp = naked_distribution.getSourcePackage(name="pmount")53 dsp = naked_distribution.getSourcePackage(name="pmount")
54 self.assertEqual(None, dsp.summary)54 self.assertEqual(None, dsp.summary)
5555
diff --git a/lib/lp/registry/tests/test_distroseries.py b/lib/lp/registry/tests/test_distroseries.py
index ab3c6bf..168386f 100644
--- a/lib/lp/registry/tests/test_distroseries.py
+++ b/lib/lp/registry/tests/test_distroseries.py
@@ -449,6 +449,12 @@ class TestDistroSeries(TestCaseWithFactory):
449 ]449 ]
450 )450 )
451451
452 def test_getSourcePackage_channel(self):
453 distroseries = self.factory.makeDistroSeries()
454 spn = self.factory.makeSourcePackageName()
455 source_package = distroseries.getSourcePackage(spn, channel="stable")
456 self.assertEqual("stable", source_package.channel)
457
452458
453class TestDistroSeriesPackaging(TestCaseWithFactory):459class TestDistroSeriesPackaging(TestCaseWithFactory):
454460
diff --git a/lib/lp/registry/tests/test_sourcepackage.py b/lib/lp/registry/tests/test_sourcepackage.py
index b839a18..a74f23a 100644
--- a/lib/lp/registry/tests/test_sourcepackage.py
+++ b/lib/lp/registry/tests/test_sourcepackage.py
@@ -506,6 +506,66 @@ class TestSourcePackage(TestCaseWithFactory):
506 sourcepackage.personHasDriverRights(distroseries.owner)506 sourcepackage.personHasDriverRights(distroseries.owner)
507 )507 )
508508
509 def test_channel(self):
510 source_package = self.factory.makeSourcePackage(channel="stable")
511 self.assertEqual("stable", source_package.channel)
512
513 def test_hash(self):
514 spn = self.factory.makeSourcePackageName()
515 distroseries = self.factory.makeDistroSeries()
516 self.assertEqual(
517 hash(
518 self.factory.makeSourcePackage(
519 sourcepackagename=spn, distroseries=distroseries
520 )
521 ),
522 hash(
523 self.factory.makeSourcePackage(
524 sourcepackagename=spn, distroseries=distroseries
525 )
526 ),
527 )
528 self.assertNotEqual(
529 hash(
530 self.factory.makeSourcePackage(
531 sourcepackagename=spn,
532 distroseries=distroseries,
533 channel="stable",
534 )
535 ),
536 hash(
537 self.factory.makeSourcePackage(
538 sourcepackagename=spn,
539 distroseries=distroseries,
540 channel="beta",
541 )
542 ),
543 )
544
545 def test_eq(self):
546 spn = self.factory.makeSourcePackageName()
547 distroseries = self.factory.makeDistroSeries()
548 self.assertEqual(
549 self.factory.makeSourcePackage(
550 sourcepackagename=spn, distroseries=distroseries
551 ),
552 self.factory.makeSourcePackage(
553 sourcepackagename=spn, distroseries=distroseries
554 ),
555 )
556 self.assertNotEqual(
557 self.factory.makeSourcePackage(
558 sourcepackagename=spn,
559 distroseries=distroseries,
560 channel="stable",
561 ),
562 self.factory.makeSourcePackage(
563 sourcepackagename=spn,
564 distroseries=distroseries,
565 channel="beta",
566 ),
567 )
568
509569
510class TestSourcePackageWebService(WebServiceTestCase):570class TestSourcePackageWebService(WebServiceTestCase):
511 def test_setPackaging(self):571 def test_setPackaging(self):
diff --git a/lib/lp/soyuz/tests/test_binarypackagebuild.py b/lib/lp/soyuz/tests/test_binarypackagebuild.py
index ef513d3..939e6c1 100644
--- a/lib/lp/soyuz/tests/test_binarypackagebuild.py
+++ b/lib/lp/soyuz/tests/test_binarypackagebuild.py
@@ -433,13 +433,19 @@ class BaseTestCaseWithThreeBuilds(TestCaseWithFactory):
433 """Publish some builds for the test archive."""433 """Publish some builds for the test archive."""
434 super().setUp()434 super().setUp()
435 self.ds = self.factory.makeDistroSeries()435 self.ds = self.factory.makeDistroSeries()
436 self.builds = self.makeBuilds()
437 self.sources = [
438 build.current_source_publication for build in self.builds
439 ]
440
441 def makeBuilds(self):
436 i386_das = self.factory.makeDistroArchSeries(442 i386_das = self.factory.makeDistroArchSeries(
437 distroseries=self.ds, architecturetag="i386"443 distroseries=self.ds, architecturetag="i386"
438 )444 )
439 hppa_das = self.factory.makeDistroArchSeries(445 hppa_das = self.factory.makeDistroArchSeries(
440 distroseries=self.ds, architecturetag="hppa"446 distroseries=self.ds, architecturetag="hppa"
441 )447 )
442 self.builds = [448 return [
443 self.factory.makeBinaryPackageBuild(449 self.factory.makeBinaryPackageBuild(
444 archive=self.ds.main_archive, distroarchseries=i386_das450 archive=self.ds.main_archive, distroarchseries=i386_das
445 ),451 ),
@@ -449,12 +455,10 @@ class BaseTestCaseWithThreeBuilds(TestCaseWithFactory):
449 pocket=PackagePublishingPocket.PROPOSED,455 pocket=PackagePublishingPocket.PROPOSED,
450 ),456 ),
451 self.factory.makeBinaryPackageBuild(457 self.factory.makeBinaryPackageBuild(
452 archive=self.ds.main_archive, distroarchseries=hppa_das458 archive=self.ds.main_archive,
459 distroarchseries=hppa_das,
453 ),460 ),
454 ]461 ]
455 self.sources = [
456 build.current_source_publication for build in self.builds
457 ]
458462
459463
460class TestBuildSet(TestCaseWithFactory):464class TestBuildSet(TestCaseWithFactory):
diff --git a/lib/lp/soyuz/tests/test_hasbuildrecords.py b/lib/lp/soyuz/tests/test_hasbuildrecords.py
index b0b8e8c..07c5c26 100644
--- a/lib/lp/soyuz/tests/test_hasbuildrecords.py
+++ b/lib/lp/soyuz/tests/test_hasbuildrecords.py
@@ -2,7 +2,7 @@
2# GNU Affero General Public License version 3 (see the file LICENSE).2# GNU Affero General Public License version 3 (see the file LICENSE).
33
4"""Test implementations of the IHasBuildRecords interface."""4"""Test implementations of the IHasBuildRecords interface."""
55from testscenarios import WithScenarios
6from zope.component import getUtility6from zope.component import getUtility
7from zope.security.proxy import removeSecurityProxy7from zope.security.proxy import removeSecurityProxy
88
@@ -14,12 +14,11 @@ from lp.buildmaster.interfaces.buildfarmjob import (
14)14)
15from lp.registry.interfaces.person import IPersonSet15from lp.registry.interfaces.person import IPersonSet
16from lp.registry.interfaces.pocket import PackagePublishingPocket16from lp.registry.interfaces.pocket import PackagePublishingPocket
17from lp.registry.interfaces.sourcepackage import SourcePackageType
17from lp.registry.model.sourcepackage import SourcePackage18from lp.registry.model.sourcepackage import SourcePackage
18from lp.services.database.interfaces import IStore
19from lp.soyuz.enums import ArchivePurpose19from lp.soyuz.enums import ArchivePurpose
20from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild20from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
21from lp.soyuz.interfaces.buildrecords import IHasBuildRecords21from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
22from lp.soyuz.model.publishing import SourcePackagePublishingHistory
23from lp.soyuz.tests.test_binarypackagebuild import BaseTestCaseWithThreeBuilds22from lp.soyuz.tests.test_binarypackagebuild import BaseTestCaseWithThreeBuilds
24from lp.soyuz.tests.test_publishing import SoyuzTestPublisher23from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
25from lp.testing import TestCaseWithFactory, person_logged_in24from lp.testing import TestCaseWithFactory, person_logged_in
@@ -256,30 +255,64 @@ class TestBuilderHasBuildRecords(TestHasBuildRecordsInterface):
256 )255 )
257256
258257
259class TestSourcePackageHasBuildRecords(TestHasBuildRecordsInterface):258class TestSourcePackageHasBuildRecords(
259 WithScenarios, TestHasBuildRecordsInterface
260):
260 """Test the SourcePackage implementation of IHasBuildRecords."""261 """Test the SourcePackage implementation of IHasBuildRecords."""
261262
263 scenarios = [
264 (
265 "channel",
266 {"channel": "stable", "format": SourcePackageType.CI_BUILD},
267 ),
268 ("no_channel", {"channel": None, "format": None}),
269 ]
270
262 def setUp(self):271 def setUp(self):
263 super().setUp()272 super().setUp()
264 gedit_name = self.builds[0].source_package_release.sourcepackagename273 gedit_name = self.builds[0].source_package_release.sourcepackagename
265 self.context = SourcePackage(274 self.context = SourcePackage(
266 gedit_name, self.builds[0].distro_arch_series.distroseries275 gedit_name,
276 self.builds[0].distro_arch_series.distroseries,
277 channel=self.channel,
267 )278 )
268279
269 # Convert the other two builds to be builds of280 def makeBuilds(self):
270 # gedit as well so that the one source package (gedit) will have281 i386_das = self.factory.makeDistroArchSeries(
271 # three builds.282 distroseries=self.ds, architecturetag="i386"
272 for build in self.builds[1:3]:283 )
273 spr = build.source_package_release284 hppa_das = self.factory.makeDistroArchSeries(
274 removeSecurityProxy(spr).sourcepackagename = gedit_name285 distroseries=self.ds, architecturetag="hppa"
275 IStore(SourcePackagePublishingHistory).find(286 )
276 SourcePackagePublishingHistory, sourcepackagerelease=spr287 spn = self.factory.makeSourcePackageName()
277 ).set(sourcepackagenameID=gedit_name.id)288 builds = [
278289 self.factory.makeBinaryPackageBuild(
290 sourcepackagename=spn,
291 archive=self.ds.main_archive,
292 distroarchseries=i386_das,
293 channel=self.channel,
294 format=self.format,
295 ),
296 self.factory.makeBinaryPackageBuild(
297 sourcepackagename=spn,
298 archive=self.ds.main_archive,
299 distroarchseries=i386_das,
300 channel=self.channel,
301 format=self.format,
302 ),
303 self.factory.makeBinaryPackageBuild(
304 sourcepackagename=spn,
305 archive=self.ds.main_archive,
306 distroarchseries=hppa_das,
307 channel=self.channel,
308 format=self.format,
309 ),
310 ]
279 # Set them as successfully built311 # Set them as successfully built
280 for build in self.builds:312 for build in builds:
281 build.updateStatus(BuildStatus.BUILDING)313 build.updateStatus(BuildStatus.BUILDING)
282 build.updateStatus(BuildStatus.FULLYBUILT)314 build.updateStatus(BuildStatus.FULLYBUILT)
315 return builds
283316
284 def test_get_build_records(self):317 def test_get_build_records(self):
285 # We can fetch builds records from a SourcePackage.318 # We can fetch builds records from a SourcePackage.
@@ -290,7 +323,7 @@ class TestSourcePackageHasBuildRecords(TestHasBuildRecordsInterface):
290 builds = self.context.getBuildRecords(323 builds = self.context.getBuildRecords(
291 pocket=PackagePublishingPocket.RELEASE324 pocket=PackagePublishingPocket.RELEASE
292 ).count()325 ).count()
293 self.assertEqual(2, builds)326 self.assertEqual(3, builds)
294 builds = self.context.getBuildRecords(327 builds = self.context.getBuildRecords(
295 pocket=PackagePublishingPocket.UPDATES328 pocket=PackagePublishingPocket.UPDATES
296 ).count()329 ).count()
diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
index 40a959c..b83a946 100644
--- a/lib/lp/testing/factory.py
+++ b/lib/lp/testing/factory.py
@@ -31,6 +31,7 @@ from functools import wraps
31from io import BytesIO31from io import BytesIO
32from itertools import count32from itertools import count
33from textwrap import dedent33from textwrap import dedent
34from typing import Optional
3435
35import pytz36import pytz
36import six37import six
@@ -2385,6 +2386,7 @@ class LaunchpadObjectFactory(ObjectFactory):
2385 self.makeSourcePackagePublishingHistory(2386 self.makeSourcePackagePublishingHistory(
2386 distroseries=target.distroseries,2387 distroseries=target.distroseries,
2387 sourcepackagename=target.sourcepackagename,2388 sourcepackagename=target.sourcepackagename,
2389 channel=target.channel,
2388 )2390 )
2389 if IDistributionSourcePackage.providedBy(target):2391 if IDistributionSourcePackage.providedBy(target):
2390 if publish:2392 if publish:
@@ -4575,6 +4577,7 @@ class LaunchpadObjectFactory(ObjectFactory):
4575 distroseries=None,4577 distroseries=None,
4576 publish=False,4578 publish=False,
4577 owner=None,4579 owner=None,
4580 channel: Optional[str] = None,
4578 ):4581 ):
4579 """Make an `ISourcePackage`.4582 """Make an `ISourcePackage`.
45804583
@@ -4590,7 +4593,9 @@ class LaunchpadObjectFactory(ObjectFactory):
4590 distroseries = self.makeDistroSeries(owner=owner)4593 distroseries = self.makeDistroSeries(owner=owner)
4591 if publish:4594 if publish:
4592 self.makeSourcePackagePublishingHistory(4595 self.makeSourcePackagePublishingHistory(
4593 distroseries=distroseries, sourcepackagename=sourcepackagename4596 distroseries=distroseries,
4597 sourcepackagename=sourcepackagename,
4598 channel=channel,
4594 )4599 )
4595 with dbuser("statistician"):4600 with dbuser("statistician"):
4596 DistributionSourcePackageCache(4601 DistributionSourcePackageCache(
@@ -4599,7 +4604,9 @@ class LaunchpadObjectFactory(ObjectFactory):
4599 archive=distroseries.main_archive,4604 archive=distroseries.main_archive,
4600 name=sourcepackagename.name,4605 name=sourcepackagename.name,
4601 )4606 )
4602 return distroseries.getSourcePackage(sourcepackagename)4607 return distroseries.getSourcePackage(
4608 sourcepackagename, channel=channel
4609 )
46034610
4604 def getAnySourcePackageUrgency(self):4611 def getAnySourcePackageUrgency(self):
4605 return SourcePackageUrgency.MEDIUM4612 return SourcePackageUrgency.MEDIUM
@@ -4892,6 +4899,8 @@ class LaunchpadObjectFactory(ObjectFactory):
4892 processor=None,4899 processor=None,
4893 sourcepackagename=None,4900 sourcepackagename=None,
4894 arch_indep=None,4901 arch_indep=None,
4902 channel=None,
4903 format=None,
4895 ):4904 ):
4896 """Create a BinaryPackageBuild.4905 """Create a BinaryPackageBuild.
48974906
@@ -4950,12 +4959,14 @@ class LaunchpadObjectFactory(ObjectFactory):
4950 component=multiverse,4959 component=multiverse,
4951 distroseries=distroarchseries.distroseries,4960 distroseries=distroarchseries.distroseries,
4952 sourcepackagename=sourcepackagename,4961 sourcepackagename=sourcepackagename,
4962 format=format,
4953 )4963 )
4954 self.makeSourcePackagePublishingHistory(4964 self.makeSourcePackagePublishingHistory(
4955 distroseries=distroarchseries.distroseries,4965 distroseries=distroarchseries.distroseries,
4956 archive=archive,4966 archive=archive,
4957 sourcepackagerelease=source_package_release,4967 sourcepackagerelease=source_package_release,
4958 pocket=pocket,4968 pocket=pocket,
4969 channel=channel,
4959 )4970 )
4960 if status is None:4971 if status is None:
4961 status = BuildStatus.NEEDSBUILD4972 status = BuildStatus.NEEDSBUILD

Subscribers

People subscribed via source and target branches

to status/vote changes: