1#!/usr/bin/env python 2# Copyright (c) 2012 The Chromium Authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Main functions for the Layout Test Analyzer module.""" 7 8from datetime import datetime 9import optparse 10import os 11import sys 12import time 13 14import layouttest_analyzer_helpers 15from layouttest_analyzer_helpers import DEFAULT_REVISION_VIEW_URL 16import layouttests 17from layouttests import DEFAULT_LAYOUTTEST_SVN_VIEW_LOCATION 18 19from test_expectations import TestExpectations 20from trend_graph import TrendGraph 21 22# Predefined result directory. 23DEFAULT_RESULT_DIR = 'result' 24# TODO(shadi): Remove graph functions as they are not used any more. 25DEFAULT_GRAPH_FILE = os.path.join('graph', 'graph.html') 26# TODO(shadi): Check if these files are needed any more. 27DEFAULT_STATS_CSV_FILENAME = 'stats.csv' 28DEFAULT_ISSUES_CSV_FILENAME = 'issues.csv' 29# TODO(shadi): These are used only for |debug| mode. What is debug mode for? 30# AFAIK, we don't run debug mode, should be safe to remove. 31# Predefined result files for debug. 32CUR_TIME_FOR_DEBUG = '2011-09-11-19' 33CURRENT_RESULT_FILE_FOR_DEBUG = os.path.join(DEFAULT_RESULT_DIR, 34 CUR_TIME_FOR_DEBUG) 35PREV_TIME_FOR_DEBUG = '2011-09-11-18' 36 37# Text to append at the end of every analyzer result email. 38DEFAULT_EMAIL_APPEND_TEXT = ( 39 '<b><a href="https://groups.google.com/a/google.com/group/' 40 'layout-test-analyzer-result/topics">Email History</a></b><br>' 41 ) 42 43 44def ParseOption(): 45 """Parse command-line options using OptionParser. 46 47 Returns: 48 an object containing all command-line option information. 49 """ 50 option_parser = optparse.OptionParser() 51 52 option_parser.add_option('-r', '--receiver-email-address', 53 dest='receiver_email_address', 54 help=('receiver\'s email address. ' 55 'Result email is not sent if this is not ' 56 'specified.')) 57 option_parser.add_option('-g', '--debug-mode', dest='debug', 58 help=('Debug mode is used when you want to debug ' 59 'the analyzer by using local file rather ' 60 'than getting data from SVN. This shortens ' 61 'the debugging time (off by default).'), 62 action='store_true', default=False) 63 option_parser.add_option('-t', '--trend-graph-location', 64 dest='trend_graph_location', 65 help=('Location of the bug trend file; ' 66 'file is expected to be in Google ' 67 'Visualization API trend-line format ' 68 '(defaults to %default).'), 69 default=DEFAULT_GRAPH_FILE) 70 option_parser.add_option('-n', '--test-group-file-location', 71 dest='test_group_file_location', 72 help=('Location of the test group file; ' 73 'file is expected to be in CSV format ' 74 'and lists all test name patterns. ' 75 'When this option is not specified, ' 76 'the value of --test-group-name is used ' 77 'for a test name pattern.'), 78 default=None) 79 option_parser.add_option('-x', '--test-group-name', 80 dest='test_group_name', 81 help=('A name of test group. Either ' 82 '--test_group_file_location or this option ' 83 'needs to be specified.')) 84 option_parser.add_option('-d', '--result-directory-location', 85 dest='result_directory_location', 86 help=('Name of result directory location ' 87 '(default to %default).'), 88 default=DEFAULT_RESULT_DIR) 89 option_parser.add_option('-b', '--email-appended-text-file-location', 90 dest='email_appended_text_file_location', 91 help=('File location of the email appended text. ' 92 'The text is appended in the status email. ' 93 '(default to %default and no text is ' 94 'appended in that case).'), 95 default=None) 96 option_parser.add_option('-c', '--email-only-change-mode', 97 dest='email_only_change_mode', 98 help=('With this mode, email is sent out ' 99 'only when there is a change in the ' 100 'analyzer result compared to the previous ' 101 'result (off by default)'), 102 action='store_true', default=False) 103 option_parser.add_option('-q', '--dashboard-file-location', 104 dest='dashboard_file_location', 105 help=('Location of dashboard file. The results are ' 106 'not reported to the dashboard if this ' 107 'option is not specified.')) 108 option_parser.add_option('-z', '--issue-detail-mode', 109 dest='issue_detail_mode', 110 help=('With this mode, email includes issue details ' 111 '(links to the flakiness dashboard)' 112 ' (off by default)'), 113 action='store_true', default=False) 114 return option_parser.parse_args()[0] 115 116 117def GetCurrentAndPreviousResults(debug, test_group_file_location, 118 test_group_name, result_directory_location): 119 """Get current and the latest previous analyzer results. 120 121 In debug mode, they are read from predefined files. In non-debug mode, 122 current analyzer results are dynamically obtained from Blink SVN and 123 the latest previous result is read from the corresponding file. 124 125 Args: 126 debug: please refer to |options|. 127 test_group_file_location: please refer to |options|. 128 test_group_name: please refer to |options|. 129 result_directory_location: please refer to |options|. 130 131 Returns: 132 a tuple of the following: 133 prev_time: the previous time string that is compared against. 134 prev_analyzer_result_map: previous analyzer result map. Please refer to 135 layouttest_analyzer_helpers.AnalyzerResultMap. 136 analyzer_result_map: current analyzer result map. Please refer to 137 layouttest_analyzer_helpers.AnalyzerResultMap. 138 """ 139 if not debug: 140 if not test_group_file_location and not test_group_name: 141 print ('Either --test-group-name or --test_group_file_location must be ' 142 'specified. Exiting this program.') 143 sys.exit() 144 filter_names = [] 145 if test_group_file_location and os.path.exists(test_group_file_location): 146 filter_names = layouttests.LayoutTests.GetLayoutTestNamesFromCSV( 147 test_group_file_location) 148 parent_location_list = ( 149 layouttests.LayoutTests.GetParentDirectoryList(filter_names)) 150 recursion = True 151 else: 152 # When test group CSV file is not specified, test group name 153 # (e.g., 'media') is used for getting layout tests. 154 # The tests are in 155 # http://src.chromium.org/blink/trunk/LayoutTests/media 156 # Filtering is not set so all HTML files are considered as valid tests. 157 # Also, we look for the tests recursively. 158 if not test_group_file_location or ( 159 not os.path.exists(test_group_file_location)): 160 print ('Warning: CSV file (%s) does not exist. So it is ignored and ' 161 '%s is used for obtaining test names') % ( 162 test_group_file_location, test_group_name) 163 if not test_group_name.endswith('/'): 164 test_group_name += '/' 165 parent_location_list = [test_group_name] 166 filter_names = None 167 recursion = True 168 layouttests_object = layouttests.LayoutTests( 169 parent_location_list=parent_location_list, recursion=recursion, 170 filter_names=filter_names) 171 analyzer_result_map = layouttest_analyzer_helpers.AnalyzerResultMap( 172 layouttests_object.JoinWithTestExpectation(TestExpectations())) 173 result = layouttest_analyzer_helpers.FindLatestResult( 174 result_directory_location) 175 if result: 176 (prev_time, prev_analyzer_result_map) = result 177 else: 178 prev_time = None 179 prev_analyzer_result_map = None 180 else: 181 analyzer_result_map = layouttest_analyzer_helpers.AnalyzerResultMap.Load( 182 CURRENT_RESULT_FILE_FOR_DEBUG) 183 prev_time = PREV_TIME_FOR_DEBUG 184 prev_analyzer_result_map = ( 185 layouttest_analyzer_helpers.AnalyzerResultMap.Load( 186 os.path.join(DEFAULT_RESULT_DIR, prev_time))) 187 return (prev_time, prev_analyzer_result_map, analyzer_result_map) 188 189 190def SendEmail(prev_time, prev_analyzer_result_map, analyzer_result_map, 191 appended_text_to_email, email_only_change_mode, debug, 192 receiver_email_address, test_group_name, issue_detail_mode): 193 """Send result status email. 194 195 Args: 196 prev_time: the previous time string that is compared against. 197 prev_analyzer_result_map: previous analyzer result map. Please refer to 198 layouttest_analyzer_helpers.AnalyzerResultMap. 199 analyzer_result_map: current analyzer result map. Please refer to 200 layouttest_analyzer_helpers.AnalyzerResultMap. 201 appended_text_to_email: the text string to append to the status email. 202 email_only_change_mode: please refer to |options|. 203 debug: please refer to |options|. 204 receiver_email_address: please refer to |options|. 205 test_group_name: please refer to |options|. 206 issue_detail_mode: please refer to |options|. 207 208 Returns: 209 a tuple of the following: 210 result_change: a boolean indicating whether there is a change in the 211 result compared with the latest past result. 212 diff_map: please refer to 213 layouttest_analyzer_helpers.SendStatusEmail(). 214 simple_rev_str: a simple version of revision string that is sent in 215 the email. 216 rev: the latest revision number for the given test group. 217 rev_date: the latest revision date for the given test group. 218 email_content: email content string (without 219 |appended_text_to_email|) that will be shown on the dashboard. 220 """ 221 rev = '' 222 rev_date = '' 223 email_content = '' 224 if prev_analyzer_result_map: 225 diff_map = analyzer_result_map.CompareToOtherResultMap( 226 prev_analyzer_result_map) 227 result_change = (any(diff_map['whole']) or any(diff_map['skip']) or 228 any(diff_map['nonskip'])) 229 # Email only when |email_only_change_mode| is False or there 230 # is a change in the result compared to the last result. 231 simple_rev_str = '' 232 if not email_only_change_mode or result_change: 233 prev_time_in_float = datetime.strptime(prev_time, '%Y-%m-%d-%H') 234 prev_time_in_float = time.mktime(prev_time_in_float.timetuple()) 235 if debug: 236 cur_time_in_float = datetime.strptime(CUR_TIME_FOR_DEBUG, 237 '%Y-%m-%d-%H') 238 cur_time_in_float = time.mktime(cur_time_in_float.timetuple()) 239 else: 240 cur_time_in_float = time.time() 241 (rev_str, simple_rev_str, rev, rev_date) = ( 242 layouttest_analyzer_helpers.GetRevisionString(prev_time_in_float, 243 cur_time_in_float, 244 diff_map)) 245 email_content = analyzer_result_map.ConvertToString(prev_time, 246 diff_map, 247 issue_detail_mode) 248 if receiver_email_address: 249 layouttest_analyzer_helpers.SendStatusEmail( 250 prev_time, analyzer_result_map, diff_map, 251 receiver_email_address, test_group_name, 252 appended_text_to_email, email_content, rev_str, 253 email_only_change_mode) 254 if simple_rev_str: 255 simple_rev_str = '\'' + simple_rev_str + '\'' 256 else: 257 simple_rev_str = 'undefined' # GViz uses undefined for NONE. 258 else: 259 # Initial result should be written to tread-graph if there are no previous 260 # results. 261 result_change = True 262 diff_map = None 263 simple_rev_str = 'undefined' 264 email_content = analyzer_result_map.ConvertToString(None, diff_map, 265 issue_detail_mode) 266 return (result_change, diff_map, simple_rev_str, rev, rev_date, 267 email_content) 268 269 270def UpdateTrendGraph(start_time, analyzer_result_map, diff_map, simple_rev_str, 271 trend_graph_location): 272 """Update trend graph in GViz. 273 274 Annotate the graph with revision information. 275 276 Args: 277 start_time: the script starting time as a float value. 278 analyzer_result_map: current analyzer result map. Please refer to 279 layouttest_analyzer_helpers.AnalyzerResultMap. 280 diff_map: a map that has 'whole', 'skip' and 'nonskip' as keys. 281 Please refer to |diff_map| in 282 |layouttest_analyzer_helpers.SendStatusEmail()|. 283 simple_rev_str: a simple version of revision string that is sent in 284 the email. 285 trend_graph_location: the location of the trend graph that needs to be 286 updated. 287 288 Returns: 289 a dictionary that maps result data category ('whole', 'skip', 'nonskip', 290 'passingrate') to information tuple (a dictionary that maps test name 291 to its description, annotation, simple_rev_string) of the given result 292 data category. These tuples are used for trend graph update. 293 """ 294 # Trend graph update (if specified in the command-line argument) when 295 # there is change from the last result. 296 # Currently, there are two graphs (graph1 is for 'whole', 'skip', 297 # 'nonskip' and the graph2 is for 'passingrate'). Please refer to 298 # graph/graph.html. 299 # Sample JS annotation for graph1: 300 # [new Date(2011,8,12,10,41,32),224,undefined,'',52,undefined, 301 # undefined, 12, 'test1,','<a href="http://t</a>,',], 302 # This example lists 'whole' triple and 'skip' triple and 303 # 'nonskip' triple. Each triple is (the number of tests that belong to 304 # the test group, linked text, a link). The following code generates this 305 # automatically based on rev_string etc. 306 trend_graph = TrendGraph(trend_graph_location) 307 datetime_string = start_time.strftime('%Y,%m,%d,%H,%M,%S') 308 data_map = {} 309 passingrate_anno = '' 310 for test_group in ['whole', 'skip', 'nonskip']: 311 anno = 'undefined' 312 # Extract test description. 313 test_map = {} 314 for (test_name, value) in ( 315 analyzer_result_map.result_map[test_group].iteritems()): 316 test_map[test_name] = value['desc'] 317 test_str = '' 318 links = '' 319 if diff_map and diff_map[test_group]: 320 for i in [0, 1]: 321 for (name, _) in diff_map[test_group][i]: 322 test_str += name + ',' 323 # This is link to test HTML in SVN. 324 links += ('<a href="%s%s">%s</a>' % 325 (DEFAULT_LAYOUTTEST_SVN_VIEW_LOCATION, name, name)) 326 if test_str: 327 anno = '\'' + test_str + '\'' 328 # The annotation of passing rate is a union of all annotations. 329 passingrate_anno += anno 330 if links: 331 links = '\'' + links + '\'' 332 else: 333 links = 'undefined' 334 if test_group is 'whole': 335 data_map[test_group] = (test_map, anno, links) 336 else: 337 data_map[test_group] = (test_map, anno, simple_rev_str) 338 if not passingrate_anno: 339 passingrate_anno = 'undefined' 340 data_map['passingrate'] = ( 341 str(analyzer_result_map.GetPassingRate()), passingrate_anno, 342 simple_rev_str) 343 trend_graph.Update(datetime_string, data_map) 344 return data_map 345 346 347def UpdateDashboard(dashboard_file_location, test_group_name, data_map, 348 layouttest_root_path, rev, rev_date, email, 349 email_content): 350 """Update dashboard HTML file. 351 352 Args: 353 dashboard_file_location: the file location for the dashboard file. 354 test_group_name: please refer to |options|. 355 data_map: a dictionary that maps result data category ('whole', 'skip', 356 'nonskip', 'passingrate') to information tuple (a dictionary that maps 357 test name to its description, annotation, simple_rev_string) of the 358 given result data category. These tuples are used for trend graph 359 update. 360 layouttest_root_path: A location string where layout tests are stored. 361 rev: the latest revision number for the given test group. 362 rev_date: the latest revision date for the given test group. 363 email: email address of the owner for the given test group. 364 email_content: email content string (without |appended_text_to_email|) 365 that will be shown on the dashboard. 366 """ 367 # Generate a HTML file that contains all test names for each test group. 368 escaped_tg_name = test_group_name.replace('/', '_') 369 for tg in ['whole', 'skip', 'nonskip']: 370 file_name = os.path.join( 371 os.path.dirname(dashboard_file_location), 372 escaped_tg_name + '_' + tg + '.html') 373 file_object = open(file_name, 'wb') 374 file_object.write('<table border="1">') 375 sorted_testnames = data_map[tg][0].keys() 376 sorted_testnames.sort() 377 for testname in sorted_testnames: 378 file_object.write(( 379 '<tr><td><a href="%s">%s</a></td><td><a href="%s">dashboard</a>' 380 '</td><td>%s</td></tr>') % ( 381 layouttest_root_path + testname, testname, 382 ('http://test-results.appspot.com/dashboards/' 383 'flakiness_dashboard.html#tests=%s') % testname, 384 data_map[tg][0][testname])) 385 file_object.write('</table>') 386 file_object.close() 387 email_content_with_link = '' 388 if email_content: 389 file_name = os.path.join(os.path.dirname(dashboard_file_location), 390 escaped_tg_name + '_email.html') 391 file_object = open(file_name, 'wb') 392 file_object.write(email_content) 393 file_object.close() 394 email_content_with_link = '<a href="%s_email.html">info</a>' % ( 395 escaped_tg_name) 396 test_group_str = ( 397 '<td><a href="%(test_group_path)s">%(test_group_name)s</a></td>' 398 '<td><a href="%(graph_path)s">graph</a></td>' 399 '<td><a href="%(all_tests_path)s">%(all_tests_count)d</a></td>' 400 '<td><a href="%(skip_tests_path)s">%(skip_tests_count)d</a></td>' 401 '<td><a href="%(nonskip_tests_path)s">%(nonskip_tests_count)d</a></td>' 402 '<td>%(fail_rate)d%%</td>' 403 '<td>%(passing_rate)d%%</td>' 404 '<td><a href="%(rev_url)s">%(rev)s</a></td>' 405 '<td>%(rev_date)s</td>' 406 '<td><a href="mailto:%(email)s">%(email)s</a></td>' 407 '<td>%(email_content)s</td>\n') % { 408 # Dashboard file and graph must be in the same directory 409 # to make the following link work. 410 'test_group_path': layouttest_root_path + '/' + test_group_name, 411 'test_group_name': test_group_name, 412 'graph_path': escaped_tg_name + '.html', 413 'all_tests_path': escaped_tg_name + '_whole.html', 414 'all_tests_count': len(data_map['whole'][0]), 415 'skip_tests_path': escaped_tg_name + '_skip.html', 416 'skip_tests_count': len(data_map['skip'][0]), 417 'nonskip_tests_path': escaped_tg_name + '_nonskip.html', 418 'nonskip_tests_count': len(data_map['nonskip'][0]), 419 'fail_rate': 100 - float(data_map['passingrate'][0]), 420 'passing_rate': float(data_map['passingrate'][0]), 421 'rev_url': DEFAULT_REVISION_VIEW_URL % rev, 422 'rev': rev, 423 'rev_date': rev_date, 424 'email': email, 425 'email_content': email_content_with_link 426 } 427 layouttest_analyzer_helpers.ReplaceLineInFile( 428 dashboard_file_location, '<td>' + test_group_name + '</td>', 429 test_group_str) 430 431 432def main(): 433 """A main function for the analyzer.""" 434 options = ParseOption() 435 start_time = datetime.now() 436 437 (prev_time, prev_analyzer_result_map, analyzer_result_map) = ( 438 GetCurrentAndPreviousResults(options.debug, 439 options.test_group_file_location, 440 options.test_group_name, 441 options.result_directory_location)) 442 (result_change, diff_map, simple_rev_str, rev, rev_date, email_content) = ( 443 SendEmail(prev_time, prev_analyzer_result_map, analyzer_result_map, 444 DEFAULT_EMAIL_APPEND_TEXT, 445 options.email_only_change_mode, options.debug, 446 options.receiver_email_address, options.test_group_name, 447 options.issue_detail_mode)) 448 449 # Create CSV texts and save them for bug spreadsheet. 450 (stats, issues_txt) = analyzer_result_map.ConvertToCSVText( 451 start_time.strftime('%Y-%m-%d-%H')) 452 file_object = open(os.path.join(options.result_directory_location, 453 DEFAULT_STATS_CSV_FILENAME), 'wb') 454 file_object.write(stats) 455 file_object.close() 456 file_object = open(os.path.join(options.result_directory_location, 457 DEFAULT_ISSUES_CSV_FILENAME), 'wb') 458 file_object.write(issues_txt) 459 file_object.close() 460 461 if not options.debug and (result_change or not prev_analyzer_result_map): 462 # Save the current result when result is changed or the script is 463 # executed for the first time. 464 date = start_time.strftime('%Y-%m-%d-%H') 465 file_path = os.path.join(options.result_directory_location, date) 466 analyzer_result_map.Save(file_path) 467 if result_change or not prev_analyzer_result_map: 468 data_map = UpdateTrendGraph(start_time, analyzer_result_map, diff_map, 469 simple_rev_str, options.trend_graph_location) 470 # Report the result to dashboard. 471 if options.dashboard_file_location: 472 UpdateDashboard(options.dashboard_file_location, options.test_group_name, 473 data_map, layouttests.DEFAULT_LAYOUTTEST_LOCATION, rev, 474 rev_date, options.receiver_email_address, 475 email_content) 476 477 478if '__main__' == __name__: 479 main() 480