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

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 modifications. 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 """A module for helper functions for the layouttest analyzer."""
dennis_jeffrey 2011/08/24 17:40:47 'Helper functions for the layout test analyzer.'
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 'no skipped' --> 'not skipped'
imasaki1 2011/08/25 23:57:03 Done.
33 analyzer.
34 """
35
36 def __init__(self, test_info_map):
37 """Initialize the Result based on test_info_map.
dennis_jeffrey 2011/08/24 17:40:47 'Result' --> 'AnalyzerResultMap'
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 'them to' --> 'them as'
imasaki1 2011/08/25 23:57:03 Done.
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".
dennis_jeffrey 2011/08/24 17:40:47 You first mention that the map contains keys "desc
imasaki1 2011/08/25 23:57:03 Added more comments, here.
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:
dennis_jeffrey 2011/08/24 17:40:47 I think we can just use "if test_info_map:"
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 I think we can shorten lines 58-62 above using the
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 What do you mean by a "difference string"? Also,
imasaki1 2011/08/25 23:57:03 Done.
71
72 This is used for generating email message.
dennis_jeffrey 2011/08/24 17:40:47 'email message' --> 'email messages'
imasaki1 2011/08/25 23:57:03 Done.
73
74 Args:
75 diff_map_element: the compared map generated by |CompareResultMaps()|
dennis_jeffrey 2011/08/24 17:40:47 Add a period or comma at the end of this line. If
imasaki1 2011/08/25 23:57:03 Done.
76 also, this is for each test group ('whole', 'skip', 'nonskip')
dennis_jeffrey 2011/08/24 17:40:47 Add period at end of this sentence.
imasaki1 2011/08/25 23:57:03 Done.
77
dennis_jeffrey 2011/08/24 17:40:47 What about the "type_str" arg?
imasaki1 2011/08/25 23:57:03 Done.
78 Return:
dennis_jeffrey 2011/08/24 17:40:47 'Returns:'
imasaki1 2011/08/25 23:57:03 Done.
79 a string including diff information.
dennis_jeffrey 2011/08/24 17:40:47 what do you mean by "diff information"?
imasaki1 2011/08/25 23:57:03 Done.
80 """
81 diff = len(diff_map_element[0]) - len(diff_map_element[1])
dennis_jeffrey 2011/08/24 17:40:47 I thought diff_map_element is a dictionary with ke
imasaki1 2011/08/25 23:57:03 no. It is a tuple with two lists. I will modify th
dennis_jeffrey 2011/08/26 19:01:26 Got it. Thanks.
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)
dennis_jeffrey 2011/08/24 17:40:47 was it intentional to have a '+' within the string
imasaki1 2011/08/25 23:57:03 Good catch I will add only when diff is negative.
90 str1 = ''
91 for (name, v) in diff_map_element[0]:
92 str1 += name + ","
dennis_jeffrey 2011/08/24 17:40:47 use single quotes in the string
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 'anno_map' --> 'bug_anno_map'
imasaki1 2011/08/25 23:57:03 Done.
113 annotation for the bug.
dennis_jeffrey 2011/08/24 17:40:47 'annotation' --> 'annotations'
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 I think this comment is not necessary; we can see
imasaki1 2011/08/25 23:57:03 Removed. Also, this part is replaced by function c
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])
dennis_jeffrey 2011/08/24 17:40:47 Do you use a Bug object anywhere else? If so, tha
imasaki1 2011/08/25 23:57:03 I am planning to implement several more functions
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))
dennis_jeffrey 2011/08/24 17:40:47 nit: indent the above 2 lines some more so they li
imasaki1 2011/08/25 23:57:03 Done.
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):
dennis_jeffrey 2011/08/24 17:40:47 How about 'other_result_map' instead of 'result_ma
imasaki1 2011/08/25 23:57:03 Done.
155 """Compare this result map with the other to see if any difference.
dennis_jeffrey 2011/08/24 17:40:47 'any difference' --> 'there are any differences'
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 'this result' --> 'the result map of the current o
imasaki1 2011/08/25 23:57:03 Done.
162
163 Returns:
164 a comp_result_map, which contains 'whole', 'skip' and 'nonskip' as keys.
dennis_jeffrey 2011/08/24 17:40:47 We shouldn't say 'comp_result_map' in the descript
imasaki1 2011/08/25 23:57:03 Modified comment here.
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
dennis_jeffrey 2011/08/24 17:40:47 'tests diff' --> 'test diffs'
imasaki1 2011/08/25 23:57:03 Deleted.
167 one is for a list of previous test diff.
dennis_jeffrey 2011/08/24 17:40:47 'diff' --> 'diffs'
imasaki1 2011/08/25 23:57:03 Deleted
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.
dennis_jeffrey 2011/08/24 17:40:47 Should we add a comment somewhere explicitly sayin
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 'lookintoTestExpectaionInfo' --> 'lookIntoTestExpe
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 'the string path to the file from which to read th
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 'the string path to the file in which to store the
imasaki1 2011/08/25 23:57:03 Done.
208 """
209 file_object = open(file_path, "wb")
dennis_jeffrey 2011/08/24 17:40:47 use single quotes in strings
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 'lit' --> 'list' 'tessts' --> 'tests'
imasaki1 2011/08/25 23:57:03 Done.
215
216 This is used for generating email content.
dennis_jeffrey 2011/08/24 17:40:47 Add a "Returns:" section to this docstring
imasaki1 2011/08/25 23:57:03 Done.
217 """
218 bug_map = {}
219 for (name, v) in self.result_map['nonskip'].iteritems():
220 for te_info in v['te_info']:
dennis_jeffrey 2011/08/24 17:40:47 only indent lines 220-228 by 2 spaces underneath l
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 reciever's --> receiver's
imasaki1 2011/08/25 23:57:03 Done.
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")
dennis_jeffrey 2011/08/24 17:40:47 use single quotes in strings
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 How about something like this? for type in ['skip
imasaki1 2011/08/25 23:57:03 Done.
260 now = time.time()
261
262 rev_infos = TestExpectationsHistory.GetDiffBetweenTimes(now, prev_time,
263 testname_map.keys())
264 if len(rev_infos) > 0:
dennis_jeffrey 2011/08/24 17:40:47 if rev_infos:
imasaki1 2011/08/25 23:57:03 Done.
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)
dennis_jeffrey 2011/08/24 17:40:47 Maybe combine the above 2 statements so that we do
imasaki1 2011/08/25 23:57:03 Done.
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,
dennis_jeffrey 2011/08/24 17:40:47 This seems to only be called at line 281 above. D
imasaki1 2011/08/25 23:57:03 I prefer to separate pure email function and statu
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 = """
dennis_jeffrey 2011/08/24 17:40:47 only indent all the lines within the 'try' by 2 sp
imasaki1 2011/08/25 23:57:03 Done.
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())
dennis_jeffrey 2011/08/24 17:40:47 nit: indent the above 2 lines by 1 fewer space eac
imasaki1 2011/08/25 23:57:03 Done.
325 print 'Successfully sent email'
326 except smtplib.SMTPException:
327 print 'Error: unable to send email'
dennis_jeffrey 2011/08/24 17:40:47 only indent line by 2 spaces, not 4
dennis_jeffrey 2011/08/24 17:40:47 Maybe we should also capture the exception message
imasaki1 2011/08/25 23:57:03 Done.
imasaki1 2011/08/25 23:57:03 Done.
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.
dennis_jeffrey 2011/08/24 17:40:47 add this: 'or None if |time_list| is empty.'
imasaki1 2011/08/25 23:57:03 Done.
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")
dennis_jeffrey 2011/08/24 17:40:47 Be careful: if time_list is empty, lastest_date wi
imasaki1 2011/08/25 23:57:03 Done.
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)
dennis_jeffrey 2011/08/24 17:40:47 Why not remove FindLatestTime and just put that co
imasaki1 2011/08/25 23:57:03 I used this in the unit test. This needs to be sep
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.
dennis_jeffrey 2011/08/24 17:40:47 'difference between test' --> 'differences between
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 This function seems to only identify the elements
imasaki1 2011/08/25 23:57:03 Deleted.
388
389
390 def GetDiffBetweenMaps(map1, map2, lookintoTestExpectaionInfo=False):
dennis_jeffrey 2011/08/24 17:40:47 'lookintoTestExpectaionInfo' --> 'lookIntoTestExpe
imasaki1 2011/08/25 23:57:03 Done.
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
dennis_jeffrey 2011/08/24 17:40:47 'a boolean to indicate whether to compare test exp
imasaki1 2011/08/25 23:57:03 Done.
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))
dennis_jeffrey 2011/08/24 17:40:47 There's a big chunk of code in this function that'
imasaki1 2011/08/25 23:57:03 Done.
429 return (name1_list, name2_list)
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698