Merge lp:~chad.smith/lp2kanban/branch-to-card into lp:lp2kanban

Proposed by Chad Smith
Status: Merged
Merged at revision: 120
Proposed branch: lp:~chad.smith/lp2kanban/branch-to-card
Merge into: lp:lp2kanban
Diff against target: 554 lines (+213/-46)
4 files modified
src/lp2kanban/bugs2cards.py (+70/-13)
src/lp2kanban/kanban.py (+79/-16)
src/lp2kanban/tests/common.py (+5/-2)
src/lp2kanban/tests/test_bugs2cards.py (+59/-15)
To merge this branch: bzr merge lp:~chad.smith/lp2kanban/branch-to-card
Reviewer Review Type Date Requested Status
Данило Шеган (community) Approve
Landscape Pending
Review via email: mp+280729@code.launchpad.net

Description of the change

Add initial support for branch cards and taskboards. This branch only handles syncing and moving branch "MP/blue" cards on the Landscape 2016 board.

 Support was added for branch cards inside bugs2card because a lot of the logic is common and can hopefully be consolidated in a subsequent branch.

There will be a followup branch with added logic in bugs2cards to call moveToTaskBoard for feature cards that are manually dragged to the "$Group::Landed" lane.

src/lp2kanban/kanban.py:
- Define initial LeankitTaskBoard class
- Add optional CurrentTaskBoardId property to LeankitCard
- Add LeankitCard.moveToTaskBoard method to move a card as a subtask of another card
- Since Taskboard API calls live on a different route, rework API routes defined on the LeankitConnector class

src/lp2kanban/bugs2cards.py:
- _get_mp_info to handle non-bug branch records when parsing linked MPs
- should_sync_card to return true if external_system_url is a branch
- get_card_status returns proper status for cards that have external_system_urls linked but no bugs (external_system_id) linked
- add a loop to sync_board function which processes all "branch cards" through board.getCardsWithExternalLinks(only_branches=True)

