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
1diff --git a/lib/lp/bugs/configure.zcml b/lib/lp/bugs/configure.zcml
2index 6eaa58c..12bfdd7 100644
3--- a/lib/lp/bugs/configure.zcml
4+++ b/lib/lp/bugs/configure.zcml
5@@ -211,6 +211,8 @@
6 distribution
7 distroseries
8 milestone
9+ _channel
10+ channel
11 _status
12 status
13 status_explanation
14diff --git a/lib/lp/bugs/interfaces/bugsummary.py b/lib/lp/bugs/interfaces/bugsummary.py
15index 7cb55ef..be83775 100644
16--- a/lib/lp/bugs/interfaces/bugsummary.py
17+++ b/lib/lp/bugs/interfaces/bugsummary.py
18@@ -9,7 +9,7 @@ __all__ = [
19 ]
20
21 from zope.interface import Interface
22-from zope.schema import Bool, Choice, Int, Object, Text
23+from zope.schema import Bool, Choice, Int, Object, Text, TextLine
24
25 from lp import _
26 from lp.bugs.interfaces.bugtask import BugTaskImportance, BugTaskStatusSearch
27@@ -51,6 +51,8 @@ class IBugSummary(Interface):
28 ociproject_id = Int(readonly=True)
29 ociproject = Object(IOCIProject, readonly=True)
30
31+ channel = TextLine(readonly=True)
32+
33 milestone_id = Int(readonly=True)
34 milestone = Object(IMilestone, readonly=True)
35
36diff --git a/lib/lp/bugs/interfaces/bugtask.py b/lib/lp/bugs/interfaces/bugtask.py
37index a8c4e45..d2586f7 100644
38--- a/lib/lp/bugs/interfaces/bugtask.py
39+++ b/lib/lp/bugs/interfaces/bugtask.py
40@@ -475,6 +475,7 @@ class IBugTask(IHasBug, IBugTaskDelete):
41 title=_("Series"), required=False, vocabulary="DistroSeries"
42 )
43 distroseries_id = Attribute("The distroseries ID")
44+ channel = TextLine(title=_("Channel"), required=False)
45 milestone = exported(
46 ReferenceChoice(
47 title=_("Milestone"),
48diff --git a/lib/lp/bugs/model/bugsummary.py b/lib/lp/bugs/model/bugsummary.py
49index 9084e0e..2caabf6 100644
50--- a/lib/lp/bugs/model/bugsummary.py
51+++ b/lib/lp/bugs/model/bugsummary.py
52@@ -9,6 +9,8 @@ __all__ = [
53 "get_bugsummary_filter_for_user",
54 ]
55
56+from typing import Optional
57+
58 from storm.base import Storm
59 from storm.expr import SQL, And, Or, Select
60 from storm.properties import Bool, Int, Unicode
61@@ -32,9 +34,10 @@ from lp.registry.model.product import Product
62 from lp.registry.model.productseries import ProductSeries
63 from lp.registry.model.sourcepackagename import SourcePackageName
64 from lp.registry.model.teammembership import TeamParticipation
65+from lp.services.channels import channel_list_to_string
66 from lp.services.database.enumcol import DBEnum
67 from lp.services.database.interfaces import IStore
68-from lp.services.database.stormexpr import WithMaterialized
69+from lp.services.database.stormexpr import ImmutablePgJSON, WithMaterialized
70
71
72 @implementer(IBugSummary)
73@@ -64,6 +67,8 @@ class BugSummary(Storm):
74 ociproject_id = Int(name="ociproject")
75 ociproject = Reference(ociproject_id, "OCIProject.id")
76
77+ _channel = ImmutablePgJSON(name="channel")
78+
79 milestone_id = Int(name="milestone")
80 milestone = Reference(milestone_id, Milestone.id)
81
82@@ -80,6 +85,12 @@ class BugSummary(Storm):
83
84 has_patch = Bool()
85
86+ @property
87+ def channel(self) -> Optional[str]:
88+ if self._channel is None:
89+ return None
90+ return channel_list_to_string(*self._channel)
91+
92
93 @implementer(IBugSummaryDimension)
94 class CombineBugSummaryConstraint:
95diff --git a/lib/lp/bugs/model/bugtask.py b/lib/lp/bugs/model/bugtask.py
96index c006618..8955720 100644
97--- a/lib/lp/bugs/model/bugtask.py
98+++ b/lib/lp/bugs/model/bugtask.py
99@@ -21,6 +21,7 @@ import re
100 from collections import defaultdict
101 from itertools import chain, repeat
102 from operator import attrgetter, itemgetter
103+from typing import Optional
104
105 import pytz
106 from lazr.lifecycle.event import ObjectDeletedEvent
107@@ -98,6 +99,7 @@ from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
108 from lp.registry.model.pillar import pillar_sort_key
109 from lp.registry.model.sourcepackagename import SourcePackageName
110 from lp.services import features
111+from lp.services.channels import channel_list_to_string, channel_string_to_list
112 from lp.services.database.bulk import create, load, load_related
113 from lp.services.database.constants import UTC_NOW
114 from lp.services.database.decoratedresultset import DecoratedResultSet
115@@ -111,6 +113,7 @@ from lp.services.database.sqlbase import (
116 sqlvalues,
117 )
118 from lp.services.database.stormbase import StormBase
119+from lp.services.database.stormexpr import ImmutablePgJSON
120 from lp.services.helpers import shortlist
121 from lp.services.propertycache import get_property_cache
122 from lp.services.searchbuilder import any
123@@ -171,6 +174,7 @@ def bug_target_from_key(
124 distroseries,
125 sourcepackagename,
126 ociproject,
127+ channel,
128 ):
129 """Returns the IBugTarget defined by the given DB column values."""
130 if ociproject:
131@@ -189,7 +193,7 @@ def bug_target_from_key(
132 return distribution
133 elif distroseries:
134 if sourcepackagename:
135- return distroseries.getSourcePackage(sourcepackagename)
136+ return distroseries.getSourcePackage(sourcepackagename, channel)
137 else:
138 return distroseries
139 else:
140@@ -205,6 +209,7 @@ def bug_target_to_key(target):
141 distroseries=None,
142 sourcepackagename=None,
143 ociproject=None,
144+ channel=None,
145 )
146 if IProduct.providedBy(target):
147 values["product"] = target
148@@ -220,6 +225,7 @@ def bug_target_to_key(target):
149 elif ISourcePackage.providedBy(target):
150 values["distroseries"] = target.distroseries
151 values["sourcepackagename"] = target.sourcepackagename
152+ values["channel"] = target.channel
153 elif IOCIProject.providedBy(target):
154 # De-normalize the ociproject, including also the ociproject's
155 # pillar (distribution or product).
156@@ -499,6 +505,8 @@ class BugTask(StormBase):
157 distroseries_id = Int(name="distroseries", allow_none=True)
158 distroseries = Reference(distroseries_id, "DistroSeries.id")
159
160+ _channel = ImmutablePgJSON(name="channel", allow_none=True)
161+
162 milestone_id = Int(
163 name="milestone",
164 allow_none=True,
165@@ -613,6 +621,19 @@ class BugTask(StormBase):
166 )
167
168 @property
169+ def channel(self) -> Optional[str]:
170+ if self._channel is None:
171+ return None
172+ return channel_list_to_string(*self._channel)
173+
174+ @channel.setter
175+ def channel(self, value: str) -> None:
176+ if value is None:
177+ self._channel = None
178+ else:
179+ self._channel = channel_string_to_list(value)
180+
181+ @property
182 def status(self):
183 if self._status in DB_INCOMPLETE_BUGTASK_STATUSES:
184 return BugTaskStatus.INCOMPLETE
185@@ -652,6 +673,7 @@ class BugTask(StormBase):
186 self.distroseries,
187 self.sourcepackagename,
188 self.ociproject,
189+ self.channel,
190 )
191
192 @property
193@@ -1863,6 +1885,9 @@ class BugTaskSet:
194 key["distroseries"],
195 key["sourcepackagename"],
196 key["ociproject"],
197+ channel_string_to_list(key["channel"])
198+ if key["channel"]
199+ else None,
200 status,
201 importance,
202 assignee,
203@@ -1880,6 +1905,7 @@ class BugTaskSet:
204 BugTask.distroseries,
205 BugTask.sourcepackagename,
206 BugTask.ociproject,
207+ BugTask._channel,
208 BugTask._status,
209 BugTask.importance,
210 BugTask.assignee,
211diff --git a/lib/lp/bugs/model/bugtaskflat.py b/lib/lp/bugs/model/bugtaskflat.py
212index b25a440..b636ecf 100644
213--- a/lib/lp/bugs/model/bugtaskflat.py
214+++ b/lib/lp/bugs/model/bugtaskflat.py
215@@ -1,6 +1,5 @@
216 # Copyright 2012-2020 Canonical Ltd. This software is licensed under the
217 # GNU Affero General Public License version 3 (see the file LICENSE).
218-
219 from storm.locals import Bool, DateTime, Int, List, Reference, Storm
220
221 from lp.app.enums import InformationType
222@@ -10,6 +9,7 @@ from lp.bugs.interfaces.bugtask import (
223 BugTaskStatusSearch,
224 )
225 from lp.services.database.enumcol import DBEnum
226+from lp.services.database.stormexpr import ImmutablePgJSON
227
228
229 class BugTaskFlat(Storm):
230@@ -42,6 +42,7 @@ class BugTaskFlat(Storm):
231 sourcepackagename = Reference(sourcepackagename_id, "SourcePackageName.id")
232 ociproject_id = Int(name="ociproject")
233 ociproject = Reference(ociproject_id, "OCIProject.id")
234+ channel = ImmutablePgJSON()
235 status = DBEnum(enum=(BugTaskStatus, BugTaskStatusSearch))
236 importance = DBEnum(enum=BugTaskImportance)
237 assignee_id = Int(name="assignee")
238diff --git a/lib/lp/bugs/model/tests/test_bugtask.py b/lib/lp/bugs/model/tests/test_bugtask.py
239index c3eb248..ecb258f 100644
240--- a/lib/lp/bugs/model/tests/test_bugtask.py
241+++ b/lib/lp/bugs/model/tests/test_bugtask.py
242@@ -3145,6 +3145,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
243 distroseries=None,
244 sourcepackagename=None,
245 ociproject=None,
246+ channel=None,
247 ),
248 )
249
250@@ -3159,6 +3160,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
251 distroseries=None,
252 sourcepackagename=None,
253 ociproject=None,
254+ channel=None,
255 ),
256 )
257
258@@ -3173,6 +3175,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
259 distroseries=None,
260 sourcepackagename=None,
261 ociproject=None,
262+ channel=None,
263 ),
264 )
265
266@@ -3187,6 +3190,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
267 distroseries=distroseries,
268 sourcepackagename=None,
269 ociproject=None,
270+ channel=None,
271 ),
272 )
273
274@@ -3201,6 +3205,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
275 distroseries=None,
276 sourcepackagename=dsp.sourcepackagename,
277 ociproject=None,
278+ channel=None,
279 ),
280 )
281
282@@ -3215,6 +3220,22 @@ class TestBugTargetKeys(TestCaseWithFactory):
283 distroseries=sp.distroseries,
284 sourcepackagename=sp.sourcepackagename,
285 ociproject=None,
286+ channel=None,
287+ ),
288+ )
289+
290+ def test_sourcepackage_with_channel(self):
291+ sp = self.factory.makeSourcePackage(channel="stable")
292+ self.assertTargetKeyWorks(
293+ sp,
294+ dict(
295+ product=None,
296+ productseries=None,
297+ distribution=None,
298+ distroseries=sp.distroseries,
299+ sourcepackagename=sp.sourcepackagename,
300+ ociproject=None,
301+ channel="stable",
302 ),
303 )
304
305@@ -3230,6 +3251,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
306 distroseries=None,
307 sourcepackagename=None,
308 ociproject=ociproject,
309+ channel=None,
310 ),
311 )
312
313@@ -3245,6 +3267,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
314 distroseries=None,
315 sourcepackagename=None,
316 ociproject=ociproject,
317+ channel=None,
318 ),
319 )
320
321@@ -3263,6 +3286,7 @@ class TestBugTargetKeys(TestCaseWithFactory):
322 None,
323 None,
324 None,
325+ None,
326 )
327
328
329diff --git a/lib/lp/bugs/scripts/bugsummaryrebuild.py b/lib/lp/bugs/scripts/bugsummaryrebuild.py
330index 21003b4..cf9ae8a 100644
331--- a/lib/lp/bugs/scripts/bugsummaryrebuild.py
332+++ b/lib/lp/bugs/scripts/bugsummaryrebuild.py
333@@ -24,6 +24,7 @@ from lp.registry.model.ociproject import OCIProject
334 from lp.registry.model.product import Product
335 from lp.registry.model.productseries import ProductSeries
336 from lp.registry.model.sourcepackagename import SourcePackageName
337+from lp.services.channels import channel_string_to_list
338 from lp.services.database.bulk import create
339 from lp.services.database.interfaces import IStore
340 from lp.services.database.stormexpr import Unnest
341@@ -109,7 +110,7 @@ def load_target(pid, psid, did, dsid, spnid, ociproject_id):
342 (pid, psid, did, dsid, spnid, ociproject_id),
343 ),
344 )
345- return bug_target_from_key(p, ps, d, ds, spn, ociproject)
346+ return bug_target_from_key(p, ps, d, ds, spn, ociproject, None)
347
348
349 def format_target(target):
350@@ -130,7 +131,15 @@ def format_target(target):
351 def _get_bugsummary_constraint_bits(target):
352 raw_key = bug_target_to_key(target)
353 # Map to ID columns to work around Storm bug #682989.
354- return {"%s_id" % k: v.id if v else None for (k, v) in raw_key.items()}
355+ constraint_bits = {}
356+ for name, value in raw_key.items():
357+ if name == "channel":
358+ constraint_bits["_channel"] = (
359+ channel_string_to_list(value) if value else None
360+ )
361+ else:
362+ constraint_bits["{}_id".format(name)] = value.id if value else None
363+ return constraint_bits
364
365
366 def get_bugsummary_constraint(target, cls=RawBugSummary):
367@@ -154,10 +163,19 @@ def get_bugtaskflat_constraint(target):
368 if IProduct.providedBy(target):
369 del raw_key["ociproject"]
370 # Map to ID columns to work around Storm bug #682989.
371- return [
372- getattr(BugTaskFlat, "%s_id" % k) == (v.id if v else None)
373- for (k, v) in raw_key.items()
374- ]
375+ constraint_bits = []
376+ for name, value in raw_key.items():
377+ if name == "channel":
378+ constraint_bits.append(
379+ getattr(BugTaskFlat, name)
380+ == (channel_string_to_list(value) if value else None)
381+ )
382+ else:
383+ constraint_bits.append(
384+ getattr(BugTaskFlat, "{}_id".format(name))
385+ == (value.id if value else None)
386+ )
387+ return constraint_bits
388
389
390 def get_bugsummary_rows(target):
391@@ -167,6 +185,7 @@ def get_bugsummary_rows(target):
392 with BugSummary which is actually combinedbugsummary, a view over
393 bugsummary and bugsummaryjournal.
394 """
395+ constraint = get_bugsummary_constraint(target)
396 return IStore(RawBugSummary).find(
397 (
398 RawBugSummary.status,
399@@ -178,7 +197,7 @@ def get_bugsummary_rows(target):
400 RawBugSummary.access_policy_id,
401 RawBugSummary.count,
402 ),
403- *get_bugsummary_constraint(target),
404+ *constraint,
405 )
406
407
408@@ -191,7 +210,7 @@ def get_bugsummaryjournal_rows(target):
409
410
411 def calculate_bugsummary_changes(old, new):
412- """Calculate the changes between between the new and old dicts.
413+ """Calculate the changes between the new and old dicts.
414
415 Takes {key: int} dicts, returns items from the new dict that differ
416 from the old one.
417@@ -219,18 +238,14 @@ def calculate_bugsummary_changes(old, new):
418 def apply_bugsummary_changes(target, added, updated, removed):
419 """Apply a set of BugSummary changes to the DB."""
420 bits = _get_bugsummary_constraint_bits(target)
421- target_key = tuple(
422- map(
423- bits.get,
424- (
425- "product_id",
426- "productseries_id",
427- "distribution_id",
428- "distroseries_id",
429- "sourcepackagename_id",
430- "ociproject_id",
431- ),
432- )
433+ target_key = (
434+ bits["product_id"],
435+ bits["productseries_id"],
436+ bits["distribution_id"],
437+ bits["distroseries_id"],
438+ bits["sourcepackagename_id"],
439+ bits["ociproject_id"],
440+ bits["_channel"],
441 )
442 target_cols = (
443 RawBugSummary.product_id,
444@@ -239,6 +254,7 @@ def apply_bugsummary_changes(target, added, updated, removed):
445 RawBugSummary.distroseries_id,
446 RawBugSummary.sourcepackagename_id,
447 RawBugSummary.ociproject_id,
448+ RawBugSummary._channel,
449 )
450 key_cols = (
451 RawBugSummary.status,
452diff --git a/lib/lp/bugs/scripts/bugtasktargetnamecaches.py b/lib/lp/bugs/scripts/bugtasktargetnamecaches.py
453index 70a82d4..dcbf424 100644
454--- a/lib/lp/bugs/scripts/bugtasktargetnamecaches.py
455+++ b/lib/lp/bugs/scripts/bugtasktargetnamecaches.py
456@@ -103,10 +103,10 @@ class BugTaskTargetNameCachesTunableLoop:
457 self.offset += 1
458 # Resolve the IDs to objects, and get the actual IBugTarget.
459 # If the ID is None, don't even try to get an object.
460- target_objects = (
461+ target_objects = [
462 (store.get(cls, id) if id is not None else None)
463 for cls, id in zip(target_classes, target_bits)
464- )
465+ ] + [None]
466 target = bug_target_from_key(*target_objects)
467 new_name = target.bugtargetdisplayname
468 cached_names.discard(new_name)
469diff --git a/lib/lp/registry/interfaces/distroseries.py b/lib/lp/registry/interfaces/distroseries.py
470index 03e6d68..5a718c2 100644
471--- a/lib/lp/registry/interfaces/distroseries.py
472+++ b/lib/lp/registry/interfaces/distroseries.py
473@@ -667,7 +667,7 @@ class IDistroSeriesPublic(
474 @operation_returns_entry(ISourcePackage)
475 @export_read_operation()
476 @operation_for_version("beta")
477- def getSourcePackage(name):
478+ def getSourcePackage(name, channel=None):
479 """Return a source package in this distro series by name.
480
481 The name given may be a string or an ISourcePackageName-providing
482diff --git a/lib/lp/registry/interfaces/sourcepackage.py b/lib/lp/registry/interfaces/sourcepackage.py
483index d8f776e..a4505c0 100644
484--- a/lib/lp/registry/interfaces/sourcepackage.py
485+++ b/lib/lp/registry/interfaces/sourcepackage.py
486@@ -130,6 +130,15 @@ class ISourcePackagePublic(
487
488 sourcepackagename = Attribute("SourcePackageName")
489
490+ channel = exported(
491+ TextLine(
492+ title=_("Channel"),
493+ required=False,
494+ readonly=True,
495+ description=_("The channel for this source package."),
496+ ),
497+ )
498+
499 # This is really a reference to an IProductSeries.
500 productseries = exported(
501 ReferenceChoice(
502@@ -362,11 +371,12 @@ class ISourcePackage(ISourcePackagePublic, ISourcePackageEdit):
503 class ISourcePackageFactory(Interface):
504 """A creator of source packages."""
505
506- def new(sourcepackagename, distroseries):
507+ def new(sourcepackagename, distroseries, channel=None):
508 """Create a new `ISourcePackage`.
509
510 :param sourcepackagename: An `ISourcePackageName`.
511 :param distroseries: An `IDistroSeries`.
512+ :param channel: A channel name or None.
513 :return: `ISourcePackage`.
514 """
515
516diff --git a/lib/lp/registry/model/distroseries.py b/lib/lp/registry/model/distroseries.py
517index 7844249..a128ed5 100644
518--- a/lib/lp/registry/model/distroseries.py
519+++ b/lib/lp/registry/model/distroseries.py
520@@ -13,6 +13,7 @@ __all__ = [
521 import collections
522 from io import BytesIO
523 from operator import itemgetter
524+from typing import Optional
525
526 import apt_pkg
527 from lazr.delegates import delegate_to
528@@ -1036,7 +1037,7 @@ class DistroSeries(
529 self.messagecount = messagecount
530 ztm.commit()
531
532- def getSourcePackage(self, name):
533+ def getSourcePackage(self, name, channel: Optional[str] = None):
534 """See `IDistroSeries`."""
535 if not ISourcePackageName.providedBy(name):
536 try:
537@@ -1044,7 +1045,7 @@ class DistroSeries(
538 except SQLObjectNotFound:
539 return None
540 return getUtility(ISourcePackageFactory).new(
541- sourcepackagename=name, distroseries=self
542+ sourcepackagename=name, distroseries=self, channel=channel
543 )
544
545 def getBinaryPackage(self, name):
546diff --git a/lib/lp/registry/model/sourcepackage.py b/lib/lp/registry/model/sourcepackage.py
547index 8a0280b..5aff937 100644
548--- a/lib/lp/registry/model/sourcepackage.py
549+++ b/lib/lp/registry/model/sourcepackage.py
550@@ -8,7 +8,9 @@ __all__ = [
551 "SourcePackageQuestionTargetMixin",
552 ]
553
554+import json
555 from operator import attrgetter, itemgetter
556+from typing import Optional
557
558 from storm.locals import And, Desc, Join, Store
559 from zope.component import getUtility
560@@ -42,6 +44,7 @@ from lp.registry.interfaces.sourcepackage import (
561 from lp.registry.model.hasdrivers import HasDriversMixin
562 from lp.registry.model.packaging import Packaging
563 from lp.registry.model.suitesourcepackage import SuiteSourcePackage
564+from lp.services.channels import channel_string_to_list
565 from lp.services.database.decoratedresultset import DecoratedResultSet
566 from lp.services.database.interfaces import IStore
567 from lp.services.database.sqlbase import flush_database_updates, sqlvalues
568@@ -204,7 +207,9 @@ class SourcePackage(
569 to the relevant database objects.
570 """
571
572- def __init__(self, sourcepackagename, distroseries):
573+ def __init__(
574+ self, sourcepackagename, distroseries, channel: Optional[str] = None
575+ ):
576 # We store the ID of the sourcepackagename and distroseries
577 # simply because Storm can break when accessing them
578 # with implicit flush is blocked (like in a permission check when
579@@ -213,11 +218,14 @@ class SourcePackage(
580 self.sourcepackagename = sourcepackagename
581 self.distroseries = distroseries
582 self.distroseriesID = distroseries.id
583+ self.channel = channel
584
585 @classmethod
586- def new(cls, sourcepackagename, distroseries):
587+ def new(
588+ cls, sourcepackagename, distroseries, channel: Optional[str] = None
589+ ):
590 """See `ISourcePackageFactory`."""
591- return cls(sourcepackagename, distroseries)
592+ return cls(sourcepackagename, distroseries, channel=channel)
593
594 def __repr__(self):
595 return "<%s %r %r %r>" % (
596@@ -242,6 +250,12 @@ class SourcePackage(
597 == self.sourcepackagename,
598 SourcePackagePublishingHistory.distroseries
599 == self.distroseries,
600+ SourcePackagePublishingHistory._channel
601+ == (
602+ None
603+ if self.channel is None
604+ else channel_string_to_list(self.channel)
605+ ),
606 SourcePackagePublishingHistory.archiveID.is_in(
607 self.distribution.all_distro_archive_ids
608 ),
609@@ -296,24 +310,27 @@ class SourcePackage(
610 )
611
612 @property
613+ def series_name(self):
614+ series_name = self.distroseries.fullseriesname
615+ if self.channel is not None:
616+ series_name = "%s, %s" % (series_name, self.channel)
617+ return series_name
618+
619+ @property
620 def display_name(self):
621- return "%s in %s %s" % (
622- self.sourcepackagename.name,
623- self.distribution.displayname,
624- self.distroseries.displayname,
625- )
626+ return "%s in %s" % (self.sourcepackagename.name, self.series_name)
627
628 displayname = display_name
629
630 @property
631 def bugtargetdisplayname(self):
632 """See IBugTarget."""
633- return "%s (%s)" % (self.name, self.distroseries.fullseriesname)
634+ return "%s (%s)" % (self.name, self.series_name)
635
636 @property
637 def bugtargetname(self):
638 """See `IBugTarget`."""
639- return "%s (%s)" % (self.name, self.distroseries.fullseriesname)
640+ return "%s (%s)" % (self.name, self.series_name)
641
642 @property
643 def bugtarget_parent(self):
644@@ -551,6 +568,12 @@ class SourcePackage(
645 return And(
646 BugSummary.distroseries == self.distroseries,
647 BugSummary.sourcepackagename == self.sourcepackagename,
648+ BugSummary._channel
649+ == (
650+ None
651+ if self.channel is None
652+ else channel_string_to_list(self.channel)
653+ ),
654 )
655
656 def setPackaging(self, productseries, owner):
657@@ -616,7 +639,11 @@ class SourcePackage(
658
659 def __hash__(self):
660 """See `ISourcePackage`."""
661- return hash(self.distroseriesID) ^ hash(self.sourcepackagenameID)
662+ return (
663+ hash(self.distroseriesID)
664+ ^ hash(self.sourcepackagenameID)
665+ ^ hash(self.channel)
666+ )
667
668 def __eq__(self, other):
669 """See `ISourcePackage`."""
670@@ -624,6 +651,7 @@ class SourcePackage(
671 (ISourcePackage.providedBy(other))
672 and (self.distroseries.id == other.distroseries.id)
673 and (self.sourcepackagename.id == other.sourcepackagename.id)
674+ and (self.channel == other.channel)
675 )
676
677 def __ne__(self, other):
678@@ -672,6 +700,16 @@ class SourcePackage(
679 )
680 ]
681
682+ if self.channel is None:
683+ condition_clauses.append(
684+ "SourcePackagePublishingHistory.channel IS NULL"
685+ )
686+ else:
687+ condition_clauses.append(
688+ "SourcePackagePublishingHistory.channel = '%s'::jsonb"
689+ % json.dumps(channel_string_to_list(self.channel))
690+ )
691+
692 # We re-use the optional-parameter handling provided by BuildSet
693 # here, but pass None for the name argument as we've already
694 # matched on exact source package name.
695@@ -882,16 +920,26 @@ class SourcePackage(
696
697 def weight_function(bugtask):
698 if bugtask.sourcepackagename_id == sourcepackagenameID:
699- if bugtask.distroseries_id == seriesID:
700+ if (
701+ bugtask.distroseries_id == seriesID
702+ and bugtask.channel == self.channel
703+ ):
704 return OrderedBugTask(1, bugtask.id, bugtask)
705- elif bugtask.distribution_id == distributionID:
706+ elif bugtask.distroseries_id == seriesID:
707 return OrderedBugTask(2, bugtask.id, bugtask)
708+ elif bugtask.distribution_id == distributionID:
709+ return OrderedBugTask(3, bugtask.id, bugtask)
710+ elif (
711+ bugtask.distroseries_id == seriesID
712+ and bugtask.channel == self.channel
713+ ):
714+ return OrderedBugTask(4, bugtask.id, bugtask)
715 elif bugtask.distroseries_id == seriesID:
716- return OrderedBugTask(3, bugtask.id, bugtask)
717+ return OrderedBugTask(5, bugtask.id, bugtask)
718 elif bugtask.distribution_id == distributionID:
719- return OrderedBugTask(4, bugtask.id, bugtask)
720+ return OrderedBugTask(6, bugtask.id, bugtask)
721 # Catch the default case, and where there is a task for the same
722 # sourcepackage on a different distro.
723- return OrderedBugTask(5, bugtask.id, bugtask)
724+ return OrderedBugTask(7, bugtask.id, bugtask)
725
726 return weight_function
727diff --git a/lib/lp/registry/stories/webservice/xx-source-package.rst b/lib/lp/registry/stories/webservice/xx-source-package.rst
728index d09785d..b5c17bc 100644
729--- a/lib/lp/registry/stories/webservice/xx-source-package.rst
730+++ b/lib/lp/registry/stories/webservice/xx-source-package.rst
731@@ -32,6 +32,7 @@ distribution series.
732 >>> pprint_entry(evolution)
733 bug_reported_acknowledgement: None
734 bug_reporting_guidelines: None
735+ channel: None
736 displayname: 'evolution in My-distro My-series'
737 distribution_link: 'http://.../my-distro'
738 distroseries_link: 'http://.../my-distro/my-series'
739diff --git a/lib/lp/registry/tests/test_distributionsourcepackage.py b/lib/lp/registry/tests/test_distributionsourcepackage.py
740index d8dffd9..6262cb4 100644
741--- a/lib/lp/registry/tests/test_distributionsourcepackage.py
742+++ b/lib/lp/registry/tests/test_distributionsourcepackage.py
743@@ -49,7 +49,7 @@ class TestDistributionSourcePackage(TestCaseWithFactory):
744 registrant=self.factory.makePerson(),
745 )
746 naked_distribution = removeSecurityProxy(distribution)
747- self.factory.makeSourcePackage(distroseries=distribution)
748+ self.factory.makeDistributionSourcePackage(distribution=distribution)
749 dsp = naked_distribution.getSourcePackage(name="pmount")
750 self.assertEqual(None, dsp.summary)
751
752diff --git a/lib/lp/registry/tests/test_distroseries.py b/lib/lp/registry/tests/test_distroseries.py
753index ab3c6bf..168386f 100644
754--- a/lib/lp/registry/tests/test_distroseries.py
755+++ b/lib/lp/registry/tests/test_distroseries.py
756@@ -449,6 +449,12 @@ class TestDistroSeries(TestCaseWithFactory):
757 ]
758 )
759
760+ def test_getSourcePackage_channel(self):
761+ distroseries = self.factory.makeDistroSeries()
762+ spn = self.factory.makeSourcePackageName()
763+ source_package = distroseries.getSourcePackage(spn, channel="stable")
764+ self.assertEqual("stable", source_package.channel)
765+
766
767 class TestDistroSeriesPackaging(TestCaseWithFactory):
768
769diff --git a/lib/lp/registry/tests/test_sourcepackage.py b/lib/lp/registry/tests/test_sourcepackage.py
770index b839a18..a74f23a 100644
771--- a/lib/lp/registry/tests/test_sourcepackage.py
772+++ b/lib/lp/registry/tests/test_sourcepackage.py
773@@ -506,6 +506,66 @@ class TestSourcePackage(TestCaseWithFactory):
774 sourcepackage.personHasDriverRights(distroseries.owner)
775 )
776
777+ def test_channel(self):
778+ source_package = self.factory.makeSourcePackage(channel="stable")
779+ self.assertEqual("stable", source_package.channel)
780+
781+ def test_hash(self):
782+ spn = self.factory.makeSourcePackageName()
783+ distroseries = self.factory.makeDistroSeries()
784+ self.assertEqual(
785+ hash(
786+ self.factory.makeSourcePackage(
787+ sourcepackagename=spn, distroseries=distroseries
788+ )
789+ ),
790+ hash(
791+ self.factory.makeSourcePackage(
792+ sourcepackagename=spn, distroseries=distroseries
793+ )
794+ ),
795+ )
796+ self.assertNotEqual(
797+ hash(
798+ self.factory.makeSourcePackage(
799+ sourcepackagename=spn,
800+ distroseries=distroseries,
801+ channel="stable",
802+ )
803+ ),
804+ hash(
805+ self.factory.makeSourcePackage(
806+ sourcepackagename=spn,
807+ distroseries=distroseries,
808+ channel="beta",
809+ )
810+ ),
811+ )
812+
813+ def test_eq(self):
814+ spn = self.factory.makeSourcePackageName()
815+ distroseries = self.factory.makeDistroSeries()
816+ self.assertEqual(
817+ self.factory.makeSourcePackage(
818+ sourcepackagename=spn, distroseries=distroseries
819+ ),
820+ self.factory.makeSourcePackage(
821+ sourcepackagename=spn, distroseries=distroseries
822+ ),
823+ )
824+ self.assertNotEqual(
825+ self.factory.makeSourcePackage(
826+ sourcepackagename=spn,
827+ distroseries=distroseries,
828+ channel="stable",
829+ ),
830+ self.factory.makeSourcePackage(
831+ sourcepackagename=spn,
832+ distroseries=distroseries,
833+ channel="beta",
834+ ),
835+ )
836+
837
838 class TestSourcePackageWebService(WebServiceTestCase):
839 def test_setPackaging(self):
840diff --git a/lib/lp/soyuz/tests/test_binarypackagebuild.py b/lib/lp/soyuz/tests/test_binarypackagebuild.py
841index ef513d3..939e6c1 100644
842--- a/lib/lp/soyuz/tests/test_binarypackagebuild.py
843+++ b/lib/lp/soyuz/tests/test_binarypackagebuild.py
844@@ -433,13 +433,19 @@ class BaseTestCaseWithThreeBuilds(TestCaseWithFactory):
845 """Publish some builds for the test archive."""
846 super().setUp()
847 self.ds = self.factory.makeDistroSeries()
848+ self.builds = self.makeBuilds()
849+ self.sources = [
850+ build.current_source_publication for build in self.builds
851+ ]
852+
853+ def makeBuilds(self):
854 i386_das = self.factory.makeDistroArchSeries(
855 distroseries=self.ds, architecturetag="i386"
856 )
857 hppa_das = self.factory.makeDistroArchSeries(
858 distroseries=self.ds, architecturetag="hppa"
859 )
860- self.builds = [
861+ return [
862 self.factory.makeBinaryPackageBuild(
863 archive=self.ds.main_archive, distroarchseries=i386_das
864 ),
865@@ -449,12 +455,10 @@ class BaseTestCaseWithThreeBuilds(TestCaseWithFactory):
866 pocket=PackagePublishingPocket.PROPOSED,
867 ),
868 self.factory.makeBinaryPackageBuild(
869- archive=self.ds.main_archive, distroarchseries=hppa_das
870+ archive=self.ds.main_archive,
871+ distroarchseries=hppa_das,
872 ),
873 ]
874- self.sources = [
875- build.current_source_publication for build in self.builds
876- ]
877
878
879 class TestBuildSet(TestCaseWithFactory):
880diff --git a/lib/lp/soyuz/tests/test_hasbuildrecords.py b/lib/lp/soyuz/tests/test_hasbuildrecords.py
881index b0b8e8c..07c5c26 100644
882--- a/lib/lp/soyuz/tests/test_hasbuildrecords.py
883+++ b/lib/lp/soyuz/tests/test_hasbuildrecords.py
884@@ -2,7 +2,7 @@
885 # GNU Affero General Public License version 3 (see the file LICENSE).
886
887 """Test implementations of the IHasBuildRecords interface."""
888-
889+from testscenarios import WithScenarios
890 from zope.component import getUtility
891 from zope.security.proxy import removeSecurityProxy
892
893@@ -14,12 +14,11 @@ from lp.buildmaster.interfaces.buildfarmjob import (
894 )
895 from lp.registry.interfaces.person import IPersonSet
896 from lp.registry.interfaces.pocket import PackagePublishingPocket
897+from lp.registry.interfaces.sourcepackage import SourcePackageType
898 from lp.registry.model.sourcepackage import SourcePackage
899-from lp.services.database.interfaces import IStore
900 from lp.soyuz.enums import ArchivePurpose
901 from lp.soyuz.interfaces.binarypackagebuild import IBinaryPackageBuild
902 from lp.soyuz.interfaces.buildrecords import IHasBuildRecords
903-from lp.soyuz.model.publishing import SourcePackagePublishingHistory
904 from lp.soyuz.tests.test_binarypackagebuild import BaseTestCaseWithThreeBuilds
905 from lp.soyuz.tests.test_publishing import SoyuzTestPublisher
906 from lp.testing import TestCaseWithFactory, person_logged_in
907@@ -256,30 +255,64 @@ class TestBuilderHasBuildRecords(TestHasBuildRecordsInterface):
908 )
909
910
911-class TestSourcePackageHasBuildRecords(TestHasBuildRecordsInterface):
912+class TestSourcePackageHasBuildRecords(
913+ WithScenarios, TestHasBuildRecordsInterface
914+):
915 """Test the SourcePackage implementation of IHasBuildRecords."""
916
917+ scenarios = [
918+ (
919+ "channel",
920+ {"channel": "stable", "format": SourcePackageType.CI_BUILD},
921+ ),
922+ ("no_channel", {"channel": None, "format": None}),
923+ ]
924+
925 def setUp(self):
926 super().setUp()
927 gedit_name = self.builds[0].source_package_release.sourcepackagename
928 self.context = SourcePackage(
929- gedit_name, self.builds[0].distro_arch_series.distroseries
930+ gedit_name,
931+ self.builds[0].distro_arch_series.distroseries,
932+ channel=self.channel,
933 )
934
935- # Convert the other two builds to be builds of
936- # gedit as well so that the one source package (gedit) will have
937- # three builds.
938- for build in self.builds[1:3]:
939- spr = build.source_package_release
940- removeSecurityProxy(spr).sourcepackagename = gedit_name
941- IStore(SourcePackagePublishingHistory).find(
942- SourcePackagePublishingHistory, sourcepackagerelease=spr
943- ).set(sourcepackagenameID=gedit_name.id)
944-
945+ def makeBuilds(self):
946+ i386_das = self.factory.makeDistroArchSeries(
947+ distroseries=self.ds, architecturetag="i386"
948+ )
949+ hppa_das = self.factory.makeDistroArchSeries(
950+ distroseries=self.ds, architecturetag="hppa"
951+ )
952+ spn = self.factory.makeSourcePackageName()
953+ builds = [
954+ self.factory.makeBinaryPackageBuild(
955+ sourcepackagename=spn,
956+ archive=self.ds.main_archive,
957+ distroarchseries=i386_das,
958+ channel=self.channel,
959+ format=self.format,
960+ ),
961+ self.factory.makeBinaryPackageBuild(
962+ sourcepackagename=spn,
963+ archive=self.ds.main_archive,
964+ distroarchseries=i386_das,
965+ channel=self.channel,
966+ format=self.format,
967+ ),
968+ self.factory.makeBinaryPackageBuild(
969+ sourcepackagename=spn,
970+ archive=self.ds.main_archive,
971+ distroarchseries=hppa_das,
972+ channel=self.channel,
973+ format=self.format,
974+ ),
975+ ]
976 # Set them as successfully built
977- for build in self.builds:
978+ for build in builds:
979 build.updateStatus(BuildStatus.BUILDING)
980 build.updateStatus(BuildStatus.FULLYBUILT)
981+ return builds
982
983 def test_get_build_records(self):
984 # We can fetch builds records from a SourcePackage.
985@@ -290,7 +323,7 @@ class TestSourcePackageHasBuildRecords(TestHasBuildRecordsInterface):
986 builds = self.context.getBuildRecords(
987 pocket=PackagePublishingPocket.RELEASE
988 ).count()
989- self.assertEqual(2, builds)
990+ self.assertEqual(3, builds)
991 builds = self.context.getBuildRecords(
992 pocket=PackagePublishingPocket.UPDATES
993 ).count()
994diff --git a/lib/lp/testing/factory.py b/lib/lp/testing/factory.py
995index 40a959c..b83a946 100644
996--- a/lib/lp/testing/factory.py
997+++ b/lib/lp/testing/factory.py
998@@ -31,6 +31,7 @@ from functools import wraps
999 from io import BytesIO
1000 from itertools import count
1001 from textwrap import dedent
1002+from typing import Optional
1003
1004 import pytz
1005 import six
1006@@ -2385,6 +2386,7 @@ class LaunchpadObjectFactory(ObjectFactory):
1007 self.makeSourcePackagePublishingHistory(
1008 distroseries=target.distroseries,
1009 sourcepackagename=target.sourcepackagename,
1010+ channel=target.channel,
1011 )
1012 if IDistributionSourcePackage.providedBy(target):
1013 if publish:
1014@@ -4575,6 +4577,7 @@ class LaunchpadObjectFactory(ObjectFactory):
1015 distroseries=None,
1016 publish=False,
1017 owner=None,
1018+ channel: Optional[str] = None,
1019 ):
1020 """Make an `ISourcePackage`.
1021
1022@@ -4590,7 +4593,9 @@ class LaunchpadObjectFactory(ObjectFactory):
1023 distroseries = self.makeDistroSeries(owner=owner)
1024 if publish:
1025 self.makeSourcePackagePublishingHistory(
1026- distroseries=distroseries, sourcepackagename=sourcepackagename
1027+ distroseries=distroseries,
1028+ sourcepackagename=sourcepackagename,
1029+ channel=channel,
1030 )
1031 with dbuser("statistician"):
1032 DistributionSourcePackageCache(
1033@@ -4599,7 +4604,9 @@ class LaunchpadObjectFactory(ObjectFactory):
1034 archive=distroseries.main_archive,
1035 name=sourcepackagename.name,
1036 )
1037- return distroseries.getSourcePackage(sourcepackagename)
1038+ return distroseries.getSourcePackage(
1039+ sourcepackagename, channel=channel
1040+ )
1041
1042 def getAnySourcePackageUrgency(self):
1043 return SourcePackageUrgency.MEDIUM
1044@@ -4892,6 +4899,8 @@ class LaunchpadObjectFactory(ObjectFactory):
1045 processor=None,
1046 sourcepackagename=None,
1047 arch_indep=None,
1048+ channel=None,
1049+ format=None,
1050 ):
1051 """Create a BinaryPackageBuild.
1052
1053@@ -4950,12 +4959,14 @@ class LaunchpadObjectFactory(ObjectFactory):
1054 component=multiverse,
1055 distroseries=distroarchseries.distroseries,
1056 sourcepackagename=sourcepackagename,
1057+ format=format,
1058 )
1059 self.makeSourcePackagePublishingHistory(
1060 distroseries=distroarchseries.distroseries,
1061 archive=archive,
1062 sourcepackagerelease=source_package_release,
1063 pocket=pocket,
1064+ channel=channel,
1065 )
1066 if status is None:
1067 status = BuildStatus.NEEDSBUILD

Subscribers

People subscribed via source and target branches

to status/vote changes: