summaryrefslogtreecommitdiffstats
path: root/media/tools/bug_hunter
diff options
context:
space:
mode:
authorimasaki@google.com <imasaki@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-21 22:06:46 +0000
committerimasaki@google.com <imasaki@google.com@0039d316-1c4b-4281-b951-d872f2087c98>2011-12-21 22:06:46 +0000
commit96aac6825eb2b9843ce24aaf15e8b527183c8117 (patch)
tree8ee59ba3a9fd039e18ed256219f0c2d240512ae5 /media/tools/bug_hunter
parentf506b603e8c075b73990263b9317ce35b0cc35fe (diff)
downloadchromium_src-96aac6825eb2b9843ce24aaf15e8b527183c8117.zip
chromium_src-96aac6825eb2b9843ce24aaf15e8b527183c8117.tar.gz
chromium_src-96aac6825eb2b9843ce24aaf15e8b527183c8117.tar.bz2
Initial checkin of chromium bug hunter script. Bug Hunter script will look into Chromium bug using specified query with specified time period and email the result to specified email address. It uses Issue tracker API (http://code.google.com/p/support/wiki/IssueTrackerAPI).
Review URL: http://codereview.chromium.org/8729012 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@115419 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'media/tools/bug_hunter')
-rw-r--r--media/tools/bug_hunter/bug_hunter.py372
-rw-r--r--media/tools/bug_hunter/bug_hunter_test.py97
-rw-r--r--media/tools/bug_hunter/bug_hunter_unittest.py183
3 files changed, 652 insertions, 0 deletions
diff --git a/media/tools/bug_hunter/bug_hunter.py b/media/tools/bug_hunter/bug_hunter.py
new file mode 100644
index 0000000..96a4289
--- /dev/null
+++ b/media/tools/bug_hunter/bug_hunter.py
@@ -0,0 +1,372 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This script queries the Chromium issue tracker and e-mails the results.
+
+It queries issue tracker using Issue Tracker API. The query
+parameters can be specified by command-line arguments. For example, with the
+following command:
+
+ 'python bug_hunter.py -q video Status:Unconfirmed OR audio Status:Unconfirmed
+ -s sender@chromium.org -r receiver@chromium.org -v 100 -u days'
+
+You will find all 'Unconfirmed' issues created in the last 100 days containing
+'video' or 'audio' in their content/comments. The content of these issues are
+sent to receiver@chromium.org.
+
+TODO(imasaki): users can specify the interval as say: "100d" for "100 days".
+
+There are two limitations in the current implementation of issue tracker API
+and UI:
+* only outermost OR is valid. For example, the query
+ 'video OR audio Status:Unconfirmed' is translated into
+ 'video OR (audio AND Status:Unconfirmed)'
+* brackets are not supported. For example, the query
+ '(video OR audio) Status:Unconfirmed' does not work.
+
+You need to install following to run this script
+ gdata-python-client (http://code.google.com/p/gdata-python-client/)
+ rfc3339.py (http://henry.precheur.org/projects/rfc3339)
+
+Links:
+* Chromium issue tracker: http://code.google.com/p/chromium/issues/list
+* Issue tracker API: http://code.google.com/p/support/wiki/IssueTrackerAPI
+* Search tips for the issue tracker:
+ http://code.google.com/p/chromium/issues/searchtips
+"""
+
+import csv
+import datetime
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
+import logging
+from operator import itemgetter
+import optparse
+import re
+import smtplib
+import socket
+import sys
+import urllib
+
+try:
+ import gdata.data
+ import gdata.projecthosting.client
+except ImportError:
+ logging.error('gdata-client needs to be installed. Please install\n'
+ 'and try again (http://code.google.com/p/gdata-python-client/)')
+ sys.exit(1)
+
+try:
+ import rfc3339
+except ImportError:
+ logging.error('rfc3339 needs to be installed. Please install\n'
+ 'and try again (http://henry.precheur.org/projects/rfc3339)')
+ sys.exit(1)
+
+# A list of default values.
+_DEFAULT_INTERVAL_UNIT = 'hours'
+_DEFAULT_ISSUE_ELEMENT_IN_EMAIL = ('author', 'status', 'state', 'content',
+ 'comments', 'labels', 'urls')
+_DEFAULT_PROJECT_NAME = 'chromium'
+_DEFAULT_QUERY_TITLE = 'potential media bugs'
+_DEFAULT_QUERY = ('video -has:Feature -has:Owner -label:nomedia '
+ 'status:Unconfirmed OR audio -has:Feature -has:Owner '
+ '-label:nomedia status:Unconfirmed')
+_DEFAULT_OUTPUT_FILENAME = 'output.csv'
+_DETAULT_MAX_COMMENTS = 1000
+
+_INTERVAL_UNIT_CHOICES = ('hours', 'days', 'weeks')
+
+# URLs in this list are excluded from URL extraction from bug
+# content/comments. Each list element should not contain the url ending in
+# '/'. For example, the element should be 'http://www.google.com' but not
+# 'http://www.google.com/'
+_URL_EXCLUSION_LIST = ('http://www.youtube.com/html5',
+ 'http://www.google.com')
+_ISSUE_ELEMENT_IN_EMAIL_CHOICES = ('issue_id', 'author', 'status', 'state',
+ 'content', 'comments', 'labels', 'urls')
+
+
+def ParseArgs():
+ """Returns options dictionary from parsed command line arguments."""
+ parser = optparse.OptionParser()
+
+ parser.add_option('-e', '--email-entries',
+ help=('A comma-separated list of issue entries that are '
+ 'sent in the email content. '
+ 'Possible strings are %s. Default: %%default.' %
+ ', '.join(_ISSUE_ELEMENT_IN_EMAIL_CHOICES)),
+ default=','.join(_DEFAULT_ISSUE_ELEMENT_IN_EMAIL))
+ parser.add_option('-l', '--max-comments',
+ help=('The maximum number of comments returned for each '
+ 'issue in a reverse chronological order. '
+ 'Default: %default.'),
+ type='int', default=_DETAULT_MAX_COMMENTS)
+ parser.add_option('-o', '--output-filename',
+ help=('Filename for result output in CSV format. '
+ 'Default: %default.'),
+ default=_DEFAULT_OUTPUT_FILENAME, metavar='FILE')
+ parser.add_option('-p', '--project-name', default=_DEFAULT_PROJECT_NAME,
+ help='Project name string. Default: %default')
+ parser.add_option('-q', '--query', default=_DEFAULT_QUERY,
+ help=('Query to be used to find bugs. The detail can be '
+ 'found in Chromium Issue tracker page '
+ 'http://code.google.com/p/chromium/issues/searchtips.'
+ ' Default: "%default".'))
+ parser.add_option('-r', '--receiver-email-address',
+ help="Receiver's email address (Required).")
+ parser.add_option('-s', '--sender-email-address',
+ help="Sender's email address (Required).")
+ parser.add_option('-t', '--query-title',
+ default=_DEFAULT_QUERY_TITLE, dest='query_title',
+ help=('Query title string used in the subject of the '
+ 'result email. Default: %default.'))
+ parser.add_option('-u', '--interval_unit', default=_DEFAULT_INTERVAL_UNIT,
+ choices=_INTERVAL_UNIT_CHOICES,
+ help=('Unit name for |interval_value|. Valid options are '
+ '%s. Default: %%default' % (
+ ', '.join(_INTERVAL_UNIT_CHOICES))))
+ parser.add_option('-v', '--interval-value', type='int',
+ help=('Interval value to find bugs. '
+ 'The script looks for bugs during '
+ 'that interval (up to now). This option is used in '
+ 'conjunction with |--interval_unit| option. '
+ 'The script looks for all bugs if this is not '
+ 'specified.'))
+
+ options = parser.parse_args()[0]
+
+ options.email_entries = options.email_entries.split(',')
+ options.email_entries = [entry for entry in options.email_entries
+ if entry in _ISSUE_ELEMENT_IN_EMAIL_CHOICES]
+ if not options.email_entries:
+ logging.warning('No issue elements in email in option. '
+ 'Default email entries will be used.')
+ options.email_entries = _DEFAULT_ISSUE_ELEMENT_IN_EMAIL
+ logging.info('The following is the issue elements in email: %s ' + (
+ ', '.join(options.email_entries)))
+ return options
+
+
+class BugHunter(object):
+ """This class queries issue trackers and e-mails the results."""
+
+ _ISSUE_SEARCH_LINK_BASE = ('http://code.google.com/p/chromium/issues/list?'
+ 'can=2&colspec=ID+Pri+Mstone+ReleaseBlock+Area'
+ '+Feature+Status+Owner+Summary&cells=tiles'
+ '&sort=-id')
+ # TODO(imasaki): Convert these into template library.
+ _EMAIL_ISSUE_TEMPLATE = ('<li><a href="http://crbug.com/%(issue_id)s">'
+ '%(issue_id)s %(title)s</a> ')
+ _EMAIL_SUBJECT_TEMPLATE = ('BugHunter found %(n_issues)d %(query_title)s '
+ 'bug%(plural)s%(time_msg)s!')
+ _EMAIL_MSG_TEMPLATE = ('<a href="%(link_base)s&q=%(unquote_query_text)s">'
+ 'Used Query</a>: %(query_text)s<br><br>'
+ 'The number of issues : %(n_issues)d<br>'
+ '<ul>%(issues)s</ul>')
+
+ def __init__(self, options):
+ """Sets up initial state for Bug Hunter.
+
+ Args:
+ options: Command-line options.
+ """
+ self._client = gdata.projecthosting.client.ProjectHostingClient()
+ self._options = options
+ self._issue_template = BugHunter._EMAIL_ISSUE_TEMPLATE
+ for entry in options.email_entries:
+ self._issue_template += '%%(%s)s ' % entry
+ self._issue_template += '</li>'
+
+ def GetComments(self, issue_id, max_comments):
+ """Get comments for a issue.
+
+ Args:
+ issue_id: Issue id for each issue in the issue tracker.
+ max_comments: The maximum number of comments to be returned. The comments
+ are returned in a reverse chronological order.
+
+ Returns:
+ A list of (author name, comments, updated time) tuples.
+ """
+ comments_feed = self._client.get_comments(self._options.project_name,
+ issue_id)
+ comment_list = [(comment.content.text, comment.author[0].name.text,
+ comment.updated.text)
+ for comment
+ in list(reversed(comments_feed.entry))[0:max_comments]]
+ return comment_list
+
+ def GetIssues(self):
+ """Get issues from issue tracker and return them.
+
+ Returns:
+ A list of issues in descending order by issue_id. Each element in the
+ list is a dictionary where the keys are 'issue_id', 'title', 'author',
+ 'status', 'state', 'content', 'comments', 'labels', 'urls'.
+ Returns an empty list when there is no matching issue.
+ """
+ min_time = None
+ if self._options.interval_value:
+ # Issue Tracker Data API uses RFC 3339 timestamp format, For example:
+ # 2005-08-09T10:57:00-08:00
+ # (http://code.google.com/p/support/wiki/IssueTrackerAPIPython)
+ delta = datetime.timedelta(
+ **{self._options.interval_unit: self._options.interval_value})
+ dt = datetime.datetime.now() - delta
+ min_time = rfc3339.rfc3339(dt)
+
+ query = gdata.projecthosting.client.Query(text_query=self._options.query,
+ max_results=1000,
+ published_min=min_time)
+
+ feed = self._client.get_issues(self._options.project_name, query=query)
+ if not feed.entry:
+ logging.info('No issues available to match query %s.',
+ self._options.query)
+ return []
+ issues = []
+ for entry in feed.entry:
+ # The fully qualified id is a URL. We just want the number.
+ issue_id = entry.id.text.split('/')[-1]
+ if not issue_id.isdigit():
+ logging.warning('Issue_id is not correct: %s. Skipping.', issue_id)
+ continue
+ label_list = [label.text for label in entry.label]
+ comments = ''
+ if 'comments' in self._options.email_entries:
+ comments = ''.join(
+ [''.join(comment) if not comment else ''
+ for comment
+ in self.GetComments(issue_id, self._options.max_comments)])
+ content = BugHunterUtils.StripHTML(entry.content.text)
+ url_list = list(
+ set(re.findall(r'(https?://\S+)', content + comments)))
+ url_list = [url for url in url_list
+ if not url.rstrip('/') in _URL_EXCLUSION_LIST]
+ issues.append({'issue_id': issue_id, 'title': entry.title.text,
+ 'author': entry.author[0].name.text,
+ 'status': entry.status.text,
+ 'state': entry.state.text, 'content': content,
+ 'comments': comments, 'labels': label_list,
+ 'urls': url_list})
+ return sorted(issues, key=itemgetter('issue_id'), reverse=True)
+
+ def _SetUpEmailSubjectMsg(self, issues):
+ """Set up email subject and its content.
+
+ Args:
+ issues: Please refer to the return value in GetIssues().
+
+ Returns:
+ A tuple of two strings (email subject and email content).
+ """
+ time_msg = ''
+ if self._options.interval_value:
+ time_msg = ' in the past %s %s%s' % (
+ self._options.interval_value, self._options.interval_unit[:-1],
+ 's' if self._options.interval_value > 1 else '')
+ subject = BugHunter._EMAIL_SUBJECT_TEMPLATE % {
+ 'n_issues': len(issues),
+ 'query_title': self._options.query_title,
+ 'plural': 's' if len(issues) > 1 else '',
+ 'time_msg': time_msg}
+ content = BugHunter._EMAIL_MSG_TEMPLATE % {
+ 'link_base': BugHunter._ISSUE_SEARCH_LINK_BASE,
+ 'unquote_query_text': urllib.quote(self._options.query),
+ 'query_text': self._options.query,
+ 'n_issues': len(issues),
+ 'issues': ''.join(
+ [self._issue_template % issue for issue in issues])}
+ return (subject, content)
+
+ def SendResultEmail(self, issues):
+ """Send result email.
+
+ Args:
+ issues: Please refer to the return value in GetIssues().
+ """
+ subject, content = self._SetUpEmailSubjectMsg(issues)
+ BugHunterUtils.SendEmail(
+ content, self._options.sender_email_address,
+ self._options.receiver_email_address, subject)
+
+ def WriteIssuesToFileInCSV(self, issues, filename):
+ """Write issues to a file in CSV format.
+
+ Args:
+ issues: Please refer to the return value in GetIssues().
+ filename: File name for CSV file.
+ """
+ with open(filename, 'w') as f:
+ writer = csv.writer(f)
+ # Write header first.
+ writer.writerow(issues[0].keys())
+ for issue in issues:
+ writer.writerow(
+ [unicode(value).encode('utf-8') for value in issue.values()])
+
+
+class BugHunterUtils(object):
+ """Utility class for Bug Hunter."""
+
+ @staticmethod
+ def StripHTML(string_with_html):
+ """Strip HTML tags from string.
+
+ Args:
+ string_with_html: A string with HTML tags.
+
+ Returns:
+ A string without HTML tags.
+ """
+ return re.sub('<[^<]+?>', '', string_with_html)
+
+ @staticmethod
+ def SendEmail(message, sender_email_address, receivers_email_address,
+ subject):
+ """Send email using localhost's mail server.
+
+ Args:
+ message: Email message to be sent.
+ sender_email_address: Sender's email address.
+ receivers_email_address: Receiver's email address.
+ subject: Email subject.
+
+ Returns:
+ True if successful; False, otherwise.
+ """
+ try:
+ html = '<html><head></head><body>%s</body></html>' % message
+ msg = MIMEMultipart('alternative')
+ msg['Subject'] = subject
+ msg['From'] = sender_email_address
+ msg['To'] = receivers_email_address
+ msg.attach(MIMEText(html.encode('utf-8'), 'html', _charset='utf-8'))
+ smtp_obj = smtplib.SMTP('localhost')
+ smtp_obj.sendmail(sender_email_address, receivers_email_address,
+ msg.as_string())
+ logging.info('Successfully sent email.')
+ smtp_obj.quit()
+ return True
+ except smtplib.SMTPException:
+ logging.exception('Authentication failed, unable to send email.')
+ except (socket.gaierror, socket.error, socket.herror):
+ logging.exception('Unable to send email.')
+ return False
+
+
+def Main():
+ ops = ParseArgs()
+ bh = BugHunter(ops)
+ issues = bh.GetIssues()
+ if issues and ops.sender_email_address and ops.receiver_email_address:
+ bh.SendResultEmail(issues)
+ if issues:
+ bh.WriteIssuesToFileInCSV(issues, ops.output_filename)
+
+
+if __name__ == '__main__':
+ Main()
diff --git a/media/tools/bug_hunter/bug_hunter_test.py b/media/tools/bug_hunter/bug_hunter_test.py
new file mode 100644
index 0000000..1b969e5
--- /dev/null
+++ b/media/tools/bug_hunter/bug_hunter_test.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Integration tests for bug hunter."""
+
+import csv
+from optparse import Values
+import os
+import unittest
+
+from bug_hunter import BugHunter
+
+try:
+ import gdata.data
+ import gdata.projecthosting.client
+except ImportError:
+ logging.error('gdata-client needs to be installed. Please install\n'
+ 'and try again (http://code.google.com/p/gdata-python-client/)')
+ sys.exit(1)
+
+
+class BugHunterTest(unittest.TestCase):
+ """Unit tests for the Bug Hunter class."""
+ _TEST_FILENAME = 'test.csv'
+
+ def _CleanTestFile(self):
+ if os.path.exists(self._TEST_FILENAME):
+ os.remove(self._TEST_FILENAME)
+
+ def setUp(self):
+ self._CleanTestFile()
+
+ def tearDown(self):
+ self._CleanTestFile()
+
+ def _GetIssue(self):
+ return [{'issue_id': '0', 'title': 'title', 'author': 'author',
+ 'status': 'status', 'state': 'state', 'content': 'content',
+ 'comments': [], 'labels': [], 'urls': []}]
+
+ def _GetDefaultOption(self, set_10_days_ago, query='steps'):
+ ops = Values()
+ ops.query = query
+ if set_10_days_ago:
+ ops.interval_value = 10
+ ops.interval_unit = 'days'
+ else:
+ ops.interval_value = None
+ ops.email_entries = ['comments']
+ ops.project_name = 'chromium'
+ ops.query_title = 'query title'
+ ops.max_comments = None
+ return ops
+
+ def testGetIssueReturnedIssue(self):
+ bh = BugHunter(
+ self._GetDefaultOption(False,
+ query=('audio opened-after:2010/10/10'
+ ' opened-before:2010/10/20')))
+ self.assertEquals(len(bh.GetIssues()), 18)
+
+ def testGetIssueReturnedIssueWithStatus(self):
+ ops = self._GetDefaultOption(False)
+ ops.query = 'Feature:Media* Status:Unconfirmed'
+ issues = BugHunter(ops).GetIssues()
+ for issue in issues:
+ self.assertEquals(issue['status'], 'Unconfirmed')
+
+ def testGetIssueReturnNoIssue(self):
+ ops = self._GetDefaultOption(True)
+ ops.query = 'thisshouldnotmatchpleaseignorethis*'
+ self.assertFalse(BugHunter(ops).GetIssues())
+
+ def testGetComments(self):
+ comments = BugHunter(self._GetDefaultOption(False)).GetComments(100000, 2)
+ self.assertEquals(len(comments), 2)
+ expected_comments = [(None, 'rby...@chromium.org',
+ '2011-10-31T19:54:40.000Z'),
+ (None, 'backer@chromium.org',
+ '2011-10-14T13:59:37.000Z')]
+ self.assertEquals(comments, expected_comments)
+
+ def testWriteIssuesToFileInCSV(self):
+ ops = self._GetDefaultOption(False)
+ bh = BugHunter(ops)
+ bh.WriteIssuesToFileInCSV(self._GetIssue(), self._TEST_FILENAME)
+
+ with open(self._TEST_FILENAME, 'r') as f:
+ reader = csv.reader(f)
+ self.assertEquals(reader.next(), ['status', 'content', 'state',
+ 'issue_id', 'urls', 'title', 'labels',
+ 'author', 'comments'])
+ self.assertEquals(reader.next(), ['status', 'content', 'state', '0',
+ '[]', 'title', '[]', 'author', '[]'])
+ self.assertRaises(StopIteration, reader.next)
diff --git a/media/tools/bug_hunter/bug_hunter_unittest.py b/media/tools/bug_hunter/bug_hunter_unittest.py
new file mode 100644
index 0000000..5dc8fd4
--- /dev/null
+++ b/media/tools/bug_hunter/bug_hunter_unittest.py
@@ -0,0 +1,183 @@
+#!/usr/bin/env python
+# Copyright (c) 2011 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit Tests for bug hunter."""
+
+import logging
+from optparse import Values
+import smtplib
+import sys
+import unittest
+
+from bug_hunter import BugHunter
+from bug_hunter import BugHunterUtils
+
+try:
+ import atom.data
+ import gdata.data
+ import gdata.projecthosting.client
+except ImportError:
+ logging.error('gdata-client needs to be installed. Please install\n'
+ 'and try again (http://code.google.com/p/gdata-python-client/)')
+ sys.exit(1)
+
+
+class MockClient(object):
+ """A mock class for gdata.projecthosting.client.ProjectHostingClient.
+
+ Mocking the very simple method invocations for get_issues() and
+ get_comments().
+ """
+
+ def _CreateIssues(self, n_issues):
+ feed = gdata.projecthosting.data.IssuesFeed()
+ for i in xrange(n_issues):
+ feed.entry.append(gdata.projecthosting.data.IssueEntry(
+ title=atom.data.Title(text='title'),
+ content=atom.data.Content(text='http://www.content.com'),
+ id=atom.data.Id(text='/' + str(i)),
+ status=gdata.projecthosting.data.Status(text='Unconfirmed'),
+ state=gdata.projecthosting.data.State(text='open'),
+ label=[gdata.projecthosting.data.Label('label1')],
+ author=[atom.data.Author(name=atom.data.Name(text='author'))]))
+ return feed
+
+ def get_issues(self, project_name, query):
+ """Get issues using mock object without calling the issue tracker API.
+
+ Based on query argument, this returns the dummy issues. The number of
+ dummy issues are specified in query.text_query.
+
+ Args:
+ project_name: A string for project name in the issue tracker.
+ query: A query object for querying the issue tracker.
+
+ Returns:
+ A IssuesFeed object that contains a simple test issue.
+ """
+ n_issues = 1
+ if query.text_query.isdigit():
+ n_issues = int(query.text_query)
+ return self._CreateIssues(n_issues)
+
+ def get_comments(self, project_name, issue_id):
+ """Get comments using mock object without calling the issue tracker API.
+
+ Args:
+ project_name: A string for project name in the issue tracker.
+ issue_id: Issue_id string.
+
+ Returns:
+ A CommentsFeed object that contains a simple test comment.
+ """
+ feed = gdata.projecthosting.data.CommentsFeed()
+ feed.entry = [gdata.projecthosting.data.CommentEntry(
+ id=atom.data.Id(text='/0'),
+ content=atom.data.Content(text='http://www.comments.com'),
+ updated=atom.data.Updated(text='Updated'),
+ author=[atom.data.Author(name=atom.data.Name(text='cauthor'))])]
+ return feed
+
+
+class BugHunterUnitTest(unittest.TestCase):
+ """Unit tests for the Bug Hunter class."""
+
+ def setUp(self):
+ self._old_client = gdata.projecthosting.client.ProjectHostingClient
+ gdata.projecthosting.client.ProjectHostingClient = MockClient
+
+ def tearDown(self):
+ gdata.projecthosting.client.ProjectHostingClient = self._old_client
+
+ def _GetDefaultOption(self, set_10_days_ago, query='steps'):
+ ops = Values()
+ ops.query = query
+ if set_10_days_ago:
+ ops.interval_value = 10
+ ops.interval_unit = 'days'
+ else:
+ ops.interval_value = None
+ ops.email_entries = ['comments']
+ ops.project_name = 'chromium'
+ ops.query_title = 'query title'
+ ops.max_comments = None
+ return ops
+
+ def _GetIssue(self, n_issues):
+ issues = []
+ for i in xrange(n_issues):
+ issues.append({'issue_id': str(i), 'title': 'title', 'author': 'author',
+ 'status': 'status', 'state': 'state',
+ 'content': 'content', 'comments': [],
+ 'labels': [], 'urls': []})
+ return issues
+
+ def testSetUpEmailSubjectMsg(self):
+ bh = BugHunter(self._GetDefaultOption(False))
+ subject, content = bh._SetUpEmailSubjectMsg(self._GetIssue(1))
+ self.assertEquals(subject,
+ 'BugHunter found 1 query title bug!')
+ self.assertEquals(content,
+ ('<a href="http://code.google.com/p/chromium/issues/'
+ 'list?can=2&colspec=ID+Pri+Mstone+ReleaseBlock+Area+'
+ 'Feature+Status+Owner+Summary&cells=tiles&sort=-id&'
+ 'q=steps">Used Query</a>: steps<br><br>The number of '
+ 'issues : 1<br><ul><li><a href="http://crbug.com/0">0 '
+ 'title</a> [] </li></ul>'))
+
+ def testSetUpEmailSubjectMsgMultipleIssues(self):
+ bh = BugHunter(self._GetDefaultOption(False))
+ subject, content = bh._SetUpEmailSubjectMsg(self._GetIssue(2))
+ self.assertEquals(subject,
+ 'BugHunter found 2 query title bugs!')
+
+ def testSetUpEmailSubjectMsgWith10DaysAgoAndAssertSubject(self):
+ bh = BugHunter(self._GetDefaultOption(True))
+ subject, _ = bh._SetUpEmailSubjectMsg(self._GetIssue(1))
+ self.assertEquals(subject,
+ ('BugHunter found 1 query title bug in the past 10 '
+ 'days!'))
+
+ def testGetIssuesWithMockClient(self):
+ bh = BugHunter(self._GetDefaultOption(False,
+ query=('dummy')))
+ expected_issues = [{'issue_id': '0', 'title': 'title', 'author': 'author',
+ 'status': 'Unconfirmed', 'state': 'open',
+ 'content': 'http://www.content.com',
+ 'comments': '', 'labels': ['label1'],
+ 'urls': ['http://www.content.com']}]
+ self.assertEquals(expected_issues, bh.GetIssues())
+
+
+class MockSmtp(object):
+ """A mock class for SMTP."""
+
+ def __init__(self, server):
+ pass
+
+ def sendmail(self, sender_email_address, receivers_email_addresses,
+ msg):
+ # TODO(imasaki): Do something here.
+ return True
+
+ def quit(self):
+ pass
+
+
+class BugHunterUtilsTest(unittest.TestCase):
+ """Unit tests for the Bug Hunter utility."""
+
+ def testStripHTML(self):
+ self.assertEquals(BugHunterUtils.StripHTML('<p>X</p>'), 'X')
+
+ def testStripHTMLEmpty(self):
+ self.assertEquals(BugHunterUtils.StripHTML(''), '')
+
+ def testSendEmail(self):
+ smtplib.SMTP = MockSmtp
+ self.assertEqual(BugHunterUtils.SendEmail('message', 'sender_email_address',
+ 'receivers_email_addresses',
+ 'subject'),
+ True)