Merge lp:~mabac/svammel/bug-749561-set-series-and-milestone into lp:svammel

Proposed by Mattias Backman
Status: Merged
Approved by: Mattias Backman
Approved revision: 102
Merged at revision: 99
Proposed branch: lp:~mabac/svammel/bug-749561-set-series-and-milestone
Merge into: lp:svammel
Diff against target: 472 lines (+260/-39)
4 files modified
bug_reporting.py (+93/-7)
config.py (+6/-0)
file-failures.py (+7/-3)
tests/test_fail_filer.py (+154/-29)
To merge this branch: bzr merge lp:~mabac/svammel/bug-749561-set-series-and-milestone
Reviewer Review Type Date Requested Status
James Westby (community) Approve
Matthias Klose Pending
Review via email: mp+57514@code.launchpad.net

Description of the change

Hi,

This branch adds the option to set milestones and series on newly created bugs.

--milestone and --target-series options are added.

Milestone and series are looked up in Ubuntu and on any error setting them, the bug is closed. I have limited permissions for nominating series, so everything is not thoroughly tested.

Thanks,

Mattias

To post a comment you must log in.
Revision history for this message
Mattias Backman (mabac) wrote :

Reminder to self:

147 + importance=args.importance, milestone=args.milestone,

should be milestone_name=

Revision history for this message
Mattias Backman (mabac) wrote :

Try something like this

    $ python file-failures.py --archive test-rebuild-20110114-arm -vv --bugtag testseries --bugtag svammel --logfile logstatus.log --serviceroot qastaging --target-series natty --milestone ubuntu-11.04

There is a namespace clash on the command line with --series and --target-series which probably increases confusion.

If setting either milestone or series fails the bug will be closed and the script bails. Unless --ignore-errors is passed.

Revision history for this message
Matthias Klose (doko) wrote :

looks fine.

+ # createBug creates only one bugtask, but addNomination
+ # may add another, so we use the last one.
+ set_bugtask_milestone(bug.bug_tasks[0:2][-1], milestone)

just wondering how robust this is ...

Revision history for this message
Mattias Backman (mabac) wrote :

> + # createBug creates only one bugtask, but addNomination
> + # may add another, so we use the last one.
> + set_bugtask_milestone(bug.bug_tasks[0:2][-1], milestone)
>
> just wondering how robust this is ...

Can't say that writing that was my proudest moment... I hope that we
can clarify how addNomination works before this feature is done. I
have however not been able to fully test this since I don't seem to
have proper permissions anywhere in the system.

Thanks,

Mattias

Revision history for this message
James Westby (james-w) wrote :

Hi,

Sorry for the delay in reviewing this.

8 +class AttributeError(Exception):

Could we avoid re-defining a standard exception, that's only going to lead to confusion.

If the standard exception doesn't do what you want please change the name of
this one.

59 + set_bugtask_milestone(bug.bug_tasks[0:2][-1], milestone)

Like Mattias I am uneasy about this, it looks very fragile. Is there
some rule you can verbalise about which task is needed at this point?
If so then we can try and code that up, rather than relying on ordering
of the tasks here.

Everything else looks good.

Thanks,

James

review: Needs Fixing
97. By Mattias Backman

Remove exception which collided with standard Exception and fix tests.

98. By Mattias Backman

Find correct bugtask for milestone instead of assuming that it is the last one.

Revision history for this message
Mattias Backman (mabac) wrote :

> Sorry for the delay in reviewing this.

No problem. I had left it in status "work in progress" until recently too.
>
> 8       +class AttributeError(Exception):
>
> Could we avoid re-defining a standard exception, that's only going to lead to confusion.

That's my mistake, didn't check for a standard exception with that
name. Removed.

> 59      +                set_bugtask_milestone(bug.bug_tasks[0:2][-1], milestone)
>
> Like Mattias I am uneasy about this, it looks very fragile. Is there
> some rule you can verbalise about which task is needed at this point?
> If so then we can try and code that up, rather than relying on ordering
> of the tasks here.

That rule should be to use the only bug_task if no series has been
nominated, and to use the bug_task that actually is targeted to the
specified series otherwise. Trivial now that I think about it, thanks.
:) I've changed this as well.

Thanks,

Mattias

99. By Mattias Backman

Automatically approve series nomination, to be able to proceed with setting milestone on bug_task.

100. By Mattias Backman

Only auto approve if user is allowed to do so.

Revision history for this message
Mattias Backman (mabac) wrote :

Another piece of the puzzle is that the user might be able to add a nomination but is not allowed to approve it. The new bugtask is only created when the nomination is approved.

Now the script will add a nomination and approve it immediately if the user has the permission to do so. If the nomination is added but cannot be approved, setting a milestone will fail, and I think that has to be since the user tried to do something she hasn't proper permissions for.

Just adding nominations in one pass would work, and setting milestones without nominating a series first is also ok. Only nominating a series and setting a milestone will require special permissions, I think.

Revision history for this message
James Westby (james-w) wrote :

53 + set_bugtask_milestone(ms_bugtask, milestone)

I would probably add a check here whether the bugtask was found, asking them to
report a bug if not, otherwise they will get a confusing error.

This looks much better now, thanks.

> Another piece of the puzzle is that the user might be able to add a nomination but is not allowed to
> approve it. The new bugtask is only created when the nomination is approved.

I think an error if they request an impossible situation is fine. Wouldn't the --continue-on-error
support deal with that anyway if they wanted?

As it stands though I don't think it will continue if the person can't do it, as the code
for finding the task now assumes that a series task will be created if --series is specified.

Perhaps it should not assume that and then decide what to do based on --continue-on-error?

Thanks,

James

review: Approve
101. By Mattias Backman

Move getting bug_task for milestone to immediately after nomination.

102. By Mattias Backman

Continue after failed addNomination if --ignore-errors passed.

Revision history for this message
Mattias Backman (mabac) wrote :

> 53 + set_bugtask_milestone(ms_bugtask, milestone)
>
> I would probably add a check here whether the bugtask was found, asking them
> to
> report a bug if not, otherwise they will get a confusing error.

Good idea, I have added that.

> > Another piece of the puzzle is that the user might be able to add a
> nomination but is not allowed to
> > approve it. The new bugtask is only created when the nomination is approved.
>
> I think an error if they request an impossible situation is fine. Wouldn't the
> --continue-on-error
> support deal with that anyway if they wanted?

Yes it would. I changed it a bit so now series nomination can fail in a couple of ways and still try to set milestone if --ignore-errors is passed.

>
> As it stands though I don't think it will continue if the person can't do it,
> as the code
> for finding the task now assumes that a series task will be created if
> --series is specified.
>
> Perhaps it should not assume that and then decide what to do based on
> --continue-on-error?

Now it doesn't assume that and will set milestone even if the user has no permissions for targeting series.

I'll merge this in the morning if no-one comments on the last revisions.

Thanks,

Mattias

Revision history for this message
James Westby (james-w) wrote :

