09402b2dc3f72850f5aa834759f060390eb547de
[WebKit.git] / Tools / Scripts / webkitpy / common / net / bugzilla / bugzilla_unittest.py
1 # Copyright (C) 2011 Google Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are
5 # met:
6 #
7 #    * Redistributions of source code must retain the above copyright
8 # notice, this list of conditions and the following disclaimer.
9 #    * Redistributions in binary form must reproduce the above
10 # copyright notice, this list of conditions and the following disclaimer
11 # in the documentation and/or other materials provided with the
12 # distribution.
13 #    * Neither the name of Google Inc. nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29 import StringIO
30 import datetime
31 import unittest
32
33 from .bugzilla import Bugzilla, BugzillaQueries, CommitQueueFlag, EditUsersParser
34
35 from webkitpy.common.config import urls
36 from webkitpy.common.config.committers import Reviewer, Committer, Contributor, CommitterList
37 from webkitpy.common.system.outputcapture import OutputCapture
38 from webkitpy.common.net.web_mock import MockBrowser
39 from webkitpy.thirdparty.mock import Mock
40 from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup
41
42
43 class BugzillaTest(unittest.TestCase):
44     _example_attachment = '''
45         <attachment
46           isobsolete="1"
47           ispatch="1"
48           isprivate="0"
49         >
50         <attachid>33721</attachid>
51         <date>2009-07-29 10:23 PDT</date>
52         <desc>Fixed whitespace issue</desc>
53         <filename>patch</filename>
54         <type>text/plain</type>
55         <size>9719</size>
56         <attacher>christian.plesner.hansen@gmail.com</attacher>
57           <flag name="review"
58                 id="17931"
59                 status="+"
60                 setter="one@test.com"
61            />
62           <flag name="commit-queue"
63                 id="17932"
64                 status="+"
65                 setter="two@test.com"
66            />
67         </attachment>
68 '''
69     _expected_example_attachment_parsing = {
70         'attach_date': datetime.datetime(2009, 07, 29, 10, 23),
71         'bug_id': 100,
72         'is_obsolete': True,
73         'is_patch': True,
74         'id': 33721,
75         'url': "https://bugs.webkit.org/attachment.cgi?id=33721",
76         'name': "Fixed whitespace issue",
77         'type': "text/plain",
78         'review': '+',
79         'reviewer_email': 'one@test.com',
80         'commit-queue': '+',
81         'committer_email': 'two@test.com',
82         'attacher_email': 'christian.plesner.hansen@gmail.com',
83     }
84
85     def test_url_creation(self):
86         # FIXME: These would be all better as doctests
87         bugs = Bugzilla()
88         self.assertIsNone(bugs.bug_url_for_bug_id(None))
89         self.assertIsNone(bugs.short_bug_url_for_bug_id(None))
90         self.assertIsNone(bugs.attachment_url_for_id(None))
91
92     def test_parse_bug_id(self):
93         # Test that we can parse the urls we produce.
94         bugs = Bugzilla()
95         self.assertEqual(12345, urls.parse_bug_id(bugs.short_bug_url_for_bug_id(12345)))
96         self.assertEqual(12345, urls.parse_bug_id(bugs.bug_url_for_bug_id(12345)))
97         self.assertEqual(12345, urls.parse_bug_id(bugs.bug_url_for_bug_id(12345, xml=True)))
98
99     _bug_xml = """
100     <bug>
101           <bug_id>32585</bug_id>
102           <creation_ts>2009-12-15 15:17 PST</creation_ts>
103           <short_desc>bug to test webkit-patch&apos;s and commit-queue&apos;s failures</short_desc>
104           <delta_ts>2009-12-27 21:04:50 PST</delta_ts>
105           <reporter_accessible>1</reporter_accessible>
106           <cclist_accessible>1</cclist_accessible>
107           <classification_id>1</classification_id>
108           <classification>Unclassified</classification>
109           <product>WebKit</product>
110           <component>Tools / Tests</component>
111           <version>528+ (Nightly build)</version>
112           <rep_platform>PC</rep_platform>
113           <op_sys>Mac OS X 10.5</op_sys>
114           <bug_status>NEW</bug_status>
115           <priority>P2</priority>
116           <bug_severity>Normal</bug_severity>
117           <target_milestone>---</target_milestone>
118           <everconfirmed>1</everconfirmed>
119           <reporter name="Eric Seidel">eric@webkit.org</reporter>
120           <assigned_to name="Nobody">webkit-unassigned@lists.webkit.org</assigned_to>
121           <cc>foo@bar.com</cc>
122     <cc>example@example.com</cc>
123           <long_desc isprivate="0">
124             <who name="Eric Seidel">eric@webkit.org</who>
125             <bug_when>2009-12-15 15:17:28 PST</bug_when>
126             <thetext>bug to test webkit-patch and commit-queue failures
127
128 Ignore this bug.  Just for testing failure modes of webkit-patch and the commit-queue.</thetext>
129           </long_desc>
130           <attachment
131               isobsolete="0"
132               ispatch="1"
133               isprivate="0"
134           >
135             <attachid>45548</attachid>
136             <date>2009-12-27 23:51 PST</date>
137             <desc>Patch</desc>
138             <filename>bug-32585-20091228005112.patch</filename>
139             <type>text/plain</type>
140             <size>10882</size>
141             <attacher>mjs@apple.com</attacher>
142
143               <token>1261988248-dc51409e9c421a4358f365fa8bec8357</token>
144               <data encoding="base64">SW5kZXg6IFdlYktpdC9tYWMvQ2hhbmdlTG9nCj09PT09PT09PT09PT09PT09PT09PT09PT09PT09
145 removed-because-it-was-really-long
146 ZEZpbmlzaExvYWRXaXRoUmVhc29uOnJlYXNvbl07Cit9CisKIEBlbmQKIAogI2VuZGlmCg==
147 </data>
148
149             <flag name="review"
150                 id="27602"
151                 status="?"
152                 setter="mjs@apple.com"
153             />
154         </attachment>
155     </bug>
156 """
157
158     _single_bug_xml = """
159 <?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
160 <!DOCTYPE bugzilla SYSTEM "https://bugs.webkit.org/bugzilla.dtd">
161 <bugzilla version="3.2.3"
162           urlbase="https://bugs.webkit.org/"
163           maintainer="admin@webkit.org"
164           exporter="eric@webkit.org"
165 >
166 %s
167 </bugzilla>
168 """ % _bug_xml
169
170     _expected_example_bug_parsing = {
171         "id" : 32585,
172         "title" : u"bug to test webkit-patch's and commit-queue's failures",
173         "cc_emails" : ["foo@bar.com", "example@example.com"],
174         "reporter_email" : "eric@webkit.org",
175         "assigned_to_email" : "webkit-unassigned@lists.webkit.org",
176         "bug_status": "NEW",
177         "attachments" : [{
178             "attach_date": datetime.datetime(2009, 12, 27, 23, 51),
179             'name': u'Patch',
180             'url' : "https://bugs.webkit.org/attachment.cgi?id=45548",
181             'is_obsolete': False,
182             'review': '?',
183             'is_patch': True,
184             'attacher_email': 'mjs@apple.com',
185             'bug_id': 32585,
186             'type': 'text/plain',
187             'id': 45548
188         }],
189         "comments" : [{
190                 'comment_date': datetime.datetime(2009, 12, 15, 15, 17, 28),
191                 'comment_email': 'eric@webkit.org',
192                 'text': """bug to test webkit-patch and commit-queue failures
193
194 Ignore this bug.  Just for testing failure modes of webkit-patch and the commit-queue.""",
195         }]
196     }
197
198     # FIXME: This should move to a central location and be shared by more unit tests.
199     def _assert_dictionaries_equal(self, actual, expected):
200         # Make sure we aren't parsing more or less than we expect
201         self.assertItemsEqual(actual.keys(), expected.keys())
202
203         for key, expected_value in expected.items():
204             self.assertEqual(actual[key], expected_value, ("Failure for key: %s: Actual='%s' Expected='%s'" % (key, actual[key], expected_value)))
205
206     def test_parse_bug_dictionary_from_xml(self):
207         bug = Bugzilla()._parse_bug_dictionary_from_xml(self._single_bug_xml)
208         self._assert_dictionaries_equal(bug, self._expected_example_bug_parsing)
209
210     _sample_multi_bug_xml = """
211 <bugzilla version="3.2.3" urlbase="https://bugs.webkit.org/" maintainer="admin@webkit.org" exporter="eric@webkit.org">
212     %s
213     %s
214 </bugzilla>
215 """ % (_bug_xml, _bug_xml)
216
217     def test_parse_bugs_from_xml(self):
218         bugzilla = Bugzilla()
219         bugs = bugzilla._parse_bugs_from_xml(self._sample_multi_bug_xml)
220         self.assertEqual(len(bugs), 2)
221         self.assertEqual(bugs[0].id(), self._expected_example_bug_parsing['id'])
222         bugs = bugzilla._parse_bugs_from_xml("")
223         self.assertEqual(len(bugs), 0)
224
225     # This could be combined into test_bug_parsing later if desired.
226     def test_attachment_parsing(self):
227         bugzilla = Bugzilla()
228         soup = BeautifulSoup(self._example_attachment)
229         attachment_element = soup.find("attachment")
230         attachment = bugzilla._parse_attachment_element(attachment_element, self._expected_example_attachment_parsing['bug_id'])
231         self.assertTrue(attachment)
232         self._assert_dictionaries_equal(attachment, self._expected_example_attachment_parsing)
233
234     _sample_attachment_detail_page = """
235 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
236                       "http://www.w3.org/TR/html4/loose.dtd">
237 <html>
238   <head>
239     <title>
240   Attachment 41073 Details for Bug 27314</title>
241 <div id="bug_title">
242   Attachment 41073 Details for
243   <a class="bz_bug_link bz_status_ASSIGNED " title="Sample bug" href="show_bug.cgi?id=27314">Bug 27314</a>: Sample bug</div>
244 """
245
246     def test_attachment_detail_bug_parsing(self):
247         bugzilla = Bugzilla()
248         self.assertEqual(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
249
250     def test_add_cc_to_bug(self):
251         bugzilla = Bugzilla()
252         bugzilla.browser = MockBrowser()
253         bugzilla.authenticate = lambda: None
254         expected_logs = "Adding ['adam@example.com'] to the CC list for bug 42\n"
255         OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam@example.com"]], expected_logs=expected_logs)
256
257     def _mock_control_item(self, name):
258         mock_item = Mock()
259         mock_item.name = name
260         return mock_item
261
262     def _mock_find_control(self, item_names=[], selected_index=0):
263         mock_control = Mock()
264         mock_control.items = [self._mock_control_item(name) for name in item_names]
265         mock_control.value = [item_names[selected_index]] if item_names else None
266         return lambda name, type: mock_control
267
268     def _assert_reopen(self, item_names=None, selected_index=None, extra_logs=None):
269         bugzilla = Bugzilla()
270         bugzilla.browser = MockBrowser()
271         bugzilla.authenticate = lambda: None
272
273         mock_find_control = self._mock_find_control(item_names, selected_index)
274         bugzilla.browser.find_control = mock_find_control
275         expected_logs = "Re-opening bug 42\n['comment']\n"
276         if extra_logs:
277             expected_logs += extra_logs
278         OutputCapture().assert_outputs(self, bugzilla.reopen_bug, [42, ["comment"]], expected_logs=expected_logs)
279
280     def test_reopen_bug(self):
281         self._assert_reopen(item_names=["REOPENED", "RESOLVED", "CLOSED"], selected_index=1)
282         self._assert_reopen(item_names=["UNCONFIRMED", "RESOLVED", "CLOSED"], selected_index=1)
283         extra_logs = "Did not reopen bug 42, it appears to already be open with status ['NEW'].\n"
284         self._assert_reopen(item_names=["NEW", "RESOLVED"], selected_index=0, extra_logs=extra_logs)
285
286     def test_file_object_for_upload(self):
287         bugzilla = Bugzilla()
288         file_object = StringIO.StringIO()
289         unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
290         utf8_tor = unicode_tor.encode("utf-8")
291         self.assertEqual(bugzilla._file_object_for_upload(file_object), file_object)
292         self.assertEqual(bugzilla._file_object_for_upload(utf8_tor).read(), utf8_tor)
293         self.assertEqual(bugzilla._file_object_for_upload(unicode_tor).read(), utf8_tor)
294
295     def test_filename_for_upload(self):
296         bugzilla = Bugzilla()
297         mock_file = Mock()
298         mock_file.name = "foo"
299         self.assertEqual(bugzilla._filename_for_upload(mock_file, 1234), 'foo')
300         mock_timestamp = lambda: "now"
301         filename = bugzilla._filename_for_upload(StringIO.StringIO(), 1234, extension="patch", timestamp=mock_timestamp)
302         self.assertEqual(filename, "bug-1234-now.patch")
303
304     def test_commit_queue_flag(self):
305         bugzilla = Bugzilla()
306
307         bugzilla.committers = CommitterList(reviewers=[Reviewer("WebKit Reviewer", "reviewer@webkit.org")],
308             committers=[Committer("WebKit Committer", "committer@webkit.org")],
309             contributors=[Contributor("WebKit Contributor", "contributor@webkit.org")])
310
311         def assert_commit_queue_flag(commit_flag, expected, username=None):
312             bugzilla.username = username
313             capture = OutputCapture()
314             capture.capture_output()
315             try:
316                 self.assertEqual(bugzilla._commit_queue_flag(commit_flag), expected)
317             finally:
318                 capture.restore_output()
319
320         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='unknown@webkit.org')
321         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='unknown@webkit.org')
322         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='?', username='unknown@webkit.org')
323
324         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='contributor@webkit.org')
325         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='contributor@webkit.org')
326         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='?', username='contributor@webkit.org')
327
328         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='committer@webkit.org')
329         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='committer@webkit.org')
330         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='+', username='committer@webkit.org')
331
332         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_nothing, expected='X', username='reviewer@webkit.org')
333         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_commit_queue, expected='?', username='reviewer@webkit.org')
334         assert_commit_queue_flag(commit_flag=CommitQueueFlag.mark_for_landing, expected='+', username='reviewer@webkit.org')
335
336     def test__check_create_bug_response(self):
337         bugzilla = Bugzilla()
338
339         title_html_bugzilla_323 = "<title>Bug 101640 Submitted</title>"
340         self.assertEqual(bugzilla._check_create_bug_response(title_html_bugzilla_323), '101640')
341
342         title_html_bugzilla_425 = "<title>Bug 101640 Submitted &ndash; Testing webkit-patch again</title>"
343         self.assertEqual(bugzilla._check_create_bug_response(title_html_bugzilla_425), '101640')
344
345
346 class BugzillaQueriesTest(unittest.TestCase):
347     _sample_request_page = """
348 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
349                       "http://www.w3.org/TR/html4/loose.dtd">
350 <html>
351   <head>
352     <title>Request Queue</title>
353   </head>
354 <body>
355
356 <h3>Flag: review</h3>
357   <table class="requests" cellspacing="0" cellpadding="4" border="1">
358     <tr>
359         <th>Requester</th>
360         <th>Requestee</th>
361         <th>Bug</th>
362         <th>Attachment</th>
363         <th>Created</th>
364     </tr>
365     <tr>
366         <td>Shinichiro Hamaji &lt;hamaji&#64;chromium.org&gt;</td>
367         <td></td>
368         <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
369         <td><a href="attachment.cgi?id=40511&amp;action=review">
370 40511: Patch v0</a></td>
371         <td>2009-10-02 04:58 PST</td>
372     </tr>
373     <tr>
374         <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
375         <td></td>
376         <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
377         <td><a href="attachment.cgi?id=40722&amp;action=review">
378 40722: Media controls, the simple approach</a></td>
379         <td>2009-10-06 09:13 PST</td>
380     </tr>
381     <tr>
382         <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
383         <td></td>
384         <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
385         <td><a href="attachment.cgi?id=40723&amp;action=review">
386 40723: Adjust the media slider thumb size</a></td>
387         <td>2009-10-06 09:15 PST</td>
388     </tr>
389   </table>
390 </body>
391 </html>
392 """
393     _sample_quip_page = u"""
394 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
395                       "http://www.w3.org/TR/html4/loose.dtd">
396 <html>
397   <head>
398     <title>Bugzilla Quip System</title>
399   </head>
400   <body>
401     <h2>
402
403       Existing quips:
404     </h2>
405     <ul>
406         <li>Everything should be made as simple as possible, but not simpler. - Albert Einstein</li>
407         <li>Good artists copy. Great artists steal. - Pablo Picasso</li>
408         <li>\u00e7gua mole em pedra dura, tanto bate at\u008e que fura.</li>
409
410     </ul>
411   </body>
412 </html>
413 """
414
415     def _assert_result_count(self, queries, html, count):
416         self.assertEqual(queries._parse_result_count(html), count)
417
418     def test_parse_result_count(self):
419         queries = BugzillaQueries(None)
420         # Pages with results, always list the count at least twice.
421         self._assert_result_count(queries, '<span class="bz_result_count">314 bugs found.</span><span class="bz_result_count">314 bugs found.</span>', 314)
422         self._assert_result_count(queries, '<span class="bz_result_count">Zarro Boogs found.</span>', 0)
423         self._assert_result_count(queries, '<span class="bz_result_count">\n \nOne bug found.</span>', 1)
424         self.assertRaises(Exception, queries._parse_result_count, ['Invalid'])
425
426     def test_request_page_parsing(self):
427         queries = BugzillaQueries(None)
428         self.assertEqual([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
429         self.assertEqual([40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page, datetime.datetime(2009, 10, 4, 11, 38, 44)))
430
431     def test_quip_page_parsing(self):
432         queries = BugzillaQueries(None)
433         expected_quips = ["Everything should be made as simple as possible, but not simpler. - Albert Einstein", "Good artists copy. Great artists steal. - Pablo Picasso", u"\u00e7gua mole em pedra dura, tanto bate at\u008e que fura."]
434         self.assertEqual(expected_quips, queries._parse_quips(self._sample_quip_page))
435
436     def test_load_query(self):
437         queries = BugzillaQueries(Mock())
438         queries._load_query("request.cgi?action=queue&type=review&group=type")
439
440
441 class EditUsersParserTest(unittest.TestCase):
442     _example_user_results = """
443         <div id="bugzilla-body">
444         <p>1 user found.</p>
445         <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
446           <tr bgcolor="#6666FF">
447               <th align="left">Edit user...
448               </th>
449               <th align="left">Real name
450               </th>
451               <th align="left">Account History
452               </th>
453           </tr>
454           <tr>
455               <td >
456                   <a href="editusers.cgi?action=edit&amp;userid=1234&amp;matchvalue=login_name&amp;groupid=&amp;grouprestrict=&amp;matchtype=substr&amp;matchstr=abarth%40webkit.org">
457                 abarth&#64;webkit.org
458                   </a>
459               </td>
460               <td >
461                 Adam Barth
462               </td>
463               <td >
464                   <a href="editusers.cgi?action=activity&amp;userid=1234&amp;matchvalue=login_name&amp;groupid=&amp;grouprestrict=&amp;matchtype=substr&amp;matchstr=abarth%40webkit.org">
465                 View
466                   </a>
467               </td>
468           </tr>
469         </table>
470     """
471
472     _example_empty_user_results = """
473     <div id="bugzilla-body">
474     <p>0 users found.</p>
475     <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
476       <tr bgcolor="#6666FF">
477           <th align="left">Edit user...
478           </th>
479           <th align="left">Real name
480           </th>
481           <th align="left">Account History
482           </th>
483       </tr>
484       <tr><td colspan="3" align="center"><i>&lt;none&gt;</i></td></tr>
485     </table>
486     """
487
488     def _assert_login_userid_pairs(self, results_page, expected_logins):
489         parser = EditUsersParser()
490         logins = parser.login_userid_pairs_from_edit_user_results(results_page)
491         self.assertEqual(logins, expected_logins)
492
493     def test_logins_from_editusers_results(self):
494         self._assert_login_userid_pairs(self._example_user_results, [("abarth@webkit.org", 1234)])
495         self._assert_login_userid_pairs(self._example_empty_user_results, [])
496
497     _example_user_page = """<table class="main"><tr>
498   <th><label for="login">Login name:</label></th>
499   <td>eric&#64;webkit.org
500   </td>
501 </tr>
502 <tr>
503   <th><label for="name">Real name:</label></th>
504   <td>Eric Seidel
505   </td>
506 </tr>
507     <tr>
508       <th>Group access:</th>
509       <td>
510         <table class="groups">
511           <tr>
512           </tr>
513           <tr>
514             <th colspan="2">User is a member of these groups</th>
515           </tr>
516             <tr class="direct">
517               <td class="checkbox"><input type="checkbox"
518                            id="group_7"
519                            name="group_7"
520                            value="1" checked="checked" /></td>
521               <td class="groupname">
522                 <label for="group_7">
523                   <strong>canconfirm:</strong>
524                   Can confirm a bug.
525                 </label>
526               </td>
527             </tr>
528             <tr class="direct">
529               <td class="checkbox"><input type="checkbox"
530                            id="group_6"
531                            name="group_6"
532                            value="1" /></td>
533               <td class="groupname">
534                 <label for="group_6">
535                   <strong>editbugs:</strong>
536                   Can edit all aspects of any bug.
537                 /label>
538               </td>
539             </tr>
540         </table>
541       </td>
542     </tr>
543
544   <tr>
545     <th>Product responsibilities:</th>
546     <td>
547         <em>none</em>
548     </td>
549   </tr>
550 </table>"""
551
552     def test_user_dict_from_edit_user_page(self):
553         parser = EditUsersParser()
554         user_dict = parser.user_dict_from_edit_user_page(self._example_user_page)
555         expected_user_dict = {u'login': u'eric@webkit.org', u'groups': set(['canconfirm']), u'name': u'Eric Seidel'}
556         self.assertEqual(expected_user_dict, user_dict)