Blame testing/runcppunittests.py

Packit f0b94e
#!/usr/bin/env python
Packit f0b94e
#
Packit f0b94e
# This Source Code Form is subject to the terms of the Mozilla Public
Packit f0b94e
# License, v. 2.0. If a copy of the MPL was not distributed with this
Packit f0b94e
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
Packit f0b94e
Packit f0b94e
from __future__ import with_statement
Packit f0b94e
import sys
Packit f0b94e
import os
Packit f0b94e
from optparse import OptionParser
Packit f0b94e
from os import environ as env
Packit f0b94e
import manifestparser
Packit f0b94e
import mozprocess
Packit f0b94e
import mozinfo
Packit f0b94e
import mozcrash
Packit f0b94e
import mozfile
Packit f0b94e
import mozlog
Packit f0b94e
Packit f0b94e
SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
Packit f0b94e
Packit f0b94e
# Export directory js/src for tests that need it.
Packit f0b94e
env['CPP_UNIT_TESTS_DIR_JS_SRC'] = os.path.abspath(os.path.join(SCRIPT_DIR, "..", ".."))
Packit f0b94e
Packit f0b94e
Packit f0b94e
class CPPUnitTests(object):
Packit f0b94e
    # Time (seconds) to wait for test process to complete
Packit f0b94e
    TEST_PROC_TIMEOUT = 900
Packit f0b94e
    # Time (seconds) in which process will be killed if it produces no output.
Packit f0b94e
    TEST_PROC_NO_OUTPUT_TIMEOUT = 300
Packit f0b94e
Packit f0b94e
    def run_one_test(self, prog, env, symbols_path=None, interactive=False,
Packit f0b94e
                     timeout_factor=1):
Packit f0b94e
        """
Packit f0b94e
        Run a single C++ unit test program.
Packit f0b94e
Packit f0b94e
        Arguments:
Packit f0b94e
        * prog: The path to the test program to run.
Packit f0b94e
        * env: The environment to use for running the program.
Packit f0b94e
        * symbols_path: A path to a directory containing Breakpad-formatted
Packit f0b94e
                        symbol files for producing stack traces on crash.
Packit f0b94e
        * timeout_factor: An optional test-specific timeout multiplier.
Packit f0b94e
Packit f0b94e
        Return True if the program exits with a zero status, False otherwise.
Packit f0b94e
        """
Packit f0b94e
        basename = os.path.basename(prog)
Packit f0b94e
        self.log.test_start(basename)
Packit f0b94e
        with mozfile.TemporaryDirectory() as tempdir:
Packit f0b94e
            if interactive:
Packit f0b94e
                # For tests run locally, via mach, print output directly
Packit f0b94e
                proc = mozprocess.ProcessHandler([prog],
Packit f0b94e
                                                 cwd=tempdir,
Packit f0b94e
                                                 env=env,
Packit f0b94e
                                                 storeOutput=False)
Packit f0b94e
            else:
Packit f0b94e
                proc = mozprocess.ProcessHandler([prog],
Packit f0b94e
                                                 cwd=tempdir,
Packit f0b94e
                                                 env=env,
Packit f0b94e
                                                 storeOutput=True,
Packit f0b94e
                                                 processOutputLine=lambda _: None)
Packit f0b94e
            # TODO: After bug 811320 is fixed, don't let .run() kill the process,
Packit f0b94e
            # instead use a timeout in .wait() and then kill to get a stack.
Packit f0b94e
            test_timeout = CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor
Packit f0b94e
            proc.run(timeout=test_timeout,
Packit f0b94e
                     outputTimeout=CPPUnitTests.TEST_PROC_NO_OUTPUT_TIMEOUT)
Packit f0b94e
            proc.wait()
Packit f0b94e
            if proc.output:
Packit f0b94e
                output = "\n%s" % "\n".join(proc.output)
Packit f0b94e
                self.log.process_output(proc.pid, output, command=[prog])
Packit f0b94e
            if proc.timedOut:
Packit f0b94e
                message = "timed out after %d seconds" % CPPUnitTests.TEST_PROC_TIMEOUT
Packit f0b94e
                self.log.test_end(basename, status='TIMEOUT', expected='PASS',
Packit f0b94e
                                  message=message)
