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