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

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

Issue 7671049: Refactor/Simplify the test expectation code in media/tools/layout_tests/ (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: Modification based on CR comments. 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
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. 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 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """A module for TestExpectionsManager. 6 """A Module for the Webkit layout test test expectation related class."""
7 7
8 TestExpectaionManager manages data for Webkit layout tests. 8 import re
9 """ 9 import urllib2
10 10
11 import csv 11 # Default Webkit SVN location for chromium test expectation file.
12 import re 12 # TODO(imasaki) : support multiple test expectations files.
13 import time 13 DEFAULT_TEST_EXPECTATION_LOCATION = (
14 import urllib 14 'http://svn.webkit.org/repository/webkit/trunk/'
15 import pysvn 15 'LayoutTests/platform/chromium/test_expectations.txt')
16 16
17 from csv_utils import CsvUtils 17 # The following is from test expectation syntax. The detail can be found in
18 from layout_test_test_case import LayoutTestCaseManager 18 # http://www.chromium.org/developers/testing/
19 from test_case_patterns import TestCasePatterns 19 # webkit-layout-tests#TOC-Test-Expectations
20 # <decision> ::== [SKIP] [WONTFIX] [SLOW]
21 DECISION_NAMES = ['SKIP', 'WONTFIX', 'SLOW']
22 # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC]
23 PLATFORM_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC']
24 # <config> ::== RELEASE | DEBUG
25 CONFIG_NAMES = ['RELEASE', 'DEBUG']
26 # <EXPECTATION_NAMES> ::== \
27 # [FAIL] [PASS] [CRASH] [TIMEOUT] [IMAGE] [TEXT] [IMAGE+TEXT]
28 EXPECTATION_NAMES = ['FAIL', 'PASS', 'CRASH',
29 'TIMEOUT', 'IMAGE', 'TEXT',
30 'IMAGE+TEXT']
20 31
21 32
22 class TestExpectationsManager(object): 33 class TestExpectations(object):
23 """This class manages test expectations data for Webkit layout tests. 34 """A class to model the content of test expectation file for analysis.
24 35
25 The detail of the test expectation file can be found in 36 The raw test expectation file can be found in
26 http://trac.webkit.org/wiki/TestExpectations. 37 |DEFAULT_TEST_EXPECTATION_LOCATION|.
38 It is necessary to parse this file and store meaningful information for
39 the analysis (joining with existing layout tests using a test name).
40 Instance variable |all_test_expectation_info| is used.
41 A test name such as 'media/video-source-type.html' is used for the key
42 to store information. However, a test name can appear multiple times in
43 the test expectation file. So, the map should keep all the occurrences
44 information. For example, the current test expectation file has the following
45 two entries:
46 BUGWK58587 LINUX DEBUG GPU : media/video-zoom.html = IMAGE
47 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE
48 In this case, all_test_expectation_info['media/video-zoom.html'] will have
49 a list with two elements, each of which is the map of the test expectation
50 information. In other words,this is the generated map after parsing both
51 lines.
52 {'media/video-zoom.html': [{'LINUX':True, 'DEBUG':True ....},
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 Are the values always True/False? If so ISTM you'
imasaki1 2011/08/20 08:25:36 This map also contains 'Bugs' which holds a list o
53 {'MAC':True, 'GPU':True ....}]
54 """
27 55
28 This class does the following: 56 @staticmethod
29 (1) get test expectation file from WebKit subversion source 57 def GetAllDataNames():
30 repository using pysvn. 58 """get all names relating to test expectation data.
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 All comments should start with capital letter (ple
imasaki1 2011/08/20 08:25:36 Done.
31 (2) parse the file and generate CSV entries. 59
32 (3) create hyperlinks where appropriate (e.g., test case name). 60 Returns:
61 a list of all names (keywords) used in the test expectation files.
33 """ 62 """
63 return DECISION_NAMES + PLATFORM_NAMES + CONFIG_NAMES + EXPECTATION_NAMES
34 64
35 DEFAULT_TEST_EXPECTATION_DIR = ( 65 def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION):
36 'http://svn.webkit.org/repository/webkit/trunk/' 66 """read the test expectation file from the specified URL and parse it.
37 'LayoutTests/platform/chromium/')
38 67
39 DEFAULT_TEST_EXPECTATION_LOCATION = ( 68 All the parsed information is stored in the instance variable
40 'http://svn.webkit.org/repository/webkit/trunk/' 69 |all_test_expectation_info|, which is the map where its key is test name
41 'LayoutTests/platform/chromium/test_expectations.txt') 70 and its value is a list of the test expectation entries information map
71 such as 'SKIP' or 'Comments' or 'Bugs'.
42 72
43 CHROME_BUG_URL = 'http://code.google.com/p/chromium/issues/detail?id=' 73 Args:
74 url: A URL string for the test expectation file.
75 """
76 self.all_test_expectation_info = {}
77 resp = urllib2.urlopen(url)
78 if resp.code != 200:
79 raise NameError('Test expectation file does not exist in %s' % source)
80 # Start parsing each line.
81 lines = resp.read().split('\n')
82 comments = ''
83 for line in lines:
84 if line.startswith('//'):
85 # Comments can be Multiple lines.
86 comments += line.replace('//', '')
87 elif not line:
88 comments = ''
89 else:
90 test_expectation_info = self.ParseLine(line, comments)
91 testname = TestExpectations.ExtractTestName(line)
92 if not testname in self.all_test_expectation_info:
93 self.all_test_expectation_info[testname] = []
94 # This is a list for multiple entries.
95 self.all_test_expectation_info[testname].append(test_expectation_info)
44 96
45 WEBKIT_BUG_URL = 'https://bugs.webkit.org/show_bug.cgi?id=' 97 @staticmethod
98 def ExtractTestName(line):
99 """extract either a test name or a directory name from each line.
46 100
47 FLAKINESS_DASHBOARD_LINK = ( 101 Args:
48 'http://test-results.appspot.com/dashboards/' 102 line: each line in the test expectation file.
49 'flakiness_dashboard.html#tests=')
50 103
51 TEST_CASE_PATTERNS = TestCasePatterns() 104 Returns:
105 a test name or directory name string. Returns '' if no matching.
106 """
107 # First try to find test case where ending with .html.
108 matches = re.search(r':\s+(\S+.html)', line)
109 # Next try to find directory
110 if matches:
111 return matches.group(1)
112 matches = re.search(r':\s+(\S+)', line)
113 if matches:
114 return matches.group(1)
115 else:
116 return ''
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 raise?
imasaki1 2011/08/20 08:25:36 Done.
Ami GONE FROM CHROMIUM 2011/08/20 17:52:23 I meant raise a sensible exception. Doesn't this
imasaki1 2011/08/20 18:07:38 Done. Also added unit test for this case.
52 117
53 OTHER_FIELD_NAMES = ['TestCase', 'Media', 'Flaky', 'WebKitD', 'Bug', 118 @staticmethod
54 'Test'] 119 def ParseLine(line, comments):
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 |comments| should be |comment_prefix| or something
imasaki1 2011/08/20 08:25:36 Done.
120 """Parse each line in test expectation and update test expectation info.
55 121
56 # The following is from test expectation syntax. 122 This looks for each name in |GetAllDataNames()| in line and store in the
57 # BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] 123 map and returns it. Also, store comments and bugs in the map.
58 DECISION_COLUMN_NAMES = ['SKIP', 'SLOW']
59 # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC]
60 PLATFORM_COLUMN_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC']
61 #<config> ::== RELEASE | DEBUG
62 CONFIG_COLUMN_NAMES = ['RELEASE', 'DEBUG']
63 # <EXPECTATION_COLUMN_NAMES> ::== \
64 # [FAIL] [PASS] [CRASH] [TIMEOUT] [IMAGE] [TEXT] [IMAGE+TEXT]
65 EXPECTATION_COLUMN_NAMES = ['FAIL', 'PASS', 'CRASH',
66 'TIMEOUT', 'IMAGE', 'TEXT',
67 'IMAGE+TEXT']
68 124
69 # These are coming from metadata in comments in the test expectation file. 125 Args:
70 COMMENT_COLUMN_NAMES = ['UNIMPLEMENTED', 'KNOWNISSUE', 'TESTISSUE', 126 line: each line in the test expectation. For example,
71 'WONTFIX'] 127 "BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE"
128 comments: comments in the test expectation. Usually, it exist just before
129 the entry.
72 130
73 RAW_COMMENT_COLUMN_NAME = 'COMMENTS' 131 Returns:
74 132 a map containing the test expectation entries an and comments and bugs.
75 def __init__(self): 133 """
76 """Initialize the test cases.""" 134 test_expectation_info = {}
77 self.testcases = [] 135 for name in TestExpectations.GetAllDataNames():
78 136 if not line:
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 Isn't this guarded against before calling this fun
imasaki1 2011/08/20 08:25:36 Deleted
79 def get_column_indexes(self, column_names): 137 # Clear the comments if there is space (this is typical the case).
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 s/typical/typically/
imasaki1 2011/08/20 08:25:36 Done.
80 """Get column indexes for given column names. 138 comments = ''
81 139 if name in line:
82 Args: 140 test_expectation_info[name] = True
Ami GONE FROM CHROMIUM 2011/08/20 04:16:50 This is going to incorrectly count names appearing
imasaki1 2011/08/20 08:25:36 Comments are dealt in __init__ function. So, the l
Ami GONE FROM CHROMIUM 2011/08/20 17:52:23 I was referring to line-ending comments, not multi
imasaki1 2011/08/20 18:07:38 Modified... Added unit test for this.
83 column_names: a list of column names. 141 # Store comments.
84 142 test_expectation_info['Comments'] = comments
85 Returns: 143 # Store bugs.
86 a list of indexes for the given column names. 144 bugs = re.findall(r'BUG\w+', line)
87 """ 145 if len(bugs) > 0:
88 all_field_names = self.get_all_column_names(True, True) 146 test_expectation_info['Bugs'] = bugs
89 return [all_field_names.index( 147 return test_expectation_info
90 field_name) for field_name in column_names]
91
92 def get_all_column_names(self, include_other_fields, include_comments):
93 """Get all column names that are used in CSV file.
94
95 Args:
96 include_other_fields: a boolean that indicates the result should
97 include other column names (defined in OTHER_COLUMNS_NAMES).
98 include_comments: a boolean that indicates the result should
99 include comment-related names.
100
101 Returns:
102 a list that contains column names.
103 """
104 return_list = (
105 self.DECISION_COLUMN_NAMES + self.PLATFORM_COLUMN_NAMES +
106 self.CONFIG_COLUMN_NAMES + self.EXPECTATION_COLUMN_NAMES)
107 if include_other_fields:
108 return_list = self.OTHER_FIELD_NAMES + return_list
109 if include_comments:
110 return_list.extend(self.COMMENT_COLUMN_NAMES)
111 return_list.append(self.RAW_COMMENT_COLUMN_NAME)
112 return return_list
113
114 def get_test_case_element(self, test_case, column_names):
115 """Get test case elements.
116
117 A test case is a collection of test case elements.
118
119 Args:
120 test_case: test case data that contains all test case elements.
121 column_names: column names for specifying test case elements.
122
123 Returns:
124 A list of test case elements for the given column names.
125 """
126 field_indexes = self.get_column_indexes(column_names)
127 test_case = self.get_test_case_by_name(test_case)
128 if test_case is None:
129 return []
130 return [test_case[fid] for fid in field_indexes]
131
132 def get_test_case_by_name(self, target):
133 """Get test case object by test case name.
134
135 Args:
136 target: test case name.
137
138 Returns:
139 A test case with the test case name or None if the test case
140 cannot be found.
141 """
142 for testcase in self.testcases:
143 # Test case name is stored in the first column.
144 if testcase[0] == target:
145 return testcase
146 return None
147
148 def get_all_test_case_names(self):
149 """Get all test case names.
150
151 Returns:
152 A list of test case names.
153 """
154 return [testcase[0] for testcase in self.testcases]
155
156 def generate_link_for_bug(self, bug):
157 """Generate link for a bug.
158
159 Parse the bug description accordingly. The bug description can be like
160 the following: BUGWK1234, BUGCR1234, BUGFOO.
161
162 Args:
163 bug: A string bug description
164
165 Returns:
166 A string that represents a bug link. Returns an empty
167 string if match is not found.
168 """
169 pattern_for_webkit_bug = 'BUGWK(\d+)'
170 match = re.search(pattern_for_webkit_bug, bug)
171 if match is not None:
172 return self.WEBKIT_BUG_URL + match.group(1)
173 pattern_for_chrome_bug = 'BUGCR(\d+)'
174 match = re.search(pattern_for_chrome_bug, bug)
175 if match is not None:
176 return self.CHROME_BUG_URL + match.group(1)
177 pattern_for_other_bug = 'BUG(\S+)'
178 match = re.search(pattern_for_other_bug, bug)
179 if match is not None:
180 return 'mailto:%s@chromium.org' % match.group(1).lower()
181 return ''
182
183 def generate_link_for_dashboard(self, testcase_name, webkit):
184 """Generate link for flakiness dashboard.
185
186 Args:
187 testcase_name: a string test case name.
188 webkit: a boolean indicating whether to use the webkit dashboard.
189
190 Returns:
191 A string link to the flakiness dashboard.
192 """
193 url = self.FLAKINESS_DASHBOARD_LINK + urllib.quote(testcase_name)
194 if webkit:
195 url += '&master=webkit.org'
196 return url
197
198 def parse_line(self, line, previous_comment_info_list, test_case_patterns,
199 media_test_cases_only, writer):
200 """Parse each line in test_expectations.txt.
201
202 The format of each line is as follows:
203 BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] [<platform>] [<config>]
204 : <url> = <EXPCTATION>
205 The example is:
206 BUG123 BUG345 MAC : media/hoge.html
207 WONTFIX SKIP BUG19635 : media/restore-from-page-cache.html
208 = TIMEOUT
209
210 Args:
211 line: a text for each line in the test expectation file.
212 previous_comment_info_list: a list of comments in previous lines.
213 test_case_patterns: a string for test case pattern.
214 media_test_cases_only: a boolean for media test case only.
215 writer: writer for intermediate results.
216 """
217 bugs = re.findall(r'BUG\w+', line)
218 testcases = re.search(r':\s+(\S+.[html|xml|svg|js])', line)
219 if testcases is not None:
220 testcase = testcases.group(1)
221 data = []
222 data.append(testcase)
223
224 if LayoutTestCaseManager.check_test_case_matches_pattern(
225 testcase, test_case_patterns):
226 data.append('Y')
227 media_test_case = True
228 else:
229 data.append('N')
230 media_test_case = False
231
232 dlink = self.generate_link_for_dashboard(testcase, False)
233 # Generate dashboard link.
234 data.append(
235 CsvUtils.generate_hyperlink_in_csv(dlink, 'Y', ''))
236 dlink = self.generate_link_for_dashboard(testcase, True)
237 # Generate dashboard link.
238 data.append(
239 CsvUtils.generate_hyperlink_in_csv(dlink, 'Y', ''))
240 for bug in bugs:
241 blink = self.generate_link_for_bug(bug)
242 data.append(
243 CsvUtils.generate_hyperlink_in_csv(blink, bug, ''))
244 # Fill the gap for test case with less bug/test.
245 for i in range(2 - len(bugs)):
246 data.append('')
247
248 # Fill all data with 'N' (default).
249 for field_name in self.get_all_column_names(False, False):
250 if field_name in line:
251 data.append('Y')
252 else:
253 data.append('N')
254 # Comments will be accumulated in previous comments.
255 # This is for multiple-line comment.
256 for previous_comment_info in previous_comment_info_list:
257 data.append(previous_comment_info)
258 # Write only media related test case if specified.
259 if (media_test_case and media_test_cases_only
260 or (not media_test_cases_only)):
261 writer.writerow(data)
262 self.testcases.append(data)
263
264 def process_comments(self, comments):
265 """Process comments in the test expectations file.
266
267 Comments may contain special keywords such as UNIMPLEMENTED,
268 KNOWNISSUE, TESTISSUE (in TestExpectationsManager.COMMENT_COLUMN_NAMES)
269 and may be multiple lines.
270
271 Args:
272 comments: A raw comment from the test expectation file.
273 It is above test case name.
274
275 Returns:
276 A list of 'Y' or 'N' whether the comments contain
277 each column name in TestExpectationsManager.COMMENT_COLUMN_NAMES
278 in the comments.
279 """
280 return_list = []
281 for ccn in TestExpectationsManager.COMMENT_COLUMN_NAMES:
282 if ccn in comments:
283 return_list.append('Y')
284 else:
285 return_list.append('N')
286 return_list.append(comments)
287 return return_list
288
289 def get_and_save_content(self, location, output):
290 """Simply get test expectation from the specified location and save it.
291
292 Args:
293 location: SVN location of the test expectation file.
294 output: an output file path including file name.
295 """
296 client = pysvn.Client()
297 file_object = open(output, 'w')
298 file_object.write(client.cat(location))
299 file_object.close()
300
301 def get_and_save_content_media_only(self, location, output):
302 """Simply get test expectation from the specified location.
303
304 It also saves it (media only).
305
306 Args:
307 location: SVN location of the test expectation file.
308 output: an output file path including file name.
309 """
310 file_object = file(location, 'r')
311 text_list = list(file_object)
312 output_text = ''
313 for txt in text_list:
314 if txt.startswith('//'):
315 output_text += txt
316 else:
317 for pattern in (
318 self.TEST_CASE_PATTERNS.get_test_case_pattern(
319 'media')):
320 if re.search(pattern, txt) is not None:
321 output_text += txt
322 break
323 file_object.close()
324 # Save output.
325 file_object = open(output, 'w')
326 file_object.write(output_text)
327 file_object.close()
328
329 def get_and_parse_content(self, location, output, media_test_case_only):
330 """Get test_expectations.txt and parse the content.
331
332 The comments are parsed as well since it contains keywords.
333
334 Args:
335 location: SVN location of the test expectation file.
336 output: an output file path including file name.
337 media_test_case_only: A boolean indicating whether media
338 test cases should only be processed.
339 """
340 if location.startswith('http'):
341 # Get from SVN.
342 client = pysvn.Client()
343 # Check out the current version of the pysvn project.
344 txt = client.cat(location)
345 else:
346 if location.endswith('.csv'):
347 # No parsing in the case of CSV file.
348 # Direct reading.
349 file_object = file(location, 'r')
350 self.testcases = list(csv.reader(file_object))
351 file_object.close()
352 return
353 file_object = open(location, 'r')
354 txt = file_object.read()
355 file_object.close()
356
357 file_object = file(output, 'wb')
358 writer = csv.writer(file_object)
359 writer.writerow(self.get_all_column_names(True, True))
360 lines = txt.split('\n')
361 process = False
362 previous_comment = ''
363 previous_comment_list = []
364 for line in lines:
365 if line.isspace() or str is '':
366 continue
367 line.strip()
368 if line.startswith('//'):
369 # There are comments.
370 line = line.replace('//', '')
371 if process is True:
372 previous_comment = line
373 else:
374 previous_comment = previous_comment + '\n' + line
375 previous_comment_list = self.process_comments(previous_comment)
376 process = False
377 else:
378 self.parse_line(
379 line, previous_comment_list,
380 self.TEST_CASE_PATTERNS.get_test_case_pattern('media'),
381 media_test_case_only, writer)
382 process = True
383 file_object.close()
384
385 def get_te_diff_between_times(self, te_location, start, end, patterns,
386 change, checkchange):
387 """Get test expectation diff output for a given time period.
388
389 Args:
390 te_location: SVN location of the test expectation file.
391 start: a date object for the start of the time period.
392 end: a date object for the end of the time period.
393 patterns: test case name patterns of the test cases that
394 we are interested in. This could be test case name
395 as it is in the case of exact matching.
396 change: the number of change in occurrence of test case in
397 test expectation.
398 checkchange: a boolean to indicate if we want to check the
399 change in occurrence as specified in |change| argument.
400
401 Returns:
402 a list of tuples (old_revision, new_revision,
403 diff_line, author, date, commit_message) that matches
404 the condition.
405 """
406 client = pysvn.Client()
407 client.checkout(te_location, 'tmp', recurse=False)
408 logs = client.log('tmp/test_expectations.txt',
409 revision_start=pysvn.Revision(
410 pysvn.opt_revision_kind.date, start),
411 revision_end=pysvn.Revision(
412 pysvn.opt_revision_kind.date, end))
413 result_list = []
414 for i in xrange(len(logs) - 1):
415 # PySVN.log returns logs in reverse chronological order.
416 new_rev = logs[i].revision.number
417 old_rev = logs[i + 1].revision.number
418 # Getting information about new revision.
419 author = logs[i].author
420 date = logs[i].date
421 message = logs[i].message
422 text = client.diff('/tmp', 'tmp/test_expectations.txt',
423 revision1=pysvn.Revision(
424 pysvn.opt_revision_kind.number, old_rev),
425 revision2=pysvn.Revision(
426 pysvn.opt_revision_kind.number, new_rev))
427 lines = text.split('\n')
428 for line in lines:
429 for pattern in patterns:
430 matches = re.findall(pattern, line)
431 if matches:
432 if checkchange:
433 if ((line[0] == '+' and change > 0) or
434 (line[0] == '-' and change < 0)):
435 result_list.append((old_rev, new_rev, line,
436 author, date, message))
437 else:
438 if line[0] == '+' or line[0] == '-':
439 result_list.append((old_rev, new_rev, line,
440 author, date, message))
441 return result_list
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698