Packit f0b94e
                return False
Packit f0b94e
            if mozcrash.check_for_crashes(tempdir, symbols_path,
Packit f0b94e
                                          test_name=basename):
Packit f0b94e
                self.log.test_end(basename, status='CRASH', expected='PASS')
Packit f0b94e
                return False
Packit f0b94e
            result = proc.proc.returncode == 0
Packit f0b94e
            if not result:
Packit f0b94e
                self.log.test_end(basename, status='FAIL', expected='PASS',
Packit f0b94e
                                  message=("test failed with return code %d" %
Packit f0b94e
                                           proc.proc.returncode))
Packit f0b94e
            else:
Packit f0b94e
                self.log.test_end(basename, status='PASS', expected='PASS')
Packit f0b94e
            return result
Packit f0b94e
Packit f0b94e
    def build_core_environment(self, env={}):
Packit f0b94e
        """
Packit f0b94e
        Add environment variables likely to be used across all platforms, including remote systems.
Packit f0b94e
        """
Packit f0b94e
        env["MOZ_XRE_DIR"] = self.xre_path
Packit f0b94e
        # TODO: switch this to just abort once all C++ unit tests have
Packit f0b94e
        # been fixed to enable crash reporting
Packit f0b94e
        env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
Packit f0b94e
        env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
Packit f0b94e
        env["MOZ_CRASHREPORTER"] = "1"
Packit f0b94e
        return env
Packit f0b94e
Packit f0b94e
    def build_environment(self):
Packit f0b94e
        """
Packit f0b94e
        Create and return a dictionary of all the appropriate env variables and values.
Packit f0b94e
        On a remote system, we overload this to set different values and are missing things
Packit f0b94e
        like os.environ and PATH.
Packit f0b94e
        """
Packit f0b94e
        if not os.path.isdir(self.xre_path):
Packit f0b94e
            raise Exception("xre_path does not exist: %s", self.xre_path)
Packit f0b94e
        env = dict(os.environ)
Packit f0b94e
        env = self.build_core_environment(env)
Packit f0b94e
        pathvar = ""
Packit f0b94e
        libpath = self.xre_path
Packit f0b94e
        if mozinfo.os == "linux":
Packit f0b94e
            pathvar = "LD_LIBRARY_PATH"
Packit f0b94e
        elif mozinfo.os == "mac":
Packit f0b94e
            applibpath = os.path.join(os.path.dirname(libpath), 'MacOS')
Packit f0b94e
            if os.path.exists(applibpath):
Packit f0b94e
                # Set the library load path to Contents/MacOS if we're run from
Packit f0b94e
                # the app bundle.
Packit f0b94e
                libpath = applibpath
Packit f0b94e
            pathvar = "DYLD_LIBRARY_PATH"
Packit f0b94e
        elif mozinfo.os == "win":
Packit f0b94e
            pathvar = "PATH"
Packit f0b94e
        if pathvar:
Packit f0b94e
            if pathvar in env:
Packit f0b94e
                env[pathvar] = "%s%s%s" % (libpath, os.pathsep, env[pathvar])
Packit f0b94e
            else:
Packit f0b94e
                env[pathvar] = libpath
Packit f0b94e
Packit f0b94e
        if mozinfo.info["asan"]:
Packit f0b94e
            # Use llvm-symbolizer for ASan if available/required
Packit f0b94e
            llvmsym = os.path.join(
Packit f0b94e
                self.xre_path,
Packit f0b94e
                "llvm-symbolizer" + mozinfo.info["bin_suffix"].encode('ascii'))
Packit f0b94e
            if os.path.isfile(llvmsym):
Packit f0b94e
                env["ASAN_SYMBOLIZER_PATH"] = llvmsym
Packit f0b94e
                self.log.info("ASan using symbolizer at %s" % llvmsym)
Packit f0b94e
            else:
Packit f0b94e
                self.log.info("Failed to find ASan symbolizer at %s" % llvmsym)
Packit f0b94e
Packit f0b94e
            # media/mtransport tests statically link in NSS, which
Packit f0b94e
            # causes ODR violations. See bug 1215679.
