1#!/usr/bin/env python
2# Copyright (c) 2012 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"""Generate and process code coverage.
7
8TODO(jrg): rename this from coverage_posix.py to coverage_all.py!
9
10Written for and tested on Mac, Linux, and Windows.  To use this script
11to generate coverage numbers, please run from within a gyp-generated
12project.
13
14All platforms, to set up coverage:
15  cd ...../chromium ; src/tools/gyp/gyp_dogfood -Dcoverage=1 src/build/all.gyp
16
17Run coverage on...
18Mac:
19  ( cd src/chrome ; xcodebuild -configuration Debug -target coverage )
20Linux:
21  ( cd src/chrome ; hammer coverage )
22  # In particular, don't try and run 'coverage' from src/build
23
24
25--directory=DIR: specify directory that contains gcda files, and where
26  a "coverage" directory will be created containing the output html.
27  Example name:   ..../chromium/src/xcodebuild/Debug.
28  If not specified (e.g. buildbot) we will try and figure it out based on
29  other options (e.g. --target and --build-dir; see below).
30
31--genhtml: generate html output.  If not specified only lcov is generated.
32
33--all_unittests: if present, run all files named *_unittests that we
34  can find.
35
36--fast_test: make the tests run real fast (just for testing)
37
38--strict: if a test fails, we continue happily.  --strict will cause
39  us to die immediately.
40
41--trim=False: by default we trim away tests known to be problematic on
42  specific platforms.  If set to false we do NOT trim out tests.
43
44--xvfb=True: By default we use Xvfb to make sure DISPLAY is valid
45  (Linux only).  if set to False, do not use Xvfb.  TODO(jrg): convert
46  this script from the compile stage of a builder to a
47  RunPythonCommandInBuildDir() command to avoid the need for this
48  step.
49
50--timeout=SECS: if a subprocess doesn't have output within SECS,
51  assume it's a hang.  Kill it and give up.
52
53--bundles=BUNDLEFILE: a file containing a python list of coverage
54  bundles to be eval'd.  Example contents of the bundlefile:
55    ['../base/base.gyp:base_unittests']
56  This is used as part of the coverage bot.
57  If no other bundlefile-finding args are used (--target,
58  --build-dir), this is assumed to be an absolute path.
59  If those args are used, find BUNDLEFILE in a way consistent with
60  other scripts launched by buildbot.  Example of another script
61  launched by buildbot:
62  http://src.chromium.org/viewvc/chrome/trunk/tools/buildbot/scripts/slave/runtest.py
63
64--target=NAME: specify the build target (e.g. 'Debug' or 'Release').
65  This is used by buildbot scripts to help us find the output directory.
66  Must be used with --build-dir.
67
68--build-dir=DIR: According to buildbot comments, this is the name of
69  the directory within the buildbot working directory in which the
70  solution, Debug, and Release directories are found.
71  It's usually "src/build", but on mac it's $DIR/../xcodebuild and on
72  Linux it's $DIR/out.
73  This is used by buildbot scripts to help us find the output directory.
74  Must be used with --target.
75
76--no_exclusions: Do NOT use the exclusion list.  This script keeps a
77  list of tests known to be problematic under coverage.  For example,
78  ProcessUtilTest.SpawnChild will crash inside __gcov_fork() when
79  using the MacOS 10.6 SDK.  Use of --no_exclusions prevents the use
80  of this exclusion list.
81
82--dont-clear-coverage-data: Normally we clear coverage data from
83  previous runs.  If this arg is used we do NOT clear the coverage
84  data.
85
86Strings after all options are considered tests to run.  Test names
87have all text before a ':' stripped to help with gyp compatibility.
88For example, ../base/base.gyp:base_unittests is interpreted as a test
89named "base_unittests".
90"""
91
92import glob
93import logging
94import optparse
95import os
96import Queue
97import re
98import shutil
99import signal
100import subprocess
101import sys
102import tempfile
103import threading
104import time
105import traceback
106
107"""Global list of child PIDs to kill when we die."""
108gChildPIDs = []
109
110"""Exclusion list.  Format is
111   { platform: { testname: (exclusion1, exclusion2, ... ), ... } }
112
113   Platform is a match for sys.platform and can be a list.
114   Matching code does an 'if sys.platform in (the key):'.
115   Similarly, matching does an 'if testname in thefulltestname:'
116
117   The Chromium convention has traditionally been to place the
118   exclusion list in a distinct file.  Unlike valgrind (which has
119   frequent changes when things break and are fixed), the expectation
120   here is that exclusions remain relatively constant (e.g. OS bugs).
121   If that changes, revisit the decision to place inclusions in this
122   script.
123
124   Details:
125     ProcessUtilTest.SpawnChild: chokes in __gcov_fork on 10.6
126     IPCFuzzingTest.MsgBadPayloadArgs: ditto
127     PanelBrowserNavigatorTest.NavigateFromCrashedPanel: Fails on coverage bot.
128     WebGLConformanceTests.conformance_attribs_gl_enable_vertex_attrib: Fails
129     with timeout (45000 ms) exceeded error. crbug.com/143248
130     WebGLConformanceTests.conformance_attribs_gl_disabled_vertex_attrib:
131     ditto.
132     WebGLConformanceTests.conformance_attribs_gl_vertex_attrib_zero_issues:
133     ditto.
134     WebGLConformanceTests.conformance_attribs_gl_vertex_attrib: ditto.
135     WebGLConformanceTests.conformance_attribs_gl_vertexattribpointer_offsets:
136     ditto.
137     WebGLConformanceTests.conformance_attribs_gl_vertexattribpointer: ditto.
138     WebGLConformanceTests.conformance_buffers_buffer_bind_test: After
139     disabling WebGLConformanceTests specified above, this test fails when run
140     on local machine.
141     WebGLConformanceTests.conformance_buffers_buffer_data_array_buffer: ditto.
142     WebGLConformanceTests.conformance_buffers_index_validation_copies_indices:
143     ditto.
144     WebGLConformanceTests.
145     conformance_buffers_index_validation_crash_with_buffer_sub_data: ditto.
146     WebGLConformanceTests.
147     conformance_buffers_index_validation_verifies_too_many_indices: ditto.
148     WebGLConformanceTests.
149     conformance_buffers_index_validation_with_resized_buffer: ditto.
150     WebGLConformanceTests.conformance_canvas_buffer_offscreen_test: ditto.
151     WebGLConformanceTests.conformance_canvas_buffer_preserve_test: ditto.
152     WebGLConformanceTests.conformance_canvas_canvas_test: ditto.
153     WebGLConformanceTests.conformance_canvas_canvas_zero_size: ditto.
154     WebGLConformanceTests.
155     conformance_canvas_drawingbuffer_static_canvas_test: ditto.
156     WebGLConformanceTests.conformance_canvas_drawingbuffer_test: ditto.
157     PageCycler*.*: Fails on coverage bot with "Missing test directory
158     /....../slave/coverage-dbg-linux/build/src/data/page_cycler/moz" error.
159     *FrameRateCompositingTest.*: Fails with
160     "FATAL:chrome_content_browser_client.cc(893)] Check failed:
161     command_line->HasSwitch(switches::kEnableStatsTable)."
162     *FrameRateNoVsyncCanvasInternalTest.*: ditto.
163     *FrameRateGpuCanvasInternalTest.*: ditto.
164     IndexedDBTest.Perf: Fails with 'Timeout reached in WaitUntilCookieValue'
165     error.
166     TwoClientPasswordsSyncTest.DeleteAll: Fails on coverage bot.
167     MigrationTwoClientTest.MigrationHellWithoutNigori: Fails with timeout
168     (45000 ms) exceeded error.
169     TwoClientSessionsSyncTest.DeleteActiveSession: ditto.
170     MultipleClientSessionsSyncTest.EncryptedAndChanged: ditto.
171     MigrationSingleClientTest.AllTypesIndividuallyTriggerNotification: ditto.
172     *OldPanelResizeBrowserTest.*: crbug.com/143247
173     *OldPanelDragBrowserTest.*: ditto.
174     *OldPanelBrowserTest.*: ditto.
175     *OldPanelAndDesktopNotificationTest.*: ditto.
176     *OldDockedPanelBrowserTest.*: ditto.
177     *OldDetachedPanelBrowserTest.*: ditto.
178     PanelDragBrowserTest.AttachWithSqueeze: ditto.
179     *PanelBrowserTest.*: ditto.
180     *DockedPanelBrowserTest.*: ditto.
181     *DetachedPanelBrowserTest.*: ditto.
182     AutomatedUITest.TheOneAndOnlyTest: crbug.com/143419
183     AutomatedUITestBase.DragOut: ditto
184
185"""
186gTestExclusions = {
187  'darwin2': { 'base_unittests': ('ProcessUtilTest.SpawnChild',),
188               'ipc_tests': ('IPCFuzzingTest.MsgBadPayloadArgs',), },
189  'linux2': {
190    'gpu_tests':
191        ('WebGLConformanceTests.conformance_attribs_gl_enable_vertex_attrib',
192         'WebGLConformanceTests.'
193             'conformance_attribs_gl_disabled_vertex_attrib',
194         'WebGLConformanceTests.'
195             'conformance_attribs_gl_vertex_attrib_zero_issues',
196         'WebGLConformanceTests.conformance_attribs_gl_vertex_attrib',
197         'WebGLConformanceTests.'
198             'conformance_attribs_gl_vertexattribpointer_offsets',
199         'WebGLConformanceTests.conformance_attribs_gl_vertexattribpointer',
200         'WebGLConformanceTests.conformance_buffers_buffer_bind_test',
201         'WebGLConformanceTests.'
202             'conformance_buffers_buffer_data_array_buffer',
203         'WebGLConformanceTests.'
204             'conformance_buffers_index_validation_copies_indices',
205         'WebGLConformanceTests.'
206             'conformance_buffers_index_validation_crash_with_buffer_sub_data',
207         'WebGLConformanceTests.'
208             'conformance_buffers_index_validation_verifies_too_many_indices',
209         'WebGLConformanceTests.'
210             'conformance_buffers_index_validation_with_resized_buffer',
211         'WebGLConformanceTests.conformance_canvas_buffer_offscreen_test',
212         'WebGLConformanceTests.conformance_canvas_buffer_preserve_test',
213         'WebGLConformanceTests.conformance_canvas_canvas_test',
214         'WebGLConformanceTests.conformance_canvas_canvas_zero_size',
215         'WebGLConformanceTests.'
216             'conformance_canvas_drawingbuffer_static_canvas_test',
217         'WebGLConformanceTests.conformance_canvas_drawingbuffer_test',),
218    'performance_ui_tests':
219        ('*PageCycler*.*',
220         '*FrameRateCompositingTest.*',
221         '*FrameRateNoVsyncCanvasInternalTest.*',
222         '*FrameRateGpuCanvasInternalTest.*',
223         'IndexedDBTest.Perf',),
224    'sync_integration_tests':
225        ('TwoClientPasswordsSyncTest.DeleteAll',
226         'MigrationTwoClientTest.MigrationHellWithoutNigori',
227         'TwoClientSessionsSyncTest.DeleteActiveSession',
228         'MultipleClientSessionsSyncTest.EncryptedAndChanged',
229         'MigrationSingleClientTest.'
230         'AllTypesIndividuallyTriggerNotification',),
231    'interactive_ui_tests':
232        ('*OldPanelResizeBrowserTest.*',
233         '*OldPanelDragBrowserTest.*',
234         '*OldPanelBrowserTest.*',
235         '*OldPanelAndDesktopNotificationTest.*',
236         '*OldDockedPanelBrowserTest.*',
237         '*OldDetachedPanelBrowserTest.*',
238         'PanelDragBrowserTest.AttachWithSqueeze',
239         '*PanelBrowserTest.*',
240         '*DockedPanelBrowserTest.*',
241         '*DetachedPanelBrowserTest.*',),
242    'automated_ui_tests':
243        ('AutomatedUITest.TheOneAndOnlyTest',
244         'AutomatedUITestBase.DragOut',), },
245}
246
247"""Since random tests are failing/hanging on coverage bot, we are enabling
248   tests feature by feature. crbug.com/159748
249"""
250gTestInclusions = {
251  'linux2': {
252    'browser_tests':
253        (# 'src/chrome/browser/downloads'
254         'SavePageBrowserTest.*',
255         'SavePageAsMHTMLBrowserTest.*',
256         'DownloadQueryTest.*',
257         'DownloadDangerPromptTest.*',
258         'DownloadTest.*',
259         # 'src/chrome/browser/net'
260         'CookiePolicyBrowserTest.*',
261         'FtpBrowserTest.*',
262         'LoadTimingObserverTest.*',
263         'PredictorBrowserTest.*',
264         'ProxyBrowserTest.*',
265         # 'src/chrome/browser/extensions'
266         'Extension*.*',
267         'WindowOpenPanelDisabledTest.*',
268         'WindowOpenPanelTest.*',
269         'WebstoreStandalone*.*',
270         'CommandLineWebstoreInstall.*',
271         'WebViewTest.*',
272         'RequirementsCheckerBrowserTest.*',
273         'ProcessManagementTest.*',
274         'PlatformAppBrowserTest.*',
275         'PlatformAppDevToolsBrowserTest.*',
276         'LazyBackgroundPageApiTest.*',
277         'IsolatedAppTest.*',
278         'PanelMessagingTest.*',
279         'GeolocationApiTest.*',
280         'ClipboardApiTest.*',
281         'ExecuteScriptApiTest.*',
282         'CalculatorBrowserTest.*',
283         'ChromeAppAPITest.*',
284         'AppApiTest.*',
285         'BlockedAppApiTest.*',
286         'AppBackgroundPageApiTest.*',
287         'WebNavigationApiTest.*',
288         'UsbApiTest.*',
289         'TabCaptureApiTest.*',
290         'SystemInfo*.*',
291         'SyncFileSystemApiTest.*',
292         'SocketApiTest.*',
293         'SerialApiTest.*',
294         'RecordApiTest.*',
295         'PushMessagingApiTest.*',
296         'ProxySettingsApiTest.*',
297         'ExperimentalApiTest.*',
298         'OmniboxApiTest.*',
299         'OffscreenTabsApiTest.*',
300         'NotificationApiTest.*',
301         'MediaGalleriesPrivateApiTest.*',
302         'PlatformAppMediaGalleriesBrowserTest.*',
303         'GetAuthTokenFunctionTest.*',
304         'LaunchWebAuthFlowFunctionTest.*',
305         'FileSystemApiTest.*',
306         'ScriptBadgeApiTest.*',
307         'PageAsBrowserActionApiTest.*',
308         'PageActionApiTest.*',
309         'BrowserActionApiTest.*',
310         'DownloadExtensionTest.*',
311         'DnsApiTest.*',
312         'DeclarativeApiTest.*',
313         'BluetoothApiTest.*',
314         'AllUrlsApiTest.*',
315         # 'src/chrome/browser/nacl_host'
316         'nacl_host.*',
317         # 'src/chrome/browser/automation'
318         'AutomationMiscBrowserTest.*',
319         # 'src/chrome/browser/autofill'
320         'FormStructureBrowserTest.*',
321         'AutofillPopupViewBrowserTest.*',
322         'AutofillTest.*',
323         # 'src/chrome/browser/autocomplete'
324         'AutocompleteBrowserTest.*',
325         # 'src/chrome/browser/captive_portal'
326         'CaptivePortalBrowserTest.*',
327         # 'src/chrome/browser/geolocation'
328         'GeolocationAccessTokenStoreTest.*',
329         'GeolocationBrowserTest.*',
330         # 'src/chrome/browser/nacl_host'
331         'NaClGdbTest.*',
332         # 'src/chrome/browser/devtools'
333         'DevToolsSanityTest.*',
334         'DevToolsExtensionTest.*',
335         'DevToolsExperimentalExtensionTest.*',
336         'WorkerDevToolsSanityTest.*',
337         # 'src/chrome/browser/first_run'
338         'FirstRunBrowserTest.*',
339         # 'src/chrome/browser/importer'
340         'ToolbarImporterUtilsTest.*',
341         # 'src/chrome/browser/page_cycler'
342         'PageCyclerBrowserTest.*',
343         'PageCyclerCachedBrowserTest.*',
344         # 'src/chrome/browser/performance_monitor'
345         'PerformanceMonitorBrowserTest.*',
346         'PerformanceMonitorUncleanExitBrowserTest.*',
347         'PerformanceMonitorSessionRestoreBrowserTest.*',
348         # 'src/chrome/browser/prerender'
349         'PrerenderBrowserTest.*',
350         'PrerenderBrowserTestWithNaCl.*',
351         'PrerenderBrowserTestWithExtensions.*',
352         'PrefetchBrowserTest.*',
353         'PrefetchBrowserTestNoPrefetching.*', ),
354  },
355}
356
357
358def TerminateSignalHandler(sig, stack):
359  """When killed, try and kill our child processes."""
360  signal.signal(sig, signal.SIG_DFL)
361  for pid in gChildPIDs:
362    if 'kill' in os.__all__:  # POSIX
363      os.kill(pid, sig)
364    else:
365      subprocess.call(['taskkill.exe', '/PID', str(pid)])
366  sys.exit(0)
367
368
369class RunTooLongException(Exception):
370  """Thrown when a command runs too long without output."""
371  pass
372
373class BadUserInput(Exception):
374  """Thrown when arguments from the user are incorrectly formatted."""
375  pass
376
377
378class RunProgramThread(threading.Thread):
379  """A thread to run a subprocess.
380
381  We want to print the output of our subprocess in real time, but also
382  want a timeout if there has been no output for a certain amount of
383  time.  Normal techniques (e.g. loop in select()) aren't cross
384  platform enough. the function seems simple: "print output of child, kill it
385  if there is no output by timeout.  But it was tricky to get this right
386  in a x-platform way (see warnings about deadlock on the python
387  subprocess doc page).
388
389  """
390  # Constants in our queue
391  PROGRESS = 0
392  DONE = 1
393
394  def __init__(self, cmd):
395    super(RunProgramThread, self).__init__()
396    self._cmd = cmd
397    self._process = None
398    self._queue = Queue.Queue()
399    self._retcode = None
400
401  def run(self):
402    if sys.platform in ('win32', 'cygwin'):
403      return self._run_windows()
404    else:
405      self._run_posix()
406
407  def _run_windows(self):
408    # We need to save stdout to a temporary file because of a bug on the
409    # windows implementation of python which can deadlock while waiting
410    # for the IO to complete while writing to the PIPE and the pipe waiting
411    # on us and us waiting on the child process.
412    stdout_file = tempfile.TemporaryFile()
413    try:
414      self._process = subprocess.Popen(self._cmd,
415                                       stdin=subprocess.PIPE,
416                                       stdout=stdout_file,
417                                       stderr=subprocess.STDOUT)
418      gChildPIDs.append(self._process.pid)
419      try:
420        # To make sure that the buildbot don't kill us if we run too long
421        # without any activity on the console output, we look for progress in
422        # the length of the temporary file and we print what was accumulated so
423        # far to the output console to make the buildbot know we are making some
424        # progress.
425        previous_tell = 0
426        # We will poll the process until we get a non-None return code.
427        self._retcode = None
428        while self._retcode is None:
429          self._retcode = self._process.poll()
430          current_tell = stdout_file.tell()
431          if current_tell > previous_tell:
432            # Report progress to our main thread so we don't timeout.
433            self._queue.put(RunProgramThread.PROGRESS)
434            # And print what was accumulated to far.
435            stdout_file.seek(previous_tell)
436            print stdout_file.read(current_tell - previous_tell),
437            previous_tell = current_tell
438          # Don't be selfish, let other threads do stuff while we wait for
439          # the process to complete.
440          time.sleep(0.5)
441        # OK, the child process has exited, let's print its output to our
442        # console to create debugging logs in case they get to be needed.
443        stdout_file.flush()
444        stdout_file.seek(previous_tell)
445        print stdout_file.read(stdout_file.tell() - previous_tell)
446      except IOError, e:
447        logging.exception('%s', e)
448        pass
449    finally:
450      stdout_file.close()
451
452    # If we get here the process is done.
453    gChildPIDs.remove(self._process.pid)
454    self._queue.put(RunProgramThread.DONE)
455
456  def _run_posix(self):
457    """No deadlock problem so use the simple answer.  The windows solution
458    appears to add extra buffering which we don't want on other platforms."""
459    self._process = subprocess.Popen(self._cmd,
460                                     stdout=subprocess.PIPE,
461                                     stderr=subprocess.STDOUT)
462    gChildPIDs.append(self._process.pid)
463    try:
464      while True:
465        line = self._process.stdout.readline()
466        if not line:  # EOF
467          break
468        print line,
469        self._queue.put(RunProgramThread.PROGRESS, True)
470    except IOError:
471      pass
472    # If we get here the process is done.
473    gChildPIDs.remove(self._process.pid)
474    self._queue.put(RunProgramThread.DONE)
475
476  def stop(self):
477    self.kill()
478
479  def kill(self):
480    """Kill our running process if needed.  Wait for kill to complete.
481
482    Should be called in the PARENT thread; we do not self-kill.
483    Returns the return code of the process.
484    Safe to call even if the process is dead.
485    """
486    if not self._process:
487      return self.retcode()
488    if 'kill' in os.__all__:  # POSIX
489      os.kill(self._process.pid, signal.SIGKILL)
490    else:
491      subprocess.call(['taskkill.exe', '/PID', str(self._process.pid)])
492    return self.retcode()
493
494  def retcode(self):
495    """Return the return value of the subprocess.
496
497    Waits for process to die but does NOT kill it explicitly.
498    """
499    if self._retcode == None:  # must be none, not 0/False
500      self._retcode = self._process.wait()
501    return self._retcode
502
503  def RunUntilCompletion(self, timeout):
504    """Run thread until completion or timeout (in seconds).
505
506    Start the thread.  Let it run until completion, or until we've
507    spent TIMEOUT without seeing output.  On timeout throw
508    RunTooLongException.
509    """
510    self.start()
511    while True:
512      try:
513        x = self._queue.get(True, timeout)
514        if x == RunProgramThread.DONE:
515          return self.retcode()
516      except Queue.Empty, e:  # timed out
517        logging.info('TIMEOUT (%d seconds exceeded with no output): killing' %
518                     timeout)
519        self.kill()
520        raise RunTooLongException()
521
522
523class Coverage(object):
524  """Doitall class for code coverage."""
525
526  def __init__(self, options, args):
527    super(Coverage, self).__init__()
528    logging.basicConfig(level=logging.DEBUG)
529    self.directory = options.directory
530    self.options = options
531    self.args = args
532    self.ConfirmDirectory()
533    self.directory_parent = os.path.dirname(self.directory)
534    self.output_directory = os.path.join(self.directory, 'coverage')
535    if not os.path.exists(self.output_directory):
536      os.mkdir(self.output_directory)
537    # The "final" lcov-format file
538    self.coverage_info_file = os.path.join(self.directory, 'coverage.info')
539    # If needed, an intermediate VSTS-format file
540    self.vsts_output = os.path.join(self.directory, 'coverage.vsts')
541    # Needed for Windows.
542    self.src_root = options.src_root
543    self.FindPrograms()
544    self.ConfirmPlatformAndPaths()
545    self.tests = []             # This can be a list of strings, lists or both.
546    self.xvfb_pid = 0
547    self.test_files = []        # List of files with test specifications.
548    self.test_filters = {}      # Mapping from testname->--gtest_filter arg.
549    logging.info('self.directory: ' + self.directory)
550    logging.info('self.directory_parent: ' + self.directory_parent)
551
552  def FindInPath(self, program):
553    """Find program in our path.  Return abs path to it, or None."""
554    if not 'PATH' in os.environ:
555      logging.fatal('No PATH environment variable?')
556      sys.exit(1)
557    paths = os.environ['PATH'].split(os.pathsep)
558    for path in paths:
559      fullpath = os.path.join(path, program)
560      if os.path.exists(fullpath):
561        return fullpath
562    return None
563
564  def FindPrograms(self):
565    """Find programs we may want to run."""
566    if self.IsPosix():
567      self.lcov_directory = os.path.join(sys.path[0],
568                                         '../../third_party/lcov/bin')
569      self.lcov = os.path.join(self.lcov_directory, 'lcov')
570      self.mcov = os.path.join(self.lcov_directory, 'mcov')
571      self.genhtml = os.path.join(self.lcov_directory, 'genhtml')
572      self.programs = [self.lcov, self.mcov, self.genhtml]
573    else:
574      # Hack to get the buildbot working.
575      os.environ['PATH'] += r';c:\coverage\coverage_analyzer'
576      os.environ['PATH'] += r';c:\coverage\performance_tools'
577      # (end hack)
578      commands = ['vsperfcmd.exe', 'vsinstr.exe', 'coverage_analyzer.exe']
579      self.perf = self.FindInPath('vsperfcmd.exe')
580      self.instrument = self.FindInPath('vsinstr.exe')
581      self.analyzer = self.FindInPath('coverage_analyzer.exe')
582      if not self.perf or not self.instrument or not self.analyzer:
583        logging.fatal('Could not find Win performance commands.')
584        logging.fatal('Commands needed in PATH: ' + str(commands))
585        sys.exit(1)
586      self.programs = [self.perf, self.instrument, self.analyzer]
587
588  def PlatformBuildPrefix(self):
589    """Return a platform specific build directory prefix.
590
591    This prefix is prepended to the build target (Debug, Release) to
592    identify output as relative to the build directory.
593    These values are specific to Chromium's use of gyp.
594    """
595    if self.IsMac():
596      return '../xcodebuild'
597    if self.IsWindows():
598      return  ''
599    else:  # Linux
600      return '../out'  # assumes make, unlike runtest.py
601
602  def ConfirmDirectory(self):
603    """Confirm correctness of self.directory.
604
605    If it exists, happiness.  If not, try and figure it out in a
606    manner similar to FindBundlesFile().  The 'figure it out' case
607    happens with buildbot where the directory isn't specified
608    explicitly.
609    """
610    if (not self.directory and
611        not (self.options.target and self.options.build_dir)):
612      logging.fatal('Must use --directory or (--target and --build-dir)')
613      sys.exit(1)
614
615    if not self.directory:
616      self.directory = os.path.join(self.options.build_dir,
617                                    self.PlatformBuildPrefix(),
618                                    self.options.target)
619
620    if os.path.exists(self.directory):
621      logging.info('Directory: ' + self.directory)
622      return
623    else:
624      logging.fatal('Directory ' +
625                    self.directory + ' doesn\'t exist')
626      sys.exit(1)
627
628
629  def FindBundlesFile(self):
630    """Find the bundlesfile.
631
632    The 'bundles' file can be either absolute path, or (if we are run
633    from buildbot) we need to find it based on other hints (--target,
634    --build-dir, etc).
635    """
636    # If no bundle file, no problem!
637    if not self.options.bundles:
638      return
639    # If true, we're buildbot.  Form a path.
640    # Else assume absolute.
641    if self.options.target and self.options.build_dir:
642      fullpath = os.path.join(self.options.build_dir,
643                              self.PlatformBuildPrefix(),
644                              self.options.target,
645                              self.options.bundles)
646      self.options.bundles = fullpath
647
648    if os.path.exists(self.options.bundles):
649      logging.info('BundlesFile: ' + self.options.bundles)
650      return
651    else:
652      logging.fatal('bundlefile ' +
653                    self.options.bundles + ' doesn\'t exist')
654      sys.exit(1)
655
656
657  def FindTests(self):
658    """Find unit tests to run; set self.tests to this list.
659
660    Assume all non-option items in the arg list are tests to be run.
661    """
662    # Before we begin, find the bundles file if not an absolute path.
663    self.FindBundlesFile()
664
665    # Small tests: can be run in the "chromium" directory.
666    # If asked, run all we can find.
667    if self.options.all_unittests:
668      self.tests += glob.glob(os.path.join(self.directory, '*_unittests'))
669      self.tests += glob.glob(os.path.join(self.directory, '*unit_tests'))
670    elif self.options.all_browsertests:
671      # Run all tests in browser_tests and content_browsertests.
672      self.tests += glob.glob(os.path.join(self.directory, 'browser_tests'))
673      self.tests += glob.glob(os.path.join(self.directory,
674                                           'content_browsertests'))
675
676    # Tests can come in as args directly, indirectly (through a file
677    # of test lists) or as a file of bundles.
678    all_testnames = self.args[:]  # Copy since we might modify
679
680    for test_file in self.options.test_files:
681      f = open(test_file)
682      for line in f:
683        line = re.sub(r"#.*$", "", line)
684        line = re.sub(r"\s*", "", line)
685        if re.match("\s*$"):
686          continue
687        all_testnames.append(line)
688      f.close()
689
690    tests_from_bundles = None
691    if self.options.bundles:
692      try:
693        tests_from_bundles = eval(open(self.options.bundles).read())
694      except IOError:
695        logging.fatal('IO error in bundle file ' +
696                      self.options.bundles + ' (doesn\'t exist?)')
697      except (NameError, SyntaxError):
698        logging.fatal('Parse or syntax error in bundle file ' +
699                      self.options.bundles)
700      if hasattr(tests_from_bundles, '__iter__'):
701        all_testnames += tests_from_bundles
702      else:
703        logging.fatal('Fatal error with bundle file; could not get list from' +
704                      self.options.bundles)
705        sys.exit(1)
706
707    # If told explicit tests, run those (after stripping the name as
708    # appropriate)
709    for testname in all_testnames:
710      mo = re.search(r"(.*)\[(.*)\]$", testname)
711      gtest_filter = None
712      if mo:
713        gtest_filter = mo.group(2)
714        testname = mo.group(1)
715      if ':' in testname:
716        testname = testname.split(':')[1]
717      # We need 'pyautolib' to run pyauto tests and 'pyautolib' itself is not an
718      # executable. So skip this test from adding into coverage_bundles.py.
719      if testname == 'pyautolib':
720        continue
721      self.tests += [os.path.join(self.directory, testname)]
722      if gtest_filter:
723        self.test_filters[testname] = gtest_filter
724
725    # Add 'src/test/functional/pyauto_functional.py' to self.tests.
726    # This file with '-v --suite=CODE_COVERAGE' arguments runs all pyauto tests.
727    # Pyauto tests are failing randomly on coverage bots. So excluding them.
728    # self.tests += [['src/chrome/test/functional/pyauto_functional.py',
729    #                '-v',
730    #                '--suite=CODE_COVERAGE']]
731
732    # Medium tests?
733    # Not sure all of these work yet (e.g. page_cycler_tests)
734    # self.tests += glob.glob(os.path.join(self.directory, '*_tests'))
735
736    # If needed, append .exe to tests since vsinstr.exe likes it that
737    # way.
738    if self.IsWindows():
739      for ind in range(len(self.tests)):
740        test = self.tests[ind]
741        test_exe = test + '.exe'
742        if not test.endswith('.exe') and os.path.exists(test_exe):
743          self.tests[ind] = test_exe
744
745  def TrimTests(self):
746    """Trim specific tests for each platform."""
747    if self.IsWindows():
748      return
749      # TODO(jrg): remove when not needed
750      inclusion = ['unit_tests']
751      keep = []
752      for test in self.tests:
753        for i in inclusion:
754          if i in test:
755            keep.append(test)
756      self.tests = keep
757      logging.info('After trimming tests we have ' + ' '.join(self.tests))
758      return
759    if self.IsLinux():
760      # self.tests = filter(lambda t: t.endswith('base_unittests'), self.tests)
761      return
762    if self.IsMac():
763      exclusion = ['automated_ui_tests']
764      punted = []
765      for test in self.tests:
766        for e in exclusion:
767          if test.endswith(e):
768            punted.append(test)
769      self.tests = filter(lambda t: t not in punted, self.tests)
770      if punted:
771        logging.info('Tests trimmed out: ' + str(punted))
772
773  def ConfirmPlatformAndPaths(self):
774    """Confirm OS and paths (e.g. lcov)."""
775    for program in self.programs:
776      if not os.path.exists(program):
777        logging.fatal('Program missing: ' + program)
778        sys.exit(1)
779
780  def Run(self, cmdlist, ignore_error=False, ignore_retcode=None,
781          explanation=None):
782    """Run the command list; exit fatally on error.
783
784    Args:
785      cmdlist: a list of commands (e.g. to pass to subprocess.call)
786      ignore_error: if True log an error; if False then exit.
787      ignore_retcode: if retcode is non-zero, exit unless we ignore.
788
789    Returns: process return code.
790    Throws: RunTooLongException if the process does not produce output
791    within TIMEOUT seconds; timeout is specified as a command line
792    option to the Coverage class and is set on init.
793    """
794    logging.info('Running ' + str(cmdlist))
795    t = RunProgramThread(cmdlist)
796    retcode = t.RunUntilCompletion(self.options.timeout)
797
798    if retcode:
799      if ignore_error or retcode == ignore_retcode:
800        logging.warning('COVERAGE: %s unhappy but errors ignored  %s' %
801                        (str(cmdlist), explanation or ''))
802      else:
803        logging.fatal('COVERAGE:  %s failed; return code: %d' %
804                      (str(cmdlist), retcode))
805        sys.exit(retcode)
806    return retcode
807
808  def IsPosix(self):
809    """Return True if we are POSIX."""
810    return self.IsMac() or self.IsLinux()
811
812  def IsMac(self):
813    return sys.platform == 'darwin'
814
815  def IsLinux(self):
816    return sys.platform.startswith('linux')
817
818  def IsWindows(self):
819    """Return True if we are Windows."""
820    return sys.platform in ('win32', 'cygwin')
821
822  def ClearData(self):
823    """Clear old gcda files and old coverage info files."""
824    if self.options.dont_clear_coverage_data:
825      print 'Clearing of coverage data NOT performed.'
826      return
827    print 'Clearing coverage data from previous runs.'
828    if os.path.exists(self.coverage_info_file):
829      os.remove(self.coverage_info_file)
830    if self.IsPosix():
831      subprocess.call([self.lcov,
832                       '--directory', self.directory_parent,
833                       '--zerocounters'])
834      shutil.rmtree(os.path.join(self.directory, 'coverage'))
835      if self.options.all_unittests:
836        if os.path.exists(os.path.join(self.directory, 'unittests_coverage')):
837          shutil.rmtree(os.path.join(self.directory, 'unittests_coverage'))
838      elif self.options.all_browsertests:
839        if os.path.exists(os.path.join(self.directory,
840                                       'browsertests_coverage')):
841          shutil.rmtree(os.path.join(self.directory, 'browsertests_coverage'))
842      else:
843        if os.path.exists(os.path.join(self.directory, 'total_coverage')):
844          shutil.rmtree(os.path.join(self.directory, 'total_coverage'))
845
846  def BeforeRunOneTest(self, testname):
847    """Do things before running each test."""
848    if not self.IsWindows():
849      return
850    # Stop old counters if needed
851    cmdlist = [self.perf, '-shutdown']
852    self.Run(cmdlist, ignore_error=True)
853    # Instrument binaries
854    for fulltest in self.tests:
855      if os.path.exists(fulltest):
856        # See http://support.microsoft.com/kb/939818 for details on args
857        cmdlist = [self.instrument, '/d:ignorecverr', '/COVERAGE', fulltest]
858        self.Run(cmdlist, ignore_retcode=4,
859                 explanation='OK with a multiple-instrument')
860    # Start new counters
861    cmdlist = [self.perf, '-start:coverage', '-output:' + self.vsts_output]
862    self.Run(cmdlist)
863
864  def BeforeRunAllTests(self):
865    """Called right before we run all tests."""
866    if self.IsLinux() and self.options.xvfb:
867      self.StartXvfb()
868
869  def GtestFilter(self, fulltest, excl=None):
870    """Return a --gtest_filter=BLAH for this test.
871
872    Args:
873      fulltest: full name of test executable
874      exclusions: the exclusions list.  Only set in a unit test;
875        else uses gTestExclusions.
876    Returns:
877      String of the form '--gtest_filter=BLAH', or None.
878    """
879    positive_gfilter_list = []
880    negative_gfilter_list = []
881
882    # Exclude all flaky, failing, disabled and maybe tests;
883    # they don't count for code coverage.
884    negative_gfilter_list += ('*.FLAKY_*', '*.FAILS_*',
885                              '*.DISABLED_*', '*.MAYBE_*')
886
887    if not self.options.no_exclusions:
888      exclusions = excl or gTestExclusions
889      excldict = exclusions.get(sys.platform)
890      if excldict:
891        for test in excldict.keys():
892          # example: if base_unittests in ../blah/blah/base_unittests.exe
893          if test in fulltest:
894            negative_gfilter_list += excldict[test]
895
896    inclusions = gTestInclusions
897    include_dict = inclusions.get(sys.platform)
898    if include_dict:
899      for test in include_dict.keys():
900        if test in fulltest:
901          positive_gfilter_list += include_dict[test]
902
903    fulltest_basename = os.path.basename(fulltest)
904    if fulltest_basename in self.test_filters:
905      specific_test_filters = self.test_filters[fulltest_basename].split('-')
906      if len(specific_test_filters) > 2:
907        logging.error('Multiple "-" symbols in filter list: %s' %
908          self.test_filters[fulltest_basename])
909        raise BadUserInput()
910      if len(specific_test_filters) == 2:
911        # Remove trailing ':'
912        specific_test_filters[0] = specific_test_filters[0][:-1]
913
914      if specific_test_filters[0]: # Test for no positive filters.
915        positive_gfilter_list += specific_test_filters[0].split(':')
916      if len(specific_test_filters) > 1:
917        negative_gfilter_list += specific_test_filters[1].split(':')
918
919    if not positive_gfilter_list and not negative_gfilter_list:
920      return None
921
922    result = '--gtest_filter='
923    if positive_gfilter_list:
924      result += ':'.join(positive_gfilter_list)
925    if negative_gfilter_list:
926      if positive_gfilter_list: result += ':'
927      result += '-' + ':'.join(negative_gfilter_list)
928    return result
929
930  def RunTests(self):
931    """Run all unit tests and generate appropriate lcov files."""
932    self.BeforeRunAllTests()
933    for fulltest in self.tests:
934      if type(fulltest) is str:
935        if not os.path.exists(fulltest):
936          logging.info(fulltest + ' does not exist')
937          if self.options.strict:
938            sys.exit(2)
939        else:
940          logging.info('%s path exists' % fulltest)
941        cmdlist = [fulltest, '--gtest_print_time']
942
943        # If asked, make this REAL fast for testing.
944        if self.options.fast_test:
945          logging.info('Running as a FAST test for testing')
946          # cmdlist.append('--gtest_filter=RenderWidgetHost*')
947          # cmdlist.append('--gtest_filter=CommandLine*')
948          cmdlist.append('--gtest_filter=C*')
949
950        # Possibly add a test-specific --gtest_filter
951        filter = self.GtestFilter(fulltest)
952        if filter:
953          cmdlist.append(filter)
954      elif type(fulltest) is list:
955        cmdlist = fulltest
956
957      self.BeforeRunOneTest(fulltest)
958      logging.info('Running test ' + str(cmdlist))
959      try:
960        retcode = self.Run(cmdlist, ignore_retcode=True)
961      except SystemExit:  # e.g. sys.exit() was called somewhere in here
962        raise
963      except:  # can't "except WindowsError" since script runs on non-Windows
964        logging.info('EXCEPTION while running a unit test')
965        logging.info(traceback.format_exc())
966        retcode = 999
967      self.AfterRunOneTest(fulltest)
968
969      if retcode:
970        logging.info('COVERAGE: test %s failed; return code: %d.' %
971                      (fulltest, retcode))
972        if self.options.strict:
973          logging.fatal('Test failure is fatal.')
974          sys.exit(retcode)
975    self.AfterRunAllTests()
976
977  def AfterRunOneTest(self, testname):
978    """Do things right after running each test."""
979    if not self.IsWindows():
980      return
981    # Stop counters
982    cmdlist = [self.perf, '-shutdown']
983    self.Run(cmdlist)
984    full_output = self.vsts_output + '.coverage'
985    shutil.move(full_output, self.vsts_output)
986    # generate lcov!
987    self.GenerateLcovWindows(testname)
988
989  def AfterRunAllTests(self):
990    """Do things right after running ALL tests."""
991    # On POSIX we can do it all at once without running out of memory.
992    # This contrasts with Windows where we must do it after each test.
993    if self.IsPosix():
994      self.GenerateLcovPosix()
995    # Only on Linux do we have the Xvfb step.
996    if self.IsLinux() and self.options.xvfb:
997      self.StopXvfb()
998
999  def StartXvfb(self):
1000    """Start Xvfb and set an appropriate DISPLAY environment.  Linux only.
1001
1002    Copied from http://src.chromium.org/viewvc/chrome/trunk/tools/buildbot/
1003      scripts/slave/slave_utils.py?view=markup
1004    with some simplifications (e.g. no need to use xdisplaycheck, save
1005    pid in var not file, etc)
1006    """
1007    logging.info('Xvfb: starting')
1008    proc = subprocess.Popen(["Xvfb", ":9", "-screen", "0", "1024x768x24",
1009                             "-ac"],
1010                            stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1011    self.xvfb_pid = proc.pid
1012    if not self.xvfb_pid:
1013      logging.info('Could not start Xvfb')
1014      return
1015    os.environ['DISPLAY'] = ":9"
1016    # Now confirm, giving a chance for it to start if needed.
1017    logging.info('Xvfb: confirming')
1018    for test in range(10):
1019      proc = subprocess.Popen('xdpyinfo >/dev/null', shell=True)
1020      pid, retcode = os.waitpid(proc.pid, 0)
1021      if retcode == 0:
1022        break
1023      time.sleep(0.5)
1024    if retcode != 0:
1025      logging.info('Warning: could not confirm Xvfb happiness')
1026    else:
1027      logging.info('Xvfb: OK')
1028
1029  def StopXvfb(self):
1030    """Stop Xvfb if needed.  Linux only."""
1031    if self.xvfb_pid:
1032      logging.info('Xvfb: killing')
1033      try:
1034        os.kill(self.xvfb_pid, signal.SIGKILL)
1035      except:
1036        pass
1037      del os.environ['DISPLAY']
1038      self.xvfb_pid = 0
1039
1040  def CopyCoverageFileToDestination(self, coverage_folder):
1041    coverage_dir = os.path.join(self.directory, coverage_folder)
1042    if not os.path.exists(coverage_dir):
1043      os.makedirs(coverage_dir)
1044    shutil.copyfile(self.coverage_info_file, os.path.join(coverage_dir,
1045                                                          'coverage.info'))
1046
1047  def GenerateLcovPosix(self):
1048    """Convert profile data to lcov on Mac or Linux."""
1049    start_dir = os.getcwd()
1050    logging.info('GenerateLcovPosix: start_dir=' + start_dir)
1051    if self.IsLinux():
1052      # With Linux/make (e.g. the coverage_run target), the current
1053      # directory for this command is .../build/src/chrome but we need
1054      # to be in .../build/src for the relative path of source files
1055      # to be correct.  However, when run from buildbot, the current
1056      # directory is .../build.  Accommodate.
1057      # On Mac source files are compiled with abs paths so this isn't
1058      # a problem.
1059      # This is a bit of a hack.  The best answer is to require this
1060      # script be run in a specific directory for all cases (from
1061      # Makefile or from buildbot).
1062      if start_dir.endswith('chrome'):
1063        logging.info('coverage_posix.py: doing a "cd .." '
1064                     'to accomodate Linux/make PWD')
1065        os.chdir('..')
1066      elif start_dir.endswith('build'):
1067        logging.info('coverage_posix.py: doing a "cd src" '
1068                     'to accomodate buildbot PWD')
1069        os.chdir('src')
1070      else:
1071        logging.info('coverage_posix.py: NOT changing directory.')
1072    elif self.IsMac():
1073      pass
1074
1075    command = [self.mcov,
1076               '--directory',
1077               os.path.join(start_dir, self.directory_parent),
1078               '--output',
1079               os.path.join(start_dir, self.coverage_info_file)]
1080    logging.info('Assembly command: ' + ' '.join(command))
1081    retcode = subprocess.call(command)
1082    if retcode:
1083      logging.fatal('COVERAGE: %s failed; return code: %d' %
1084                    (command[0], retcode))
1085      if self.options.strict:
1086        sys.exit(retcode)
1087    if self.IsLinux():
1088      os.chdir(start_dir)
1089
1090    # Copy the unittests coverage information to a different folder.
1091    if self.options.all_unittests:
1092      self.CopyCoverageFileToDestination('unittests_coverage')
1093    elif self.options.all_browsertests:
1094      # Save browsertests only coverage information.
1095      self.CopyCoverageFileToDestination('browsertests_coverage')
1096    else:
1097      # Save the overall coverage information.
1098      self.CopyCoverageFileToDestination('total_coverage')
1099
1100    if not os.path.exists(self.coverage_info_file):
1101      logging.fatal('%s was not created.  Coverage run failed.' %
1102                    self.coverage_info_file)
1103      sys.exit(1)
1104
1105  def GenerateLcovWindows(self, testname=None):
1106    """Convert VSTS format to lcov.  Appends coverage data to sum file."""
1107    lcov_file = self.vsts_output + '.lcov'
1108    if os.path.exists(lcov_file):
1109      os.remove(lcov_file)
1110    # generates the file (self.vsts_output + ".lcov")
1111
1112    cmdlist = [self.analyzer,
1113               '-sym_path=' + self.directory,
1114               '-src_root=' + self.src_root,
1115               '-noxml',
1116               self.vsts_output]
1117    self.Run(cmdlist)
1118    if not os.path.exists(lcov_file):
1119      logging.fatal('Output file %s not created' % lcov_file)
1120      sys.exit(1)
1121    logging.info('Appending lcov for test %s to %s' %
1122                 (testname, self.coverage_info_file))
1123    size_before = 0
1124    if os.path.exists(self.coverage_info_file):
1125      size_before = os.stat(self.coverage_info_file).st_size
1126    src = open(lcov_file, 'r')
1127    dst = open(self.coverage_info_file, 'a')
1128    dst.write(src.read())
1129    src.close()
1130    dst.close()
1131    size_after = os.stat(self.coverage_info_file).st_size
1132    logging.info('Lcov file growth for %s: %d --> %d' %
1133                 (self.coverage_info_file, size_before, size_after))
1134
1135  def GenerateHtml(self):
1136    """Convert lcov to html."""
1137    # TODO(jrg): This isn't happy when run with unit_tests since V8 has a
1138    # different "base" so V8 includes can't be found in ".".  Fix.
1139    command = [self.genhtml,
1140               self.coverage_info_file,
1141               '--output-directory',
1142               self.output_directory]
1143    print >>sys.stderr, 'html generation command: ' + ' '.join(command)
1144    retcode = subprocess.call(command)
1145    if retcode:
1146      logging.fatal('COVERAGE: %s failed; return code: %d' %
1147                    (command[0], retcode))
1148      if self.options.strict:
1149        sys.exit(retcode)
1150
1151def CoverageOptionParser():
1152  """Return an optparse.OptionParser() suitable for Coverage object creation."""
1153  parser = optparse.OptionParser()
1154  parser.add_option('-d',
1155                    '--directory',
1156                    dest='directory',
1157                    default=None,
1158                    help='Directory of unit test files')
1159  parser.add_option('-a',
1160                    '--all_unittests',
1161                    dest='all_unittests',
1162                    default=False,
1163                    help='Run all tests we can find (*_unittests)')
1164  parser.add_option('-b',
1165                    '--all_browsertests',
1166                    dest='all_browsertests',
1167                    default=False,
1168                    help='Run all tests in browser_tests '
1169                         'and content_browsertests')
1170  parser.add_option('-g',
1171                    '--genhtml',
1172                    dest='genhtml',
1173                    default=False,
1174                    help='Generate html from lcov output')
1175  parser.add_option('-f',
1176                    '--fast_test',
1177                    dest='fast_test',
1178                    default=False,
1179                    help='Make the tests run REAL fast by doing little.')
1180  parser.add_option('-s',
1181                    '--strict',
1182                    dest='strict',
1183                    default=False,
1184                    help='Be strict and die on test failure.')
1185  parser.add_option('-S',
1186                    '--src_root',
1187                    dest='src_root',
1188                    default='.',
1189                    help='Source root (only used on Windows)')
1190  parser.add_option('-t',
1191                    '--trim',
1192                    dest='trim',
1193                    default=True,
1194                    help='Trim out tests?  Default True.')
1195  parser.add_option('-x',
1196                    '--xvfb',
1197                    dest='xvfb',
1198                    default=True,
1199                    help='Use Xvfb for tests?  Default True.')
1200  parser.add_option('-T',
1201                    '--timeout',
1202                    dest='timeout',
1203                    default=5.0 * 60.0,
1204                    type="int",
1205                    help='Timeout before bailing if a subprocess has no output.'
1206                    '  Default is 5min  (Buildbot is 10min.)')
1207  parser.add_option('-B',
1208                    '--bundles',
1209                    dest='bundles',
1210                    default=None,
1211                    help='Filename of bundles for coverage.')
1212  parser.add_option('--build-dir',
1213                    dest='build_dir',
1214                    default=None,
1215                    help=('Working directory for buildbot build.'
1216                          'used for finding bundlefile.'))
1217  parser.add_option('--target',
1218                    dest='target',
1219                    default=None,
1220                    help=('Buildbot build target; '
1221                          'used for finding bundlefile (e.g. Debug)'))
1222  parser.add_option('--no_exclusions',
1223                    dest='no_exclusions',
1224                    default=None,
1225                    help=('Disable the exclusion list.'))
1226  parser.add_option('--dont-clear-coverage-data',
1227                    dest='dont_clear_coverage_data',
1228                    default=False,
1229                    action='store_true',
1230                    help=('Turn off clearing of cov data from a prev run'))
1231  parser.add_option('-F',
1232                    '--test-file',
1233                    dest="test_files",
1234                    default=[],
1235                    action='append',
1236                    help=('Specify a file from which tests to be run will ' +
1237                          'be extracted'))
1238  return parser
1239
1240
1241def main():
1242  # Print out the args to help someone do it by hand if needed
1243  print >>sys.stderr, sys.argv
1244
1245  # Try and clean up nice if we're killed by buildbot, Ctrl-C, ...
1246  signal.signal(signal.SIGINT, TerminateSignalHandler)
1247  signal.signal(signal.SIGTERM, TerminateSignalHandler)
1248
1249  parser = CoverageOptionParser()
1250  (options, args) = parser.parse_args()
1251  if options.all_unittests and options.all_browsertests:
1252    print 'Error! Can not have all_unittests and all_browsertests together!'
1253    sys.exit(1)
1254  coverage = Coverage(options, args)
1255  coverage.ClearData()
1256  coverage.FindTests()
1257  if options.trim:
1258    coverage.TrimTests()
1259  coverage.RunTests()
1260  if options.genhtml:
1261    coverage.GenerateHtml()
1262  return 0
1263
1264
1265if __name__ == '__main__':
1266  sys.exit(main())
1267