summaryrefslogtreecommitdiffstats
path: root/webkit/tools/leak_tests/run_node_leak_test.py
blob: f45869ac6df051f6a24b0f644c7c87156b86aecb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
#!/usr/bin/python
# Copyright (c) 2006-2008 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Run node leak tests using the test_shell.

TODO(pjohnson): Add a way for layout tests (and other local files in the
working copy) to be easily run by specifying paths relative to webkit (or
something similar).
"""

import logging
import optparse
import os
import random
import re
import sys

import google.logging_utils
import google.path_utils
import google.platform_utils
import google.process_utils

# Magic exit code to indicate a new fix.
REBASELINE_EXIT_CODE = -88

# Status codes.
PASS, FAIL, REBASELINE = range(3)

# The test list files are found in this subdirectory, which must be a sibling
# to this script itself.
TEST_FILE_DIR = 'test_lists'

# TODO(pjohnson): Find a way to avoid this duplicate code.  This function has
# been shamelessly taken from layout_tests/layout_package.
_webkit_root = None

def WebKitRoot():
  """Returns the full path to the directory containing webkit.sln.  Raises
  PathNotFound if we're unable to find webkit.sln.
  """

  global _webkit_root
  if _webkit_root:
    return _webkit_root
  webkit_sln_path = google.path_utils.FindUpward(google.path_utils.ScriptDir(),
                                                 'webkit.sln')
  _webkit_root = os.path.dirname(webkit_sln_path)
  return _webkit_root

def GetAbsolutePath(path):
  platform_util = google.platform_utils.PlatformUtility(WebKitRoot())
  return platform_util.GetAbsolutePath(path)

# TODO(pjohnson): Find a way to avoid this duplicated code.  This function has
# been mostly copied from another function, TestShellBinary, in
# layout_tests/layout_package.
def TestShellTestBinary(target):
  """Gets the full path to the test_shell_tests binary for the target build
  configuration.  Raises PathNotFound if the file doesn't exist.
  """

  full_path = os.path.join(WebKitRoot(), target, 'test_shell_tests.exe')
  if not os.path.exists(full_path):
    # Try chrome's output directory in case test_shell was built by chrome.sln.
    full_path = google.path_utils.FindUpward(WebKitRoot(), 'chrome', target,
                                             'test_shell_tests.exe')
    if not os.path.exists(full_path):
      raise PathNotFound('unable to find test_shell_tests at %s' % full_path)
  return full_path

class NodeLeakTestRunner:
  """A class for managing running a series of node leak tests.
  """

  def __init__(self, options, urls):
    """Collect a list of URLs to test.

    Args:
      options: a dictionary of command line options
      urls: a list of URLs in the format:
            (url, expected_node_leaks, expected_js_leaks) tuples
    """

    self._options = options
    self._urls = urls

    self._test_shell_test_binary = TestShellTestBinary(options.target)

    self._node_leak_matcher = re.compile('LEAK: (\d+) Node')
    self._js_leak_matcher = re.compile('Leak (\d+) JS wrappers')

  def RunCommand(self, command):
    def FindMatch(line, matcher, group_number):
      match = matcher.match(line)
      if match:
        return int(match.group(group_number))
      return 0

    (code, output) = google.process_utils.RunCommandFull(command, verbose=True,
                                                         collect_output=True,
                                                         print_output=False)
    node_leaks = 0
    js_leaks = 0

    # Print a row of dashes.
    if code != 0:
      print '-' * 75
      print 'OUTPUT'
      print

    for line in output:
      # Sometimes multiple leak lines are printed out, which is why we
      # accumulate them here.
      node_leaks += FindMatch(line, self._node_leak_matcher, 1)
      js_leaks += FindMatch(line, self._js_leak_matcher, 1)

      # If the code indicates there was an error, print the output to help
      # figure out what happened.
      if code != 0:
        print line

    # Print a row of dashes.
    if code != 0:
      print '-' * 75
      print

    return (code, node_leaks, js_leaks)

  def RunUrl(self, test_url, expected_node_leaks, expected_js_leaks):
    shell_args = ['--gtest_filter=NodeLeakTest.*TestURL',
                  '--time-out-ms=' + str(self._options.time_out_ms),
                  '--test-url=' + test_url,
                  '--playback-mode']

    if self._options.cache_dir != '':
      shell_args.append('--cache-dir=' + self._options.cache_dir)

    command = [self._test_shell_test_binary] + shell_args
    (exit_code, actual_node_leaks, actual_js_leaks) = self.RunCommand(command)

    logging.info('%s\n' % test_url)

    if exit_code != 0:
      # There was a crash, or something else went wrong, so duck out early.
      logging.error('Test returned: %d\n' % exit_code)
      return FAIL

    result = ('TEST RESULT\n'
              '    Node Leaks: %d (actual), %d (expected)\n'
              '    JS Leaks: %d (actual), %d (expected)\n' %
              (actual_node_leaks, expected_node_leaks,
               actual_js_leaks, expected_js_leaks))

    success = (actual_node_leaks <= expected_node_leaks and
               actual_js_leaks <= expected_js_leaks)

    if success:
      logging.info(result)
    else:
      logging.error(result)
      logging.error('Unexpected leaks found!\n')
      return FAIL

    if (expected_node_leaks > actual_node_leaks or
        expected_js_leaks > actual_js_leaks):
       logging.warn('Expectations may need to be re-baselined.\n')
       # TODO(pjohnson): Return REBASELINE here once bug 1177263 is fixed and
       # the expectations have been lowered again.

    return PASS

  def Run(self):
    status = PASS
    results = [0, 0, 0]
    failed_urls = []
    rebaseline_urls = []

    for (test_url, expected_node_leaks, expected_js_leaks) in self._urls:
      result = self.RunUrl(test_url, expected_node_leaks, expected_js_leaks)
      if result == PASS:
        results[PASS] += 1
      elif result == FAIL:
        results[FAIL] += 1
        failed_urls.append(test_url)
        status = FAIL
      elif result == REBASELINE:
        results[REBASELINE] += 1
        rebaseline_urls.append(test_url)
        if status != FAIL:
          status = REBASELINE
    return (status, results, failed_urls, rebaseline_urls)

def main(options, args):
  if options.seed != None:
    random.seed(options.seed)

  # Set up logging so any messages below logging.WARNING are sent to stdout,
  # otherwise they are sent to stderr.
  google.logging_utils.config_root(level=logging.INFO,
                                   threshold=logging.WARNING)

  if options.url_list == '':
    logging.error('URL test list required')
    sys.exit(1)

  url_list = os.path.join(os.path.dirname(sys.argv[0]), TEST_FILE_DIR,
                          options.url_list)
  url_list = GetAbsolutePath(url_list);

  lines = []
  file = None
  try:
    file = open(url_list, 'r')
    lines = file.readlines()
  finally:
    if file != None:
      file.close()

  expected_matcher = re.compile('(\d+)\s*,\s*(\d+)')

  urls = []
  for line in lines:
    line = line.strip()
    if len(line) == 0 or line.startswith('#'):
      continue
    list = line.rsplit('=', 1)
    if len(list) < 2:
      logging.error('Line "%s" is not formatted correctly' % line)
      continue
    match = expected_matcher.match(list[1].strip())
    if not match:
      logging.error('Line "%s" is not formatted correctly' % line)
      continue
    urls.append((list[0].strip(), int(match.group(1)), int(match.group(2))))

  random.shuffle(urls)
  runner = NodeLeakTestRunner(options, urls)
  (status, results, failed_urls, rebaseline_urls) = runner.Run()

  logging.info('SUMMARY\n'
               '    %d passed\n'
               '    %d failed\n'
               '    %d re-baseline\n' %
               (results[0], results[1], results[2]))

  if len(failed_urls) > 0:
    failed_string = '\n'.join('    ' + url for url in failed_urls)
    logging.error('FAILED URLs\n%s\n' % failed_string)

  if len(rebaseline_urls) > 0:
    rebaseline_string = '\n'.join('    ' + url for url in rebaseline_urls)
    logging.warn('RE-BASELINE URLs\n%s\n' % rebaseline_string)

  if status == FAIL:
    return 1
  elif status == REBASELINE:
    return REBASELINE_EXIT_CODE
  return 0

if '__main__' == __name__:
  option_parser = optparse.OptionParser()
  option_parser.add_option('', '--target', default='Debug',
                           help='build target (Debug or Release)')
  option_parser.add_option('', '--cache-dir', default='',
                           help='use a specified cache directory')
  option_parser.add_option('', '--url-list', default='',
                           help='URL input file, with leak expectations, '
                                 'relative to webkit/tools/leak_tests')
  option_parser.add_option('', '--time-out-ms', default=30000,
                           help='time out for each test')
  option_parser.add_option('', '--seed', default=None,
                           help='seed for random number generator, use to '
                                'reproduce the exact same order for a '
                                'specific run')
  options, args = option_parser.parse_args()
  sys.exit(main(options, args))