| OLD | NEW |
| (Empty) |
| 1 #!/usr/bin/python | |
| 2 # Copyright (c) 2006-2008 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 """Run node leak tests using the test_shell. | |
| 7 | |
| 8 TODO(pjohnson): Add a way for layout tests (and other local files in the | |
| 9 working copy) to be easily run by specifying paths relative to webkit (or | |
| 10 something similar). | |
| 11 """ | |
| 12 | |
| 13 import logging | |
| 14 import optparse | |
| 15 import os | |
| 16 import random | |
| 17 import re | |
| 18 import sys | |
| 19 | |
| 20 import google.logging_utils | |
| 21 import google.path_utils | |
| 22 import google.platform_utils | |
| 23 import google.process_utils | |
| 24 | |
| 25 # Magic exit code to indicate a new fix. | |
| 26 REBASELINE_EXIT_CODE = -88 | |
| 27 | |
| 28 # Status codes. | |
| 29 PASS, FAIL, REBASELINE = range(3) | |
| 30 | |
| 31 # The test list files are found in this subdirectory, which must be a sibling | |
| 32 # to this script itself. | |
| 33 TEST_FILE_DIR = 'test_lists' | |
| 34 | |
| 35 # TODO(pjohnson): Find a way to avoid this duplicate code. This function has | |
| 36 # been shamelessly taken from layout_tests/layout_package. | |
| 37 _webkit_root = None | |
| 38 | |
| 39 def WebKitRoot(): | |
| 40 """Returns the full path to the directory containing webkit.sln. Raises | |
| 41 PathNotFound if we're unable to find webkit.sln. | |
| 42 """ | |
| 43 | |
| 44 global _webkit_root | |
| 45 if _webkit_root: | |
| 46 return _webkit_root | |
| 47 webkit_sln_path = google.path_utils.FindUpward(google.path_utils.ScriptDir(), | |
| 48 'webkit.sln') | |
| 49 _webkit_root = os.path.dirname(webkit_sln_path) | |
| 50 return _webkit_root | |
| 51 | |
| 52 def GetAbsolutePath(path): | |
| 53 platform_util = google.platform_utils.PlatformUtility(WebKitRoot()) | |
| 54 return platform_util.GetAbsolutePath(path) | |
| 55 | |
| 56 # TODO(pjohnson): Find a way to avoid this duplicated code. This function has | |
| 57 # been mostly copied from another function, TestShellBinary, in | |
| 58 # layout_tests/layout_package. | |
| 59 def TestShellTestBinary(target): | |
| 60 """Gets the full path to the test_shell_tests binary for the target build | |
| 61 configuration. Raises PathNotFound if the file doesn't exist. | |
| 62 """ | |
| 63 | |
| 64 full_path = os.path.join(WebKitRoot(), target, 'test_shell_tests.exe') | |
| 65 if not os.path.exists(full_path): | |
| 66 # Try chrome's output directory in case test_shell was built by chrome.sln. | |
| 67 full_path = google.path_utils.FindUpward(WebKitRoot(), 'chrome', target, | |
| 68 'test_shell_tests.exe') | |
| 69 if not os.path.exists(full_path): | |
| 70 raise PathNotFound('unable to find test_shell_tests at %s' % full_path) | |
| 71 return full_path | |
| 72 | |
| 73 class NodeLeakTestRunner: | |
| 74 """A class for managing running a series of node leak tests. | |
| 75 """ | |
| 76 | |
| 77 def __init__(self, options, urls): | |
| 78 """Collect a list of URLs to test. | |
| 79 | |
| 80 Args: | |
| 81 options: a dictionary of command line options | |
| 82 urls: a list of URLs in the format: | |
| 83 (url, expected_node_leaks, expected_js_leaks) tuples | |
| 84 """ | |
| 85 | |
| 86 self._options = options | |
| 87 self._urls = urls | |
| 88 | |
| 89 self._test_shell_test_binary = TestShellTestBinary(options.target) | |
| 90 | |
| 91 self._node_leak_matcher = re.compile('LEAK: (\d+) Node') | |
| 92 self._js_leak_matcher = re.compile('Leak (\d+) JS wrappers') | |
| 93 | |
| 94 def RunCommand(self, command): | |
| 95 def FindMatch(line, matcher, group_number): | |
| 96 match = matcher.match(line) | |
| 97 if match: | |
| 98 return int(match.group(group_number)) | |
| 99 return 0 | |
| 100 | |
| 101 (code, output) = google.process_utils.RunCommandFull(command, verbose=True, | |
| 102 collect_output=True, | |
| 103 print_output=False) | |
| 104 node_leaks = 0 | |
| 105 js_leaks = 0 | |
| 106 | |
| 107 # Print a row of dashes. | |
| 108 if code != 0: | |
| 109 print '-' * 75 | |
| 110 print 'OUTPUT' | |
| 111 print | |
| 112 | |
| 113 for line in output: | |
| 114 # Sometimes multiple leak lines are printed out, which is why we | |
| 115 # accumulate them here. | |
| 116 node_leaks += FindMatch(line, self._node_leak_matcher, 1) | |
| 117 js_leaks += FindMatch(line, self._js_leak_matcher, 1) | |
| 118 | |
| 119 # If the code indicates there was an error, print the output to help | |
| 120 # figure out what happened. | |
| 121 if code != 0: | |
| 122 print line | |
| 123 | |
| 124 # Print a row of dashes. | |
| 125 if code != 0: | |
| 126 print '-' * 75 | |
| 127 print | |
| 128 | |
| 129 return (code, node_leaks, js_leaks) | |
| 130 | |
| 131 def RunUrl(self, test_url, expected_node_leaks, expected_js_leaks): | |
| 132 shell_args = ['--gtest_filter=NodeLeakTest.*TestURL', | |
| 133 '--time-out-ms=' + str(self._options.time_out_ms), | |
| 134 '--test-url=' + test_url, | |
| 135 '--playback-mode'] | |
| 136 | |
| 137 if self._options.cache_dir != '': | |
| 138 shell_args.append('--cache-dir=' + self._options.cache_dir) | |
| 139 | |
| 140 command = [self._test_shell_test_binary] + shell_args | |
| 141 (exit_code, actual_node_leaks, actual_js_leaks) = self.RunCommand(command) | |
| 142 | |
| 143 logging.info('%s\n' % test_url) | |
| 144 | |
| 145 if exit_code != 0: | |
| 146 # There was a crash, or something else went wrong, so duck out early. | |
| 147 logging.error('Test returned: %d\n' % exit_code) | |
| 148 return FAIL | |
| 149 | |
| 150 result = ('TEST RESULT\n' | |
| 151 ' Node Leaks: %d (actual), %d (expected)\n' | |
| 152 ' JS Leaks: %d (actual), %d (expected)\n' % | |
| 153 (actual_node_leaks, expected_node_leaks, | |
| 154 actual_js_leaks, expected_js_leaks)) | |
| 155 | |
| 156 success = (actual_node_leaks <= expected_node_leaks and | |
| 157 actual_js_leaks <= expected_js_leaks) | |
| 158 | |
| 159 if success: | |
| 160 logging.info(result) | |
| 161 else: | |
| 162 logging.error(result) | |
| 163 logging.error('Unexpected leaks found!\n') | |
| 164 return FAIL | |
| 165 | |
| 166 if (expected_node_leaks > actual_node_leaks or | |
| 167 expected_js_leaks > actual_js_leaks): | |
| 168 logging.warn('Expectations may need to be re-baselined.\n') | |
| 169 # TODO(pjohnson): Return REBASELINE here once bug 1177263 is fixed and | |
| 170 # the expectations have been lowered again. | |
| 171 | |
| 172 return PASS | |
| 173 | |
| 174 def Run(self): | |
| 175 status = PASS | |
| 176 results = [0, 0, 0] | |
| 177 failed_urls = [] | |
| 178 rebaseline_urls = [] | |
| 179 | |
| 180 for (test_url, expected_node_leaks, expected_js_leaks) in self._urls: | |
| 181 result = self.RunUrl(test_url, expected_node_leaks, expected_js_leaks) | |
| 182 if result == PASS: | |
| 183 results[PASS] += 1 | |
| 184 elif result == FAIL: | |
| 185 results[FAIL] += 1 | |
| 186 failed_urls.append(test_url) | |
| 187 status = FAIL | |
| 188 elif result == REBASELINE: | |
| 189 results[REBASELINE] += 1 | |
| 190 rebaseline_urls.append(test_url) | |
| 191 if status != FAIL: | |
| 192 status = REBASELINE | |
| 193 return (status, results, failed_urls, rebaseline_urls) | |
| 194 | |
| 195 def main(options, args): | |
| 196 if options.seed != None: | |
| 197 random.seed(options.seed) | |
| 198 | |
| 199 # Set up logging so any messages below logging.WARNING are sent to stdout, | |
| 200 # otherwise they are sent to stderr. | |
| 201 google.logging_utils.config_root(level=logging.INFO, | |
| 202 threshold=logging.WARNING) | |
| 203 | |
| 204 if options.url_list == '': | |
| 205 logging.error('URL test list required') | |
| 206 sys.exit(1) | |
| 207 | |
| 208 url_list = os.path.join(os.path.dirname(sys.argv[0]), TEST_FILE_DIR, | |
| 209 options.url_list) | |
| 210 url_list = GetAbsolutePath(url_list); | |
| 211 | |
| 212 lines = [] | |
| 213 file = None | |
| 214 try: | |
| 215 file = open(url_list, 'r') | |
| 216 lines = file.readlines() | |
| 217 finally: | |
| 218 if file != None: | |
| 219 file.close() | |
| 220 | |
| 221 expected_matcher = re.compile('(\d+)\s*,\s*(\d+)') | |
| 222 | |
| 223 urls = [] | |
| 224 for line in lines: | |
| 225 line = line.strip() | |
| 226 if len(line) == 0 or line.startswith('#'): | |
| 227 continue | |
| 228 list = line.rsplit('=', 1) | |
| 229 if len(list) < 2: | |
| 230 logging.error('Line "%s" is not formatted correctly' % line) | |
| 231 continue | |
| 232 match = expected_matcher.match(list[1].strip()) | |
| 233 if not match: | |
| 234 logging.error('Line "%s" is not formatted correctly' % line) | |
| 235 continue | |
| 236 urls.append((list[0].strip(), int(match.group(1)), int(match.group(2)))) | |
| 237 | |
| 238 random.shuffle(urls) | |
| 239 runner = NodeLeakTestRunner(options, urls) | |
| 240 (status, results, failed_urls, rebaseline_urls) = runner.Run() | |
| 241 | |
| 242 logging.info('SUMMARY\n' | |
| 243 ' %d passed\n' | |
| 244 ' %d failed\n' | |
| 245 ' %d re-baseline\n' % | |
| 246 (results[0], results[1], results[2])) | |
| 247 | |
| 248 if len(failed_urls) > 0: | |
| 249 failed_string = '\n'.join(' ' + url for url in failed_urls) | |
| 250 logging.error('FAILED URLs\n%s\n' % failed_string) | |
| 251 | |
| 252 if len(rebaseline_urls) > 0: | |
| 253 rebaseline_string = '\n'.join(' ' + url for url in rebaseline_urls) | |
| 254 logging.warn('RE-BASELINE URLs\n%s\n' % rebaseline_string) | |
| 255 | |
| 256 if status == FAIL: | |
| 257 return 1 | |
| 258 elif status == REBASELINE: | |
| 259 return REBASELINE_EXIT_CODE | |
| 260 return 0 | |
| 261 | |
| 262 if '__main__' == __name__: | |
| 263 option_parser = optparse.OptionParser() | |
| 264 option_parser.add_option('', '--target', default='Debug', | |
| 265 help='build target (Debug or Release)') | |
| 266 option_parser.add_option('', '--cache-dir', default='', | |
| 267 help='use a specified cache directory') | |
| 268 option_parser.add_option('', '--url-list', default='', | |
| 269 help='URL input file, with leak expectations, ' | |
| 270 'relative to webkit/tools/leak_tests') | |
| 271 option_parser.add_option('', '--time-out-ms', default=30000, | |
| 272 help='time out for each test') | |
| 273 option_parser.add_option('', '--seed', default=None, | |
| 274 help='seed for random number generator, use to ' | |
| 275 'reproduce the exact same order for a ' | |
| 276 'specific run') | |
| 277 options, args = option_parser.parse_args() | |
| 278 sys.exit(main(options, args)) | |
| 279 | |
| OLD | NEW |