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

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: Modified based on CR comments. Created 9 years, 3 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|.
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),
437 GetDiffBetweenMapsHelper(map2, map1, lookIntoTestExpectationInfo))
OLDNEW
« no previous file with comments | « media/tools/layout_tests/layouttest_analyzer.py ('k') | media/tools/layout_tests/layouttest_analyzer_helpers_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698