This looks good to me, so merge whenever you like :-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bug_reporting.py'
2--- bug_reporting.py 2011-04-15 06:59:55 +0000
3+++ bug_reporting.py 2011-04-20 14:50:23 +0000
4@@ -36,34 +36,114 @@
5 return project.self_link
6
7
8-def file_bug_by_url(target_url, description, title, tags, lp, importance=None):
9+def file_bug_by_url(target_url, description, title, tags, lp, importance=None,
10+ milestone_name=None, series_name=None):
11 """ File a bug on the bug target specified by target_url.
12
13 Returns the newly created bug or None if no bug was filed.
14 """
15+ logger.debug("lp.bugs.createBug(description=%s..., title=%s, target=%s, " \
16+ "tags=%s)" % (description[:20], title, target_url, tags))
17+
18 bug = lp.bugs.createBug(description=description,
19 title=title,
20 target=target_url,
21 tags=tags)
22 if bug is not None:
23 logger.info('Filed bug %i: %s' % (bug.id, bug.web_link))
24- if importance is not None:
25- set_bugtask_importance(bug, importance)
26+ try:
27+ target = lp.distributions['ubuntu']
28+ logger.debug("Series and milestone target is %s." % target)
29+ ms_bugtask = None
30+ if series_name is not None:
31+ series = target.getSeries(name_or_version=series_name)
32+ logger.debug("Adding nomination for series %s." % series)
33+ try:
34+ nomination = bug.addNomination(target=series)
35+ except:
36+ if continue_on_error():
37+ logger.warning("Cannot add nomination for series %s.",
38+ series)
39+ nomination = None
40+ else:
41+ raise
42+
43+ if nomination is not None and nomination.canApprove():
44+ logger.debug("Approving nomination %s." % nomination)
45+ nomination.approve()
46+ for candidate_bugtask in bug.bug_tasks:
47+ if candidate_bugtask.target == series:
48+ ms_bugtask = candidate_bugtask
49+ if ms_bugtask is None:
50+ raise Exception(
51+ "A bug task to set milestone was not found, " \
52+ "please report this bug on the svammel project.")
53+
54+ elif continue_on_error():
55+ logger.warning("Cannot approve nomination %s.", nomination)
56+ else:
57+ raise AttributeError(
58+ "Unable to approve nomination %s." % nomination)
59+
60+ if milestone_name is not None:
61+ milestone = target.getMilestone(name=milestone_name)
62+ logger.debug("Target milestone is %s." % milestone)
63+ if milestone is None:
64+ raise AttributeError(
65+ "Milestone '%s' not valid for target '%s'." % \
66+ (milestone_name, target.name))
67+
68+ if ms_bugtask is None:
69+ # If series nomination is done the bug_task
70+ # has already been found.
71+ ms_bugtask = bug.bug_tasks[0]
72+ set_bugtask_milestone(ms_bugtask, milestone)
73+
74+ if importance is not None:
75+ set_bug_importance(bug, importance)
76+ except Exception, e:
77+ if continue_on_error():
78+ logger.error("Error '%s' but will proceed.", e)
79+ else:
80+ logger.critical(
81+ "Closing bug before terminating, due to '%s'.", e)
82+ for bug_task in bug.bug_tasks:
83+ close_bug(bug_task, BUG_INVALID_STATUS)
84+ raise
85 else:
86 logger.error("Unable to file bug '%s'." % title)
87
88 return bug
89
90
91-def set_bugtask_importance(bug, importance):
92+def set_bug_importance(bug, importance):
93+ """Sets importance of all bug_tasks belonging to a bug."""
94 try:
95 for bug_task in bug.bug_tasks:
96+ logger.debug("Setting importance %s for %s" % \
97+ (importance, bug_task.title))
98 bug_task.importance = importance
99 bug_task.lp_save()
100 except:
101 if continue_on_error():
102 logger.error("Unable to set bug importance to '%s' but " \
103- "will proceed." % importance)
104+ "will proceed." % importance)
105+ else:
106+ raise
107+
108+
109+def set_bugtask_milestone(bug_task, milestone):
110+ """Sets the milestone for a bugtask."""
111+ try:
112+ logger.debug("Setting milestone %s for %s" % \
113+ (milestone.name, bug_task.title))
114+
115+ bug_task.milestone = milestone
116+ bug_task.lp_save()
117+ except:
118+ if continue_on_error():
119+ logger.error("Unable to set milestone '%s' but will " \
120+ "proceed." % (milestone.name))
121 else:
122 raise
123
124@@ -80,10 +160,16 @@
125 return len(dupe_bugs[:1]) > 0
126
127
128-def close_bug(url, close_status, lp):
129+def close_bug_by_url(url, close_status, lp):
130+ """ Close the bugtask by setting the status."""
131+
132+ bug_task = lp.load(url)
133+ close_bug(bug_task, close_status)
134+
135+
136+def close_bug(bug_task, close_status):
137 """ Close the bugtask by setting status to Invalid."""
138
139- bug_task = lp.load(url)
140 logger.info("Changing status for %s [%s ==> %s]" % \
141 (bug_task.title, bug_task.status, close_status))
142 bug_task.status = close_status
143
144=== modified file 'config.py'
145--- config.py 2011-04-15 06:47:57 +0000
146+++ config.py 2011-04-20 14:50:23 +0000
147@@ -168,5 +168,11 @@
148 '--ignore-errors', action='store_true', dest='ignoreerrors',
149 help='Ignore errors that do not prevent proper completion, such as ' \
150 'failure to set bug attributes.')
151+ parser.add_argument(
152+ '--milestone',
153+ help='If specified, will be set on all created bugs.')
154+ parser.add_argument(
155+ '--target-series', dest='targetseries',
156+ help='If specified, will be nominated for all created bugs.')
157
158 return parser
159
160=== modified file 'file-failures.py'
161--- file-failures.py 2011-04-15 07:40:10 +0000
162+++ file-failures.py 2011-04-20 14:50:23 +0000
163@@ -31,7 +31,7 @@
164 from bug_reporting import (
165 file_bug_by_url,
166 bug_already_filed_by_url,
167- close_bug,
168+ close_bug_by_url,
169 BUG_INVALID_STATUS,
170 BUG_FIXED_STATUS,
171 )
172@@ -139,7 +139,10 @@
173 (spph.package_name, spph.version, bug_tags))
174 if not args.dryrun:
175 bug = file_bug_by_url(spph.url, bug_description, bug_title,
176- bug_tags, get_launchpad(), args.importance)
177+ bug_tags, get_launchpad(),
178+ importance=args.importance,
179+ milestone_name=args.milestone,
180+ series_name=args.targetseries)
181 if bug is not None and args.logfile is not None:
182 bug_log.write_entry(
183 LogEntry(spph.package_name, spph.version, fail_platform,
184@@ -161,7 +164,8 @@
185 new_status = BUG_INVALID_STATUS
186 else:
187 new_status = BUG_FIXED_STATUS
188- close_bug(log_entry.bugtask_url, new_status, get_launchpad())
189+ close_bug_by_url(log_entry.bugtask_url, new_status,
190+ get_launchpad())
191 else:
192 bug = get_launchpad().load(log_entry.bugtask_url)
193 for bug_task in bug.bug_tasks:
194
195=== modified file 'tests/test_fail_filer.py'
196--- tests/test_fail_filer.py 2011-04-15 10:16:29 +0000
197+++ tests/test_fail_filer.py 2011-04-20 14:50:23 +0000
198@@ -37,9 +37,14 @@
199 filter_spph,
200 get_build_list,
201 )
202+import bug_reporting
203 from bug_reporting import (
204 bug_already_filed_by_url,
205- set_bugtask_importance,
206+ set_bug_importance,
207+ set_bugtask_milestone,
208+ close_bug,
209+ file_bug_by_url,
210+ BUG_INVALID_STATUS,
211 )
212 from config import (
213 get_service_root,
214@@ -117,7 +122,6 @@
215 Launchpad, 'login_with', classmethod(mock_func)))
216 init('staging', 'dummyapp', 'dummycache')
217
218-
219 def test_filter_only_checks_other_archives(self):
220 self.init_dummy_config()
221 archive_with_failure = self.MockArchive('failarchive')
222@@ -129,7 +133,8 @@
223 ]
224 spph = SPPH('packagetotest', '1')
225 failrecord = self.MockRecord(TEST_FAIL_ARCH, datetime.now(),
226- FAILED_BUILD_STATE, archive = archive_with_failure)
227+ FAILED_BUILD_STATE,
228+ archive=archive_with_failure)
229 spph.add_build_record(failrecord)
230 spph_list = dict()
231 spph_list[spph.package_name] = spph
232@@ -139,7 +144,6 @@
233 self.assertTrue(other_archive_2.has_been_called)
234 self.assertFalse(archive_with_failure.has_been_called)
235
236-
237 def test_filter_fail_record_not_filtered(self):
238 self.init_dummy_config()
239 spph = SPPH('packagefail', '1')
240@@ -189,10 +193,12 @@
241 packagename = 'packagesubstring'
242 archive = self.MockArchive('archivename')
243 exactrecord = self.MockRecord(TEST_FAIL_ARCH, datetime.now(),
244- SUCCESS_BUILD_STATE, packagename, archive)
245+ SUCCESS_BUILD_STATE, packagename,
246+ archive)
247 nonexactrecord = self.MockRecord(TEST_FAIL_ARCH, datetime.now(),
248 SUCCESS_BUILD_STATE,
249- 'pre' + packagename + 'post', archive)
250+ 'pre' + packagename + 'post',
251+ archive)
252 archive.setRecordsToReturn([exactrecord, nonexactrecord])
253 build_records = get_build_list(archive, SUCCESS_BUILD_STATE,
254 packagename)
255@@ -248,7 +254,8 @@
256 fail_spph = SPPH(packagename, version)
257 archive = self.MockArchive('archivename')
258 failrecord = self.MockRecord(TEST_FAIL_ARCH, datetime.now(),
259- FAILED_BUILD_STATE, packagename, version, archive)
260+ FAILED_BUILD_STATE, packagename, version,
261+ archive)
262 successrecord = self.MockRecord(TEST_OK_ARCHS[0], datetime.now(),
263 SUCCESS_BUILD_STATE, packagename,
264 version, archive)
265@@ -526,19 +533,85 @@
266 self.assertTrue(get_highest_version([None, None]) is None)
267
268
269-class TestFileBug(TestCase):
270+class TestFileBug(TestCaseWithFixtures):
271+
272+ class TestFailError(Exception):
273+ def __init__(self, value):
274+ self.value = value
275+
276+ def __str__(self):
277+ return repr(self.value)
278+
279+ class MockLaunchpad(object):
280+ class MockProject(object):
281+ def searchTasks(self, tags, tags_combinator):
282+ return self.bug_tasks
283+
284+ def __init__(self, bug_tasks):
285+ self.bug_tasks = bug_tasks
286+
287+ class Bugs(object):
288+ def createBug(self, description, title, target, tags):
289+ bug = TestFileBug.MockBug()
290+ bug_task1 = TestFileBug.MockBugTask(False)
291+ bug_task1.status = 'New'
292+ bug.add_bug_task(bug_task1)
293+ bug_task2 = TestFileBug.MockBugTask(False)
294+ bug_task2.status = 'New'
295+ bug.add_bug_task(bug_task2)
296+ return bug
297+
298+ def __init__(self, bug_tasks):
299+ self.bugs = self.Bugs()
300+ self.project = self.MockProject(bug_tasks)
301+ self.distributions = dict()
302+ self.distributions['ubuntu'] = TestFileBug.MockTarget()
303+
304+ def load(self, url):
305+ return self.project
306+
307+ class MockMilestone:
308+ def __init__(self, name):
309+ self.name = name
310
311 class MockBug:
312 def __init__(self):
313 self.bug_tasks = []
314+ self.id = 1
315+ self.web_link = 'http://dummy'
316+ self.title = 'Test bug'
317+
318 def add_bug_task(self, bug_task):
319 self.bug_tasks.append(bug_task)
320
321+ class MockTarget:
322+ class MockSeries:
323+ def __init__(self):
324+ self.name = 'Series'
325+
326+ def getMilestone(self, name):
327+ return TestFileBug.MockMilestone(name)
328+
329+ def __init__(self):
330+ self.name = 'Target'
331+ self.current_series = self.MockSeries()
332+
333+ def getMilestone(self, name):
334+ return TestFileBug.MockMilestone(name)
335+
336+ def getSeries(self, name):
337+ return self.MockSeries()
338+
339 class MockBugTask:
340+
341 def __init__(self, will_fail_on_save):
342 self.will_fail_on_save = will_fail_on_save
343 self.importance = 'Undecided'
344 self.has_saved = False
345+ self.target = TestFileBug.MockTarget()
346+ self.title = ''
347+ self.status = ''
348+
349 def lp_save(self):
350 if self.will_fail_on_save:
351 raise Exception()
352@@ -549,47 +622,99 @@
353 super(TestFileBug, self).setUp()
354
355 def test_duplicate_exists(self):
356- class MockLaunchpad(object):
357-
358- def load(obj, url):
359- class MockProject(object):
360- def searchTasks(obj, tags, tags_combinator):
361- return ['something here']
362- return MockProject()
363-
364+ lp = self.MockLaunchpad(['something here'])
365 self.assertTrue(bug_already_filed_by_url('https://dummy', [],
366- MockLaunchpad()))
367+ lp))
368
369 def test_duplicate_does_not_exist(self):
370- class MockLaunchpad(object):
371-
372- def load(obj, url):
373- class MockProject(object):
374- def searchTasks(obj, tags, tags_combinator):
375- return []
376- return MockProject()
377-
378+ lp = self.MockLaunchpad([])
379 self.assertFalse(bug_already_filed_by_url('https://dummy', [],
380- MockLaunchpad()))
381+ lp))
382+
383+ def test_close_bug_if_setting_importance_fails(self):
384+
385+ def raise_func(bug, importance):
386+ self.saved_bug = bug
387+ raise self.TestFailError('Setting importance failed.')
388+
389+ self.useFixture(MockSomethingFixture(
390+ bug_reporting, 'set_bug_importance', raise_func))
391+ try:
392+ self.saved_bug = None
393+ bug = file_bug_by_url('http://dummy', 'description', 'title', [],
394+ self.MockLaunchpad([]), importance='Low')
395+ self.fail('Error in test case, file_bug_by_url should have '\
396+ 'raised an error.')
397+ except self.TestFailError:
398+ for bug_task in self.saved_bug.bug_tasks:
399+ self.assertEquals(BUG_INVALID_STATUS, bug_task.status)
400+
401+ def test_close_bug_if_setting_milestone_fails(self):
402+
403+ def raise_func(bug_task, milestone):
404+ self.saved_bug_task = bug_task
405+ raise self.TestFailError('Setting milestone failed.')
406+
407+ self.useFixture(MockSomethingFixture(
408+ bug_reporting, 'set_bugtask_milestone', raise_func))
409+ try:
410+ self.saved_bug_task = None
411+ bug = file_bug_by_url('http://dummy', 'description', 'title', [],
412+ self.MockLaunchpad([]),
413+ milestone_name='1.0.0')
414+ self.fail('Error in test case, file_bug_by_url should have ' \
415+ 'raised an error.')
416+ except self.TestFailError:
417+ self.assertEquals(BUG_INVALID_STATUS, self.saved_bug_task.status)
418+
419+ def test_close_bug_saves(self):
420+ bug_task = self.MockBugTask(False)
421+ close_bug(bug_task, 'status')
422+ self.assertEquals(True, bug_task.has_saved)
423
424 def test_set_importance_saves(self):
425 bug = self.MockBug()
426 bug_task = self.MockBugTask(False)
427 bug.add_bug_task(bug_task)
428- set_bugtask_importance(bug, 'High')
429+ set_bug_importance(bug, 'High')
430 self.assertEquals(True, bug_task.has_saved)
431
432 def test_set_importance_can_ignore_errors(self):
433 set_ignore_errors(True)
434 bug = self.MockBug()
435 bug.add_bug_task(self.MockBugTask(True))
436- set_bugtask_importance(bug, 'High')
437+ set_bug_importance(bug, 'High')
438
439 def test_set_importance_raises(self):
440 set_ignore_errors(False)
441 bug = self.MockBug()
442 bug.add_bug_task(self.MockBugTask(True))
443- self.assertRaises(Exception, set_bugtask_importance, bug, 'High')
444+ self.assertRaises(Exception, set_bug_importance, bug, 'High')
445+
446+ def test_set_milestone_saves(self):
447+ bug_task = self.MockBugTask(False)
448+ milestone = self.MockMilestone('0.5.0')
449+ set_bugtask_milestone(bug_task, milestone)
450+ self.assertEquals(milestone, bug_task.milestone)
451+ self.assertTrue(bug_task.has_saved)
452+
453+ def test_set_milestone_can_ignore_errors(self):
454+ set_ignore_errors(True)
455+ bug_task = self.MockBugTask(True)
456+ milestone = self.MockMilestone('0.5.0')
457+ set_bugtask_milestone(bug_task, milestone)
458+
459+ def test_set_milestone_save_fail_raises(self):
460+ set_ignore_errors(False)
461+ bug_task = self.MockBugTask(True)
462+ milestone = self.MockMilestone('0.5.0')
463+ self.assertRaises(Exception, set_bugtask_milestone, bug_task,
464+ milestone)
465+
466+ def test_set_milestone_no_milestone_raises(self):
467+ set_ignore_errors(False)
468+ bug_task = self.MockBugTask(True)
469+ self.assertRaises(Exception, set_bugtask_milestone, bug_task, None)
470
471
472 class TestConfigInit(TestCaseWithFixtures):

Subscribers

People subscribed via source and target branches