Merge lp:~dpb/lp2kanban/filter-bugs-correct-movement into lp:lp2kanban

Proposed by David Britton
Status: Merged
Merged at revision: 142
Proposed branch: lp:~dpb/lp2kanban/filter-bugs-correct-movement
Merge into: lp:lp2kanban
Diff against target: 604 lines (+247/-52)
8 files modified
Makefile (+4/-2)
README (+6/-0)
jenkins.sh (+3/-2)
setup.py (+1/-0)
src/lp2kanban/bugs2cards.py (+91/-19)
src/lp2kanban/kanban.py (+31/-23)
src/lp2kanban/tests/common.py (+6/-2)
src/lp2kanban/tests/test_bugs2cards.py (+105/-4)
To merge this branch: bzr merge lp:~dpb/lp2kanban/filter-bugs-correct-movement
Reviewer Review Type Date Requested Status
🤖 Landscape Builder test results Needs Fixing
Eric Snow (community) Approve
Landscape Pending
Review via email: mp+304273@code.launchpad.net

Commit message

- Remove archive from default board fetch, this speeds up operations significantly. I don't have any way exposed externally from the config to turn this back on, but we don't use this, so I'm skipping for now.
- Remove cards inside taskboard from board fetch. This speeds up operations significantly, and we currently have no use case for automation moving inside a taskboard, nor does the config support doing these kinds of operations for us.
- Add clean make target
- Update README to talk about using a virtualenv for testing
- Add bug filter and card filter for testing individual moves on the board
- Add logging (follow on branch will convert all prints to logging calls
- log interesting paths for easier post-mortem log analysis (Moves and syncs)
- Explicitly skip attempting to move for no-op cases (before we relied on lkk to noop our request for us)

Description of the change

- Remove archive from default board fetch, this speeds up operations significantly. I don't have any way exposed externally from the config to turn this back on, but we don't use this, so I'm skipping for now.
- Remove cards inside taskboard from board fetch. This speeds up operations significantly, and we currently have no use case for automation moving inside a taskboard, nor does the config support doing these kinds of operations for us.
- Add clean make target
- Update README to talk about using a virtualenv for testing
- Add bug filter and card filter for testing individual moves on the board
- Add logging (follow on branch will convert all prints to logging calls
- log interesting paths for easier post-mortem log analysis (Moves and syncs)
- Explicitly skip attempting to move for no-op cases (before we relied on lkk to noop our request for us)

After doing this, I cleaned up lp:~landscape/landscape/lp2kanban-configs, and added better descriptions to that file.

To post a comment you must log in.
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :
review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: http_proxy=$ci_proxy https_proxy=$ci_proxy make ci-test
Result: Success
Revno: 163
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/9632/

review: Approve (test results)
Revision history for this message
Adam Collard (adam-collard) :
Revision history for this message
Eric Snow (ericsnowcurrently) :
Revision history for this message
David Britton (dpb) wrote :

Fixed Adam's comments

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: http_proxy=$CI_PROXY https_proxy=$CI_PROXY make ci-test
Result: Fail
Revno: 164
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10135/

review: Needs Fixing (test results)
Revision history for this message
David Britton (dpb) wrote :

Addressed all comments.

Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: http_proxy=$CI_PROXY https_proxy=$CI_PROXY make ci-test
Result: Fail
Revno: 165
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10136/

review: Needs Fixing (test results)
Revision history for this message
Eric Snow (ericsnowcurrently) wrote :

Other than one small fix, +1. Thanks for taking the time on this!

review: Approve
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: http_proxy=$CI_PROXY https_proxy=$CI_PROXY make ci-test
Result: Fail
Revno: 166
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10137/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) :
review: Abstain (executing tests)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

Command: http_proxy=$CI_PROXY https_proxy=$CI_PROXY make ci-test
Result: Fail
Revno: 167
Branch: lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement
Jenkins: https://ci.lscape.net/job/latch-test/10138/

review: Needs Fixing (test results)
Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

The attempt to merge lp:~davidpbritton/lp2kanban/filter-bugs-correct-movement into lp:lp2kanban failed. Below is the output from the failed tests.

test -d venv || virtualenv venv; \
 . venv/bin/activate; \
 make check
New python executable in venv/bin/python
Installing setuptools, pip...done.
make[1]: Entering directory `/var/tmp/tarmac/lp2kanban/trunk'
mkdir download-cache
python bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg
make[1]: Leaving directory `/var/tmp/tarmac/lp2kanban/trunk'

Traceback (most recent call last):
  File "bootstrap.py", line 80, in <module>
    exec(urlopen('https://bootstrap.pypa.io/ez_setup.py').read(), ez)
  File "/usr/lib/python2.7/urllib2.py", line 127, in urlopen
    return _opener.open(url, data, timeout)
  File "/usr/lib/python2.7/urllib2.py", line 404, in open
    response = self._open(req, data)
  File "/usr/lib/python2.7/urllib2.py", line 422, in _open
    '_open', req)
  File "/usr/lib/python2.7/urllib2.py", line 382, in _call_chain
    result = func(*args)
  File "/usr/lib/python2.7/urllib2.py", line 1222, in https_open
    return self.do_open(httplib.HTTPSConnection, req)
  File "/usr/lib/python2.7/urllib2.py", line 1184, in do_open
    raise URLError(err)
