Roll src/third_party/WebKit 787a07c:716df21 (svn 201034:201036)
[chromium-blink-merge.git] / media / tools / layout_tests / layouttest_analyzer_helpers.py
blobe5791bc62a69b77b8e98c4be6960b38e8da9c132
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Helper functions for the layout test analyzer."""
7 from datetime import datetime
8 from email.mime.multipart import MIMEMultipart
9 from email.mime.text import MIMEText
10 import fileinput
11 import os
12 import pickle
13 import re
14 import smtplib
15 import socket
16 import sys
17 import time
19 from bug import Bug
20 from test_expectations_history import TestExpectationsHistory
22 DEFAULT_TEST_EXPECTATION_PATH = ('trunk/LayoutTests/TestExpectations')
23 LEGACY_DEFAULT_TEST_EXPECTATION_PATH = (
24 'trunk/LayoutTests/platform/chromium/test_expectations.txt')
25 REVISION_LOG_URL = ('http://build.chromium.org/f/chromium/perf/dashboard/ui/'
26 'changelog_blink.html?url=/trunk/LayoutTests/%s&range=%d:%d')
27 DEFAULT_REVISION_VIEW_URL = 'http://src.chromium.org/viewvc/blink?revision=%s'
30 class AnalyzerResultMap:
31 """A class to deal with joined result produed by the analyzer.
33 The join is done between layouttests and the test_expectations object
34 (based on the test expectation file). The instance variable |result_map|
35 contains the following keys: 'whole','skip','nonskip'. The value of 'whole'
36 contains information about all layouttests. The value of 'skip' contains
37 information about skipped layouttests where it has 'SKIP' in its entry in
38 the test expectation file. The value of 'nonskip' contains all information
39 about non skipped layout tests, which are in the test expectation file but
40 not skipped. The information is exactly same as the one parsed by the
41 analyzer.
42 """
44 def __init__(self, test_info_map):
45 """Initialize the AnalyzerResultMap based on test_info_map.
47 Test_info_map contains all layouttest information. The job here is to
48 classify them as 'whole', 'skip' or 'nonskip' based on that information.
50 Args:
51 test_info_map: the result map of |layouttests.JoinWithTestExpectation|.
52 The key of the map is test name such as 'media/media-foo.html'.
53 The value of the map is a map that contains the following keys:
54 'desc'(description), 'te_info' (test expectation information),
55 which is a list of test expectation information map. The key of the
56 test expectation information map is test expectation keywords such
57 as "SKIP" and other keywords (for full list of keywords, please
58 refer to |test_expectations.ALL_TE_KEYWORDS|).
59 """
60 self.result_map = {}
61 self.result_map['whole'] = {}
62 self.result_map['skip'] = {}
63 self.result_map['nonskip'] = {}
64 if test_info_map:
65 for (k, value) in test_info_map.iteritems():
66 self.result_map['whole'][k] = value
67 if 'te_info' in value:
68 # Don't count SLOW PASS, WONTFIX, or ANDROID tests as failures.
69 if any([True for x in value['te_info'] if set(x.keys()) ==
70 set(['SLOW', 'PASS', 'Bugs', 'Comments', 'Platforms']) or
71 'WONTFIX' in x or x['Platforms'] == ['ANDROID']]):
72 continue
73 if any([True for x in value['te_info'] if 'SKIP' in x]):
74 self.result_map['skip'][k] = value
75 else:
76 self.result_map['nonskip'][k] = value
78 @staticmethod
79 def GetDiffString(diff_map_element, type_str):
80 """Get difference string out of diff map element.
82 The difference string shows difference between two analyzer results
83 (for example, a result for now and a result for sometime in the past)
84 in HTML format (with colors). This is used for generating email messages.
86 Args:
87 diff_map_element: An element of the compared map generated by
88 |CompareResultMaps()|. The element has two lists of test cases. One
89 is for test names that are in the current result but NOT in the
90 previous result. The other is for test names that are in the previous
91 results but NOT in the current result. Please refer to comments in
92 |CompareResultMaps()| for details.
93 type_str: a string indicating the test group to which |diff_map_element|
94 belongs; used for color determination. Must be 'whole', 'skip', or
95 'nonskip'.
97 Returns:
98 a string in HTML format (with colors) to show difference between two
99 analyzer results.
101 if not diff_map_element[0] and not diff_map_element[1]:
102 return 'No Change'
103 color = ''
104 diff = len(diff_map_element[0]) - len(diff_map_element[1])
105 if diff > 0 and type_str != 'whole':
106 color = 'red'
107 else:
108 color = 'green'
109 diff_sign = ''
110 if diff > 0:
111 diff_sign = '+'
112 if not diff:
113 whole_str = 'No Change'
114 else:
115 whole_str = '<font color="%s">%s%d</font>' % (color, diff_sign, diff)
116 colors = ['red', 'green']
117 if type_str == 'whole':
118 # Bug 107773 - when we increase the number of tests,
119 # the name of the tests are in red, it should be green
120 # since it is good thing.
121 colors = ['green', 'red']
122 str1 = ''
123 for (name, _) in diff_map_element[0]:
124 str1 += '<font color="%s">%s,</font>' % (colors[0], name)
125 str2 = ''
126 for (name, _) in diff_map_element[1]:
127 str2 += '<font color="%s">%s,</font>' % (colors[1], name)
128 if str1 or str2:
129 whole_str += ':'
130 if str1:
131 whole_str += str1
132 if str2:
133 whole_str += str2
134 # Remove the last occurrence of ','.
135 whole_str = ''.join(whole_str.rsplit(',', 1))
136 return whole_str
138 def GetPassingRate(self):
139 """Get passing rate.
141 Returns:
142 layout test passing rate of this result in percent.
144 Raises:
145 ValueEror when the number of tests in test group "whole" is equal
146 or less than that of "skip".
148 delta = len(self.result_map['whole'].keys()) - (
149 len(self.result_map['skip'].keys()))
150 if delta <= 0:
151 raise ValueError('The number of tests in test group "whole" is equal or '
152 'less than that of "skip"')
153 return 100 - len(self.result_map['nonskip'].keys()) * 100.0 / delta
155 def ConvertToCSVText(self, current_time):
156 """Convert |self.result_map| into stats and issues text in CSV format.
158 Both are used as inputs for Google spreadsheet.
160 Args:
161 current_time: a string depicting a time in year-month-day-hour
162 format (e.g., 2011-11-08-16).
164 Returns:
165 a tuple of stats and issues_txt
166 stats: analyzer result in CSV format that shows:
167 (current_time, the number of tests, the number of skipped tests,
168 the number of failing tests, passing rate)
169 For example,
170 "2011-11-10-15,204,22,12,94"
171 issues_txt: issues listed in CSV format that shows:
172 (BUGWK or BUGCR, bug number, the test expectation entry,
173 the name of the test)
174 For example,
175 "BUGWK,71543,TIMEOUT PASS,media/media-element-play-after-eos.html,
176 BUGCR,97657,IMAGE CPU MAC TIMEOUT PASS,media/audio-repaint.html,"
178 stats = ','.join([current_time, str(len(self.result_map['whole'].keys())),
179 str(len(self.result_map['skip'].keys())),
180 str(len(self.result_map['nonskip'].keys())),
181 str(self.GetPassingRate())])
182 issues_txt = ''
183 for bug_txt, test_info_list in (
184 self.GetListOfBugsForNonSkippedTests().iteritems()):
185 matches = re.match(r'(BUG(CR|WK))(\d+)', bug_txt)
186 bug_suffix = ''
187 bug_no = ''
188 if matches:
189 bug_suffix = matches.group(1)
190 bug_no = matches.group(3)
191 issues_txt += bug_suffix + ',' + bug_no + ','
192 for test_info in test_info_list:
193 test_name, te_info = test_info
194 issues_txt += ' '.join(te_info.keys()) + ',' + test_name + ','
195 issues_txt += '\n'
196 return stats, issues_txt
198 def ConvertToString(self, prev_time, diff_map, issue_detail_mode):
199 """Convert this result to HTML display for email.
201 Args:
202 prev_time: the previous time string that are compared against.
203 diff_map: the compared map generated by |CompareResultMaps()|.
204 issue_detail_mode: includes the issue details in the output string if
205 this is True.
207 Returns:
208 a analyzer result string in HTML format.
210 return_str = ''
211 if diff_map:
212 return_str += (
213 '<b>Statistics (Diff Compared to %s):</b><ul>'
214 '<li>The number of tests: %d (%s)</li>'
215 '<li>The number of failing skipped tests: %d (%s)</li>'
216 '<li>The number of failing non-skipped tests: %d (%s)</li>'
217 '<li>Passing rate: %.2f %%</li></ul>') % (
218 prev_time, len(self.result_map['whole'].keys()),
219 AnalyzerResultMap.GetDiffString(diff_map['whole'], 'whole'),
220 len(self.result_map['skip'].keys()),
221 AnalyzerResultMap.GetDiffString(diff_map['skip'], 'skip'),
222 len(self.result_map['nonskip'].keys()),
223 AnalyzerResultMap.GetDiffString(diff_map['nonskip'], 'nonskip'),
224 self.GetPassingRate())
225 if issue_detail_mode:
226 return_str += '<b>Current issues about failing non-skipped tests:</b>'
227 for (bug_txt, test_info_list) in (
228 self.GetListOfBugsForNonSkippedTests().iteritems()):
229 return_str += '<ul>%s' % Bug(bug_txt)
230 for test_info in test_info_list:
231 (test_name, te_info) = test_info
232 gpu_link = ''
233 if 'GPU' in te_info:
234 gpu_link = 'group=%40ToT%20GPU%20Mesa%20-%20chromium.org&'
235 dashboard_link = ('http://test-results.appspot.com/dashboards/'
236 'flakiness_dashboard.html#%stests=%s') % (
237 gpu_link, test_name)
238 return_str += '<li><a href="%s">%s</a> (%s) </li>' % (
239 dashboard_link, test_name, ' '.join(
240 [key for key in te_info.keys() if key != 'Platforms']))
241 return_str += '</ul>\n'
242 return return_str
244 def CompareToOtherResultMap(self, other_result_map):
245 """Compare this result map with the other to see if there are any diff.
247 The comparison is done for layouttests which belong to 'whole', 'skip',
248 or 'nonskip'.
250 Args:
251 other_result_map: another result map to be compared against the result
252 map of the current object.
254 Returns:
255 a map that has 'whole', 'skip' and 'nonskip' as keys.
256 Please refer to |diff_map| in |SendStatusEmail()|.
258 comp_result_map = {}
259 for name in ['whole', 'skip', 'nonskip']:
260 if name == 'nonskip':
261 # Look into expectation to get diff only for non-skipped tests.
262 lookIntoTestExpectationInfo = True
263 else:
264 # Otherwise, only test names are compared to get diff.
265 lookIntoTestExpectationInfo = False
266 comp_result_map[name] = GetDiffBetweenMaps(
267 self.result_map[name], other_result_map.result_map[name],
268 lookIntoTestExpectationInfo)
269 return comp_result_map
271 @staticmethod
272 def Load(file_path):
273 """Load the object from |file_path| using pickle library.
275 Args:
276 file_path: the string path to the file from which to read the result.
278 Returns:
279 a AnalyzerResultMap object read from |file_path|.
281 file_object = open(file_path)
282 analyzer_result_map = pickle.load(file_object)
283 file_object.close()
284 return analyzer_result_map
286 def Save(self, file_path):
287 """Save the object to |file_path| using pickle library.
289 Args:
290 file_path: the string path to the file in which to store the result.
292 file_object = open(file_path, 'wb')
293 pickle.dump(self, file_object)
294 file_object.close()
296 def GetListOfBugsForNonSkippedTests(self):
297 """Get a list of bugs for non-skipped layout tests.
299 This is used for generating email content.
301 Returns:
302 a mapping from bug modifier text (e.g., BUGCR1111) to a test name and
303 main test information string which excludes comments and bugs.
304 This is used for grouping test names by bug.
306 bug_map = {}
307 for (name, value) in self.result_map['nonskip'].iteritems():
308 for te_info in value['te_info']:
309 main_te_info = {}
310 for k in te_info.keys():
311 if k != 'Comments' and k != 'Bugs':
312 main_te_info[k] = True
313 if 'Bugs' in te_info:
314 for bug in te_info['Bugs']:
315 if bug not in bug_map:
316 bug_map[bug] = []
317 bug_map[bug].append((name, main_te_info))
318 return bug_map
321 def SendStatusEmail(prev_time, analyzer_result_map, diff_map,
322 receiver_email_address, test_group_name,
323 appended_text_to_email, email_content, rev_str,
324 email_only_change_mode):
325 """Send status email.
327 Args:
328 prev_time: the date string such as '2011-10-09-11'. This format has been
329 used in this analyzer.
330 analyzer_result_map: current analyzer result.
331 diff_map: a map that has 'whole', 'skip' and 'nonskip' as keys.
332 The values of the map are the result of |GetDiffBetweenMaps()|.
333 The element has two lists of test cases. One (with index 0) is for
334 test names that are in the current result but NOT in the previous
335 result. The other (with index 1) is for test names that are in the
336 previous results but NOT in the current result.
337 For example (test expectation information is omitted for
338 simplicity),
339 comp_result_map['whole'][0] = ['foo1.html']
340 comp_result_map['whole'][1] = ['foo2.html']
341 This means that current result has 'foo1.html' but it is NOT in the
342 previous result. This also means the previous result has 'foo2.html'
343 but it is NOT in the current result.
344 receiver_email_address: receiver's email address.
345 test_group_name: string representing the test group name (e.g., 'media').
346 appended_text_to_email: a text which is appended at the end of the status
347 email.
348 email_content: an email content string that will be shown on the dashboard.
349 rev_str: a revision string that contains revision information that is sent
350 out in the status email. It is obtained by calling
351 |GetRevisionString()|.
352 email_only_change_mode: send email only when there is a change if this is
353 True. Otherwise, always send email after each run.
355 if rev_str:
356 email_content += '<br><b>Revision Information:</b>'
357 email_content += rev_str
358 localtime = time.asctime(time.localtime(time.time()))
359 change_str = ''
360 if email_only_change_mode:
361 change_str = 'Status Change '
362 subject = 'Layout Test Analyzer Result %s(%s): %s' % (change_str,
363 test_group_name,
364 localtime)
365 SendEmail('no-reply@chromium.org', [receiver_email_address],
366 subject, email_content + appended_text_to_email)
369 def GetRevisionString(prev_time, current_time, diff_map):
370 """Get a string for revision information during the specified time period.
372 Args:
373 prev_time: the previous time as a floating point number expressed
374 in seconds since the epoch, in UTC.
375 current_time: the current time as a floating point number expressed
376 in seconds since the epoch, in UTC. It is typically obtained by
377 time.time() function.
378 diff_map: a map that has 'whole', 'skip' and 'nonskip' as keys.
379 Please refer to |diff_map| in |SendStatusEmail()|.
381 Returns:
382 a tuple of strings:
383 1) full string containing links, author, date, and line for each
384 change in the test expectation file.
385 2) shorter string containing only links to the change. Used for
386 trend graph annotations.
387 3) last revision number for the given test group.
388 4) last revision date for the given test group.
390 if not diff_map:
391 return ('', '', '', '')
392 testname_map = {}
393 for test_group in ['skip', 'nonskip']:
394 for i in range(2):
395 for (k, _) in diff_map[test_group][i]:
396 testname_map[k] = True
397 rev_infos = TestExpectationsHistory.GetDiffBetweenTimes(prev_time,
398 current_time,
399 testname_map.keys())
400 rev_str = ''
401 simple_rev_str = ''
402 rev = ''
403 rev_date = ''
404 if rev_infos:
405 # Get latest revision number and date.
406 rev = rev_infos[-1][1]
407 rev_date = rev_infos[-1][3]
408 for rev_info in rev_infos:
409 (old_rev, new_rev, author, date, _, target_lines) = rev_info
411 # test_expectations.txt was renamed to TestExpectations at r119317.
412 new_path = DEFAULT_TEST_EXPECTATION_PATH
413 if new_rev < 119317:
414 new_path = LEGACY_DEFAULT_TEST_EXPECTATION_PATH
415 old_path = DEFAULT_TEST_EXPECTATION_PATH
416 if old_rev < 119317:
417 old_path = LEGACY_DEFAULT_TEST_EXPECTATION_PATH
419 link = REVISION_LOG_URL % (new_path, old_rev, new_rev)
420 rev_str += '<ul><a href="%s">%s->%s</a>\n' % (link, old_rev, new_rev)
421 simple_rev_str = '<a href="%s">%s->%s</a>,' % (link, old_rev, new_rev)
422 rev_str += '<li>%s</li>\n' % author
423 rev_str += '<li>%s</li>\n<ul>' % date
424 for line in target_lines:
425 # Find *.html pattern (test name) and replace it with the link to
426 # flakiness dashboard.
427 test_name_pattern = r'(\S+.html)'
428 match = re.search(test_name_pattern, line)
429 if match:
430 test_name = match.group(1)
431 gpu_link = ''
432 if 'GPU' in line:
433 gpu_link = 'group=%40ToT%20GPU%20Mesa%20-%20chromium.org&'
434 dashboard_link = ('http://test-results.appspot.com/dashboards/'
435 'flakiness_dashboard.html#%stests=%s') % (
436 gpu_link, test_name)
437 line = line.replace(test_name, '<a href="%s">%s</a>' % (
438 dashboard_link, test_name))
439 # Find bug text and replace it with the link to the bug.
440 bug = Bug(line)
441 if bug.bug_txt:
442 line = '<li>%s</li>\n' % line.replace(bug.bug_txt, str(bug))
443 rev_str += line
444 rev_str += '</ul></ul>'
445 return (rev_str, simple_rev_str, rev, rev_date)
448 def SendEmail(sender_email_address, receivers_email_addresses, subject,
449 message):
450 """Send email using localhost's mail server.
452 Args:
453 sender_email_address: sender's email address.
454 receivers_email_addresses: receiver's email addresses.
455 subject: subject string.
456 message: email message.
458 try:
459 html_top = """
460 <html>
461 <head></head>
462 <body>
464 html_bot = """
465 </body>
466 </html>
468 html = html_top + message + html_bot
469 msg = MIMEMultipart('alternative')
470 msg['Subject'] = subject
471 msg['From'] = sender_email_address
472 msg['To'] = receivers_email_addresses[0]
473 part1 = MIMEText(html, 'html')
474 smtp_obj = smtplib.SMTP('localhost')
475 msg.attach(part1)
476 smtp_obj.sendmail(sender_email_address, receivers_email_addresses,
477 msg.as_string())
478 print 'Successfully sent email'
479 except smtplib.SMTPException, ex:
480 print 'Authentication failed:', ex
481 print 'Error: unable to send email'
482 except (socket.gaierror, socket.error, socket.herror), ex:
483 print ex
484 print 'Error: unable to send email'
487 def FindLatestTime(time_list):
488 """Find latest time from |time_list|.
490 The current status is compared to the status of the latest file in
491 |RESULT_DIR|.
493 Args:
494 time_list: a list of time string in the form of 'Year-Month-Day-Hour'
495 (e.g., 2011-10-23-23). Strings not in this format are ignored.
497 Returns:
498 a string representing latest time among the time_list or None if
499 |time_list| is empty or no valid date string in |time_list|.
501 if not time_list:
502 return None
503 latest_date = None
504 for time_element in time_list:
505 try:
506 item_date = datetime.strptime(time_element, '%Y-%m-%d-%H')
507 if latest_date is None or latest_date < item_date:
508 latest_date = item_date
509 except ValueError:
510 # Do nothing.
511 pass
512 if latest_date:
513 return latest_date.strftime('%Y-%m-%d-%H')
514 else:
515 return None
518 def ReplaceLineInFile(file_path, search_exp, replace_line):
519 """Replace line which has |search_exp| with |replace_line| within a file.
521 Args:
522 file_path: the file that is being replaced.
523 search_exp: search expression to find a line to be replaced.
524 replace_line: the new line.
526 for line in fileinput.input(file_path, inplace=1):
527 if search_exp in line:
528 line = replace_line
529 sys.stdout.write(line)
532 def FindLatestResult(result_dir):
533 """Find the latest result in |result_dir| and read and return them.
535 This is used for comparison of analyzer result between current analyzer
536 and most known latest result.
538 Args:
539 result_dir: the result directory.
541 Returns:
542 A tuple of filename (latest_time) and the latest analyzer result.
543 Returns None if there is no file or no file that matches the file
544 patterns used ('%Y-%m-%d-%H').
546 dir_list = os.listdir(result_dir)
547 file_name = FindLatestTime(dir_list)
548 if not file_name:
549 return None
550 file_path = os.path.join(result_dir, file_name)
551 return (file_name, AnalyzerResultMap.Load(file_path))
554 def GetDiffBetweenMaps(map1, map2, lookIntoTestExpectationInfo=False):
555 """Get difference between maps.
557 Args:
558 map1: analyzer result map to be compared.
559 map2: analyzer result map to be compared.
560 lookIntoTestExpectationInfo: a boolean to indicate whether to compare
561 test expectation information in addition to just the test case names.
563 Returns:
564 a tuple of |name1_list| and |name2_list|. |Name1_list| contains all test
565 name and the test expectation information in |map1| but not in |map2|.
566 |Name2_list| contains all test name and the test expectation
567 information in |map2| but not in |map1|.
570 def GetDiffBetweenMapsHelper(map1, map2, lookIntoTestExpectationInfo):
571 """A helper function for GetDiffBetweenMaps.
573 Args:
574 map1: analyzer result map to be compared.
575 map2: analyzer result map to be compared.
576 lookIntoTestExpectationInfo: a boolean to indicate whether to compare
577 test expectation information in addition to just the test case names.
579 Returns:
580 a list of tuples (name, te_info) that are in |map1| but not in |map2|.
582 name_list = []
583 for (name, value1) in map1.iteritems():
584 if name in map2:
585 if lookIntoTestExpectationInfo and 'te_info' in value1:
586 list1 = value1['te_info']
587 list2 = map2[name]['te_info']
588 te_diff = [item for item in list1 if not item in list2]
589 if te_diff:
590 name_list.append((name, te_diff))
591 else:
592 name_list.append((name, value1))
593 return name_list
595 return (GetDiffBetweenMapsHelper(map1, map2, lookIntoTestExpectationInfo),
596 GetDiffBetweenMapsHelper(map2, map1, lookIntoTestExpectationInfo))