#!/usr/bin/env python
# Copyright (c) 2012 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.
"""Main functions for the Layout Test Analyzer module."""
from datetime import datetime
import optparse
import os
import sys
import time
import layouttest_analyzer_helpers
from layouttest_analyzer_helpers import DEFAULT_REVISION_VIEW_URL
import layouttests
from layouttests import DEFAULT_LAYOUTTEST_SVN_VIEW_LOCATION
from test_expectations import TestExpectations
from trend_graph import TrendGraph
# Predefined result directory.
DEFAULT_RESULT_DIR = 'result'
# TODO(shadi): Remove graph functions as they are not used any more.
DEFAULT_GRAPH_FILE = os.path.join('graph', 'graph.html')
# TODO(shadi): Check if these files are needed any more.
DEFAULT_STATS_CSV_FILENAME = 'stats.csv'
DEFAULT_ISSUES_CSV_FILENAME = 'issues.csv'
# TODO(shadi): These are used only for |debug| mode. What is debug mode for?
# AFAIK, we don't run debug mode, should be safe to remove.
# Predefined result files for debug.
CUR_TIME_FOR_DEBUG = '2011-09-11-19'
CURRENT_RESULT_FILE_FOR_DEBUG = os.path.join(DEFAULT_RESULT_DIR,
CUR_TIME_FOR_DEBUG)
PREV_TIME_FOR_DEBUG = '2011-09-11-18'
# Text to append at the end of every analyzer result email.
DEFAULT_EMAIL_APPEND_TEXT = (
'Email History
'
)
def ParseOption():
"""Parse command-line options using OptionParser.
Returns:
an object containing all command-line option information.
"""
option_parser = optparse.OptionParser()
option_parser.add_option('-r', '--receiver-email-address',
dest='receiver_email_address',
help=('receiver\'s email address. '
'Result email is not sent if this is not '
'specified.'))
option_parser.add_option('-g', '--debug-mode', dest='debug',
help=('Debug mode is used when you want to debug '
'the analyzer by using local file rather '
'than getting data from SVN. This shortens '
'the debugging time (off by default).'),
action='store_true', default=False)
option_parser.add_option('-t', '--trend-graph-location',
dest='trend_graph_location',
help=('Location of the bug trend file; '
'file is expected to be in Google '
'Visualization API trend-line format '
'(defaults to %default).'),
default=DEFAULT_GRAPH_FILE)
option_parser.add_option('-n', '--test-group-file-location',
dest='test_group_file_location',
help=('Location of the test group file; '
'file is expected to be in CSV format '
'and lists all test name patterns. '
'When this option is not specified, '
'the value of --test-group-name is used '
'for a test name pattern.'),
default=None)
option_parser.add_option('-x', '--test-group-name',
dest='test_group_name',
help=('A name of test group. Either '
'--test_group_file_location or this option '
'needs to be specified.'))
option_parser.add_option('-d', '--result-directory-location',
dest='result_directory_location',
help=('Name of result directory location '
'(default to %default).'),
default=DEFAULT_RESULT_DIR)
option_parser.add_option('-b', '--email-appended-text-file-location',
dest='email_appended_text_file_location',
help=('File location of the email appended text. '
'The text is appended in the status email. '
'(default to %default and no text is '
'appended in that case).'),
default=None)
option_parser.add_option('-c', '--email-only-change-mode',
dest='email_only_change_mode',
help=('With this mode, email is sent out '
'only when there is a change in the '
'analyzer result compared to the previous '
'result (off by default)'),
action='store_true', default=False)
option_parser.add_option('-q', '--dashboard-file-location',
dest='dashboard_file_location',
help=('Location of dashboard file. The results are '
'not reported to the dashboard if this '
'option is not specified.'))
option_parser.add_option('-z', '--issue-detail-mode',
dest='issue_detail_mode',
help=('With this mode, email includes issue details '
'(links to the flakiness dashboard)'
' (off by default)'),
action='store_true', default=False)
return option_parser.parse_args()[0]
def GetCurrentAndPreviousResults(debug, test_group_file_location,
test_group_name, result_directory_location):
"""Get current and the latest previous analyzer results.
In debug mode, they are read from predefined files. In non-debug mode,
current analyzer results are dynamically obtained from Blink SVN and
the latest previous result is read from the corresponding file.
Args:
debug: please refer to |options|.
test_group_file_location: please refer to |options|.
test_group_name: please refer to |options|.
result_directory_location: please refer to |options|.
Returns:
a tuple of the following:
prev_time: the previous time string that is compared against.
prev_analyzer_result_map: previous analyzer result map. Please refer to
layouttest_analyzer_helpers.AnalyzerResultMap.
analyzer_result_map: current analyzer result map. Please refer to
layouttest_analyzer_helpers.AnalyzerResultMap.
"""
if not debug:
if not test_group_file_location and not test_group_name:
print ('Either --test-group-name or --test_group_file_location must be '
'specified. Exiting this program.')
sys.exit()
filter_names = []
if test_group_file_location and os.path.exists(test_group_file_location):
filter_names = layouttests.LayoutTests.GetLayoutTestNamesFromCSV(
test_group_file_location)
parent_location_list = (
layouttests.LayoutTests.GetParentDirectoryList(filter_names))
recursion = True
else:
# When test group CSV file is not specified, test group name
# (e.g., 'media') is used for getting layout tests.
# The tests are in
# http://src.chromium.org/blink/trunk/LayoutTests/media
# Filtering is not set so all HTML files are considered as valid tests.
# Also, we look for the tests recursively.
if not test_group_file_location or (
not os.path.exists(test_group_file_location)):
print ('Warning: CSV file (%s) does not exist. So it is ignored and '
'%s is used for obtaining test names') % (
test_group_file_location, test_group_name)
if not test_group_name.endswith('/'):
test_group_name += '/'
parent_location_list = [test_group_name]
filter_names = None
recursion = True
layouttests_object = layouttests.LayoutTests(
parent_location_list=parent_location_list, recursion=recursion,
filter_names=filter_names)
analyzer_result_map = layouttest_analyzer_helpers.AnalyzerResultMap(
layouttests_object.JoinWithTestExpectation(TestExpectations()))
result = layouttest_analyzer_helpers.FindLatestResult(
result_directory_location)
if result:
(prev_time, prev_analyzer_result_map) = result
else:
prev_time = None
prev_analyzer_result_map = None
else:
analyzer_result_map = layouttest_analyzer_helpers.AnalyzerResultMap.Load(
CURRENT_RESULT_FILE_FOR_DEBUG)
prev_time = PREV_TIME_FOR_DEBUG
prev_analyzer_result_map = (
layouttest_analyzer_helpers.AnalyzerResultMap.Load(
os.path.join(DEFAULT_RESULT_DIR, prev_time)))
return (prev_time, prev_analyzer_result_map, analyzer_result_map)
def SendEmail(prev_time, prev_analyzer_result_map, analyzer_result_map,
appended_text_to_email, email_only_change_mode, debug,
receiver_email_address, test_group_name, issue_detail_mode):
"""Send result status email.
Args:
prev_time: the previous time string that is compared against.
prev_analyzer_result_map: previous analyzer result map. Please refer to
layouttest_analyzer_helpers.AnalyzerResultMap.
analyzer_result_map: current analyzer result map. Please refer to
layouttest_analyzer_helpers.AnalyzerResultMap.
appended_text_to_email: the text string to append to the status email.
email_only_change_mode: please refer to |options|.
debug: please refer to |options|.
receiver_email_address: please refer to |options|.
test_group_name: please refer to |options|.
issue_detail_mode: please refer to |options|.
Returns:
a tuple of the following:
result_change: a boolean indicating whether there is a change in the
result compared with the latest past result.
diff_map: please refer to
layouttest_analyzer_helpers.SendStatusEmail().
simple_rev_str: a simple version of revision string that is sent in
the email.
rev: the latest revision number for the given test group.
rev_date: the latest revision date for the given test group.
email_content: email content string (without
|appended_text_to_email|) that will be shown on the dashboard.
"""
rev = ''
rev_date = ''
email_content = ''
if prev_analyzer_result_map:
diff_map = analyzer_result_map.CompareToOtherResultMap(
prev_analyzer_result_map)
result_change = (any(diff_map['whole']) or any(diff_map['skip']) or
any(diff_map['nonskip']))
# Email only when |email_only_change_mode| is False or there
# is a change in the result compared to the last result.
simple_rev_str = ''
if not email_only_change_mode or result_change:
prev_time_in_float = datetime.strptime(prev_time, '%Y-%m-%d-%H')
prev_time_in_float = time.mktime(prev_time_in_float.timetuple())
if debug:
cur_time_in_float = datetime.strptime(CUR_TIME_FOR_DEBUG,
'%Y-%m-%d-%H')
cur_time_in_float = time.mktime(cur_time_in_float.timetuple())
else:
cur_time_in_float = time.time()
(rev_str, simple_rev_str, rev, rev_date) = (
layouttest_analyzer_helpers.GetRevisionString(prev_time_in_float,
cur_time_in_float,
diff_map))
email_content = analyzer_result_map.ConvertToString(prev_time,
diff_map,
issue_detail_mode)
if receiver_email_address:
layouttest_analyzer_helpers.SendStatusEmail(
prev_time, analyzer_result_map, diff_map,
receiver_email_address, test_group_name,
appended_text_to_email, email_content, rev_str,
email_only_change_mode)
if simple_rev_str:
simple_rev_str = '\'' + simple_rev_str + '\''
else:
simple_rev_str = 'undefined' # GViz uses undefined for NONE.
else:
# Initial result should be written to tread-graph if there are no previous
# results.
result_change = True
diff_map = None
simple_rev_str = 'undefined'
email_content = analyzer_result_map.ConvertToString(None, diff_map,
issue_detail_mode)
return (result_change, diff_map, simple_rev_str, rev, rev_date,
email_content)
def UpdateTrendGraph(start_time, analyzer_result_map, diff_map, simple_rev_str,
trend_graph_location):
"""Update trend graph in GViz.
Annotate the graph with revision information.
Args:
start_time: the script starting time as a float value.
analyzer_result_map: current analyzer result map. Please refer to
layouttest_analyzer_helpers.AnalyzerResultMap.
diff_map: a map that has 'whole', 'skip' and 'nonskip' as keys.
Please refer to |diff_map| in
|layouttest_analyzer_helpers.SendStatusEmail()|.
simple_rev_str: a simple version of revision string that is sent in
the email.
trend_graph_location: the location of the trend graph that needs to be
updated.
Returns:
a dictionary that maps result data category ('whole', 'skip', 'nonskip',
'passingrate') to information tuple (a dictionary that maps test name
to its description, annotation, simple_rev_string) of the given result
data category. These tuples are used for trend graph update.
"""
# Trend graph update (if specified in the command-line argument) when
# there is change from the last result.
# Currently, there are two graphs (graph1 is for 'whole', 'skip',
# 'nonskip' and the graph2 is for 'passingrate'). Please refer to
# graph/graph.html.
# Sample JS annotation for graph1:
# [new Date(2011,8,12,10,41,32),224,undefined,'',52,undefined,
# undefined, 12, 'test1,','%s' %
(DEFAULT_LAYOUTTEST_SVN_VIEW_LOCATION, name, name))
if test_str:
anno = '\'' + test_str + '\''
# The annotation of passing rate is a union of all annotations.
passingrate_anno += anno
if links:
links = '\'' + links + '\''
else:
links = 'undefined'
if test_group is 'whole':
data_map[test_group] = (test_map, anno, links)
else:
data_map[test_group] = (test_map, anno, simple_rev_str)
if not passingrate_anno:
passingrate_anno = 'undefined'
data_map['passingrate'] = (
str(analyzer_result_map.GetPassingRate()), passingrate_anno,
simple_rev_str)
trend_graph.Update(datetime_string, data_map)
return data_map
def UpdateDashboard(dashboard_file_location, test_group_name, data_map,
layouttest_root_path, rev, rev_date, email,
email_content):
"""Update dashboard HTML file.
Args:
dashboard_file_location: the file location for the dashboard file.
test_group_name: please refer to |options|.
data_map: a dictionary that maps result data category ('whole', 'skip',
'nonskip', 'passingrate') to information tuple (a dictionary that maps
test name to its description, annotation, simple_rev_string) of the
given result data category. These tuples are used for trend graph
update.
layouttest_root_path: A location string where layout tests are stored.
rev: the latest revision number for the given test group.
rev_date: the latest revision date for the given test group.
email: email address of the owner for the given test group.
email_content: email content string (without |appended_text_to_email|)
that will be shown on the dashboard.
"""
# Generate a HTML file that contains all test names for each test group.
escaped_tg_name = test_group_name.replace('/', '_')
for tg in ['whole', 'skip', 'nonskip']:
file_name = os.path.join(
os.path.dirname(dashboard_file_location),
escaped_tg_name + '_' + tg + '.html')
file_object = open(file_name, 'wb')
file_object.write('
%s | dashboard' ' | %s |