webkit-patch: fix 'upload' command with Bugzilla 4.2.5
[WebKit-https.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 unittest2 as unittest
30 import datetime
31 import StringIO
32
33 from .bugzilla import Bugzilla, BugzillaQueries, 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 <link rel="Top" href="https://bugs.webkit.org/">
242     <link rel="Up" href="show_bug.cgi?id=27314">
243 """
244
245     def test_attachment_detail_bug_parsing(self):
246         bugzilla = Bugzilla()
247         self.assertEqual(27314, bugzilla._parse_bug_id_from_attachment_page(self._sample_attachment_detail_page))
248
249     def test_add_cc_to_bug(self):
250         bugzilla = Bugzilla()
251         bugzilla.browser = MockBrowser()
252         bugzilla.authenticate = lambda: None
253         expected_logs = "Adding ['adam@example.com'] to the CC list for bug 42\n"
254         OutputCapture().assert_outputs(self, bugzilla.add_cc_to_bug, [42, ["adam@example.com"]], expected_logs=expected_logs)
255
256     def _mock_control_item(self, name):
257         mock_item = Mock()
258         mock_item.name = name
259         return mock_item
260
261     def _mock_find_control(self, item_names=[], selected_index=0):
262         mock_control = Mock()
263         mock_control.items = [self._mock_control_item(name) for name in item_names]
264         mock_control.value = [item_names[selected_index]] if item_names else None
265         return lambda name, type: mock_control
266
267     def _assert_reopen(self, item_names=None, selected_index=None, extra_logs=None):
268         bugzilla = Bugzilla()
269         bugzilla.browser = MockBrowser()
270         bugzilla.authenticate = lambda: None
271
272         mock_find_control = self._mock_find_control(item_names, selected_index)
273         bugzilla.browser.find_control = mock_find_control
274         expected_logs = "Re-opening bug 42\n['comment']\n"
275         if extra_logs:
276             expected_logs += extra_logs
277         OutputCapture().assert_outputs(self, bugzilla.reopen_bug, [42, ["comment"]], expected_logs=expected_logs)
278
279     def test_reopen_bug(self):
280         self._assert_reopen(item_names=["REOPENED", "RESOLVED", "CLOSED"], selected_index=1)
281         self._assert_reopen(item_names=["UNCONFIRMED", "RESOLVED", "CLOSED"], selected_index=1)
282         extra_logs = "Did not reopen bug 42, it appears to already be open with status ['NEW'].\n"
283         self._assert_reopen(item_names=["NEW", "RESOLVED"], selected_index=0, extra_logs=extra_logs)
284
285     def test_file_object_for_upload(self):
286         bugzilla = Bugzilla()
287         file_object = StringIO.StringIO()
288         unicode_tor = u"WebKit \u2661 Tor Arne Vestb\u00F8!"
289         utf8_tor = unicode_tor.encode("utf-8")
290         self.assertEqual(bugzilla._file_object_for_upload(file_object), file_object)
291         self.assertEqual(bugzilla._file_object_for_upload(utf8_tor).read(), utf8_tor)
292         self.assertEqual(bugzilla._file_object_for_upload(unicode_tor).read(), utf8_tor)
293
294     def test_filename_for_upload(self):
295         bugzilla = Bugzilla()
296         mock_file = Mock()
297         mock_file.name = "foo"
298         self.assertEqual(bugzilla._filename_for_upload(mock_file, 1234), 'foo')
299         mock_timestamp = lambda: "now"
300         filename = bugzilla._filename_for_upload(StringIO.StringIO(), 1234, extension="patch", timestamp=mock_timestamp)
301         self.assertEqual(filename, "bug-1234-now.patch")
302
303     def test_commit_queue_flag(self):
304         bugzilla = Bugzilla()
305
306         bugzilla.committers = CommitterList(reviewers=[Reviewer("WebKit Reviewer", "reviewer@webkit.org")],
307             committers=[Committer("WebKit Committer", "committer@webkit.org")],
308             contributors=[Contributor("WebKit Contributor", "contributor@webkit.org")])
309
310         def assert_commit_queue_flag(mark_for_landing, mark_for_commit_queue, expected, username=None):
311             bugzilla.username = username
312             capture = OutputCapture()
313             capture.capture_output()
314             try:
315                 self.assertEqual(bugzilla._commit_queue_flag(mark_for_landing=mark_for_landing, mark_for_commit_queue=mark_for_commit_queue), expected)
316             finally:
317                 capture.restore_output()
318
319         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='unknown@webkit.org')
320         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org')
321         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org')
322         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='unknown@webkit.org')
323
324         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='contributor@webkit.org')
325         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='contributor@webkit.org')
326         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='?', username='contributor@webkit.org')
327         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='?', username='contributor@webkit.org')
328
329         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='committer@webkit.org')
330         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='committer@webkit.org')
331         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='committer@webkit.org')
332         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='committer@webkit.org')
333
334         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=False, expected='X', username='reviewer@webkit.org')
335         assert_commit_queue_flag(mark_for_landing=False, mark_for_commit_queue=True, expected='?', username='reviewer@webkit.org')
336         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=False, expected='+', username='reviewer@webkit.org')
337         assert_commit_queue_flag(mark_for_landing=True, mark_for_commit_queue=True, expected='+', username='reviewer@webkit.org')
338
339     def test__check_create_bug_response(self):
340         bugzilla = Bugzilla()
341
342         title_html_bugzilla_323 = "<title>Bug 101640 Submitted</title>"
343         self.assertEqual(bugzilla._check_create_bug_response(title_html_bugzilla_323), '101640')
344
345         title_html_bugzilla_425 = "<title>Bug 101640 Submitted &ndash; Testing webkit-patch again</title>"
346         self.assertEqual(bugzilla._check_create_bug_response(title_html_bugzilla_425), '101640')
347
348
349 class BugzillaQueriesTest(unittest.TestCase):
350     _sample_request_page = """
351 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
352                       "http://www.w3.org/TR/html4/loose.dtd">
353 <html>
354   <head>
355     <title>Request Queue</title>
356   </head>
357 <body>
358
359 <h3>Flag: review</h3>
360   <table class="requests" cellspacing="0" cellpadding="4" border="1">
361     <tr>
362         <th>Requester</th>
363         <th>Requestee</th>
364         <th>Bug</th>
365         <th>Attachment</th>
366         <th>Created</th>
367     </tr>
368     <tr>
369         <td>Shinichiro Hamaji &lt;hamaji&#64;chromium.org&gt;</td>
370         <td></td>
371         <td><a href="show_bug.cgi?id=30015">30015: text-transform:capitalize is failing in CSS2.1 test suite</a></td>
372         <td><a href="attachment.cgi?id=40511&amp;action=review">
373 40511: Patch v0</a></td>
374         <td>2009-10-02 04:58 PST</td>
375     </tr>
376     <tr>
377         <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
378         <td></td>
379         <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
380         <td><a href="attachment.cgi?id=40722&amp;action=review">
381 40722: Media controls, the simple approach</a></td>
382         <td>2009-10-06 09:13 PST</td>
383     </tr>
384     <tr>
385         <td>Zan Dobersek &lt;zandobersek&#64;gmail.com&gt;</td>
386         <td></td>
387         <td><a href="show_bug.cgi?id=26304">26304: [GTK] Add controls for playing html5 video.</a></td>
388         <td><a href="attachment.cgi?id=40723&amp;action=review">
389 40723: Adjust the media slider thumb size</a></td>
390         <td>2009-10-06 09:15 PST</td>
391     </tr>
392   </table>
393 </body>
394 </html>
395 """
396     _sample_quip_page = u"""
397 <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
398                       "http://www.w3.org/TR/html4/loose.dtd">
399 <html>
400   <head>
401     <title>Bugzilla Quip System</title>
402   </head>
403   <body>
404     <h2>
405
406       Existing quips:
407     </h2>
408     <ul>
409         <li>Everything should be made as simple as possible, but not simpler. - Albert Einstein</li>
410         <li>Good artists copy. Great artists steal. - Pablo Picasso</li>
411         <li>\u00e7gua mole em pedra dura, tanto bate at\u008e que fura.</li>
412
413     </ul>
414   </body>
415 </html>
416 """
417
418     def _assert_result_count(self, queries, html, count):
419         self.assertEqual(queries._parse_result_count(html), count)
420
421     def test_parse_result_count(self):
422         queries = BugzillaQueries(None)
423         # Pages with results, always list the count at least twice.
424         self._assert_result_count(queries, '<span class="bz_result_count">314 bugs found.</span><span class="bz_result_count">314 bugs found.</span>', 314)
425         self._assert_result_count(queries, '<span class="bz_result_count">Zarro Boogs found.</span>', 0)
426         self._assert_result_count(queries, '<span class="bz_result_count">\n \nOne bug found.</span>', 1)
427         self.assertRaises(Exception, queries._parse_result_count, ['Invalid'])
428
429     def test_request_page_parsing(self):
430         queries = BugzillaQueries(None)
431         self.assertEqual([40511, 40722, 40723], queries._parse_attachment_ids_request_query(self._sample_request_page))
432
433     def test_quip_page_parsing(self):
434         queries = BugzillaQueries(None)
435         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."]
436         self.assertEqual(expected_quips, queries._parse_quips(self._sample_quip_page))
437
438     def test_load_query(self):
439         queries = BugzillaQueries(Mock())
440         queries._load_query("request.cgi?action=queue&type=review&group=type")
441
442
443 class EditUsersParserTest(unittest.TestCase):
444     _example_user_results = """
445         <div id="bugzilla-body">
446         <p>1 user found.</p>
447         <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
448           <tr bgcolor="#6666FF">
449               <th align="left">Edit user...
450               </th>
451               <th align="left">Real name
452               </th>
453               <th align="left">Account History
454               </th>
455           </tr>
456           <tr>
457               <td >
458                   <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">
459                 abarth&#64;webkit.org
460                   </a>
461               </td>
462               <td >
463                 Adam Barth
464               </td>
465               <td >
466                   <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">
467                 View
468                   </a>
469               </td>
470           </tr>
471         </table>
472     """
473
474     _example_empty_user_results = """
475     <div id="bugzilla-body">
476     <p>0 users found.</p>
477     <table id="admin_table" border="1" cellpadding="4" cellspacing="0">
478       <tr bgcolor="#6666FF">
479           <th align="left">Edit user...
480           </th>
481           <th align="left">Real name
482           </th>
483           <th align="left">Account History
484           </th>
485       </tr>
486       <tr><td colspan="3" align="center"><i>&lt;none&gt;</i></td></tr>
487     </table>
488     """
489
490     def _assert_login_userid_pairs(self, results_page, expected_logins):
491         parser = EditUsersParser()
492         logins = parser.login_userid_pairs_from_edit_user_results(results_page)
493         self.assertEqual(logins, expected_logins)
494
495     def test_logins_from_editusers_results(self):
496         self._assert_login_userid_pairs(self._example_user_results, [("abarth@webkit.org", 1234)])
497         self._assert_login_userid_pairs(self._example_empty_user_results, [])
498
499     _example_user_page = """<table class="main"><tr>
500   <th><label for="login">Login name:</label></th>
501   <td>eric&#64;webkit.org
502   </td>
503 </tr>
504 <tr>
505   <th><label for="name">Real name:</label></th>
506   <td>Eric Seidel
507   </td>
508 </tr>
509     <tr>
510       <th>Group access:</th>
511       <td>
512         <table class="groups">
513           <tr>
514           </tr>
515           <tr>
516             <th colspan="2">User is a member of these groups</th>
517           </tr>
518             <tr class="direct">
519               <td class="checkbox"><input type="checkbox"
520                            id="group_7"
521                            name="group_7"
522                            value="1" checked="checked" /></td>
523               <td class="groupname">
524                 <label for="group_7">
525                   <strong>canconfirm:</strong>
526                   Can confirm a bug.
527                 </label>
528               </td>
529             </tr>
530             <tr class="direct">
531               <td class="checkbox"><input type="checkbox"
532                            id="group_6"
533                            name="group_6"
534                            value="1" /></td>
535               <td class="groupname">
536                 <label for="group_6">
537                   <strong>editbugs:</strong>
538                   Can edit all aspects of any bug.
539                 /label>
540               </td>
541             </tr>
542         </table>
543       </td>
544     </tr>
545
546   <tr>
547     <th>Product responsibilities:</th>
548     <td>
549         <em>none</em>
550     </td>
551   </tr>
552 </table>"""
553
554     def test_user_dict_from_edit_user_page(self):
555         parser = EditUsersParser()
556         user_dict = parser.user_dict_from_edit_user_page(self._example_user_page)
557         expected_user_dict = {u'login': u'eric@webkit.org', u'groups': set(['canconfirm']), u'name': u'Eric Seidel'}
558         self.assertEqual(expected_user_dict, user_dict)