| OLD | NEW |
| 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """A module to analyze test expectations for Webkit layout tests.""" | 5 """A module to analyze test expectations for Webkit layout tests.""" |
| 6 | 6 |
| 7 import re | |
| 8 import urllib2 | 7 import urllib2 |
| 9 | 8 |
| 9 from webkitpy.layout_tests.models.test_expectations import * |
| 10 |
| 10 # Default Webkit SVN location for chromium test expectation file. | 11 # Default Webkit SVN location for chromium test expectation file. |
| 11 # TODO(imasaki): support multiple test expectations files. | 12 # TODO(imasaki): support multiple test expectations files. |
| 12 DEFAULT_TEST_EXPECTATION_LOCATION = ( | 13 DEFAULT_TEST_EXPECTATION_LOCATION = ( |
| 13 'http://svn.webkit.org/repository/webkit/trunk/' | 14 'http://svn.webkit.org/repository/webkit/trunk/' |
| 14 'LayoutTests/platform/chromium/TestExpectations') | 15 'LayoutTests/platform/chromium/TestExpectations') |
| 15 | 16 |
| 16 # The following is from test expectation syntax. The detail can be found in | 17 # The following is from test expectation syntax. The detail can be found in |
| 17 # http://www.chromium.org/developers/testing/ | 18 # http://www.chromium.org/developers/testing/webkit-layout-tests#TOC-Test-Expect
ations |
| 18 # webkit-layout-tests#TOC-Test-Expectations | |
| 19 # <decision> ::== [SKIP] [WONTFIX] [SLOW] | 19 # <decision> ::== [SKIP] [WONTFIX] [SLOW] |
| 20 DECISION_NAMES = ['SKIP', 'WONTFIX', 'SLOW'] | 20 DECISION_NAMES = ['SKIP', 'WONTFIX', 'SLOW'] |
| 21 # <config> ::== RELEASE | DEBUG | 21 # <config> ::== RELEASE | DEBUG |
| 22 CONFIG_NAMES = ['RELEASE', 'DEBUG'] | 22 CONFIG_NAMES = ['RELEASE', 'DEBUG'] |
| 23 # Only hard code keywords we don't expect to change. Determine the rest from | 23 # Only hard code keywords we don't expect to change. Determine the rest from |
| 24 # the format of the status line. | 24 # the format of the status line. |
| 25 KNOWN_TE_KEYWORDS = DECISION_NAMES + CONFIG_NAMES | 25 KNOWN_TE_KEYWORDS = DECISION_NAMES + CONFIG_NAMES |
| 26 | 26 |
| 27 | 27 |
| 28 class TestExpectations(object): | 28 class TestExpectations(object): |
| 29 """A class to model the content of test expectation file for analysis. | 29 """A class to model the content of test expectation file for analysis. |
| 30 | 30 |
| 31 The raw test expectation file can be found in | 31 This class retrieves the TestExpectations file via HTTP from WebKit and uses |
| 32 |DEFAULT_TEST_EXPECTATION_LOCATION|. | 32 the WebKit layout test processor to process each line. |
| 33 It is necessary to parse this file and store meaningful information for | 33 |
| 34 the analysis (joining with existing layout tests using a test name). | 34 The resulting dictionary is stored in |all_test_expectation_info| and looks |
| 35 Instance variable |all_test_expectation_info| is used. | 35 like: |
| 36 A test name such as 'media/video-source-type.html' is used for the key | 36 |
| 37 to store information. However, a test name can appear multiple times in | 37 {'<test name>': [{'<modifier0>': True, '<modifier1>': True, ..., |
| 38 the test expectation file. So, the map should keep all the occurrence | 38 'Platforms: ['<platform0>', ... ], 'Bugs': ['....']}]} |
| 39 information. For example, the current test expectation file has the following | 39 |
| 40 two entries: | 40 Duplicate keys are merged (though technically they shouldn't exist). |
| 41 BUGWK58587 LINUX DEBUG GPU : media/video-zoom.html = IMAGE | 41 |
| 42 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE | 42 Example: |
| 43 In this case, all_test_expectation_info['media/video-zoom.html'] will have | 43 crbug.com/145590 [ Android ] \ |
| 44 a list with two elements, each of which is the map of the test expectation | 44 platform/chromium/media/video-frame-size-change.html [ Timeout ] |
| 45 information. | 45 webkit.org/b/84724 [ SnowLeopard ] \ |
| 46 platform/chromium/media/video-frame-size-change.html \ |
| 47 [ ImageOnlyFailure Pass ] |
| 48 |
| 49 {'platform/chromium/media/video-frame-size-change.html': [{'IMAGE': True, |
| 50 'Bugs': ['BUGWK84724', 'BUGCR145590'], 'Comments': '', |
| 51 'Platforms': ['SNOWLEOPARD', 'ANDROID'], 'TIMEOUT': True, 'PASS': True}]} |
| 46 """ | 52 """ |
| 47 | 53 |
| 48 def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION): | 54 def __init__(self, url=DEFAULT_TEST_EXPECTATION_LOCATION): |
| 49 """Read the test expectation file from the specified URL and parse it. | 55 """Read the test expectation file from the specified URL and parse it. |
| 50 | 56 |
| 51 All parsed information is stored into instance variable | |
| 52 |all_test_expectation_info|, which is a dictionary mapping a string test | |
| 53 name to a list of dictionaries containing test expectation entry | |
| 54 information. An example of such dictionary: | |
| 55 {'media/video-zoom.html': [{'LINUX': True, 'DEBUG': True ....}, | |
| 56 {'MAC': True, 'GPU': True ....}] | |
| 57 which is produced from the lines: | |
| 58 BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE | |
| 59 BUGCR86714 LINUX DEBUG : media/video-zoom.html = IMAGE | |
| 60 | |
| 61 Args: | 57 Args: |
| 62 url: A URL string for the test expectation file. | 58 url: A URL string for the test expectation file. |
| 63 | 59 |
| 64 Raises: | 60 Raises: |
| 65 NameError when the test expectation file cannot be retrieved from |url|. | 61 NameError when the test expectation file cannot be retrieved from |url|. |
| 66 """ | 62 """ |
| 67 self.all_test_expectation_info = {} | 63 self.all_test_expectation_info = {} |
| 68 resp = urllib2.urlopen(url) | 64 resp = urllib2.urlopen(url) |
| 69 if resp.code != 200: | 65 if resp.code != 200: |
| 70 raise NameError('Test expectation file does not exist in %s' % url) | 66 raise NameError('Test expectation file does not exist in %s' % url) |
| 71 # Start parsing each line. | 67 # Start parsing each line. |
| 72 comments = '' | |
| 73 for line in resp.read().split('\n'): | 68 for line in resp.read().split('\n'): |
| 74 if line.startswith('//'): | 69 line = line.strip() |
| 75 # Comments can be multiple lines. | 70 # Skip comments. |
| 76 comments += line.replace('//', '') | 71 if line.startswith('#'): |
| 77 elif not line: | 72 continue |
| 78 comments = '' | 73 testname, te_info = self.ParseLine(line) |
| 74 if not testname or not te_info: |
| 75 continue |
| 76 if testname in self.all_test_expectation_info: |
| 77 # Merge keys if entry already exists. |
| 78 for k in te_info.keys(): |
| 79 if (isinstance(te_info[k], list) and |
| 80 k in self.all_test_expectation_info[testname]): |
| 81 self.all_test_expectation_info[testname][0][k] += te_info[k] |
| 82 else: |
| 83 self.all_test_expectation_info[testname][0][k] = te_info[k] |
| 79 else: | 84 else: |
| 80 test_expectation_info = self.ParseLine(line, comments) | 85 self.all_test_expectation_info[testname] = [te_info] |
| 81 testname = TestExpectations.ExtractTestOrDirectoryName(line) | |
| 82 if not testname in self.all_test_expectation_info: | |
| 83 self.all_test_expectation_info[testname] = [] | |
| 84 # This is a list for multiple entries. | |
| 85 self.all_test_expectation_info[testname].append(test_expectation_info) | |
| 86 | 86 |
| 87 @staticmethod | 87 @staticmethod |
| 88 def ExtractTestOrDirectoryName(line): | 88 def ParseLine(line): |
| 89 """Extract either a test name or a directory name from each line. | 89 """Parses the provided line using WebKit's TextExpecations parser. |
| 90 | |
| 91 Please note the name in the test expectation entry can be test name or | |
| 92 directory: Such examples are: | |
| 93 BUGWK43668 SKIP : media/track/ = TIMEOUT | |
| 94 | |
| 95 Args: | |
| 96 line: a line in the test expectation file. | |
| 97 | 90 |
| 98 Returns: | 91 Returns: |
| 99 a test name or directory name string. Returns '' if no match. | 92 Tuple of test name, test expectations dictionary. See class documentation |
| 100 | 93 for the format of the dictionary |
| 101 Raises: | |
| 102 ValueError when there is no test name match. | |
| 103 """ | |
| 104 # First try to find test name ending with .html. | |
| 105 matches = re.search(r':\s+(\S+(.html|.svg))', line) | |
| 106 # Next try to find directory name. | |
| 107 if matches: | |
| 108 return matches.group(1) | |
| 109 matches = re.search(r':\s+(\S+)', line) | |
| 110 if matches: | |
| 111 return matches.group(1) | |
| 112 else: | |
| 113 raise ValueError('test or dictionary name cannot be found in the line') | |
| 114 | |
| 115 @staticmethod | |
| 116 def ParseLine(line, comment_prefix): | |
| 117 """Parse each line in test expectation and update test expectation info. | |
| 118 | |
| 119 This function checks for each entry from |ALL_TE_KEYWORDS| in the current | |
| 120 line and stores it in the test expectation info map if found. Comment | |
| 121 and bug information is also stored in the map. | |
| 122 | |
| 123 Args: | |
| 124 line: a line in the test expectation file. For example, | |
| 125 "BUGCR86714 MAC GPU : media/video-zoom.html = CRASH IMAGE" | |
| 126 comment_prefix: comments from the test expectation file occurring just | |
| 127 before the current line being parsed. | |
| 128 | |
| 129 Returns: | |
| 130 a dictionary containing test expectation info, including comment and bug | |
| 131 info. | |
| 132 """ | 94 """ |
| 133 test_expectation_info = {} | 95 test_expectation_info = {} |
| 134 # Store comments. | 96 parsed = TestExpectationParser._tokenize_line('TestExpectations', line, 0) |
| 135 inline_comments = '' | 97 if parsed.is_invalid(): |
| 136 if '//' in line: | 98 return None, None |
| 137 inline_comments = line[line.rindex('//') + 2:] | 99 |
| 138 # Remove the inline comments to avoid the case where keywords are in | 100 test_expectation_info['Comments'] = parsed.comment or '' |
| 139 # inline comments. | 101 |
| 140 line = line[0:line.rindex('//')] | 102 # Split the modifiers dictionary into the format we want. |
| 141 for name in KNOWN_TE_KEYWORDS: | 103 remaining_modifiers = list(parsed.modifiers) |
| 142 if name in line: | 104 test_expectation_info['Bugs'] = [] |
| 143 test_expectation_info[name] = True | 105 for m in parsed.modifiers: |
| 144 test_expectation_info['Comments'] = comment_prefix + inline_comments | 106 if m.startswith('BUG'): |
| 145 # Store bug informations. | 107 test_expectation_info['Bugs'].append(m) |
| 146 bugs = re.findall(r'BUG\w+', line) | 108 remaining_modifiers.remove(m) |
| 147 if bugs: | 109 elif m in KNOWN_TE_KEYWORDS: |
| 148 test_expectation_info['Bugs'] = bugs | 110 test_expectation_info[m] = True |
| 149 # Platforms should be the first tags to the left of the " : " after the BUG | 111 remaining_modifiers.remove(m) |
| 150 # information and known keywords have been removed. | 112 |
| 151 test_expectation_info['Platforms'] = [ | 113 # The modifiers left over should all be platform names. |
| 152 x for x in line.split(' : ')[0].split(' ') if x and x not in bugs and | 114 test_expectation_info['Platforms'] = list(remaining_modifiers) |
| 153 x not in KNOWN_TE_KEYWORDS] | 115 |
| 154 # Test expectations should be all the keywords to the right of " = " | 116 # Shovel the expectations and modifiers in as "<key>: True" entries. Ugly, |
| 155 test_expectation = [x for x in line.split(' = ')[-1].split(' ') if x] | 117 # but required by the rest of the pipeline for parsing. |
| 156 # Dump keywords into test expectations dictionary. | 118 for m in parsed.expectations + remaining_modifiers: |
| 157 for name in test_expectation_info['Platforms'] + test_expectation: | 119 test_expectation_info[m] = True |
| 158 test_expectation_info[name] = True | 120 |
| 159 return test_expectation_info | 121 return parsed.name, test_expectation_info |
| OLD | NEW |