Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 #!/usr/bin/python | |
| 2 # Copyright (c) 2011 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 """Helper functions for the layout test analyzer.""" | |
| 7 | |
| 8 import copy | |
| 9 from datetime import datetime | |
| 10 from email.mime.multipart import MIMEMultipart | |
| 11 from email.mime.text import MIMEText | |
| 12 import os | |
| 13 import pickle | |
| 14 import smtplib | |
| 15 import socket | |
| 16 import time | |
| 17 import urllib | |
| 18 | |
| 19 from bug import Bug | |
| 20 from test_expectations_history import TestExpectationsHistory | |
| 21 | |
| 22 | |
| 23 class AnalyzerResultMap: | |
| 24 """A class to deal with joined result produed by the analyzer. | |
| 25 | |
| 26 The join is done between layouttests and the test_expectations object | |
| 27 (based on the test expectation file). The instance variable |result_map| | |
| 28 contains the following keys: 'whole','skip','nonskip'. The value of 'whole' | |
| 29 contains information about all layouttests. The value of 'skip' contains | |
| 30 information about skipped layouttests where it has 'SKIP' in its entry in | |
| 31 the test expectation file. The value of 'nonskip' contains all information | |
| 32 about non skipped layout tests, which are in the test expectation file but | |
| 33 not skipped. The information is exactly same as the one parsed by the | |
| 34 analyzer. | |
| 35 """ | |
| 36 | |
| 37 def __init__(self, test_info_map): | |
| 38 """Initialize the AnalyzerResultMap based on test_info_map. | |
| 39 | |
| 40 Test_info_map contains all layouttest information. The job here is to | |
| 41 classify them as 'whole', 'skip' or 'nonskip' based on that information. | |
| 42 | |
| 43 Args: | |
| 44 test_info_map: the result map of |layouttests.JoinWithTestExpectation|. | |
| 45 The key of the map is test name such as 'media/media-foo.html'. | |
| 46 The value of the map is a map that contains the following keys: | |
| 47 'desc'(description), 'te_info' (test expectation information), | |
| 48 which is a list of test expectation information map. The key of the | |
| 49 test expectation information map is test expectation keywords such | |
| 50 as "SKIP" and other keywords (for full list of keywords, please | |
| 51 refer to |test_expectaions.ALL_TE_KEYWORDS|). | |
| 52 """ | |
| 53 self.result_map = {} | |
| 54 self.result_map['whole'] = {} | |
| 55 self.result_map['skip'] = {} | |
| 56 self.result_map['nonskip'] = {} | |
| 57 if test_info_map: | |
| 58 for (k, v) in test_info_map.iteritems(): | |
| 59 self.result_map['whole'][k] = v | |
| 60 if 'te_info' in v: | |
| 61 if any([True for x in v['te_info'] if 'SKIP' in x]): | |
| 62 self.result_map['skip'][k] = v | |
| 63 else: | |
| 64 self.result_map['nonskip'][k] = v | |
| 65 | |
| 66 @staticmethod | |
| 67 def GetDiffString(diff_map_element, type_str): | |
| 68 """Get difference string out of diff map element. | |
| 69 | |
| 70 The difference string shows difference between two analyzer results | |
| 71 (for example, a result for now and a result for sometime in the past) | |
| 72 in HTML format (with colors). This is used for generating email messages. | |
| 73 | |
| 74 Args: | |
| 75 diff_map_element: An element of the compared map generated by | |
| 76 |CompareResultMaps()|. The element has two lists of test cases. One | |
| 77 is for test names that are in the current result but NOT in the | |
| 78 previous result. The other is for test names that are in the previous | |
| 79 results but NOT in the current result. Please refer to comments in | |
| 80 |CompareResultMaps()| for details. | |
| 81 type_str: a string indicating the test group to which |diff_map_element| | |
| 82 belongs; used for color determination. Must be 'whole', 'skip', or | |
| 83 'nonskip'. | |
| 84 | |
| 85 Returns: | |
| 86 a string in HTML format (with colors) to show difference between two | |
| 87 analyzer results. | |
| 88 """ | |
| 89 diff = len(diff_map_element[0]) - len(diff_map_element[1]) | |
| 90 if diff == 0: | |
| 91 return 'No Change' | |
| 92 color = '' | |
| 93 if diff > 0 and type_str != 'whole': | |
| 94 color = 'red' | |
| 95 else: | |
| 96 color = 'green' | |
| 97 diff_sign = '' | |
| 98 if diff > 0: | |
| 99 diff_sign = '+' | |
| 100 str = '<font color="%s">%s%d</font>' % (color, diff_sign, diff) | |
| 101 str1 = '' | |
| 102 for (name, v) in diff_map_element[0]: | |
| 103 str1 += name + ',' | |
| 104 str1 = str1[:-1] | |
| 105 str2 = '' | |
| 106 for (name, v) in diff_map_element[1]: | |
| 107 str2 += name + ',' | |
| 108 str2 = str2[:-1] | |
| 109 if str1 or str2: | |
| 110 str += ':' | |
| 111 if str1: | |
| 112 str += '<font color="%s">%s</font> ' % (color, str1) | |
| 113 if str2: | |
| 114 str += '<font color="%s">%s</font>' % (color, str2) | |
| 115 return str | |
| 116 | |
| 117 def GetPassingRate(self): | |
| 118 """Get passing rate. | |
| 119 | |
| 120 Returns: | |
| 121 layout test passing rate of this result in percent. | |
| 122 | |
| 123 Raises: | |
| 124 ValueEror when the number of tests in test group "whole" is equal or less | |
| 125 than that of "skip". | |
| 126 """ | |
| 127 d = len(self.result_map['whole'].keys()) - ( | |
| 128 len(self.result_map['skip'].keys())) | |
| 129 if d <= 0: | |
| 130 raise ValueError('The number of tests in test group "whole" is equal or ' | |
| 131 'less than that of "skip"') | |
| 132 return 100 - len(self.result_map['nonskip'].keys()) * 100 / d | |
| 133 | |
| 134 def ConvertToString(self, prev_time, diff_map, bug_anno_map): | |
| 135 """Convert this result to HTML display for email. | |
| 136 | |
| 137 Args: | |
| 138 prev_time: the previous time string that are compared against. | |
| 139 diff_map: the compared map generated by |CompareResultMaps()|. | |
| 140 bug_anno_map: a annotation map where keys are bug names and values are | |
| 141 annotations for the bug. | |
| 142 | |
| 143 Returns: | |
| 144 a analyzer result string in HTML format. | |
| 145 """ | |
| 146 | |
| 147 str = ('<b>Statistics (Diff Compared to %s):</b><ul>' | |
| 148 '<li>The number of tests: %d (%s)</li>' | |
| 149 '<li>The number of failing skipped tests: %d (%s)</li>' | |
| 150 '<li>The number of failing non-skipped tests: %d (%s)</li>' | |
| 151 '<li>Passing rate: %d %%</li></ul>') % ( | |
| 152 prev_time, | |
| 153 len(self.result_map['whole'].keys()), | |
| 154 AnalyzerResultMap.GetDiffString(diff_map['whole'], 'whole'), | |
| 155 len(self.result_map['skip'].keys()), | |
| 156 AnalyzerResultMap.GetDiffString(diff_map['skip'], 'skip'), | |
| 157 len(self.result_map['nonskip'].keys()), | |
| 158 AnalyzerResultMap.GetDiffString(diff_map['nonskip'], 'nonskip'), | |
| 159 self.GetPassingRate()) | |
| 160 str += '<b>Current issues about failing non-skipped tests:</b>' | |
| 161 for (bug_txt, test_info_list) in ( | |
| 162 self.GetListOfBugsForNonSkippedTests().iteritems()): | |
| 163 if not bug_txt in bug_anno_map: | |
| 164 bug_anno_map[bug_txt] = '<font color="red">Needs investigation!</font>' | |
| 165 str += '<ul>%s (%s)' % (Bug(bug_txt), bug_anno_map[bug_txt]) | |
| 166 for test_info in test_info_list: | |
| 167 (test_name, te_info) = test_info | |
| 168 gpu_link = '' | |
| 169 if 'GPU' in te_info: | |
| 170 gpu_link = 'group=%40ToT%20GPU%20Mesa%20-%20chromium.org&' | |
| 171 dashboard_link = ('http://test-results.appspot.com/dashboards/' | |
| 172 'flakiness_dashboard.html#%stests=%s') % ( | |
| 173 gpu_link, test_name) | |
| 174 str += '<li><a href="%s">%s</a> (%s) </li>' % ( | |
| 175 dashboard_link, test_name, ' '.join(te_info.keys())) | |
| 176 str += '</ul>\n' | |
| 177 return str | |
| 178 | |
| 179 def CompareToOtherResultMap(self, other_result_map): | |
| 180 """Compare this result map with the other to see if there are any diff. | |
| 181 | |
| 182 The comparison is done for layouttests which belong to 'whole', 'skip', | |
| 183 or 'nonskip'. | |
| 184 | |
| 185 Args: | |
| 186 other_result_map: another result map to be compared against the result | |
| 187 map of the current object. | |
| 188 | |
| 189 Returns: | |
| 190 a map that has 'whole', 'skip' and 'nonskip' as keys. The values of the | |
| 191 map are the result of |GetDiffBetweenMaps()|. | |
| 192 The element has two lists of test cases. One (with index 0) is for | |
| 193 test names that are in the current result but NOT in the previous | |
| 194 result. The other (with index 1) is for test names that are in the | |
| 195 previous results but NOT in the current result. | |
| 196 For example (test expectation information is omitted for | |
| 197 simplicity), | |
| 198 comp_result_map['whole'][0] = ['foo1.html'] | |
| 199 comp_result_map['whole'][1] = ['foo2.html'] | |
| 200 This means that current result has 'foo1.html' but NOT in the | |
| 201 previous result. This also means the previous result has 'foo2.html' | |
| 202 but it is NOT the current result. | |
| 203 """ | |
| 204 comp_result_map = {} | |
| 205 for name in ['whole', 'skip', 'nonskip']: | |
| 206 if name == 'nonskip': | |
| 207 # Look into expectation to get diff only for non-skipped tests. | |
| 208 lookIntoTestExpectationInfo = True | |
| 209 else: | |
| 210 # Otherwise, only test names are compared to get diff. | |
| 211 lookIntoTestExpectationInfo = False | |
| 212 comp_result_map[name] = GetDiffBetweenMaps( | |
| 213 self.result_map[name], other_result_map.result_map[name], | |
| 214 lookIntoTestExpectationInfo) | |
| 215 return comp_result_map | |
| 216 | |
| 217 @staticmethod | |
| 218 def Load(file_path): | |
| 219 """Load the object from |file_path| using pickle library. | |
| 220 | |
| 221 Args: | |
| 222 file_path: the string path to the file from which to read the result. | |
| 223 | |
| 224 Returns: | |
| 225 a AnalyzerResultMap object read from |file_path|. | |
| 226 """ | |
| 227 file_object = open(file_path) | |
| 228 analyzer_result_map = pickle.load(file_object) | |
| 229 file_object.close() | |
| 230 return analyzer_result_map | |
| 231 | |
| 232 def Save(self, file_path): | |
| 233 """Save the object to |file_path| using pickle library. | |
| 234 | |
| 235 Args: | |
| 236 file_path: the string path to the file in which to store the result. | |
| 237 """ | |
| 238 file_object = open(file_path, 'wb') | |
| 239 pickle.dump(self, file_object) | |
| 240 file_object.close() | |
| 241 | |
| 242 def GetListOfBugsForNonSkippedTests(self): | |
| 243 """Get a list of bugs for non-skipped layout tests. | |
| 244 | |
| 245 This is used for generating email content. | |
| 246 | |
| 247 Returns: | |
| 248 a mapping from bug modifier text (e.g., BUGCR1111) to a test name and | |
| 249 main test information string which excludes comments and bugs. This | |
| 250 is used for grouping test names by bug. | |
| 251 """ | |
| 252 bug_map = {} | |
| 253 for (name, v) in self.result_map['nonskip'].iteritems(): | |
| 254 for te_info in v['te_info']: | |
| 255 main_te_info = {} | |
| 256 for k in te_info.keys(): | |
| 257 if k != 'Comments' and k != 'Bugs': | |
| 258 main_te_info[k] = True | |
| 259 if 'Bugs' in te_info: | |
| 260 for bug in te_info['Bugs']: | |
| 261 if bug not in bug_map: | |
| 262 bug_map[bug] = [] | |
| 263 bug_map[bug].append((name, main_te_info)) | |
| 264 return bug_map | |
| 265 | |
| 266 | |
| 267 def SendStatusEmail(prev_time, analyzer_result_map, prev_analyzer_result_map, | |
| 268 bug_anno_map, receiver_email_address): | |
| 269 """Send status email. | |
| 270 | |
| 271 Args: | |
| 272 prev_time: the date string such as '2011-10-09-11'. This format has been | |
| 273 used in this analyzer. | |
| 274 analyzer_result_map: current analyzer result. | |
| 275 prev_analyzer_result_map: previous analyzer result, which is read from | |
| 276 a file. | |
| 277 bug_anno_map: bug annotation map where bug name and annotations are | |
| 278 stored. | |
| 279 receiver_email_address: receiver's email address. | |
| 280 """ | |
| 281 diff_map = analyzer_result_map.CompareToOtherResultMap( | |
| 282 prev_analyzer_result_map) | |
| 283 str = analyzer_result_map.ConvertToString(prev_time, diff_map, bug_anno_map) | |
| 284 # Add diff info about skipped/non-skipped test. | |
| 285 prev_time = datetime.strptime(prev_time, '%Y-%m-%d-%H') | |
| 286 prev_time = time.mktime(prev_time.timetuple()) | |
| 287 testname_map = {} | |
| 288 for type in ['skip', 'nonskip']: | |
| 289 for i in range(2): | |
| 290 for (k, _) in diff_map[type][i]: | |
| 291 testname_map[k] = True | |
| 292 now = time.time() | |
| 293 | |
| 294 rev_infos = TestExpectationsHistory.GetDiffBetweenTimes(now, prev_time, | |
| 295 testname_map.keys()) | |
| 296 if rev_infos: | |
| 297 str += '<br><b>Revision Information:</b>' | |
| 298 for rev_info in rev_infos: | |
| 299 (old_rev, new_rev, author, date, message, target_lines) = rev_info | |
| 300 link = urllib.unquote('http://trac.webkit.org/changeset?new=%d%40trunk' | |
| 301 '%2FLayoutTests%2Fplatform%2Fchromium%2F' | |
| 302 'test_expectations.txt&old=%d%40trunk%2F' | |
| 303 'LayoutTests%2Fplatform%2Fchromium%2F' | |
| 304 'test_expectations.txt') % (new_rev, old_rev) | |
| 305 str += '<ul><a href="%s">%s->%s</a>\n' % (link, old_rev, new_rev) | |
| 306 str += '<li>%s</li>\n' % author | |
| 307 str += '<li>%s</li>\n<ul>' % date | |
| 308 for line in target_lines: | |
| 309 str += '<li>%s</li>\n' % line | |
| 310 str += '</ul></ul>' | |
| 311 localtime = time.asctime(time.localtime(time.time())) | |
| 312 # TODO(imasaki): remove my name from here. | |
| 313 SendEmail('imasaki@chromium.org', 'Kenji Imasaki', | |
| 314 [receiver_email_address], ['Layout Test Analyzer Result'], | |
| 315 'Layout Test Analyzer Result : ' + localtime, str) | |
| 316 | |
| 317 | |
| 318 def SendEmail(sender_email_address, sender_name, receivers_email_addresses, | |
| 319 receivers_names, subject, message): | |
| 320 """Send email using localhost's mail server. | |
| 321 | |
| 322 Args: | |
| 323 sender_email_address: sender's email address. | |
| 324 sender_name: sender's name. | |
| 325 receivers_email_addresses: receiver's email addresses. | |
| 326 receivers_names: receiver's names. | |
| 327 subject: subject string. | |
| 328 message: email message. | |
| 329 """ | |
| 330 whole_message = ''.join([ | |
| 331 'From: %s<%s>\n' % (sender_name, sender_email_address), | |
| 332 'To: %s<%s>\n' % (receivers_names[0], | |
| 333 receivers_email_addresses[0]), | |
| 334 'Subject: %s\n' % subject, message]) | |
| 335 | |
| 336 try: | |
| 337 html_top = """ | |
| 338 <html> | |
| 339 <head></head> | |
| 340 <body> | |
| 341 """ | |
| 342 html_bot = """ | |
| 343 </body> | |
| 344 </html> | |
| 345 """ | |
| 346 html = html_top + message + html_bot | |
| 347 msg = MIMEMultipart('alternative') | |
| 348 msg['Subject'] = subject | |
| 349 msg['From'] = sender_email_address | |
| 350 msg['To'] = receivers_email_addresses[0] | |
| 351 part1 = MIMEText(html, 'html') | |
| 352 smtpObj = smtplib.SMTP('localhost') | |
| 353 msg.attach(part1) | |
| 354 smtpObj.sendmail(sender_email_address, receivers_email_addresses, | |
| 355 msg.as_string()) | |
| 356 print 'Successfully sent email' | |
| 357 except smtplib.SMTPException, e: | |
| 358 print 'Authentication failed:', e | |
| 359 print 'Error: unable to send email' | |
| 360 except (socket.gaierror, socket.error, socket.herror), e: | |
| 361 print e | |
| 362 print 'Error: unable to send email' | |
| 363 | |
| 364 | |
| 365 def FindLatestTime(time_list): | |
| 366 """Find latest time from |time_list|. | |
| 367 | |
| 368 The current status is compared to the status of the latest file in | |
| 369 |RESULT_DIR|. | |
| 370 | |
| 371 Args: | |
| 372 time_list: a list of time string in the form of '2011-10-23-23'. | |
| 373 | |
| 374 Returns: | |
| 375 a string representing latest time among the time_list or None if | |
| 376 |time_list| is empty. | |
| 377 """ | |
| 378 if not time_list: | |
| 379 return None | |
| 380 latest_date = None | |
| 381 for t in time_list: | |
| 382 item_date = datetime.strptime(t, '%Y-%m-%d-%H') | |
| 383 if latest_date == None or latest_date < item_date: | |
| 384 latest_date = item_date | |
| 385 return latest_date.strftime('%Y-%m-%d-%H') | |
| 386 | |
| 387 | |
| 388 def FindLatestResult(result_dir): | |
| 389 """Find the latest result in |result_dir| and read and return them. | |
| 390 | |
| 391 This is used for comparison of analyzer result between current analyzer | |
| 392 and most known latest result. | |
| 393 | |
| 394 Args: | |
| 395 result_dir: the result directory. | |
| 396 | |
| 397 Returns: | |
| 398 a tuple of filename (latest_time) of the and the latest analyzer result. | |
| 399 """ | |
| 400 dirList = os.listdir(result_dir) | |
| 401 file_name = FindLatestTime(dirList) | |
| 402 file_path = os.path.join(result_dir, file_name) | |
| 403 return (file_name, AnalyzerResultMap.Load(file_path)) | |
| 404 | |
| 405 | |
| 406 def GetDiffBetweenMaps(map1, map2, lookIntoTestExpectationInfo=False): | |
| 407 """Get difference between maps. | |
| 408 | |
| 409 Args: | |
| 410 map1: analyzer result map to be compared. | |
| 411 map2: analyzer result map to be compared. | |
| 412 lookIntoTestExpectationInfo: a boolean to indicate whether to compare | |
| 413 test expectation information in addition to just the test case names. | |
| 414 | |
| 415 Returns: | |
| 416 a tuple of |name1_list| and |name2_list|. |Name1_list| contains all test | |
| 417 name and the test expectation information in |map1| but not in |map2|. | |
| 418 |Name2_list| contains all test name and the test expectation | |
| 419 information in |map2| but not in |map1|. | |
| 420 """ | |
| 421 | |
| 422 def GetDiffBetweenMapsHelper(map1, map2, lookIntoTestExpectationInfo): | |
| 423 name_list = [] | |
| 424 for (name, v1) in map1.iteritems(): | |
| 425 if name in map2: | |
| 426 if lookIntoTestExpectationInfo and 'te_info' in v1: | |
| 427 list1 = v1['te_info'] | |
| 428 list2 = map2[name]['te_info'] | |
| 429 te_diff = [item for item in list1 if not item in list2] | |
| 430 if te_diff: | |
| 431 name_list.append((name, te_diff)) | |
| 432 else: | |
| 433 name_list.append((name, v1)) | |
| 434 return name_list | |
| 435 | |
| 436 return (GetDiffBetweenMapsHelper(map1, map2, lookIntoTestExpectationInfo), | |
|
dennis_jeffrey
2011/08/27 00:04:50
I think there's a code structure problem here. Th
imasaki1
2011/08/29 21:32:49
I thought this is return value of GetDiffBetweenMa
dennis_jeffrey
2011/08/29 23:52:26
Yes, I mis-read the code here. I see now that the
| |
| 437 GetDiffBetweenMapsHelper(map2, map1, lookIntoTestExpectationInfo)) | |
| OLD | NEW |