Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(137)

Side by Side Diff: media/tools/layout_tests/layouttest_analyzer_helpers.py

Issue 7693018: Intial checkin of layout test analyzer. (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: Minor modification after doublecheck. Created 9 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698