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

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
« no previous file with comments | « no previous file | media/tools/layout_tests/test_expectations_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 to analyze test expectations for Webkit layout tests."""
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']
31 ALL_TE_KEYWORDS = (DECISION_NAMES + PLATFORM_NAMES + CONFIG_NAMES +
32 EXPECTATION_NAMES)
20 33
21 34
22 class TestExpectationsManager(object): 35 class TestExpectations(object):
23 """This class manages test expectations data for Webkit layout tests. 36 """A class to model the content of test expectation file for analysis.
24 37
25 The detail of the test expectation file can be found in 38 The raw test expectation file can be found in
26 http://trac.webkit.org/wiki/TestExpectations. 39 |DEFAULT_TEST_EXPECTATION_LOCATION|.
40 It is necessary to parse this file and store meaningful information for
41 the analysis (joining with existing layout tests using a test name).
42 Instance variable |all_test_expectation_info| is used.
43 A test name such as 'media/video-source-type.html' is used for the key
44 to store information. However, a test name can appear multiple times in
45 the test expectation file. So, the map should keep all the occurrence
46 information. For example, the current test expectation file has the following
47 two entries:
48 BUGWK58587 LINUX DEBUG GPU : media/video-zoom.html = IMAGE
49 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE
50 In this case, all_test_expectation_info['media/video-zoom.html'] will have
51 a list with two elements, each of which is the map of the test expectation
52 information.
53 """
27 54
28 This class does the following: 55 def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION):
29 (1) get test expectation file from WebKit subversion source 56 """Read the test expectation file from the specified URL and parse it.
30 repository using pysvn. 57
31 (2) parse the file and generate CSV entries. 58 All parsed information is stored into instance variable
32 (3) create hyperlinks where appropriate (e.g., test case name). 59 |all_test_expectation_info|, which is a dictionary mapping a string test
60 name to a list of dictionaries containing test expectation entry
61 information. An example of such dictionary:
62 {'media/video-zoom.html': [{'LINUX': True, 'DEBUG': True ....},
63 {'MAC': True, 'GPU': True ....}]
64 which is produced from the lines:
65 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE
66 BUGCR86714 LINUX DEBUG : media/video-zoom.html = IMAGE
67
68 Args:
69 url: A URL string for the test expectation file.
70
71 Raises:
72 NameError when the test expectation file cannot be retrieved from |url|.
33 """ 73 """
74 self.all_test_expectation_info = {}
75 resp = urllib2.urlopen(url)
76 if resp.code != 200:
77 raise NameError('Test expectation file does not exist in %s' % source)
78 # Start parsing each line.
79 comments = ''
80 for line in resp.read().split('\n'):
81 if line.startswith('//'):
82 # Comments can be multiple lines.
83 comments += line.replace('//', '')
84 elif not line:
85 comments = ''
86 else:
87 test_expectation_info = self.ParseLine(line, comments)
88 testname = TestExpectations.ExtractTestOrDirectoryName(line)
89 if not testname in self.all_test_expectation_info:
90 self.all_test_expectation_info[testname] = []
91 # This is a list for multiple entries.
92 self.all_test_expectation_info[testname].append(test_expectation_info)
34 93
35 DEFAULT_TEST_EXPECTATION_DIR = ( 94 @staticmethod
36 'http://svn.webkit.org/repository/webkit/trunk/' 95 def ExtractTestOrDirectoryName(line):
37 'LayoutTests/platform/chromium/') 96 """Extract either a test name or a directory name from each line.
38 97
39 DEFAULT_TEST_EXPECTATION_LOCATION = ( 98 Please note the name in the test expectation entry can be test name or
40 'http://svn.webkit.org/repository/webkit/trunk/' 99 directory: Such examples are:
41 'LayoutTests/platform/chromium/test_expectations.txt') 100 BUGWK43668 SKIP : media/track/ = TIMEOUT
42 101
43 CHROME_BUG_URL = 'http://code.google.com/p/chromium/issues/detail?id=' 102 Args:
103 line: a line in the test expectation file.
44 104
45 WEBKIT_BUG_URL = 'https://bugs.webkit.org/show_bug.cgi?id=' 105 Returns:
106 a test name or directory name string. Returns '' if no match.
46 107
47 FLAKINESS_DASHBOARD_LINK = ( 108 Raises:
48 'http://test-results.appspot.com/dashboards/' 109 ValueError when there is no test name match.
49 'flakiness_dashboard.html#tests=') 110 """
111 # First try to find test name ending with .html.
112 matches = re.search(r':\s+(\S+.html)', line)
113 # Next try to find directory name.
114 if matches:
115 return matches.group(1)
116 matches = re.search(r':\s+(\S+)', line)
117 if matches:
118 return matches.group(1)
119 else:
120 raise ValueError('test or dictionary name cannot be found in the line')
50 121
51 TEST_CASE_PATTERNS = TestCasePatterns() 122 @staticmethod
123 def ParseLine(line, comment_prefix):
124 """Parse each line in test expectation and update test expectation info.
52 125
53 OTHER_FIELD_NAMES = ['TestCase', 'Media', 'Flaky', 'WebKitD', 'Bug', 126 This function checks for each entry from |ALL_TE_KEYWORDS| in the current
54 'Test'] 127 line and stores it in the test expectation info map if found. Comment
128 and bug information is also stored in the map.
55 129
56 # The following is from test expectation syntax. 130 Args:
57 # BUG[0-9]+ [SKIP] [WONTFIX] [SLOW] 131 line: a line in the test expectation file. For example,
58 DECISION_COLUMN_NAMES = ['SKIP', 'SLOW'] 132 "BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE"
59 # <platform> ::== [GPU] [CPU] [WIN] [LINUX] [MAC] 133 comment_prefix: comments from the test expectation file occurring just
60 PLATFORM_COLUMN_NAMES = ['GPU', 'CPU', 'WIN', 'LINUX', 'MAC'] 134 before the current line being parsed.
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 135
69 # These are coming from metadata in comments in the test expectation file. 136 Returns:
70 COMMENT_COLUMN_NAMES = ['UNIMPLEMENTED', 'KNOWNISSUE', 'TESTISSUE', 137 a dictionary containing test expectation info, including comment and bug
71 'WONTFIX'] 138 info.
72 139 """
73 RAW_COMMENT_COLUMN_NAME = 'COMMENTS' 140 test_expectation_info = {}
74 141 # Store comments.
75 def __init__(self): 142 inline_comments = ''
76 """Initialize the test cases.""" 143 if '//' in line:
77 self.testcases = [] 144 inline_comments = line[line.rindex('//') + 2:]
78 145 # Remove the inline comments to avoid the case where keywords are in
79 def get_column_indexes(self, column_names): 146 # inline comments.
80 """Get column indexes for given column names. 147 line = line[0:line.rindex('//')]
81 148 for name in ALL_TE_KEYWORDS:
82 Args: 149 if name in line:
83 column_names: a list of column names. 150 test_expectation_info[name] = True
84 151 test_expectation_info['Comments'] = comment_prefix + inline_comments
85 Returns: 152 # Store bug informations.
86 a list of indexes for the given column names. 153 bugs = re.findall(r'BUG\w+', line)
87 """ 154 if bugs:
88 all_field_names = self.get_all_column_names(True, True) 155 test_expectation_info['Bugs'] = bugs
89 return [all_field_names.index( 156 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
« no previous file with comments | « no previous file | media/tools/layout_tests/test_expectations_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698