Packit f0b94e
            assert 'ASAN_OPTIONS' not in env
Packit f0b94e
            env['ASAN_OPTIONS'] = 'detect_leaks=0:detect_odr_violation=0'
Packit f0b94e
Packit f0b94e
        return env
Packit f0b94e
Packit f0b94e
    def run_tests(self, programs, xre_path, symbols_path=None, interactive=False):
Packit f0b94e
        """
Packit f0b94e
        Run a set of C++ unit test programs.
Packit f0b94e
Packit f0b94e
        Arguments:
Packit f0b94e
        * programs: An iterable containing (test path, test timeout factor) tuples
Packit f0b94e
        * xre_path: A path to a directory containing a XUL Runtime Environment.
Packit f0b94e
        * symbols_path: A path to a directory containing Breakpad-formatted
Packit f0b94e
                        symbol files for producing stack traces on crash.
Packit f0b94e
Packit f0b94e
        Returns True if all test programs exited with a zero status, False
Packit f0b94e
        otherwise.
Packit f0b94e
        """
Packit f0b94e
        self.xre_path = xre_path
Packit f0b94e
        self.log = mozlog.get_default_logger()
Packit f0b94e
        self.log.suite_start(programs, name='cppunittest')
Packit f0b94e
        env = self.build_environment()
Packit f0b94e
        pass_count = 0
Packit f0b94e
        fail_count = 0
Packit f0b94e
        for prog in programs:
Packit f0b94e
            test_path = prog[0]
Packit f0b94e
            timeout_factor = prog[1]
Packit f0b94e
            single_result = self.run_one_test(test_path, env, symbols_path,
Packit f0b94e
                                              interactive, timeout_factor)
Packit f0b94e
            if single_result:
Packit f0b94e
                pass_count += 1
Packit f0b94e
            else:
Packit f0b94e
                fail_count += 1
Packit f0b94e
        self.log.suite_end()
Packit f0b94e
Packit f0b94e
        # Mozharness-parseable summary formatting.
Packit f0b94e
        self.log.info("Result summary:")
Packit f0b94e
        self.log.info("cppunittests INFO | Passed: %d" % pass_count)
Packit f0b94e
        self.log.info("cppunittests INFO | Failed: %d" % fail_count)
Packit f0b94e
        return fail_count == 0
Packit f0b94e
Packit f0b94e
Packit f0b94e
class CPPUnittestOptions(OptionParser):
Packit f0b94e
    def __init__(self):
Packit f0b94e
        OptionParser.__init__(self)
Packit f0b94e
        self.add_option("--xre-path",
Packit f0b94e
                        action="store", type="string", dest="xre_path",
Packit f0b94e
                        default=None,
Packit f0b94e
                        help="absolute path to directory containing XRE (probably xulrunner)")
Packit f0b94e
        self.add_option("--symbols-path",
Packit f0b94e
                        action="store", type="string", dest="symbols_path",
Packit f0b94e
                        default=None,
Packit f0b94e
                        help="absolute path to directory containing breakpad symbols, or "
Packit f0b94e
                        "the URL of a zip file containing symbols")
Packit f0b94e
        self.add_option("--manifest-path",
Packit f0b94e
                        action="store", type="string", dest="manifest_path",
Packit f0b94e
                        default=None,
Packit f0b94e
                        help="path to test manifest, if different from the path to test binaries")
Packit f0b94e
Packit f0b94e
Packit f0b94e
def extract_unittests_from_args(args, environ, manifest_path):
Packit f0b94e
    """Extract unittests from args, expanding directories as needed"""
Packit f0b94e
    mp = manifestparser.TestManifest(strict=True)
Packit f0b94e
    tests = []
Packit f0b94e
    binary_path = None
Packit f0b94e
Packit f0b94e
    if manifest_path:
Packit f0b94e
        mp.read(manifest_path)
Packit f0b94e
        binary_path = os.path.abspath(args[0])
Packit f0b94e
    else:
Packit f0b94e
        for p in args:
Packit f0b94e
            if os.path.isdir(p):
Packit f0b94e
                try:
Packit f0b94e
                    mp.read(os.path.join(p, 'cppunittest.ini'))
