#!/usr/bin/env vpython3
#
# Copyright (C) 2013 Google Inc. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
# * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""Prints a list of test expectations for tests whose bugs haven't been modified recently."""
import csv
import datetime
import json
import optparse
from six import StringIO
import sys
from six.moves import urllib
from blinkpy.common.host import Host
# pylint: disable=no-name-in-module
from blinkpy.web_tests.models.test_expectations import TestExpectationParser
# FIXME: Make this a direct request to Monorail.
GOOGLE_CODE_URL = 'https://www.googleapis.com/projecthosting/v2/projects/chromium/issues/%s?key=AIzaSyDgCqT1Dt5AZWLHo4QJjyMHaCjhnFacGF0'
CRBUG_PREFIX = 'crbug.com/'
CSV_ROW_HEADERS = [
'crbug link', 'test file', 'days since last update', 'owner', 'status'
]
class BugInfo():
def __init__(self, bug_link, filename, days_since_last_update, owner,
status):
self.bug_link = bug_link
self.filename = filename
self.days_since_last_update = days_since_last_update
self.owner = owner
self.status = status
class StaleTestPrinter(object):
def __init__(self, options):
self.days = options.days
self.csv_filename = options.create_csv
self.host = Host()
self.bug_info = {}
def print_stale_tests(self):
port = self.host.port_factory.get()
expectations = port.expectations_dict()
parser = TestExpectationParser(port, all_tests=(), is_lint_mode=False)
expectations_file, expectations_contents = expectations.items()[0]
expectation_lines = parser.parse(expectations_file,
expectations_contents)
csv_rows = []
for line in expectation_lines:
row = self.check_expectations_line(line)
if row:
csv_rows.append(row)
if self.csv_filename:
self.write_csv(csv_rows)
def write_csv(self, rows):
out = StringIO.StringIO()
writer = csv.writer(out)
writer.writerow(CSV_ROW_HEADERS)
for row in rows:
writer.writerow(row)
self.host.filesystem.write_text_file(self.csv_filename, out.getvalue())
def check_expectations_line(self, line):
"""Checks the bugs in one test expectations line to see if they're stale.
Args:
line: A TestExpectationsLine instance.
Returns:
A CSV row (a list of strings), or None if there are no stale bugs.
"""
bug_links, test_name = line.bugs, line.name
try:
if bug_links:
# Prepopulate bug info.
for bug_link in bug_links:
self.populate_bug_info(bug_link, test_name)
# Return the stale bug's information.
if all(self.is_stale(bug_link) for bug_link in bug_links):
print(line.original_string.strip())
return [
bug_links[0], self.bug_info[bug_links[0]].filename,
self.bug_info[bug_links[0]].days_since_last_update,
self.bug_info[bug_links[0]].owner,
self.bug_info[bug_links[0]].status
]
except urllib.error.HTTPError as error:
if error.code == 404:
message = 'got 404, bug does not exist.'
elif error.code == 403:
message = 'got 403, not accessible. Not able to tell if it\'s stale.'
else:
message = str(error)
print(
'Error when checking %s: %s' % (','.join(bug_links), message),
sys.stderr)
return None
def populate_bug_info(self, bug_link, test_name):
if bug_link in self.bug_info:
return
# In case there's an error in the request, don't make the same request again.
bug_number = bug_link.strip(CRBUG_PREFIX)
url = GOOGLE_CODE_URL % bug_number
response = urllib.request.urlopen(url)
parsed = json.loads(response.read())
parsed_time = datetime.datetime.strptime(
parsed['updated'].split(".")[0] + "UTC", "%Y-%m-%dT%H:%M:%S%Z")
time_delta = datetime.datetime.now() - parsed_time
owner = 'none'
if 'owner' in parsed.keys():
owner = parsed['owner']['name']
self.bug_info[bug_link] = BugInfo(bug_link, test_name, time_delta.days,
owner, parsed['state'])
def is_stale(self, bug_link):
return self.bug_info[bug_link].days_since_last_update > self.days
def main(argv):
option_parser = optparse.OptionParser()
option_parser.add_option(
'--days',
type='int',
default=90,
help='Number of days to consider a bug stale.')
option_parser.add_option(
'--create-csv',
type='string',
default='',
help=
'Filename of CSV file to write stale entries to. No file will be written if no name specified.'
)
options, _ = option_parser.parse_args(argv)
printer = StaleTestPrinter(options)
printer.print_stale_tests()
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))