config/sync.ini:
  - comment out [Landscape Cisco] board definition (as that should live in this board now

To test:
buildout
./bin/test
# to test against the new landscape 2016 board
./create_creds.py (this will create your creds to modify the shared 2016 board)
Select "Change anything" button in your browser to allow updating cards
make config
./bin/py src/lp2kanban/bugs2cards.py -c configs/sync.ini -b 'Landscape 2016'

# Tweak branches or drag cards at https://canonical.leankit.com/Boards/View/102392996#workflow-view that you want to see automatically moved back to the proper lanes.

What's lacking:
 unit tests to cover bugs2card sync_board (coming in a followup branch)

To post a comment you must log in.
135. By Chad Smith

simplify moveToTaskBoard card method

136. By Chad Smith

merge ~landscape/lp2kanban/landscape-deploy to drop configs

Revision history for this message
Данило Шеган (danilo) wrote :

Please merge latest lp:~landscape/lp2kanban/landscape-deploy and drop configs altogether (merge them into lp:~landscape/landscape/lp2kanban-configs instead).

Looks good, only minor comments inline.

review: Approve
137. By Chad Smith

address review comments: approved branches that have not yet been merged will remain in the status LANDING. update copyright.

138. By Chad Smith

fix has_branch conditional to first check if card.external_system_url is not None before trying to match BRANCH_REGEX

139. By Chad Smith

card.save needs to be taskboard aware. When calling UpdateCard the parent_board.id needs to be specified, not the taskboard.id

140. By Chad Smith

add parent_card param to LeankitTaskBoard so that we have a refernce to the taskboard's card on the parent_board. Use the parent_card for card path printing

141. By Chad Smith

merge landscape-deploy to get latest changes to jenkins.sh

142. By Chad Smith

revert test makefile change

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lp2kanban/bugs2cards.py'
2--- src/lp2kanban/bugs2cards.py 2015-12-04 15:59:31 +0000
3+++ src/lp2kanban/bugs2cards.py 2015-12-17 00:04:21 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2011 Canonical Ltd
6+# Copyright 2011-2015 Canonical Ltd
7 #
8 from ConfigParser import ConfigParser
9 from argparse import ArgumentParser
10@@ -9,7 +9,9 @@
11 update_blueprints_from_work_items,
12 )
13 from lp2kanban.kanban import (
14+ BRANCH_REGEX,
15 LeankitKanban,
16+ LeankitTaskBoard,
17 Record,
18 )
19 import os.path
20@@ -164,8 +166,10 @@
21
22 def _get_mp_info():
23 mps = []
24- for bug_branch in branches:
25- for mp in bug_branch.branch.landing_targets:
26+ for branch in branches:
27+ if hasattr(branch, 'branch') :
28+ branch = branch.branch
29+ for mp in branch.landing_targets:
30 mp_info = Record(rank=None, status=None, mp=None)
31 status = mp.queue_status
32 mp_info.rank = ORDERED_STATUSES.get(status, 1)
33@@ -225,8 +229,11 @@
34 if conf.get('autosync', None) == 'on':
35 has_id = (card.external_card_id is not None and
36 card.external_card_id.strip() != '')
37- return (has_id and NO_SYNC_MARKER not in card.title and
38- NO_SYNC_MARKER not in card.description)
39+ has_branch = (card.external_system_url is not None and
40+ BRANCH_REGEX.match(card.external_system_url))
41+ if NO_SYNC_MARKER in card.title or NO_SYNC_MARKER in card.description:
42+ return False
43+ return (has_id or has_branch)
44 else:
45 return (card.title.startswith(TITLE_MARKER) or
46 DESCRIPTION_MARKER in card.description)
47@@ -299,15 +306,21 @@
48 """Return the status of the card as one of CardStatus values."""
49 # status of None means card needs not to be moved.
50 status = None
51- if bug_status in IN_PROGRESS_BUG_STATUSES:
52- status = CardStatus.CODING
53+ branch_card_status = None
54+ if branch_info:
55 if branch_info.status == 'In Progress':
56- status = CardStatus.CODING
57+ branch_card_status = CardStatus.CODING
58 elif branch_info.status == 'In Review':
59- status = CardStatus.REVIEW
60+ branch_card_status = CardStatus.REVIEW
61 elif branch_info.status == 'Approved':
62- status = CardStatus.LANDING
63+ branch_card_status = CardStatus.REVIEW
64 elif branch_info.status == 'Merged':
65+ branch_card_status = CardStatus.LANDING
66+ if bug_status is None:
67+ return branch_card_status
68+ if bug_status in IN_PROGRESS_BUG_STATUSES:
69+ status = CardStatus.CODING
70+ if branch_info.status == 'Merged':
71 if bug_status == 'Fix Committed':
72 if 'qa-needstesting' in bug_tags:
73 status = CardStatus.QA
74@@ -318,6 +331,8 @@
75 status = CardStatus.DEPLOY
76 else:
77 status = CardStatus.LANDING
78+ elif branch_card_status:
79+ return branch_card_status
80 elif bug_status in DONE_BUG_STATUSES:
81 status = CardStatus.DONE
82 return status
83@@ -457,6 +472,43 @@
84 board, project, new_bug_tag, bconf.get("bug_to_card_type"),
85 bconf.get("bug_to_card_lane"))
86 print " Syncing cards:"
87+ # Process linked branch cards
88+ for card in board.getCardsWithExternalLinks(only_branches=True):
89+ if card.external_card_id:
90+ # Cards with external_id are processed by getCardsWithExternalIds
91+ continue
92+ branch_name = card.external_system_url.replace(
93+ "https://code.launchpad.net/", "")
94+ branch = lp.branches.getByUniqueName(unique_name=branch_name)
95+ if not branch:
96+ print "Invalid branch url ({}) for card '{}'".format(
97+ card.external_system_url, card.title)
98+ continue
99+ if should_sync_card(card, bconf):
100+ branch_info = get_branch_info([branch])
101+ owner_name = BRANCH_REGEX.match(card.external_system_url).group(1)
102+ card_status = get_card_status(None, '', branch_info)
103+ card_path = card.lane.path
104+ if isinstance(card.lane.board, LeankitTaskBoard):
105+ task_card = card.lane.board.parent_card
106+ card_path = "{}::{}::{}".format(
107+ task_card.lane.path, task_card.title, card_path)
108+ if branch_info.status and should_move_card(card, bconf):
109+ assignee = Record(name=owner_name)
110+ try:
111+ move_card(card, card_status, bconf, [assignee], lp_users)
112+ except IOError as e:
113+ print " * %s (in %s)" % (card.title, card_path)
114+ print " >>> Error moving card:"
115+ print " ", e
116+ continue
117+ kanban_user = lp_users.lp_to_kanban.get(assignee.name, None)
118+ if kanban_user:
119+ card.assigned_user_id = kanban_user.id
120+ card.kanban_user = kanban_user
121+ else:
122+ print " * %s (in %s)" % (card.title, card_path)
123+
124 for card in board.getCardsWithExternalIds():
125 if should_sync_card(card, bconf):
126 try:
127@@ -475,13 +527,19 @@
128 synced, card_status, assignees, mp_url, mp_type = get_bug_status(
129 lp_bug, all_projects, lp_users.lp_to_kanban.keys())
130
131+ card_path = card.lane.path
132+ if isinstance(card.lane.board, LeankitTaskBoard):
133+ task_card = card.lane.board.parent_card
134+ card_path = "{}::{}::{}".format(
135+ task_card.lane.path, task_card.title, card_path)
136+
137 # Move card to new lane if configured to do so.
138 if card_status is not None and should_move_card(card, bconf):
139 try:
140 move_card(card, card_status, bconf, assignees, lp_users)
141 except IOError as e:
142 print " * %s: %s (in %s)" % (
143- bug_id, card.title, card.lane.path)
144+ bug_id, card.title, card_path)
145 print " >>> Error moving card:"
146 print " ", e
147 continue
148@@ -500,8 +558,7 @@
149 if mp_url and mp_type:
150 card.external_system_url = mp_url
151 card.external_system_name = mp_type
152- print " * %s: %s (in %s)" % (
153- bug_id, card.title, card.lane.path)
154+ print " * %s: %s (in %s)" % (bug_id, card.title, card_path)
155 else:
156 print " * %s: %s -- Not synced" % (
157 bug_id, card.title)
158
159=== modified file 'src/lp2kanban/kanban.py'
160--- src/lp2kanban/kanban.py 2014-03-18 20:49:48 +0000
161+++ src/lp2kanban/kanban.py 2015-12-17 00:04:21 +0000
162@@ -11,6 +11,8 @@
163
164
165 ANNOTATION_REGEX = re.compile('^\s*{.*}\s*$', re.MULTILINE|re.DOTALL)
166+BRANCH_REGEX = re.compile(
167+ '^https://code.launchpad.net/~([.\w]*)/([-\w]*)/([-\w]*)$')
168
169
170 class Record(dict):
171@@ -55,8 +57,7 @@
172
173 class LeankitConnector(object):
174 def __init__(self, account, username=None, password=None, throttle=1):
175- host = 'https://' + account + '.leankitkanban.com'
176- self.base_api_url = host + '/Kanban/Api'
177+ self.base_api_url = 'https://' + account + '.leankit.com'
178 self.http = self._configure_auth(username, password)
179 self.last_request_time = time.time() - throttle
180 self.throttle = throttle
181@@ -197,14 +198,17 @@
182 optional_attributes = [
183 'ExternalCardID', 'AssignedUserId', 'Size', 'IsBlocked',
184 'BlockReason', 'ExternalSystemName', 'ExternalSystemUrl',
185- 'ClassOfServiceId', 'DueDate',
186+ 'ClassOfServiceId', 'DueDate', 'CurrentTaskBoardId'
187 ]
188
189 def __init__(self, card_dict, lane):
190 super(LeankitCard, self).__init__(card_dict)
191
192 self.lane = lane
193- self.tags_list = set([tag.strip() for tag in self.tags.split(',')])
194+ if not self.tags:
195+ self.tags_list = []
196+ else:
197+ self.tags_list = set([tag.strip() for tag in self.tags.split(',')])
198 if '' in self.tags_list:
199 self.tags_list.remove('')
200 self.type = lane.board.cardtypes[self.type_id]
201@@ -218,7 +222,7 @@
202 tag = tag.strip()
203 if tag not in self.tags_list or self.tags.startswith(','):
204 if tag != '':
205- self.tags_list.add(tag)
206+ self.tags_list.append(tag)
207 self.tags = ', '.join(self.tags_list)
208
209 def save(self):
210@@ -241,17 +245,20 @@
211 #print "Storing %s in %s..." % (attr, self._toCamelCase(attr))
212 data[self._toCamelCase(attr)] = getattr(self, attr)
213
214+ board_id = str(self.lane.board.id)
215+ if isinstance(LeankitTaskBoard):
216+ board_id = str(self.lane.board.parent_board.id)
217 if self.is_new:
218 del data['Id']
219 del data['LaneId']
220 position = len(self.lane.cards)
221- url_parts = ['/Board', str(self.lane.board.id), 'AddCard',
222- 'Lane', str(self.lane.id), 'Position', str(position)]
223+ url_parts = ['/Kanban/Api/Board', board_id,
224+ 'AddCard', 'Lane', str(self.lane.id),
225+ 'Position', str(position)]
226 else:
227- url_parts = ['/Board', str(self.lane.board.id), 'UpdateCard']
228+ url_parts = ['/Kanban/Api/Board', board_id, 'UpdateCard']
229
230 url = '/'.join(url_parts)
231-
232 result = self.lane.board.connector.post(url, data=data)
233
234 if (self.is_new and
235@@ -271,9 +278,24 @@
236 else:
237 return None
238
239+ def moveToTaskBoard(self, target_card):
240+ """Move the current card into a subtask of target_card."""
241+ url = '/Api/Card/MoveCardToTaskboard'
242+ result = self.lane.board.connector.post(url, data={
243+ 'boardId': self.lane.board.id,
244+ 'destCardId': target_card.id,
245+ 'srcCardId': [self.id]})
246+ if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:
247+ return result.ReplyData[0]
248+ else:
249+ raise Exception(
250+ "Moving card %s (%s) to %s failed. " % (
251+ self.title, self.id, self.lane.path) +
252+ "Error %s: %s" % (result.ReplyCode, result.ReplyText))
253+
254 def _moveCard(self):
255 target_pos = len(self.lane.cards)
256- url = '/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % (
257+ url = '/Kanban/Api/Board/%d/MoveCard/%d/Lane/%d/Position/%d' % (
258 self.lane.board.id, self.id, self.lane.id, target_pos)
259 result = self.lane.board.connector.post(url, data=None)
260 if result.ReplyCode in LeankitResponseCodes.SUCCESS_CODES:
261@@ -327,6 +349,8 @@
262 text_after_json), where json_annotations contains the
263 JSON loaded with json.loads().
264 """
265+ if not self.description:
266+ self.description = ""
267 match = ANNOTATION_REGEX.search(self.description)
268 if match:
269 start = match.start()
270@@ -450,11 +474,12 @@
271 self.cards.append(card)
272 return card
273
274+
275 class LeankitBoard(Converter):
276
277 attributes = ['Id', 'Title', 'CreationDate', 'IsArchived']
278
279- base_uri = '/Boards/'
280+ base_uri = '/Kanban/Api/Boards/'
281
282 def __init__(self, board_dict, connector):
283 super(LeankitBoard, self).__init__(board_dict)
284@@ -473,12 +498,20 @@
285 self._cards_with_external_ids = set()
286 self._cards_with_description_annotations = set()
287 self._cards_with_external_links = set()
288+ self._cards_with_branches = set()
289 self.default_cardtype = None
290
291 def getCardsWithExternalIds(self):
292 return self._cards_with_external_ids
293
294- def getCardsWithExternalLinks(self):
295+ def getCardsWithExternalLinks(self, only_branches=False):
296+ """Return cards with external links
297+
298+ @param only_merge_proposals: Only return cards that have merge
299+ proposals specified as the card's external link
300+ """
301+ if only_branches:
302+ return self._cards_with_branches
303 return self._cards_with_external_links
304
305 def getCardsWithDescriptionAnnotations(self):
306@@ -491,15 +524,23 @@
307 self._populateUsers(self.details['BoardUsers'])
308 self._populateCardTypes(self.details['CardTypes'])
309 self._archive = self.connector.get(
310- "/Board/" + str(self.id) + "/Archive").ReplyData[0]
311+ "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0]
312 archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]
313 archive_lanes.extend(
314 [lane_dict['Lane'] for
315 lane_dict in self._archive[0]['ChildLanes']])
316 self._backlog = self.connector.get(
317- "/Board/" + str(self.id) + "/Backlog").ReplyData[0]
318+ "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0]
319 self._populateLanes(
320 self.details['Lanes'] + archive_lanes + self._backlog)
321+ for card in self.cards:
322+ if card.current_task_board_id: # We are a task board
323+ taskboard_data = self.connector.get(
324+ "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))
325+ card.taskboard = LeankitTaskBoard(
326+ taskboard_data["ReplyData"][0], self, card)
327+ card.taskboard.fetchDetails()
328+ self.cards.extend(card.taskboard.cards)
329 self._classifyCards()
330
331 def _classifyCards(self):
332@@ -512,12 +553,16 @@
333 self._cards_with_description_annotations.add(card)
334 if card.external_system_url:
335 self._cards_with_external_links.add(card)
336+ if BRANCH_REGEX.match(card.external_system_url):
337+ self._cards_with_branches.add(card)
338 print " - %s cards with external ids" % len(
339 self._cards_with_external_ids)
340 print " - %s cards with external links" % len(
341 self._cards_with_external_links)
342 print " - %s cards with description annotations" % len(
343 self._cards_with_description_annotations)
344+ print " - %s cards with branches" % len(
345+ self._cards_with_branches)
346
347 def _populateUsers(self, user_data):
348 self.users = {}
349@@ -629,6 +674,23 @@
350 self._printLanes(lane, indent, include_cards)
351
352
353+class LeankitTaskBoard(LeankitBoard):
354+
355+ attributes = ['Id', 'Title']
356+
357+ def __init__(self, taskboard_dict, board, parent_card):
358+ super(LeankitTaskBoard, self).__init__(taskboard_dict, board.connector)
359+ self.base_uri = '/Api/Board/%d/TaskBoard/%s/Get' % (board.id, self.id)
360+ self.cardtypes = board.cardtypes
361+ self.parent_board = board
362+ self.parent_card = parent_card
363+ self.is_archived = False
364+
365+ def fetchDetails(self):
366+ self.details = self.connector.get(self.base_uri).ReplyData[0]
367+ self._populateLanes(self.details['Lanes'])
368+
369+
370 class LeankitKanban(object):
371
372 def __init__(self, account, username=None, password=None):
373@@ -642,7 +704,7 @@
374
375 :param include_archived: if True, include archived boards as well.
376 """
377- boards_data = self.connector.get('/Boards').ReplyData
378+ boards_data = self.connector.get('/Kanban/Api/Boards').ReplyData
379 boards = []
380 for board_dict in boards_data[0]:
381 board = LeankitBoard(board_dict, self.connector)
382@@ -687,13 +749,14 @@
383 if __name__ == '__main__':
384 kanban = LeankitKanban('launchpad.leankitkanban.com',
385 'user@email', 'password')
386+
387 print "Active boards:"
388 boards = kanban.getBoards()
389 for board in boards:
390 print " * %s (%d)" % (board.title, board.id)
391
392 # Get a board by the title.
393- board_name = 'lp2kanban test'
394+ board_name = 'Landscape Test Board'
395 print "Getting board '%s'..." % board_name
396 board = kanban.getBoard(title=board_name)
397 board.printLanes()
398
399=== modified file 'src/lp2kanban/tests/common.py'
400--- src/lp2kanban/tests/common.py 2013-04-08 12:48:10 +0000
401+++ src/lp2kanban/tests/common.py 2015-12-17 00:04:21 +0000
402@@ -33,13 +33,16 @@
403 self.is_archived = is_archived
404 self._cards_with_description_annotations = set()
405 self._cards_with_external_links = set()
406+ self._cards_with_branches = set()
407 self.lanes = {}
408 self.root_lane = self.addLane('ROOT LANE')
409
410 def getCardsWithDescriptionAnnotations(self):
411 return self._cards_with_description_annotations
412
413- def getCardsWithExternalLinks(self):
414+ def getCardsWithExternalLinks(self, only_branches=False):
415+ if only_branches:
416+ return self._cards_with_branches
417 return self._cards_with_external_links
418
419 def getLaneByPath(self, path):
420@@ -93,7 +96,7 @@
421 def __init__(self, external_card_id=None, title=u"", description=u"",
422 description_annotations=None, lane=None,
423 assigned_user_id=None, external_system_name=None,
424- external_system_url=None):
425+ external_system_url=u""):
426 self.external_card_id = external_card_id
427 self.title = title
428 self.description = description
429
430=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
431--- src/lp2kanban/tests/test_bugs2cards.py 2013-04-08 12:53:06 +0000
432+++ src/lp2kanban/tests/test_bugs2cards.py 2015-12-17 00:04:21 +0000
433@@ -346,6 +346,7 @@
434 # in CODING.
435 self.assertEqual(CardStatus.CODING, status)
436
437+BRANCH_URL = "https://code.launchpad.net/~me/project/branch-name"
438
439 class CardStatusTest(unittest.TestCase):
440
441@@ -375,23 +376,43 @@
442
443 def test_should_sync_card_autosync_no(self):
444 # When autosync is 'on', cards with no external card IDs
445- # are not synced.
446+ # and no branches are not synced.
447 card = Record(title=u'no sync', description=u'',
448- external_card_id=None)
449+ external_card_id=None, external_system_url=None)
450 self.assertFalse(should_sync_card(card, {'sync_cards': 'on'}))
451
452- def test_should_sync_card_autosync_yes(self):
453+ def test_should_sync_card_autosync_no_sync_with_non_branch_urls(self):
454+ # When autosync is 'on', cards with external_system_urls which are not
455+ # valid launchpad branches are not synced.
456+ non_branch_urls = [
457+ "http://www.google.com/",
458+ "https://bugs.launchpad.net/charms/+source/hacluster/+bug/1",
459+ "https://code.launchpad.net/~person/project/blah/+merge/111"
460+ ]
461+ for url in non_branch_urls:
462+ card = Record(title=u'no sync', description=u'',
463+ external_card_id=None, external_system_url=url)
464+ self.assertFalse(should_sync_card(card, {'sync_cards': 'on'}))
465+
466+ def test_should_sync_card_autosync_synced_with_external_card_id(self):
467 # When autosync is 'on', cards with external card IDs are synced.
468 card = Record(title=u'no sync', description=u'',
469- external_card_id='11')
470+ external_card_id=u'11', external_system_url=u'')
471+ self.assertTrue(should_sync_card(card, {'autosync': 'on',
472+ 'sync_cards': 'on'}))
473+
474+ def test_should_sync_card_autosync_synced_with_branch(self):
475+ # When autosync is 'on', cards with a valid branch are synced.
476+ card = Record(title=u'no sync', description=u'',
477+ external_card_id=u'11', external_system_url=BRANCH_URL)
478 self.assertTrue(should_sync_card(card, {'autosync': 'on',
479 'sync_cards': 'on'}))
480
481 def test_should_sync_card_autosync_nosync(self):
482- # When autosync is 'on', cards with external card IDs are not
483- # synced when they contain the no-sync marker.
484+ # When autosync is 'on', cards with external card IDs or a valid
485+ # branch are not synced when they contain the no-sync marker.
486 card = Record(title=u'(no-sync)', description=u'(no-sync)',
487- external_card_id='11')
488+ external_card_id=u'11', external_system_url=BRANCH_URL)
489 self.assertFalse(should_sync_card(card, {'autosync': 'on',
490 'sync_cards': 'on'}))
491
492@@ -452,7 +473,7 @@
493 self.assertFalse(should_sync_card(card1, conf))
494 self.assertFalse(should_sync_card(card2, conf))
495
496- def test_get_card_status_noop(self):
497+ def test_get_card_status_noop_no_branch(self):
498 # For a bug in 'New', 'Triaged', 'Confirmed' or 'Incomplete' statuses,
499 # the status is unknown.
500 self.assertEqual(None, get_card_status('New', [], None))
501@@ -460,6 +481,33 @@
502 self.assertEqual(None, get_card_status('Incomplete', [], None))
503 self.assertEqual(None, get_card_status('Triaged', [], None))
504
505+ def test_get_card_status_coding_no_bug(self):
506+ # For a card with no associated bug, an 'In Progress' branch status
507+ # will be in the coding state.
508+ branch_info = Record(status='In Progress', target=None)
509+ self.assertEqual(
510+ CardStatus.CODING,
511+ get_card_status(None, [], branch_info))
512+
513+ def test_get_card_status_review_no_bug(self):
514+ # For a card with no associated bug, an 'Approved' or 'In Review'
515+ # branch status will be in the review state.
516+ branch_infos = [
517+ Record(status='In Review', target=None),
518+ Record(status='Approved', target=None)]
519+ for branch_info in branch_infos:
520+ self.assertEqual(
521+ CardStatus.REVIEW,
522+ get_card_status(None, [], branch_info))
523+
524+ def test_get_card_status_landing_no_bug(self):
525+ # For a card with no associated bug, a merged branch status will be in
526+ # the landing state.
527+ branch_info = Record(status='Merged', target=None)
528+ self.assertEqual(
529+ CardStatus.LANDING,
530+ get_card_status(None, [], branch_info))
531+
532 def test_get_card_status_coding_no_branch(self):
533 # For a bug in 'In Progress' or 'Fix Committed' status, it is
534 # considered to be in the coding state if there is no branch.
535@@ -496,16 +544,12 @@
536 get_card_status('Fix Committed', [], branch_info))
537
538 def test_get_card_status_landing(self):
539- # For a bug in 'In Progress' or 'Fix Committed' status, it is
540- # considered to be in the landing phase if there is a branch
541- # approved for landing.
542- branch_info = Record(status='Approved', target=None)
543+ # For a bug in 'In Progress' status, it is considered to be in the
544+ # landing phase if there is a branch 'Merged'.
545+ branch_info = Record(status='Merged', target=None)
546 self.assertEqual(
547 CardStatus.LANDING,
548 get_card_status('In Progress', [], branch_info))
549- self.assertEqual(
550- CardStatus.LANDING,
551- get_card_status('Fix Committed', [], branch_info))
552
553 def test_get_card_status_qa(self):
554 # For a bug in the 'Fix Committed' status, it is considered

Subscribers

People subscribed via source and target branches

to all changes: