Merge lp:~cjwatson/launchpad/snap-channels-branch into lp:launchpad

Proposed by Colin Watson
Status: Merged
Merged at revision: 18635
Proposed branch: lp:~cjwatson/launchpad/snap-channels-branch
Merge into: lp:launchpad
Diff against target: 632 lines (+285/-65)
7 files modified
lib/lp/snappy/browser/tests/test_snap.py (+3/-2)
lib/lp/snappy/browser/widgets/storechannels.py (+46/-23)
lib/lp/snappy/browser/widgets/templates/storechannels.pt (+23/-2)
lib/lp/snappy/browser/widgets/tests/test_storechannelswidget.py (+117/-22)
lib/lp/snappy/interfaces/snap.py (+3/-1)
lib/lp/snappy/validators/channels.py (+44/-7)
lib/lp/snappy/validators/tests/test_channels.py (+49/-8)
To merge this branch: bzr merge lp:~cjwatson/launchpad/snap-channels-branch
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+345171@code.launchpad.net

Commit message

Allow branches in snap store channel names.

Description of the change

These were formerly disallowed except in stable, so didn't make much sense here when channels support was first implemented, but they're fine now.

To post a comment you must log in.
Revision history for this message
William Grant (wgrant) :
review: Approve (code)
Revision history for this message
Colin Watson (cjwatson) :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/snappy/browser/tests/test_snap.py'
2--- lib/lp/snappy/browser/tests/test_snap.py 2018-04-21 10:15:26 +0000
3+++ lib/lp/snappy/browser/tests/test_snap.py 2018-05-08 16:48:28 +0000
4@@ -1410,9 +1410,10 @@
5
6 def test_store_channels_display(self):
7 snap = self.factory.makeSnap(
8- store_channels=["track/stable", "track/edge"])
9+ store_channels=["track/stable/fix-123", "track/edge/fix-123"])
10 view = create_initialized_view(snap, "+index")
11- self.assertEqual("track/stable, track/edge", view.store_channels)
12+ self.assertEqual(
13+ "track/stable/fix-123, track/edge/fix-123", view.store_channels)
14
15
16 class TestSnapRequestBuildsView(BaseTestSnapView):
17
18=== modified file 'lib/lp/snappy/browser/widgets/storechannels.py'
19--- lib/lp/snappy/browser/widgets/storechannels.py 2017-03-31 14:44:33 +0000
20+++ lib/lp/snappy/browser/widgets/storechannels.py 2018-05-08 16:48:28 +0000
21@@ -1,4 +1,4 @@
22-# Copyright 2017 Canonical Ltd. This software is licensed under the
23+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
24 # GNU Affero General Public License version 3 (see the file LICENSE).
25
26 __metaclass__ = type
27@@ -8,7 +8,10 @@
28 ]
29
30 from z3c.ptcompat import ViewPageTemplateFile
31-from zope.formlib.interfaces import IInputWidget, WidgetInputError
32+from zope.formlib.interfaces import (
33+ IInputWidget,
34+ WidgetInputError,
35+ )
36 from zope.formlib.utility import setUpWidget
37 from zope.formlib.widget import (
38 BrowserWidget,
39@@ -55,16 +58,22 @@
40 if self._widgets_set_up:
41 return
42 fields = [
43- TextLine(__name__="track", title=u"Track", required=False,
44- description=_(
45- "Track defines a series for your software. "
46- "If not specified, the default track ('latest') is "
47- "assumed.")
48- ),
49- List(__name__="risks", title=u"Risk", required=False,
50- value_type=Choice(vocabulary="SnapStoreChannel"),
51- description=_(
52- "Risks denote the stability of your software.")),
53+ TextLine(
54+ __name__="track", title=u"Track", required=False,
55+ description=_(
56+ "Track defines a series for your software. "
57+ "If not specified, the default track ('latest') is "
58+ "assumed.")),
59+ List(
60+ __name__="risks", title=u"Risk", required=False,
61+ value_type=Choice(vocabulary="SnapStoreChannel"),
62+ description=_("Risks denote the stability of your software.")),
63+ TextLine(
64+ __name__="branch", title=u"Branch", required=False,
65+ description=_(
66+ "Branches provide users with an easy way to test bug "
67+ "fixes. They are temporary and created on demand. If "
68+ "not specified, no branch is used.")),
69 ]
70
71 self.risks_widget = CustomWidgetFactory(LabeledMultiCheckBoxWidget)
72@@ -79,40 +88,48 @@
73 risks_widget = getattr(self, 'risks_widget', None)
74 return risks_widget and bool(risks_widget.vocabulary)
75
76- def buildChannelName(self, track, risk):
77- """Return channel name composed from given track and risk."""
78+ def buildChannelName(self, track, risk, branch):
79+ """Return channel name composed from given track, risk, and branch."""
80 channel = risk
81 if track and track != self._default_track:
82- channel = track + self._separator + risk
83+ channel = self._separator.join((track, channel))
84+ if branch:
85+ channel = self._separator.join((channel, branch))
86 return channel
87
88 def splitChannelName(self, channel):
89- """Return extracted track and risk from given channel name."""
90+ """Return extracted track, risk, and branch from given channel name."""
91 try:
92- track, risk = split_channel_name(channel)
93+ track, risk, branch = split_channel_name(channel)
94 except ValueError:
95 raise AssertionError("Not a valid value: %r" % channel)
96- return track, risk
97+ return track, risk, branch
98
99 def setRenderedValue(self, value):
100 """See `IWidget`."""
101 self.setUpSubWidgets()
102 if value:
103- # NOTE: atm target channels must belong to the same track
104+ # NOTE: atm target channels must belong to the same track and
105+ # branch
106 tracks = set()
107+ branches = set()
108 risks = []
109 for channel in value:
110- track, risk = self.splitChannelName(channel)
111+ track, risk, branch = self.splitChannelName(channel)
112 tracks.add(track)
113 risks.append(risk)
114- if len(tracks) != 1:
115+ branches.add(branch)
116+ if len(tracks) != 1 or len(branches) != 1:
117 raise AssertionError("Not a valid value: %r" % value)
118 track = tracks.pop()
119 self.track_widget.setRenderedValue(track)
120 self.risks_widget.setRenderedValue(risks)
121+ branch = branches.pop()
122+ self.branch_widget.setRenderedValue(branch)
123 else:
124 self.track_widget.setRenderedValue(None)
125 self.risks_widget.setRenderedValue(None)
126+ self.branch_widget.setRenderedValue(None)
127
128 def hasInput(self):
129 """See `IInputWidget`."""
130@@ -129,13 +146,19 @@
131 def getInputValue(self):
132 """See `IInputWidget`."""
133 self.setUpSubWidgets()
134+ track = self.track_widget.getInputValue()
135 risks = self.risks_widget.getInputValue()
136- track = self.track_widget.getInputValue()
137+ branch = self.branch_widget.getInputValue()
138 if track and self._separator in track:
139 error_msg = "Track name cannot include '%s'." % self._separator
140 raise WidgetInputError(
141 self.name, self.label, LaunchpadValidationError(error_msg))
142- channels = [self.buildChannelName(track, risk) for risk in risks]
143+ if branch and self._separator in branch:
144+ error_msg = "Branch name cannot include '%s'." % self._separator
145+ raise WidgetInputError(
146+ self.name, self.label, LaunchpadValidationError(error_msg))
147+ channels = [
148+ self.buildChannelName(track, risk, branch) for risk in risks]
149 return channels
150
151 def error(self):
152
153=== modified file 'lib/lp/snappy/browser/widgets/templates/storechannels.pt'
154--- lib/lp/snappy/browser/widgets/templates/storechannels.pt 2017-03-31 17:10:14 +0000
155+++ lib/lp/snappy/browser/widgets/templates/storechannels.pt 2018-05-08 16:48:28 +0000
156@@ -1,8 +1,19 @@
157+<tal:root
158+ xmlns:tal="http://xml.zope.org/namespaces/tal"
159+ xmlns:metal="http://xml.zope.org/namespaces/metal"
160+ omit-tag="">
161+
162 <table>
163 <tr>
164 <td>
165- <p class="formHelp">Channels to release this snap package to after uploading it to the store.
166- A channel is defined by a combination of an optional track and a risk, e.g. '2.1/stable', or 'stable'.
167+ <p class="formHelp">
168+ Channels to release this snap package to after uploading it to the
169+ store. A channel is defined by a combination of an optional track,
170+ a risk, and an optional branch, e.g. '2.1/stable/fix-123',
171+ '2.1/stable', 'stable/fix-123', or 'stable'.
172+ <a href="https://docs.snapcraft.io/reference/channels"
173+ target="_blank"
174+ class="sprite maybe action-icon">(?)</a>
175 </p>
176 </td>
177 </tr>
178@@ -29,7 +40,17 @@
179 </p>
180 </td>
181 </tr>
182+ <tr>
183+ <td>
184+ <tal:widget define="widget nocall:view/branch_widget">
185+ <metal:block
186+ use-macro="context/@@launchpad_widget_macros/launchpad_widget_row" />
187+ </tal:widget>
188+ </td>
189+ </tr>
190 </table>
191 </td>
192 </tr>
193 </table>
194+
195+</tal:root>
196
197=== modified file 'lib/lp/snappy/browser/widgets/tests/test_storechannelswidget.py'
198--- lib/lp/snappy/browser/widgets/tests/test_storechannelswidget.py 2017-11-10 11:28:43 +0000
199+++ lib/lp/snappy/browser/widgets/tests/test_storechannelswidget.py 2018-05-08 16:48:28 +0000
200@@ -1,4 +1,4 @@
201-# Copyright 2017 Canonical Ltd. This software is licensed under the
202+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
203 # GNU Affero General Public License version 3 (see the file LICENSE).
204
205 from __future__ import absolute_import, print_function, unicode_literals
206@@ -71,6 +71,7 @@
207 self.assertIsInstance(
208 self.widget.risks_widget.vocabulary, SnapStoreChannelVocabulary)
209 self.assertTrue(self.widget.has_risks_vocabulary)
210+ self.assertIsNotNone(getattr(self.widget, "branch_widget", None))
211
212 def test_setUpSubWidgets_second_call(self):
213 # The setUpSubWidgets method exits early if a flag is set to
214@@ -79,37 +80,63 @@
215 self.widget.setUpSubWidgets()
216 self.assertIsNone(getattr(self.widget, "track_widget", None))
217 self.assertIsNone(getattr(self.widget, "risks_widget", None))
218+ self.assertIsNone(getattr(self.widget, "branch_widget", None))
219 self.assertIsNone(self.widget.has_risks_vocabulary)
220
221- def test_buildChannelName_no_track(self):
222- self.assertEqual("edge", self.widget.buildChannelName(None, "edge"))
223+ def test_buildChannelName_no_track_or_branch(self):
224+ self.assertEqual(
225+ "edge", self.widget.buildChannelName(None, "edge", None))
226
227 def test_buildChannelName_with_track(self):
228 self.assertEqual(
229- "track/edge", self.widget.buildChannelName("track", "edge"))
230-
231- def test_splitChannelName_no_track(self):
232- self.assertEqual((None, "edge"), self.widget.splitChannelName("edge"))
233+ "track/edge", self.widget.buildChannelName("track", "edge", None))
234+
235+ def test_buildChannelName_with_branch(self):
236+ self.assertEqual(
237+ "edge/fix-123",
238+ self.widget.buildChannelName(None, "edge", "fix-123"))
239+
240+ def test_buildChannelName_with_track_and_branch(self):
241+ self.assertEqual(
242+ "track/edge/fix-123",
243+ self.widget.buildChannelName("track", "edge", "fix-123"))
244+
245+ def test_splitChannelName_no_track_or_branch(self):
246+ self.assertEqual(
247+ (None, "edge", None), self.widget.splitChannelName("edge"))
248
249 def test_splitChannelName_with_track(self):
250 self.assertEqual(
251- ("track", "edge"), self.widget.splitChannelName("track/edge"))
252+ ("track", "edge", None),
253+ self.widget.splitChannelName("track/edge"))
254+
255+ def test_splitChannelName_with_branch(self):
256+ self.assertEqual(
257+ (None, "edge", "fix-123"),
258+ self.widget.splitChannelName("edge/fix-123"))
259+
260+ def test_splitChannelName_with_track_and_branch(self):
261+ self.assertEqual(
262+ ("track", "edge", "fix-123"),
263+ self.widget.splitChannelName("track/edge/fix-123"))
264
265 def test_splitChannelName_invalid(self):
266 self.assertRaises(
267- AssertionError, self.widget.splitChannelName, "track/edge/invalid")
268+ AssertionError, self.widget.splitChannelName,
269+ "track/edge/invalid/too-long")
270
271 def test_setRenderedValue_empty(self):
272 self.widget.setRenderedValue([])
273 self.assertIsNone(self.widget.track_widget._getCurrentValue())
274 self.assertIsNone(self.widget.risks_widget._getCurrentValue())
275
276- def test_setRenderedValue_no_track(self):
277- # Channels do not include a track
278+ def test_setRenderedValue_no_track_or_branch(self):
279+ # Channels do not include a track or branch
280 risks = ['candidate', 'edge']
281 self.widget.setRenderedValue(risks)
282 self.assertIsNone(self.widget.track_widget._getCurrentValue())
283 self.assertEqual(risks, self.widget.risks_widget._getCurrentValue())
284+ self.assertIsNone(self.widget.branch_widget._getCurrentValue())
285
286 def test_setRenderedValue_with_track(self):
287 # Channels including a track
288@@ -118,12 +145,39 @@
289 self.assertEqual('2.2', self.widget.track_widget._getCurrentValue())
290 self.assertEqual(
291 ['candidate', 'edge'], self.widget.risks_widget._getCurrentValue())
292+ self.assertIsNone(self.widget.branch_widget._getCurrentValue())
293+
294+ def test_setRenderedValue_with_branch(self):
295+ # Channels including a branch
296+ channels = ['candidate/fix-123', 'edge/fix-123']
297+ self.widget.setRenderedValue(channels)
298+ self.assertIsNone(self.widget.track_widget._getCurrentValue())
299+ self.assertEqual(
300+ ['candidate', 'edge'], self.widget.risks_widget._getCurrentValue())
301+ self.assertEqual(
302+ 'fix-123', self.widget.branch_widget._getCurrentValue())
303+
304+ def test_setRenderedValue_with_track_and_branch(self):
305+ # Channels including a track and branch
306+ channels = ['2.2/candidate/fix-123', '2.2/edge/fix-123']
307+ self.widget.setRenderedValue(channels)
308+ self.assertEqual('2.2', self.widget.track_widget._getCurrentValue())
309+ self.assertEqual(
310+ ['candidate', 'edge'], self.widget.risks_widget._getCurrentValue())
311+ self.assertEqual(
312+ 'fix-123', self.widget.branch_widget._getCurrentValue())
313
314 def test_setRenderedValue_invalid_value(self):
315- # Multiple channels, different tracks, unsupported
316- channels = ['2.2/candidate', '2.1/edge']
317- self.assertRaises(
318- AssertionError, self.widget.setRenderedValue, channels)
319+ # Multiple channels, different tracks or branches, unsupported
320+ self.assertRaises(
321+ AssertionError, self.widget.setRenderedValue,
322+ ['2.2/candidate', '2.1/edge'])
323+ self.assertRaises(
324+ AssertionError, self.widget.setRenderedValue,
325+ ['candidate/fix-123', 'edge/fix-124'])
326+ self.assertRaises(
327+ AssertionError, self.widget.setRenderedValue,
328+ ['2.2/candidate', 'edge/fix-123'])
329
330 def test_hasInput_false(self):
331 # hasInput is false when there is no risk set in the form data.
332@@ -143,6 +197,7 @@
333 form = {
334 "field.channels.track": "",
335 "field.channels.risks": ["invalid"],
336+ "field.channels.branch": "",
337 }
338 self.widget.request = LaunchpadTestRequest(form=form)
339 self.assertFalse(self.widget.hasValidInput())
340@@ -152,6 +207,7 @@
341 form = {
342 "field.channels.track": "track",
343 "field.channels.risks": ["stable", "beta"],
344+ "field.channels.branch": "branch",
345 }
346 self.widget.request = LaunchpadTestRequest(form=form)
347 self.assertTrue(self.widget.hasValidInput())
348@@ -164,24 +220,62 @@
349
350 def test_getInputValue_invalid_track(self):
351 # An error is raised when the track includes a '/'.
352- form = {"field.channels.track": "tra/ck",
353- "field.channels.risks": ["beta"]}
354+ form = {
355+ "field.channels.track": "tra/ck",
356+ "field.channels.risks": ["beta"],
357+ "field.channels.branch": "",
358+ }
359 self.assertGetInputValueError(form, "Track name cannot include '/'.")
360
361- def test_getInputValue_no_track(self):
362+ def test_getInputValue_invalid_branch(self):
363+ # An error is raised when the branch includes a '/'.
364+ form = {
365+ "field.channels.track": "",
366+ "field.channels.risks": ["beta"],
367+ "field.channels.branch": "bra/nch",
368+ }
369+ self.assertGetInputValueError(form, "Branch name cannot include '/'.")
370+
371+ def test_getInputValue_no_track_or_branch(self):
372 self.widget.request = LaunchpadTestRequest(
373- form={"field.channels.track": "",
374- "field.channels.risks": ["beta", "edge"]})
375+ form={
376+ "field.channels.track": "",
377+ "field.channels.risks": ["beta", "edge"],
378+ "field.channels.branch": "",
379+ })
380 expected = ["beta", "edge"]
381 self.assertEqual(expected, self.widget.getInputValue())
382
383 def test_getInputValue_with_track(self):
384 self.widget.request = LaunchpadTestRequest(
385- form={"field.channels.track": "track",
386- "field.channels.risks": ["beta", "edge"]})
387+ form={
388+ "field.channels.track": "track",
389+ "field.channels.risks": ["beta", "edge"],
390+ "field.channels.branch": "",
391+ })
392 expected = ["track/beta", "track/edge"]
393 self.assertEqual(expected, self.widget.getInputValue())
394
395+ def test_getInputValue_with_branch(self):
396+ self.widget.request = LaunchpadTestRequest(
397+ form={
398+ "field.channels.track": "",
399+ "field.channels.risks": ["beta", "edge"],
400+ "field.channels.branch": "fix-123",
401+ })
402+ expected = ["beta/fix-123", "edge/fix-123"]
403+ self.assertEqual(expected, self.widget.getInputValue())
404+
405+ def test_getInputValue_with_track_and_branch(self):
406+ self.widget.request = LaunchpadTestRequest(
407+ form={
408+ "field.channels.track": "track",
409+ "field.channels.risks": ["beta", "edge"],
410+ "field.channels.branch": "fix-123",
411+ })
412+ expected = ["track/beta/fix-123", "track/edge/fix-123"]
413+ self.assertEqual(expected, self.widget.getInputValue())
414+
415 def test_call(self):
416 # The __call__ method sets up the widgets.
417 markup = self.widget()
418@@ -192,5 +286,6 @@
419 expected_ids = [
420 "field.channels.risks.%d" % i for i in range(len(self.risks))]
421 expected_ids.append("field.channels.track")
422+ expected_ids.append("field.channels.branch")
423 ids = [field["id"] for field in fields]
424 self.assertContentEqual(expected_ids, ids)
425
426=== modified file 'lib/lp/snappy/interfaces/snap.py'
427--- lib/lp/snappy/interfaces/snap.py 2018-04-21 10:01:22 +0000
428+++ lib/lp/snappy/interfaces/snap.py 2018-05-08 16:48:28 +0000
429@@ -583,7 +583,9 @@
430 description=_(
431 "Channels to release this snap package to after uploading it to "
432 "the store. A channel is defined by a combination of an optional "
433- " track and a risk, e.g. '2.1/stable', or 'stable'.")))
434+ " track, a risk, and an optional branch, e.g. "
435+ "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
436+ "'stable'.")))
437
438
439 class ISnapAdminAttributes(Interface):
440
441=== modified file 'lib/lp/snappy/validators/channels.py'
442--- lib/lp/snappy/validators/channels.py 2017-03-27 19:28:36 +0000
443+++ lib/lp/snappy/validators/channels.py 2018-05-08 16:48:28 +0000
444@@ -1,10 +1,12 @@
445-# Copyright 2017 Canonical Ltd. This software is licensed under the
446+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
447 # GNU Affero General Public License version 3 (see the file LICENSE).
448
449 """Validators for the .store_channels attribute."""
450
451 __metaclass__ = type
452
453+from zope.schema.vocabulary import getVocabularyRegistry
454+
455 from lp import _
456 from lp.app.validators import LaunchpadValidationError
457 from lp.services.webapp.escaping import (
458@@ -17,17 +19,42 @@
459 channel_components_delimiter = '/'
460
461
462+def _is_risk(component):
463+ """Does this channel component identify a risk?"""
464+ vocabulary = getVocabularyRegistry().get(None, "SnapStoreChannel")
465+ try:
466+ vocabulary.getTermByToken(component)
467+ except LookupError:
468+ return False
469+ else:
470+ return True
471+
472+
473 def split_channel_name(channel):
474- """Return extracted track and risk from given channel name."""
475+ """Return extracted track, risk, and branch from given channel name."""
476 components = channel.split(channel_components_delimiter)
477- if len(components) == 2:
478- track, risk = components
479+ if len(components) == 3:
480+ track, risk, branch = components
481+ elif len(components) == 2:
482+ # Identify risk to determine if this is track/risk or risk/branch.
483+ if _is_risk(components[0]):
484+ if _is_risk(components[1]):
485+ raise ValueError(
486+ "Branch name cannot match a risk name: %r" % channel)
487+ track = None
488+ risk, branch = components
489+ elif _is_risk(components[1]):
490+ track, risk = components
491+ branch = None
492+ else:
493+ raise ValueError("No valid risk provided: %r" % channel)
494 elif len(components) == 1:
495 track = None
496 risk = components[0]
497+ branch = None
498 else:
499 raise ValueError("Invalid channel name: %r" % channel)
500- return track, risk
501+ return track, risk, branch
502
503
504 def channels_validator(channels):
505@@ -35,19 +62,29 @@
506 LaunchpadValidationError.
507 """
508 tracks = set()
509+ branches = set()
510 for name in channels:
511 try:
512- track, risk = split_channel_name(name)
513+ track, risk, branch = split_channel_name(name)
514 except ValueError:
515 message = _(
516 "Invalid channel name '${name}'. Channel names must be of the "
517- "form 'track/risk' or 'risk'.",
518+ "form 'track/risk/branch', 'track/risk', 'risk/branch', or "
519+ "'risk'.",
520 mapping={'name': html_escape(name)})
521 raise LaunchpadValidationError(structured(message))
522 tracks.add(track)
523+ branches.add(branch)
524+
525+ # XXX cjwatson 2018-05-08: These are slightly arbitrary restrictions,
526+ # but they make the UI much simpler.
527
528 if len(tracks) != 1:
529 message = _("Channels must belong to the same track.")
530 raise LaunchpadValidationError(structured(message))
531
532+ if len(branches) != 1:
533+ message = _("Channels must belong to the same branch.")
534+ raise LaunchpadValidationError(structured(message))
535+
536 return True
537
538=== modified file 'lib/lp/snappy/validators/tests/test_channels.py'
539--- lib/lp/snappy/validators/tests/test_channels.py 2017-10-20 13:35:42 +0000
540+++ lib/lp/snappy/validators/tests/test_channels.py 2018-05-08 16:48:28 +0000
541@@ -1,4 +1,4 @@
542-# Copyright 2017 Canonical Ltd. This software is licensed under the
543+# Copyright 2017-2018 Canonical Ltd. This software is licensed under the
544 # GNU Affero General Public License version 3 (see the file LICENSE).
545
546 from __future__ import absolute_import, print_function, unicode_literals
547@@ -6,11 +6,14 @@
548 __metaclass__ = type
549
550 from lp.app.validators import LaunchpadValidationError
551+from lp.snappy.interfaces.snapstoreclient import ISnapStoreClient
552 from lp.snappy.validators.channels import (
553 channels_validator,
554 split_channel_name,
555 )
556 from lp.testing import TestCaseWithFactory
557+from lp.testing.fakemethod import FakeMethod
558+from lp.testing.fixture import ZopeUtilityFixture
559 from lp.testing.layers import LaunchpadFunctionalLayer
560
561
562@@ -18,17 +21,50 @@
563
564 layer = LaunchpadFunctionalLayer
565
566- def test_split_channel_name_no_track(self):
567- self.assertEqual((None, "edge"), split_channel_name("edge"))
568+ def setUp(self):
569+ super(TestChannelsValidator, self).setUp()
570+ self.risks = [
571+ {"name": "stable", "display_name": "Stable"},
572+ {"name": "candidate", "display_name": "Candidate"},
573+ {"name": "beta", "display_name": "Beta"},
574+ {"name": "edge", "display_name": "Edge"},
575+ ]
576+ snap_store_client = FakeMethod()
577+ snap_store_client.listChannels = FakeMethod(result=self.risks)
578+ self.useFixture(
579+ ZopeUtilityFixture(snap_store_client, ISnapStoreClient))
580+
581+ def test_split_channel_name_no_track_or_branch(self):
582+ self.assertEqual((None, "edge", None), split_channel_name("edge"))
583
584 def test_split_channel_name_with_track(self):
585- self.assertEqual(("track", "edge"), split_channel_name("track/edge"))
586-
587- def test_split_channel_name_invalid(self):
588- self.assertRaises(ValueError, split_channel_name, "track/edge/invalid")
589+ self.assertEqual(
590+ ("track", "edge", None), split_channel_name("track/edge"))
591+
592+ def test_split_channel_name_with_branch(self):
593+ self.assertEqual(
594+ (None, "edge", "fix-123"), split_channel_name("edge/fix-123"))
595+
596+ def test_split_channel_name_with_track_and_branch(self):
597+ self.assertEqual(
598+ ("track", "edge", "fix-123"),
599+ split_channel_name("track/edge/fix-123"))
600+
601+ def test_split_channel_name_no_risk(self):
602+ self.assertRaises(ValueError, split_channel_name, "track/fix-123")
603+
604+ def test_split_channel_name_ambiguous_risk(self):
605+ self.assertRaises(ValueError, split_channel_name, "edge/stable")
606+
607+ def test_split_channel_name_too_many_components(self):
608+ self.assertRaises(
609+ ValueError, split_channel_name, "track/edge/invalid/too-long")
610
611 def test_channels_validator_valid(self):
612+ self.assertTrue(
613+ channels_validator(['1.1/beta/fix-123', '1.1/edge/fix-123']))
614 self.assertTrue(channels_validator(['1.1/beta', '1.1/edge']))
615+ self.assertTrue(channels_validator(['beta/fix-123', 'edge/fix-123']))
616 self.assertTrue(channels_validator(['beta', 'edge']))
617
618 def test_channels_validator_multiple_tracks(self):
619@@ -36,7 +72,12 @@
620 LaunchpadValidationError, channels_validator,
621 ['1.1/stable', '2.1/edge'])
622
623+ def test_channels_validator_multiple_branches(self):
624+ self.assertRaises(
625+ LaunchpadValidationError, channels_validator,
626+ ['stable/fix-123', 'edge/fix-124'])
627+
628 def test_channels_validator_invalid_channel(self):
629 self.assertRaises(
630 LaunchpadValidationError, channels_validator,
631- ['1.1/stable/invalid'])
632+ ['1.1/stable/invalid/too-long'])