urllib2.URLError: <urlopen error Tunnel connection failed: 403 Forbidden>
make[1]: *** [bin/buildout] Error 1
make: *** [ci-test] Error 2

Revision history for this message
🤖 Landscape Builder (landscape-builder) wrote :

No approved revision specified.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2016-08-29 20:18:23 +0000
3+++ Makefile 2016-09-01 21:37:25 +0000
4@@ -17,12 +17,14 @@
5 # buildout.
6 BUILDOUT_BIN = $(PY) bin/tags bin/test
7
8-default: check
9+default: bin/test
10
11
12 download-cache:
13 mkdir download-cache
14
15+clean:
16+ @rm -rf bin
17
18 bin/buildout: download-cache
19 $(PYTHON) bootstrap.py -v 1.7.1 -f eggs -c buildout.cfg
20@@ -59,4 +61,4 @@
21 . venv/bin/activate; \
22 $(MAKE) check
23
24-.PHONY: check default configs needs-xdg-utils ci-test
25+.PHONY: check default configs needs-xdg-utils clean ci-test
26
27=== modified file 'README'
28--- README 2012-12-10 15:21:44 +0000
29+++ README 2016-09-01 21:37:25 +0000
30@@ -38,5 +38,11 @@
31 Testing
32 -------
33
34+All tests should work on 16.04+, please run in a vitrualenv though:
35+
36+ virtualenv ve
37+ source ve/bin/activate
38+ make check
39+
40 'lp2kanban test' board configuration connects to LP staging by default
41 to allow easier testing.
42
43=== modified file 'jenkins.sh'
44--- jenkins.sh 2015-12-16 16:56:38 +0000
45+++ jenkins.sh 2016-09-01 21:37:25 +0000
46@@ -1,6 +1,5 @@
47 #!/bin/bash -xe
48
49-ini="$1.ini"
50 date "+START: %c"
51
52 #export https_proxy=http://squid.external:3128
53@@ -28,5 +27,7 @@
54 # - https://stackoverflow.com/questions/1473577
55 # - https://stackoverflow.com/questions/492483
56 # - https://stackoverflow.com/questions/9932406
57-PYTHONIOENCODING="utf-8" bin/py src/lp2kanban/bugs2cards.py -c configs/${1}.ini
58+config=$1
59+shift
60+PYTHONIOENCODING="utf-8" bin/py src/lp2kanban/bugs2cards.py -c "configs/${config}.ini" "${@}"
61 date "+END: %c"
62
63=== modified file 'setup.py'
64--- setup.py 2016-04-13 16:06:23 +0000
65+++ setup.py 2016-09-01 21:37:25 +0000
66@@ -18,6 +18,7 @@
67 'requests',
68 'setuptools',
69 'simplejson',
70+ 'testfixtures',
71 'testtools',
72 ],
73 )
74
75=== modified file 'src/lp2kanban/bugs2cards.py'
76--- src/lp2kanban/bugs2cards.py 2016-08-26 17:11:41 +0000
77+++ src/lp2kanban/bugs2cards.py 2016-09-01 21:37:25 +0000
78@@ -3,6 +3,7 @@
79 from ConfigParser import ConfigParser
80 from argparse import ArgumentParser
81 from launchpadlib.launchpad import Launchpad
82+import logging
83 from lp2kanban.cards2workitems import (
84 convert_cards_to_work_items,
85 get_blueprint_linked_cards,
86@@ -45,7 +46,13 @@
87 help="List available kanban boards.")
88 parser.add_argument(
89 '-d', '--debug', action='store_const', const=True, default=False,
90- help="Turn on debugging for HTTP traffic")
91+ help="Turn on logging debug, debugging for HTTP traffic")
92+ parser.add_argument(
93+ '-B', '--bug', required=False, type=int, default=None,
94+ help="Only operate on this bug.")
95+ parser.add_argument(
96+ '-C', '--card', required=False, type=int, default=None,
97+ help="Only operate on this card id, can obtain by looking at URL.")
98 return parser
99
100
101@@ -207,7 +214,8 @@
102 return lp_bug
103
104
105-def move_cards_to_feature_taskboard(feature_card, feature_bug, source_lanes):
106+def move_cards_to_feature_taskboard(
107+ feature_card, feature_bug, source_lanes, filter_card=None):
108 """Move all cards related to subtasks of the feature_card.
109
110 This will only move cards which are in the landing_lanes or deploy_lanes.
111@@ -220,6 +228,8 @@
112 """
113 linked_cards = get_cards_for_feature(feature_card, feature_bug=feature_bug)
114 for card in linked_cards:
115+ if filter_card and filter_card != card.id:
116+ return
117 if card.type.name == "Feature": # We don't want to move feature cards
118 continue
119 card_path = card.lane.path
120@@ -349,11 +359,14 @@
121 """
122 if conf.get('sync_cards', 'off') == 'off':
123 return False
124+ logging.debug("Should I sync? - {}".format(card.title))
125 no_sync_lanes = [
126 lane_path.strip()
127 for lane_path in conf.get("no_sync_lanes", "").split(",")]
128 for no_sync_lane_path in no_sync_lanes:
129 if no_sync_lane_path and card.lane.path.endswith(no_sync_lane_path):
130+ logging.debug("Not syncing, {} in no_sync_lanes".format(
131+ card.lane.path))
132 return False
133 if conf.get('autosync', None) in ['on', 'active-projects']:
134 has_id = (card.external_card_id is not None and
135@@ -666,6 +679,7 @@
136
137 def create_cards(board, bconf, lp_users, lp_projects):
138 """Create new cards on all projects."""
139+ logging.info("Creating new cards")
140 new_bug_tag = bconf.get("bug_to_card_tag")
141 if new_bug_tag is not None:
142 for lp_project in lp_projects:
143@@ -674,10 +688,13 @@
144 bconf.get("bug_to_card_lane"), lp_users.feature_lanes)
145
146
147-def sync_cards_linked_branches(board, bconf, lp, lp_users):
148+def sync_cards_linked_branches(
149+ board, bconf, lp, lp_users, filter_card=None):
150 """Sync any cards that have linked external branches."""
151- print " Syncing branch cards:"
152+ logging.info("Syncing branch cards")
153 for card in board.getCardsWithExternalLinks(only_branches=True):
154+ if filter_card and filter_card != card.id:
155+ continue
156 if card.external_card_id:
157 # Cards with external_id are processed by getCardsWithExternalIds
158 continue
159@@ -714,9 +731,16 @@
160 print " * %s (in %s)" % (card.title, card_path)
161
162
163-def sync_cards_external_ids(board, bconf, lp, lp_users, lp_projects):
164+def sync_cards_external_ids(
165+ board, bconf, lp, lp_users, lp_projects, filter_bug=None,
166+ filter_card=None):
167 """Sync any cards that have linked external ids (bugs in launchpad)."""
168+ logging.info("Syncing cards with external ids")
169 for card in board.getCardsWithExternalIds():
170+ if filter_bug and card.external_card_id != filter_bug:
171+ continue
172+ if filter_card and filter_card != card.id:
173+ continue
174 if should_sync_card(card, bconf):
175 lp_bug = get_lp_bug(card, lp)
176 if lp_bug is None:
177@@ -763,8 +787,10 @@
178 print " * %s: %s -- Not synced" % (
179 lp_bug.id, card.title)
180
181-def move_cards_to_taskboards(board, bconf, lp, lp_users):
182+def move_cards_to_taskboards(
183+ board, bconf, lp, lp_users, filter_card=None):
184 """Move branch cards to feature cards if they are landed."""
185+ logging.info("Move cards to taskboards")
186 if "${group}" in bconf["landing_lanes"]:
187 # Replace the optional template ${group} with configured board groups.
188 # This creates a list of landing_lanes, one per group on the board.
189@@ -779,11 +805,12 @@
190 feature_card.lane.path in valid_taskboard_lanes):
191 lp_bug = get_lp_bug(feature_card, lp)
192 move_cards_to_feature_taskboard(
193- feature_card, lp_bug, valid_taskboard_lanes)
194+ feature_card, lp_bug, valid_taskboard_lanes, filter_card)
195
196
197 def convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects):
198 """Create work items out of cards if desired."""
199+ logging.info("Convert cards to work items")
200 if bconf.get("convert_cards_to_work_items", False) == "on":
201 # Sync cards to blueprint work items, if so configured.
202 print " Checking for cards to sync with work items..."
203@@ -808,25 +835,46 @@
204
205 If a card is located in a "no_move" lane, the card won't get moved.
206 """
207+ logging.debug("Evaluating for move: {}".format(card.title))
208 target_lane_path = bconf[ConfigOptionByStatus[bug_status]]
209- if assignees:
210- group = lp_users.lp_to_group.get(assignees[0].name)
211- if group:
212- target_lane_path = target_lane_path.replace("${group}", group)
213+ if "${group}" in target_lane_path:
214+ if assignees:
215+ group = lp_users.lp_to_group.get(assignees[0].name)
216+ if group:
217+ target_lane_path = target_lane_path.replace("${group}", group)
218+ else:
219+ logging.debug(
220+ "Not moving, assignee: {} not in group: {}".format(
221+ assignees[0].name, group))
222+ return
223+ else:
224+ logging.debug("Not moving, not assigned")
225+ return
226+
227 omnidirectional_card_moves = (
228 bconf.get("omnidirectional_card_moves", "on") == "on")
229 no_move_lanes = [
230 lane_path.strip()
231- for lane_path in bconf.get("no_move_lanes", "").split(",")]
232+ for lane_path in bconf.get("no_move_lanes", "").split(",")
233+ if lane_path]
234 for no_move_lane_path in no_move_lanes:
235 if no_move_lane_path and card.lane.path.endswith(no_move_lane_path):
236+ logging.debug("Not moving, {} in no_move_lanes".format(
237+ card.lane.path))
238 return
239+
240 if omnidirectional_card_moves:
241 target_lane = find_card_target_lane_omnidirectional(
242 card.lane, target_lane_path)
243 else:
244 target_lane = find_card_target_lane_next(
245 card.lane, target_lane_path)
246+ if card.lane.path == target_lane_path:
247+ logging.debug("Not moving, already in {}".format(card.lane.path))
248+ return
249+
250+ logging.debug(
251+ " * Moving: {} -> {}".format(card.lane.path, target_lane_path))
252 card.move(target_lane)
253
254
255@@ -853,13 +901,43 @@
256 continue
257
258
259+def process_board(board, bconf, filter_bug, filter_card):
260+ """Sync a given board.
261+
262+ Decomposition of main, and handles interpreting
263+ filter arguments.
264+ """
265+ lp, lp_projects, lp_users = get_lp_data(board, bconf)
266+ if not filter_bug and not filter_card:
267+ create_cards(board, bconf, lp_users, lp_projects)
268+ if not filter_bug:
269+ sync_cards_linked_branches(
270+ board, bconf, lp, lp_users, filter_card=filter_card)
271+ sync_cards_external_ids(
272+ board, bconf, lp, lp_users, lp_projects, filter_bug=filter_bug,
273+ filter_card=filter_card)
274+ if not filter_bug:
275+ move_cards_to_taskboards(
276+ board, bconf, lp, lp_users, filter_card=filter_card)
277+ if not filter_bug and not filter_card:
278+ convert_cards_to_work_items(board, bconf, lp, lp_users, lp_projects)
279+ save_board(board)
280+
281+
282 def main(argv):
283 """Update the Yellow team's kanban board with data from Launchpad."""
284 args = get_arg_parser().parse_args()
285+ if args.bug:
286+ args.bug = str(args.bug)
287
288 if args.debug:
289+ logging.basicConfig(
290+ level=logging.DEBUG, format='%(asctime)s %(levelname)s %(message)s')
291 import httplib
292 httplib.HTTPConnection.debuglevel = 1
293+ else:
294+ logging.basicConfig(
295+ level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
296
297 config = ConfigParser()
298 config.read(args.config)
299@@ -891,13 +969,7 @@
300 print "Could not retrieve board. Skipping."
301 continue
302 bconf = boards_config[board_name]
303- lp, lp_projects, lp_users = get_lp_data(board, bconf)
304- create_cards(board, bconf, lp_users, lp_projects)
305- sync_cards_linked_branches(board, bconf, lp, lp_users)
306- sync_cards_external_ids(board, bconf, lp, lp_users, lp_projects)
307- move_cards_to_taskboards(board, bconf, lp, lp_users)
308- convert_cards_to_work_items(board, bconf, lp, lp_users)
309- save_board(board)
310+ process_board(board, bconf, args.bug, args.card)
311 except (KeyError, AssertionError) as e:
312 print "Exception caught. Skipping board."
313 print_exc()
314
315=== modified file 'src/lp2kanban/kanban.py'
316--- src/lp2kanban/kanban.py 2016-04-14 20:50:10 +0000
317+++ src/lp2kanban/kanban.py 2016-09-01 21:37:25 +0000
318@@ -3,6 +3,7 @@
319
320 import requests
321 import json
322+import logging
323 import operator
324 from pprint import pprint
325 import re
326@@ -180,9 +181,8 @@
327 return name.upper()
328
329 def __setattr__(self, attr, value):
330- if ((not hasattr(self, attr) or
331- getattr(self, attr, None) != value) and
332- attr in self._watched_attrs):
333+ if (getattr(self, attr, object()) != value and
334+ attr in self._watched_attrs):
335 self.direct_setattr('is_dirty', True)
336 self.dirty_attrs.add(attr)
337 self.direct_setattr(attr, value)
338@@ -531,30 +531,38 @@
339 def getCardsWithDescriptionAnnotations(self):
340 return self._cards_with_description_annotations
341
342- def fetchDetails(self):
343+ def fetchDetails(self, include_archive=False, include_taskboards=False):
344+ """Fetch all details from the board into our datastructure.
345+
346+ include_taskboards - add all cards on taskboards into my card list
347+ include_archive - add all cards from archive into my card list.
348+ """
349 self.details = self.connector.get(
350 self.base_uri + str(self.id)).ReplyData[0]
351
352 self._populateUsers(self.details['BoardUsers'])
353 self._populateCardTypes(self.details['CardTypes'])
354- self._archive = self.connector.get(
355- "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0]
356- archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]
357- archive_lanes.extend(
358- [lane_dict['Lane'] for
359- lane_dict in self._archive[0]['ChildLanes']])
360- self._backlog = self.connector.get(
361+ lanes = self.details['Lanes']
362+ if include_archive:
363+ self._archive = self.connector.get(
364+ "/Kanban/Api/Board/" + str(self.id) + "/Archive").ReplyData[0]
365+ archive_lanes = [lane_dict['Lane'] for lane_dict in self._archive]
366+ archive_lanes.extend(
367+ [lane_dict['Lane'] for
368+ lane_dict in self._archive[0]['ChildLanes']])
369+ lanes += archive_lanes
370+ lanes += self.connector.get(
371 "/Kanban/Api/Board/" + str(self.id) + "/Backlog").ReplyData[0]
372- self._populateLanes(
373- self.details['Lanes'] + archive_lanes + self._backlog)
374- for card in self.cards:
375- if card.current_task_board_id: # We are a task board
376- taskboard_data = self.connector.get(
377- "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))
378- card.taskboard = LeankitTaskBoard(
379- taskboard_data["ReplyData"][0], self, card)
380- card.taskboard.fetchDetails()
381- self.cards.extend(card.taskboard.cards)
382+ self._populateLanes(lanes)
383+ if include_taskboards:
384+ for card in self.cards:
385+ if card.current_task_board_id: # We are a task board
386+ taskboard_data = self.connector.get(
387+ "/Kanban/Api/v1/board/%s/card/%s/taskboard" % (self.id, card.id))
388+ card.taskboard = LeankitTaskBoard(
389+ taskboard_data["ReplyData"][0], self, card)
390+ card.taskboard.fetchDetails()
391+ self.cards.extend(card.taskboard.cards)
392 self._classifyCards()
393 self._populateFeatureTagPaths()
394
395@@ -769,10 +777,10 @@
396 board = self._findBoardInCache(board_id, title)
397 return board
398
399- def getBoard(self, board_id=None, title=None):
400+ def getBoard(self, board_id=None, title=None, include_archive=False):
401 board = self._findBoard(board_id, title)
402 if board is not None:
403- board.fetchDetails()
404+ board.fetchDetails(include_archive=include_archive)
405 return board
406
407
408
409=== modified file 'src/lp2kanban/tests/common.py'
410--- src/lp2kanban/tests/common.py 2016-05-02 23:07:28 +0000
411+++ src/lp2kanban/tests/common.py 2016-09-01 21:37:25 +0000
412@@ -1,10 +1,13 @@
413 # Copyright 2012 Canonical Ltd.
414 """Common fixtures for lp2kanban tests."""
415
416+import uuid
417+
418+from lp2kanban.kanban import Record
419+
420+
421 __metaclass__ = type
422
423-from lp2kanban.kanban import Record
424-
425
426 class FauxCardType:
427
428@@ -123,6 +126,7 @@
429 self.external_system_url = external_system_url
430 self.moved_to = None
431 self.saved = False
432+ self.id = uuid.uuid1()
433
434 def save(self):
435 """A no-op for compatibility."""
436
437=== modified file 'src/lp2kanban/tests/test_bugs2cards.py'
438--- src/lp2kanban/tests/test_bugs2cards.py 2016-08-25 22:37:31 +0000
439+++ src/lp2kanban/tests/test_bugs2cards.py 2016-09-01 21:37:25 +0000
440@@ -13,6 +13,8 @@
441 import datetime
442 import unittest
443
444+from testfixtures import log_capture
445+
446 from lp2kanban.bugs2cards import (
447 CardStatus,
448 create_cards_in_project,
449@@ -50,7 +52,6 @@
450 )
451
452
453-
454 def parse_config_text(text):
455 config = ConfigParser()
456 config.readfp(StringIO(dedent(text)))
457@@ -1489,7 +1490,18 @@
458 move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users)
459 self.assertEqual("coding", card.moved_to.path)
460
461- def test_group_no_assignee(self):
462+ @log_capture()
463+ def test_noop(self, testlog):
464+ """Skip move attempt if already in right path."""
465+ card = self.qa.addCard(FauxCard())
466+ move_card(card, CardStatus.QA, self.bconf, [], self.lp_users)
467+ self.assertEqual(None, card.moved_to)
468+ testlog.check(
469+ ('root', 'DEBUG', 'Evaluating for move: '),
470+ ('root', 'DEBUG', 'Not moving, already in qa'))
471+
472+ @log_capture()
473+ def test_group_no_assignee(self, testlog):
474 # If a target lane requires a group, and the card doesn't have
475 # an assignee, the card isn't moved.
476 self.bconf["coding_lanes"] = "${group}::coding"
477@@ -1497,6 +1509,9 @@
478 card = self.backlog.addCard(FauxCard())
479 move_card(card, CardStatus.CODING, self.bconf, [], self.lp_users)
480 self.assertEqual(None, card.moved_to)
481+ testlog.check(
482+ ('root', 'DEBUG', 'Evaluating for move: '),
483+ ('root', 'DEBUG', 'Not moving, not assigned'))
484
485 def test_group_assignee_in_group(self):
486 # If a target lane requires a group, and the card has an
487@@ -1511,7 +1526,8 @@
488 self.lp_users)
489 self.assertEqual("test group::coding", card.moved_to.path)
490
491- def test_group_assignee_not_in_group(self):
492+ @log_capture()
493+ def test_group_assignee_not_in_group(self, testlog):
494 # If a target lane requires a group, and the card has an
495 # assignee that is not part of a group, the card isn't moved.
496 self.bconf["coding_lanes"] = "${group}::coding"
497@@ -1522,6 +1538,11 @@
498 card, CardStatus.CODING, self.bconf, [FauxPerson("bar")],
499 self.lp_users)
500 self.assertEqual(None, card.moved_to)
501+ testlog.check(
502+ ('root', 'DEBUG', 'Evaluating for move: '),
503+ ('root', 'DEBUG', 'Not moving, assignee: bar not in group: None'))
504+
505+
506
507 def test_no_move_lanes(self):
508 # Cards in a "no_move" lane doesn't get moved by move_card().
509@@ -1709,6 +1730,86 @@
510 self.assertEqual(self.feature_card, landed_card1.moved_to)
511 self.assertEqual(self.feature_card, landed_card2.moved_to)
512
513+ def test_sync_cards_external_ids_filter_card(self):
514+ """filter_card skips and selects correctly."""
515+ # Configure bugs2cards to sync only active-projects myproject1 and myproject2
516+ self.bconf.update({
517+ "autosync": "active-projects",
518+ "projects": "myproject",
519+ "sync_cards": "on", "move_cards": "on",
520+ "new_lanes": "new"})
521+ # Setup bug1 and bug2 in separate projects which both need to move coding -> new
522+ project1 = FauxTarget(resource_type_link="project", project="myproject")
523+ lp_users = FauxLaunchpadUsersForBoard(lp_to_kanban={"person1": "Person One"})
524+ lp_projects = [project1]
525+ bug_task1 = FauxBugTask(
526+ bug_id=123, target=project1, title='Test bug in myproject',
527+ description='Test bug description.')
528+ bug_task2 = FauxBugTask(
529+ bug_id=456, target=project1, title='Test bug in otherproject',
530+ description='Test bug description.')
531+ bug_card1 = FauxCard(
532+ title='BugCard123', external_card_id=u'123')
533+ bug_card2 = FauxCard(
534+ title='BugCard456', external_card_id=u'456')
535+ board = FauxBoard(
536+ cards=[bug_card1, bug_card2], bug_cards=[bug_card1, bug_card2])
537+ new_lane = board.addLane('new')
538+ coding_lane = board.addLane('coding')
539+ bug_card1.lane = coding_lane
540+ bug_card2.lane = coding_lane
541+ bug_type = FauxCardType(id=3, name='Bug', is_default=False)
542+ bug_card1.type = bug_type
543+ bug_card2.type = bug_type
544+ self.board.cards.extend([bug_card1, bug_card2])
545+ lp = FauxLP({"projects": {"myproject": project1},
546+ "bugs": {123: bug_task1.bug, 456: bug_task2.bug}})
547+ sync_cards_external_ids(
548+ board, self.bconf, lp, lp_users, lp_projects, filter_card=bug_card2.id)
549+ coding_lane = FauxLane(board=board, path='coding')
550+ self.assertEqual(new_lane, bug_card2.moved_to)
551+ self.assertIsNone(bug_card1.moved_to)
552+
553+ def test_sync_cards_skips_bugs_not_in_filter_bug(self):
554+ """filter_bug skips and selects bugs in sync_cards_external_ids."""
555+ # Configure bugs2cards to sync only active-projects myproject1 and myproject2
556+ self.bconf.update({
557+ "autosync": "active-projects",
558+ "projects": "myproject",
559+ "sync_cards": "on", "move_cards": "on",
560+ "new_lanes": "new"})
561+ # Setup bug1 and bug2 in separate projects which both need to move coding -> new
562+ project1 = FauxTarget(resource_type_link="project", project="myproject")
563+ lp_users = FauxLaunchpadUsersForBoard(lp_to_kanban={"person1": "Person One"})
564+ lp_projects = [project1]
565+ bug_task1 = FauxBugTask(
566+ bug_id=123, target=project1, title='Test bug in myproject',
567+ description='Test bug description.')
568+ bug_task2 = FauxBugTask(
569+ bug_id=456, target=project1, title='Test bug in otherproject',
570+ description='Test bug description.')
571+ bug_card1 = FauxCard(
572+ title='BugCard123', external_card_id=u'123')
573+ bug_card2 = FauxCard(
574+ title='BugCard456', external_card_id=u'456')
575+ board = FauxBoard(
576+ cards=[bug_card1, bug_card2], bug_cards=[bug_card1, bug_card2])
577+ new_lane = board.addLane('new')
578+ coding_lane = board.addLane('coding')
579+ bug_card1.lane = coding_lane
580+ bug_card2.lane = coding_lane
581+ bug_type = FauxCardType(id=3, name='Bug', is_default=False)
582+ bug_card1.type = bug_type
583+ bug_card2.type = bug_type
584+ self.board.cards.extend([bug_card1, bug_card2])
585+ lp = FauxLP({"projects": {"myproject": project1},
586+ "bugs": {123: bug_task1.bug, 456: bug_task2.bug}})
587+ sync_cards_external_ids(
588+ board, self.bconf, lp, lp_users, lp_projects, filter_bug=u"456")
589+ coding_lane = FauxLane(board=board, path='coding')
590+ self.assertEqual(new_lane, bug_card2.moved_to)
591+ self.assertIsNone(bug_card1.moved_to)
592+
593 def test_sync_cards_skips_bugs_of_external_projects_when_active_projects_set(self):
594 """Bugs of external projects are skipped when autosync set to active-projects."""
595 # Configure bugs2cards to sync only active-projects myproject1 and myproject2
596@@ -1744,7 +1845,7 @@
597 self.board.cards.extend([bug_card1, bug_card2])
598 # Include project1 and project2 in our active 'projects' fake LP data
599 lp = FauxLP({"projects": {"myproject": project1, "myproject2": project2},
600- "bugs": {123: bug_task1.bug, "456": bug_task2.bug}})
601+ "bugs": {123: bug_task1.bug, 456: bug_task2.bug}})
602 sync_cards_external_ids(board, self.bconf, lp, lp_users, lp_projects)
603 coding_lane = FauxLane(board=board, path='coding')
604 self.assertEqual(new_lane, bug_card1.moved_to)

Subscribers

People subscribed via source and target branches

to all changes: