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|, | |
dennis_jeffrey
2011/08/26 19:01:26
nit: change the comma at the end of this line into
imasaki1
2011/08/26 22:28:44
Done.
| |
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 map is test expectation keywords such as "SKIP" and other keywords | |
dennis_jeffrey
2011/08/26 19:01:26
Ok, I was confused here because I thought you were
imasaki1
2011/08/26 22:28:44
I understand it is confusing. I hope this is clear
| |
50 (for full list of keywords, please refer to | |
51 |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 shows |diff_map_element| belongs to which test group. | |
82 either 'whole', 'skip' or 'nonskip'. This is necessary for color | |
83 determination. | |
dennis_jeffrey
2011/08/26 19:01:26
I recommend a slight re-wording here:
type_str: a
imasaki1
2011/08/26 22:28:44
Done.
| |
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 if diff > 0: | |
98 diff_sign = '+' | |
99 else: | |
100 diff_sign = '' | |
dennis_jeffrey
2011/08/26 19:01:26
We could slightly shorten the above 4 lines like t
imasaki1
2011/08/26 22:28:44
Done.
| |
101 str = '<font color="%s">%s%d</font>' % (color, diff_sign, diff) | |
102 str1 = '' | |
103 for (name, v) in diff_map_element[0]: | |
104 str1 += name + ',' | |
105 str1 = str1[:-1] | |
106 str2 = '' | |
107 for (name, v) in diff_map_element[1]: | |
108 str2 += name + ',' | |
109 str2 = str2[:-1] | |
110 if str1 or str2: | |
111 str += ':' | |
112 if str1: | |
113 str += '<font color="%s">%s</font> ' % (color, str1) | |
114 if str2: | |
115 str += '<font color="%s">%s</font>' % (color, str2) | |
116 return str | |
117 | |
118 def GetPassingRate(self): | |
119 """Get passing rate. | |
120 | |
121 Returns: | |
122 layout test passing rate of this result in percent. | |
123 """ | |
124 d = len(self.result_map['whole'].keys()) - ( | |
125 len(self.result_map['skip'].keys())) | |
126 return 100 - len(self.result_map['nonskip'].keys()) * 100 / d | |
dennis_jeffrey
2011/08/26 19:01:26
Do we have to worry about the possibility of 'd' b
imasaki1
2011/08/26 22:28:44
Added exception here in that case.
| |
127 | |
128 def ConvertToString(self, prev_time, diff_map, bug_anno_map): | |
129 """Convert this result to HTML display for email. | |
130 | |
131 Args: | |
132 prev_time: the previous time string that are compared against. | |
133 diff_map: the compared map generated by |CompareResultMaps()|. | |
134 bug_anno_map: a annotation map where keys are bug names and values are | |
135 annotations for the bug. | |
136 | |
137 Returns: | |
138 a analyzer result string in HTML format. | |
139 """ | |
140 | |
141 str = ('<b>Statistics (Diff Compared to %s):</b><ul>' | |
142 '<li>The number of tests: %d (%s)</li>' | |
143 '<li>The number of failing skipped tests: %d (%s)</li>' | |
144 '<li>The number of failing non-skipped tests: %d (%s)</li>' | |
145 '<li>Passing rate: %d %%</li></ul>') % ( | |
146 prev_time, | |
147 len(self.result_map['whole'].keys()), | |
148 AnalyzerResultMap.GetDiffString(diff_map['whole'], 'whole'), | |
149 len(self.result_map['skip'].keys()), | |
150 AnalyzerResultMap.GetDiffString(diff_map['skip'], 'skip'), | |
151 len(self.result_map['nonskip'].keys()), | |
152 AnalyzerResultMap.GetDiffString(diff_map['nonskip'], 'nonskip'), | |
153 self.GetPassingRate()) | |
154 str += '<b>Current issues about failing non-skipped tests:</b>' | |
155 for (bug_txt, test_info_list) in ( | |
156 self.GetListOfBugsForNonSkippedTests().iteritems()): | |
157 if not bug_txt in bug_anno_map: | |
158 bug_anno_map[bug_txt] = '<font color="red">Needs investigation!</font>' | |
159 str += '<ul>%s (%s)' % (Bug(bug_txt), bug_anno_map[bug_txt]) | |
160 for test_info in test_info_list: | |
161 (test_name, te_info) = test_info | |
162 gpu_link = '' | |
163 if 'GPU' in te_info: | |
164 gpu_link = 'group=%40ToT%20GPU%20Mesa%20-%20chromium.org&' | |
165 dashboard_link = ('http://test-results.appspot.com/dashboards/' | |
166 'flakiness_dashboard.html#%stests=%s') % ( | |
167 gpu_link, test_name) | |
168 str += '<li><a href="%s">%s</a> (%s) </li>' % ( | |
169 dashboard_link, test_name, ' '.join(te_info.keys())) | |
170 str += '</ul>\n' | |
171 return str | |
172 | |
173 def CompareToOtherResultMap(self, other_result_map): | |
174 """Compare this result map with the other to see if there are any diff. | |
175 | |
176 The comparison is done for layouttests which belong to 'whole', 'skip', | |
177 or 'nonskip'. | |
178 | |
179 Args: | |
180 other_result_map: another result map to be compared against the result | |
181 map of the current object. | |
182 | |
183 Returns: | |
184 a map that has 'whole', 'skip' and 'nonskip' as keys. The values of the | |
185 map are the result of |GetDiffBetweenMaps()|. | |
186 The element has two lists of test cases. One (with index 0) is for | |
187 test names that are in the current result but NOT in the previous | |
188 result. The other (with index 1) is for test names that are in the | |
189 previous results but NOT in the current result. | |
190 For example (test expectation information is omitted for | |
191 simplicity), | |
192 comp_result_map['whole'][0] = ['foo1.html'] | |
193 comp_result_map['whole'][1] = ['foo2.html'] | |
194 This means that current result has 'foo1.html' but NOT in the | |
195 previous result. This also means the previous result has 'foo2.html' | |
196 but it is NOT the current result. This is used for comparions | |
dennis_jeffrey
2011/08/26 19:01:26
We can probably remove the 'This is used for compa
imasaki1
2011/08/26 22:28:44
Done.
| |
197 """ | |
198 comp_result_map = {} | |
199 for name in ['whole', 'skip', 'nonskip']: | |
200 if name == 'nonskip': | |
201 # Look into expectation to get diff only for non-skipped tests. | |
202 lookIntoTestExpectaionInfo = True | |
dennis_jeffrey
2011/08/26 19:01:26
Oops, you did capitalize the 'I' but forgot to add
imasaki1
2011/08/26 22:28:44
Done.
| |
203 else: | |
204 # Otherwise, only test names are compared to get diff. | |
205 lookIntoTestExpectaionInfo = False | |
206 comp_result_map[name] = GetDiffBetweenMaps( | |
207 self.result_map[name], other_result_map.result_map[name], | |
208 lookIntoTestExpectaionInfo) | |
209 return comp_result_map | |
210 | |
211 @staticmethod | |
212 def Load(file_path): | |
213 """Load the object from |file_path| using pickle library. | |
214 | |
215 Args: | |
216 file_path: the string path to the file from which to read the result. | |
217 | |
218 Returns: | |
219 a AnalyzerResultMap object read from |file_path|. | |
220 """ | |
221 file_object = open(file_path) | |
222 analyzer_result_map = pickle.load(file_object) | |
223 file_object.close() | |
224 return analyzer_result_map | |
225 | |
226 def Save(self, file_path): | |
227 """Save the object to |file_path| using pickle library. | |
228 | |
229 Args: | |
230 file_path: the string path to the file in which to store the result. | |
231 """ | |
232 file_object = open(file_path, 'wb') | |
233 pickle.dump(self, file_object) | |
234 file_object.close() | |
235 | |
236 def GetListOfBugsForNonSkippedTests(self): | |
237 """Get a list of bugs for non-skipped layout tests. | |
238 | |
239 This is used for generating email content. | |
240 | |
241 Returns: | |
242 a mapping from bug modifer text (e.g., BUGCR1111) to a test name and | |
dennis_jeffrey
2011/08/26 19:01:26
'modifer' --> 'modifier'
imasaki1
2011/08/26 22:28:44
Done.
| |
243 main test information string which excludes comments and bugs. This | |
244 is used for grouping test names by bug. | |
245 """ | |
246 bug_map = {} | |
247 for (name, v) in self.result_map['nonskip'].iteritems(): | |
248 for te_info in v['te_info']: | |
249 main_te_info = {} | |
250 for k in te_info.keys(): | |
251 if k != 'Comments' and k != 'Bugs': | |
252 main_te_info[k] = True | |
253 if 'Bugs' in te_info: | |
254 for bug in te_info['Bugs']: | |
255 if bug not in bug_map: | |
256 bug_map[bug] = [] | |
257 bug_map[bug].append((name, main_te_info)) | |
258 return bug_map | |
259 | |
260 | |
261 def SendStatusEmail(prev_time, analyzer_result_map, prev_analyzer_result_map, | |
262 bug_anno_map, receiver_email_address): | |
263 """Send status email. | |
264 | |
265 Args: | |
266 prev_time: the date string such as '2011-10-09-11'. This format has been | |
267 used in this analyzer. | |
268 analyzer_result_map: current analyzer result. | |
269 prev_analyzer_result_map: previous analyzer result, which is read from | |
270 a file. | |
271 bug_anno_map: bug annotation map where bug name and annotations are | |
272 stored. | |
273 receiver_email_address: receiver's email address. | |
274 """ | |
275 diff_map = analyzer_result_map.CompareToOtherResultMap( | |
276 prev_analyzer_result_map) | |
277 str = analyzer_result_map.ConvertToString(prev_time, diff_map, bug_anno_map) | |
278 # Add diff info about skipped/non-skipped test. | |
279 prev_time = datetime.strptime(prev_time, '%Y-%m-%d-%H') | |
280 prev_time = time.mktime(prev_time.timetuple()) | |
281 testname_map = {} | |
282 for type in ['skip', 'nonskip']: | |
283 for i in range(2): | |
284 for (k, _) in diff_map[type][i]: | |
285 testname_map[k] = True | |
286 now = time.time() | |
287 | |
288 rev_infos = TestExpectationsHistory.GetDiffBetweenTimes(now, prev_time, | |
289 testname_map.keys()) | |
290 if rev_infos: | |
291 str += '<br><b>Revision Information:</b>' | |
292 for rev_info in rev_infos: | |
293 (old_rev, new_rev, author, date, message, target_lines) = rev_info | |
294 link = urllib.unquote('http://trac.webkit.org/changeset?new=%d%40trunk' | |
295 '%2FLayoutTests%2Fplatform%2Fchromium%2F' | |
296 'test_expectations.txt&old=%d%40trunk%2FLayoutTests' | |
297 '%2Fplatform%2Fchromium%2Ftest_expectations.txt') % ( | |
dennis_jeffrey
2011/08/26 19:01:26
nit: indent the above 3 lines by 3 more spaces eac
imasaki1
2011/08/26 22:28:44
Done.
| |
298 new_rev, old_rev) | |
299 str += '<ul><a href="%s">%s->%s</a>\n' % (link, old_rev, new_rev) | |
300 str += '<li>%s</li>\n' % author | |
301 str += '<li>%s</li>\n<ul>' % date | |
302 for line in target_lines: | |
303 str += '<li>%s</li>\n' % line | |
304 str += '</ul></ul>' | |
305 localtime = time.asctime(time.localtime(time.time())) | |
306 # TODO(imasaki): remove my name from here. | |
307 SendEmail('imasaki@chromium.org', 'Kenji Imasaki', | |
308 [receiver_email_address], ['Layout Test Analyzer Result'], | |
309 'Layout Test Analyzer Result : ' + localtime, str) | |
310 | |
311 | |
312 def SendEmail(sender_email_address, sender_name, receivers_email_addresses, | |
313 receivers_names, subject, message): | |
314 """Send email using localhost's mail server. | |
315 | |
316 Args: | |
317 sender_email_address: sender's email address. | |
318 sender_name: sender's name. | |
319 receivers_email_addresses: receiver's email addresses. | |
320 receivers_names: receiver's names. | |
321 subject: subject string. | |
322 message: email message. | |
323 """ | |
324 whole_message = ''.join([ | |
325 'From: %s<%s>\n' % (sender_name, sender_email_address), | |
326 'To: %s<%s>\n' % (receivers_names[0], | |
327 receivers_email_addresses[0]), | |
328 'Subject: %s\n' % subject, message]) | |
329 | |
330 try: | |
331 html_top = """ | |
332 <html> | |
333 <head></head> | |
334 <body> | |
335 """ | |
336 html_bot = """ | |
337 </body> | |
338 </html> | |
339 """ | |
340 html = html_top + message + html_bot | |
341 msg = MIMEMultipart('alternative') | |
342 msg['Subject'] = subject | |
343 msg['From'] = sender_email_address | |
344 msg['To'] = receivers_email_addresses[0] | |
345 part1 = MIMEText(html, 'html') | |
346 smtpObj = smtplib.SMTP('localhost') | |
347 msg.attach(part1) | |
348 smtpObj.sendmail(sender_email_address, receivers_email_addresses, | |
349 msg.as_string()) | |
350 print 'Successfully sent email' | |
351 except smtplib.SMTPException, e: | |
352 print "Authentication failed:", e | |
dennis_jeffrey
2011/08/26 19:01:26
use single quotes to define this string
imasaki1
2011/08/26 22:28:44
Done.
| |
353 print 'Error: unable to send email' | |
354 except (socket.gaierror, socket.error, socket.herror), e: | |
355 print e | |
356 print 'Error: unable to send email' | |
357 | |
358 | |
359 def FindLatestTime(time_list): | |
360 """Find latest time from |time_list|. | |
361 | |
362 The current status is compared to the status of the latest file in | |
363 |RESULT_DIR|. | |
364 | |
365 Args: | |
366 time_list: a list of time string in the form of '2011-10-23-23'. | |
367 | |
368 Returns: | |
369 a string representing latest time among the time_list or None if | |
370 |time_list| is empty. | |
371 """ | |
372 if not time_list: | |
373 return None | |
374 latest_date = None | |
375 for t in time_list: | |
376 item_date = datetime.strptime(t, '%Y-%m-%d-%H') | |
377 if latest_date == None or latest_date < item_date: | |
378 latest_date = item_date | |
379 return latest_date.strftime('%Y-%m-%d-%H') | |
380 | |
381 | |
382 def FindLatestResult(result_dir): | |
383 """Find the latest result in |result_dir| and read and return them. | |
384 | |
385 This is used for comparison of analyzer result between current analyzer | |
386 and most known latest result. | |
387 | |
388 Args: | |
389 result_dir: the result directory. | |
390 | |
391 Returns: | |
392 a tuple of filename (latest_time) of the and the latest analyzer result. | |
393 """ | |
394 dirList = os.listdir(result_dir) | |
395 file_name = FindLatestTime(dirList) | |
396 file_path = os.path.join(result_dir, file_name) | |
397 return (file_name, AnalyzerResultMap.Load(file_path)) | |
398 | |
399 | |
400 def GetTEInfoListDiff(list1, list2): | |
dennis_jeffrey
2011/08/26 19:01:26
I'm curious why you removed the docstring here? I
imasaki1
2011/08/26 22:28:44
Removed this function.
| |
401 result_list = [] | |
402 for l1 in list1: | |
403 found = False | |
404 for l2 in list2: | |
405 if l1 == l2: | |
406 found = True | |
407 break | |
408 if not found: | |
409 result_list.append(l1) | |
410 return result_list | |
411 | |
412 | |
413 def GetDiffBetweenMapsHelper(map1, map2, lookIntoTestExpectaionInfo): | |
dennis_jeffrey
2011/08/26 19:01:26
'lookIntoTestExpectaionInfo' -->
'lookIntoTestExpe
dennis_jeffrey
2011/08/26 19:01:26
Since this function is only needed within the GetD
imasaki1
2011/08/26 22:28:44
Done.
imasaki1
2011/08/26 22:28:44
Done.
| |
414 """Do the map subtraction from map1 to map2 about their keys. | |
415 | |
416 Args: | |
417 a list of test names are in map1 but not in map2. | |
418 """ | |
419 name_list = [] | |
420 for (name, v1) in map1.iteritems(): | |
421 if name in map2: | |
422 if lookIntoTestExpectaionInfo and 'te_info' in v1: | |
423 te_diff = GetTEInfoListDiff(v1['te_info'], map2[name]['te_info']) | |
424 if te_diff: | |
425 name_list.append((name, te_diff)) | |
426 else: | |
427 name_list.append((name, v1)) | |
428 return name_list | |
429 | |
430 | |
431 def GetDiffBetweenMaps(map1, map2, lookIntoTestExpectaionInfo=False): | |
dennis_jeffrey
2011/08/26 19:01:26
'lookIntoTestExpectaionInfo' -->
'lookIntoTestExpe
imasaki1
2011/08/26 22:28:44
Done.
| |
432 """Get difference between maps. | |
433 | |
434 Args: | |
435 map1: analyzer result map to be compared. | |
436 map2: analyzer result map to be compared. | |
437 lookIntoTestExpectaionInfo: aa boolean to indicate whether to compare | |
dennis_jeffrey
2011/08/26 19:01:26
'aa' --> 'a'
imasaki1
2011/08/26 22:28:44
Done.
| |
438 test expectation information in addition to just the test case names. | |
439 | |
440 Returns: | |
441 a tuple of |name1_list| and |name2_list|. |Name1_list| contains all test | |
442 name and the test expectation information in |map1| but not in |map2|. | |
443 |Name2_list| contains all test name and the test expectation | |
444 information in |map2| but not in |map1|. | |
445 """ | |
446 return (GetDiffBetweenMapsHelper(map1, map2, lookIntoTestExpectaionInfo), | |
447 GetDiffBetweenMapsHelper(map2, map1, lookIntoTestExpectaionInfo)) | |
OLD | NEW |