testing/runcppunittests.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/testing/runcppunittests.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,191 @@
     1.4 +#!/usr/bin/env python
     1.5 +#
     1.6 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.7 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.9 +
    1.10 +from __future__ import with_statement
    1.11 +import sys, os, tempfile, shutil
    1.12 +from optparse import OptionParser
    1.13 +import mozprocess, mozinfo, mozlog, mozcrash, mozfile
    1.14 +from contextlib import contextmanager
    1.15 +
    1.16 +log = mozlog.getLogger('cppunittests')
    1.17 +
    1.18 +class CPPUnitTests(object):
    1.19 +    # Time (seconds) to wait for test process to complete
    1.20 +    TEST_PROC_TIMEOUT = 900
    1.21 +    # Time (seconds) in which process will be killed if it produces no output.
    1.22 +    TEST_PROC_NO_OUTPUT_TIMEOUT = 300
    1.23 +
    1.24 +    def run_one_test(self, prog, env, symbols_path=None):
    1.25 +        """
    1.26 +        Run a single C++ unit test program.
    1.27 +
    1.28 +        Arguments:
    1.29 +        * prog: The path to the test program to run.
    1.30 +        * env: The environment to use for running the program.
    1.31 +        * symbols_path: A path to a directory containing Breakpad-formatted
    1.32 +                        symbol files for producing stack traces on crash.
    1.33 +
    1.34 +        Return True if the program exits with a zero status, False otherwise.
    1.35 +        """
    1.36 +        basename = os.path.basename(prog)
    1.37 +        log.info("Running test %s", basename)
    1.38 +        with mozfile.TemporaryDirectory() as tempdir:
    1.39 +            proc = mozprocess.ProcessHandler([prog],
    1.40 +                                             cwd=tempdir,
    1.41 +                                             env=env)
    1.42 +            #TODO: After bug 811320 is fixed, don't let .run() kill the process,
    1.43 +            # instead use a timeout in .wait() and then kill to get a stack.
    1.44 +            proc.run(timeout=CPPUnitTests.TEST_PROC_TIMEOUT,
    1.45 +                     outputTimeout=CPPUnitTests.TEST_PROC_NO_OUTPUT_TIMEOUT)
    1.46 +            proc.wait()
    1.47 +            if proc.timedOut:
    1.48 +                log.testFail("%s | timed out after %d seconds",
    1.49 +                             basename, CPPUnitTests.TEST_PROC_TIMEOUT)
    1.50 +                return False
    1.51 +            if mozcrash.check_for_crashes(tempdir, symbols_path,
    1.52 +                                          test_name=basename):
    1.53 +                log.testFail("%s | test crashed", basename)
    1.54 +                return False
    1.55 +            result = proc.proc.returncode == 0
    1.56 +            if not result:
    1.57 +                log.testFail("%s | test failed with return code %d",
    1.58 +                             basename, proc.proc.returncode)
    1.59 +            return result
    1.60 +
    1.61 +    def build_core_environment(self, env = {}):
    1.62 +        """
    1.63 +        Add environment variables likely to be used across all platforms, including remote systems.
    1.64 +        """
    1.65 +        env["MOZILLA_FIVE_HOME"] = self.xre_path
    1.66 +        env["MOZ_XRE_DIR"] = self.xre_path
    1.67 +        #TODO: switch this to just abort once all C++ unit tests have
    1.68 +        # been fixed to enable crash reporting
    1.69 +        env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
    1.70 +        env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
    1.71 +        env["MOZ_CRASHREPORTER"] = "1"
    1.72 +        return env
    1.73 +
    1.74 +    def build_environment(self):
    1.75 +        """
    1.76 +        Create and return a dictionary of all the appropriate env variables and values.
    1.77 +        On a remote system, we overload this to set different values and are missing things like os.environ and PATH.
    1.78 +        """
    1.79 +        if not os.path.isdir(self.xre_path):
    1.80 +            raise Exception("xre_path does not exist: %s", self.xre_path)
    1.81 +        env = dict(os.environ)
    1.82 +        env = self.build_core_environment(env)
    1.83 +        pathvar = ""
    1.84 +        if mozinfo.os == "linux":
    1.85 +            pathvar = "LD_LIBRARY_PATH"
    1.86 +        elif mozinfo.os == "mac":
    1.87 +            pathvar = "DYLD_LIBRARY_PATH"
    1.88 +        elif mozinfo.os == "win":
    1.89 +            pathvar = "PATH"
    1.90 +        if pathvar:
    1.91 +            if pathvar in env:
    1.92 +                env[pathvar] = "%s%s%s" % (self.xre_path, os.pathsep, env[pathvar])
    1.93 +            else:
    1.94 +                env[pathvar] = self.xre_path
    1.95 +
    1.96 +        # Use llvm-symbolizer for ASan if available/required
    1.97 +        llvmsym = os.path.join(self.xre_path, "llvm-symbolizer")
    1.98 +        if os.path.isfile(llvmsym):
    1.99 +          env["ASAN_SYMBOLIZER_PATH"] = llvmsym
   1.100 +
   1.101 +        return env
   1.102 +
   1.103 +    def run_tests(self, programs, xre_path, symbols_path=None):
   1.104 +        """
   1.105 +        Run a set of C++ unit test programs.
   1.106 +
   1.107 +        Arguments:
   1.108 +        * programs: An iterable containing paths to test programs.
   1.109 +        * xre_path: A path to a directory containing a XUL Runtime Environment.
   1.110 +        * symbols_path: A path to a directory containing Breakpad-formatted
   1.111 +                        symbol files for producing stack traces on crash.
   1.112 +
   1.113 +        Returns True if all test programs exited with a zero status, False
   1.114 +        otherwise.
   1.115 +        """
   1.116 +        self.xre_path = xre_path
   1.117 +        env = self.build_environment()
   1.118 +        pass_count = 0
   1.119 +        fail_count = 0
   1.120 +        for prog in programs:
   1.121 +            single_result = self.run_one_test(prog, env, symbols_path)
   1.122 +            if single_result:
   1.123 +                pass_count += 1
   1.124 +            else:
   1.125 +                fail_count += 1
   1.126 +
   1.127 +        log.info("Result summary:")
   1.128 +        log.info("Passed: %d" % pass_count)
   1.129 +        log.info("Failed: %d" % fail_count)
   1.130 +        return fail_count == 0
   1.131 +
   1.132 +class CPPUnittestOptions(OptionParser):
   1.133 +    def __init__(self):
   1.134 +        OptionParser.__init__(self)
   1.135 +        self.add_option("--xre-path",
   1.136 +                        action = "store", type = "string", dest = "xre_path",
   1.137 +                        default = None,
   1.138 +                        help = "absolute path to directory containing XRE (probably xulrunner)")
   1.139 +        self.add_option("--symbols-path",
   1.140 +                        action = "store", type = "string", dest = "symbols_path",
   1.141 +                        default = None,
   1.142 +                        help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols")
   1.143 +        self.add_option("--skip-manifest",
   1.144 +                        action = "store", type = "string", dest = "manifest_file",
   1.145 +                        default = None,
   1.146 +                        help = "absolute path to a manifest file")
   1.147 +
   1.148 +def extract_unittests_from_args(args, manifest_file):
   1.149 +    """Extract unittests from args, expanding directories as needed"""
   1.150 +    progs = []
   1.151 +
   1.152 +    # Known files commonly packaged with the cppunittests that are not tests
   1.153 +    skipped_progs = set(['.mkdir.done', 'remotecppunittests.py', 'runcppunittests.py', 'runcppunittests.pyc'])
   1.154 +
   1.155 +    if manifest_file:
   1.156 +        skipped_progs.add(os.path.basename(manifest_file))
   1.157 +        with open(manifest_file) as f:
   1.158 +            for line in f:
   1.159 +                # strip out comment, if any
   1.160 +                prog = line.split('#')[0]
   1.161 +                if prog:
   1.162 +                    skipped_progs.add(prog.strip())
   1.163 +
   1.164 +    for p in args:
   1.165 +        if os.path.isdir(p):
   1.166 +            progs.extend([os.path.abspath(os.path.join(p, x)) for x in os.listdir(p) if not x in skipped_progs])
   1.167 +        elif p not in skipped_progs:
   1.168 +            progs.append(os.path.abspath(p))
   1.169 +
   1.170 +    return progs
   1.171 +
   1.172 +def main():
   1.173 +    parser = CPPUnittestOptions()
   1.174 +    options, args = parser.parse_args()
   1.175 +    if not args:
   1.176 +        print >>sys.stderr, """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0]
   1.177 +        sys.exit(1)
   1.178 +    if not options.xre_path:
   1.179 +        print >>sys.stderr, """Error: --xre-path is required"""
   1.180 +        sys.exit(1)
   1.181 +        
   1.182 +    progs = extract_unittests_from_args(args, options.manifest_file)
   1.183 +    options.xre_path = os.path.abspath(options.xre_path)
   1.184 +    tester = CPPUnitTests()
   1.185 +    try:
   1.186 +        result = tester.run_tests(progs, options.xre_path, options.symbols_path)
   1.187 +    except Exception, e:
   1.188 +        log.error(str(e))
   1.189 +        result = False
   1.190 +    sys.exit(0 if result else 1)
   1.191 +
   1.192 +if __name__ == '__main__':
   1.193 +    main()
   1.194 +

mercurial