Packit f0b94e
                except IOError:
Packit f0b94e
                    files = [os.path.abspath(os.path.join(p, x)) for x in os.listdir(p)]
Packit f0b94e
                    tests.extend((f, 1) for f in files
Packit f0b94e
                                 if os.access(f, os.R_OK | os.X_OK))
Packit f0b94e
            else:
Packit f0b94e
                tests.append((os.path.abspath(p), 1))
Packit f0b94e
Packit f0b94e
    # we skip the existence check here because not all tests are built
Packit f0b94e
    # for all platforms (and it will fail on Windows anyway)
Packit f0b94e
    active_tests = mp.active_tests(exists=False, disabled=False, **environ)
Packit f0b94e
    suffix = '.exe' if mozinfo.isWin else ''
Packit f0b94e
    if binary_path:
Packit f0b94e
        tests.extend([
Packit f0b94e
            (os.path.join(binary_path, test['relpath'] + suffix),
Packit f0b94e
             int(test.get('requesttimeoutfactor', 1)))
Packit f0b94e
            for test in active_tests])
Packit f0b94e
    else:
Packit f0b94e
        tests.extend([
Packit f0b94e
            (test['path'] + suffix,
Packit f0b94e
             int(test.get('requesttimeoutfactor', 1)))
Packit f0b94e
            for test in active_tests
Packit f0b94e
        ])
Packit f0b94e
Packit f0b94e
    # skip non-existing tests
Packit f0b94e
    tests = [test for test in tests if os.path.isfile(test[0])]
Packit f0b94e
Packit f0b94e
    return tests
Packit f0b94e
Packit f0b94e
Packit f0b94e
def update_mozinfo():
Packit f0b94e
    """walk up directories to find mozinfo.json update the info"""
Packit f0b94e
    path = SCRIPT_DIR
Packit f0b94e
    dirs = set()
Packit f0b94e
    while path != os.path.expanduser('~'):
Packit f0b94e
        if path in dirs:
Packit f0b94e
            break
Packit f0b94e
        dirs.add(path)
Packit f0b94e
        path = os.path.split(path)[0]
Packit f0b94e
    mozinfo.find_and_update_from_json(*dirs)
Packit f0b94e
Packit f0b94e
Packit f0b94e
def run_test_harness(options, args):
Packit f0b94e
    update_mozinfo()
Packit f0b94e
    progs = extract_unittests_from_args(args, mozinfo.info, options.manifest_path)
Packit f0b94e
    options.xre_path = os.path.abspath(options.xre_path)
Packit f0b94e
    tester = CPPUnitTests()
Packit f0b94e
    result = tester.run_tests(progs, options.xre_path, options.symbols_path)
Packit f0b94e
Packit f0b94e
    return result
Packit f0b94e
Packit f0b94e
Packit f0b94e
def main():
Packit f0b94e
    parser = CPPUnittestOptions()
Packit f0b94e
    mozlog.commandline.add_logging_group(parser)
Packit f0b94e
    options, args = parser.parse_args()
Packit f0b94e
    if not args:
Packit f0b94e
        print >>sys.stderr, """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0]
Packit f0b94e
        sys.exit(1)
Packit f0b94e
    if not options.xre_path:
Packit f0b94e
        print >>sys.stderr, """Error: --xre-path is required"""
Packit f0b94e
        sys.exit(1)
Packit f0b94e
    if options.manifest_path and len(args) > 1:
Packit f0b94e
        print >>sys.stderr, "Error: multiple arguments not supported with --test-manifest"
Packit f0b94e
        sys.exit(1)
Packit f0b94e
    log = mozlog.commandline.setup_logging("cppunittests", options,
Packit f0b94e
                                           {"tbpl": sys.stdout})
Packit f0b94e
    try:
Packit f0b94e
        result = run_test_harness(options, args)
Packit f0b94e
    except Exception as e:
Packit f0b94e
        log.error(str(e))
Packit f0b94e
        result = False
Packit f0b94e
Packit f0b94e
    sys.exit(0 if result else 1)
Packit f0b94e
Packit f0b94e
Packit f0b94e
if __name__ == '__main__':
Packit f0b94e
    main()