michael@0: #!/usr/bin/env python michael@0: # michael@0: # This Source Code Form is subject to the terms of the Mozilla Public michael@0: # License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: # file, You can obtain one at http://mozilla.org/MPL/2.0/. michael@0: michael@0: from __future__ import with_statement michael@0: import sys, os, tempfile, shutil michael@0: from optparse import OptionParser michael@0: import mozprocess, mozinfo, mozlog, mozcrash, mozfile michael@0: from contextlib import contextmanager michael@0: michael@0: log = mozlog.getLogger('cppunittests') michael@0: michael@0: class CPPUnitTests(object): michael@0: # Time (seconds) to wait for test process to complete michael@0: TEST_PROC_TIMEOUT = 900 michael@0: # Time (seconds) in which process will be killed if it produces no output. michael@0: TEST_PROC_NO_OUTPUT_TIMEOUT = 300 michael@0: michael@0: def run_one_test(self, prog, env, symbols_path=None): michael@0: """ michael@0: Run a single C++ unit test program. michael@0: michael@0: Arguments: michael@0: * prog: The path to the test program to run. michael@0: * env: The environment to use for running the program. michael@0: * symbols_path: A path to a directory containing Breakpad-formatted michael@0: symbol files for producing stack traces on crash. michael@0: michael@0: Return True if the program exits with a zero status, False otherwise. michael@0: """ michael@0: basename = os.path.basename(prog) michael@0: log.info("Running test %s", basename) michael@0: with mozfile.TemporaryDirectory() as tempdir: michael@0: proc = mozprocess.ProcessHandler([prog], michael@0: cwd=tempdir, michael@0: env=env) michael@0: #TODO: After bug 811320 is fixed, don't let .run() kill the process, michael@0: # instead use a timeout in .wait() and then kill to get a stack. michael@0: proc.run(timeout=CPPUnitTests.TEST_PROC_TIMEOUT, michael@0: outputTimeout=CPPUnitTests.TEST_PROC_NO_OUTPUT_TIMEOUT) michael@0: proc.wait() michael@0: if proc.timedOut: michael@0: log.testFail("%s | timed out after %d seconds", michael@0: basename, CPPUnitTests.TEST_PROC_TIMEOUT) michael@0: return False michael@0: if mozcrash.check_for_crashes(tempdir, symbols_path, michael@0: test_name=basename): michael@0: log.testFail("%s | test crashed", basename) michael@0: return False michael@0: result = proc.proc.returncode == 0 michael@0: if not result: michael@0: log.testFail("%s | test failed with return code %d", michael@0: basename, proc.proc.returncode) michael@0: return result michael@0: michael@0: def build_core_environment(self, env = {}): michael@0: """ michael@0: Add environment variables likely to be used across all platforms, including remote systems. michael@0: """ michael@0: env["MOZILLA_FIVE_HOME"] = self.xre_path michael@0: env["MOZ_XRE_DIR"] = self.xre_path michael@0: #TODO: switch this to just abort once all C++ unit tests have michael@0: # been fixed to enable crash reporting michael@0: env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" michael@0: env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" michael@0: env["MOZ_CRASHREPORTER"] = "1" michael@0: return env michael@0: michael@0: def build_environment(self): michael@0: """ michael@0: Create and return a dictionary of all the appropriate env variables and values. michael@0: On a remote system, we overload this to set different values and are missing things like os.environ and PATH. michael@0: """ michael@0: if not os.path.isdir(self.xre_path): michael@0: raise Exception("xre_path does not exist: %s", self.xre_path) michael@0: env = dict(os.environ) michael@0: env = self.build_core_environment(env) michael@0: pathvar = "" michael@0: if mozinfo.os == "linux": michael@0: pathvar = "LD_LIBRARY_PATH" michael@0: elif mozinfo.os == "mac": michael@0: pathvar = "DYLD_LIBRARY_PATH" michael@0: elif mozinfo.os == "win": michael@0: pathvar = "PATH" michael@0: if pathvar: michael@0: if pathvar in env: michael@0: env[pathvar] = "%s%s%s" % (self.xre_path, os.pathsep, env[pathvar]) michael@0: else: michael@0: env[pathvar] = self.xre_path michael@0: michael@0: # Use llvm-symbolizer for ASan if available/required michael@0: llvmsym = os.path.join(self.xre_path, "llvm-symbolizer") michael@0: if os.path.isfile(llvmsym): michael@0: env["ASAN_SYMBOLIZER_PATH"] = llvmsym michael@0: michael@0: return env michael@0: michael@0: def run_tests(self, programs, xre_path, symbols_path=None): michael@0: """ michael@0: Run a set of C++ unit test programs. michael@0: michael@0: Arguments: michael@0: * programs: An iterable containing paths to test programs. michael@0: * xre_path: A path to a directory containing a XUL Runtime Environment. michael@0: * symbols_path: A path to a directory containing Breakpad-formatted michael@0: symbol files for producing stack traces on crash. michael@0: michael@0: Returns True if all test programs exited with a zero status, False michael@0: otherwise. michael@0: """ michael@0: self.xre_path = xre_path michael@0: env = self.build_environment() michael@0: pass_count = 0 michael@0: fail_count = 0 michael@0: for prog in programs: michael@0: single_result = self.run_one_test(prog, env, symbols_path) michael@0: if single_result: michael@0: pass_count += 1 michael@0: else: michael@0: fail_count += 1 michael@0: michael@0: log.info("Result summary:") michael@0: log.info("Passed: %d" % pass_count) michael@0: log.info("Failed: %d" % fail_count) michael@0: return fail_count == 0 michael@0: michael@0: class CPPUnittestOptions(OptionParser): michael@0: def __init__(self): michael@0: OptionParser.__init__(self) michael@0: self.add_option("--xre-path", michael@0: action = "store", type = "string", dest = "xre_path", michael@0: default = None, michael@0: help = "absolute path to directory containing XRE (probably xulrunner)") michael@0: self.add_option("--symbols-path", michael@0: action = "store", type = "string", dest = "symbols_path", michael@0: default = None, michael@0: help = "absolute path to directory containing breakpad symbols, or the URL of a zip file containing symbols") michael@0: self.add_option("--skip-manifest", michael@0: action = "store", type = "string", dest = "manifest_file", michael@0: default = None, michael@0: help = "absolute path to a manifest file") michael@0: michael@0: def extract_unittests_from_args(args, manifest_file): michael@0: """Extract unittests from args, expanding directories as needed""" michael@0: progs = [] michael@0: michael@0: # Known files commonly packaged with the cppunittests that are not tests michael@0: skipped_progs = set(['.mkdir.done', 'remotecppunittests.py', 'runcppunittests.py', 'runcppunittests.pyc']) michael@0: michael@0: if manifest_file: michael@0: skipped_progs.add(os.path.basename(manifest_file)) michael@0: with open(manifest_file) as f: michael@0: for line in f: michael@0: # strip out comment, if any michael@0: prog = line.split('#')[0] michael@0: if prog: michael@0: skipped_progs.add(prog.strip()) michael@0: michael@0: for p in args: michael@0: if os.path.isdir(p): michael@0: progs.extend([os.path.abspath(os.path.join(p, x)) for x in os.listdir(p) if not x in skipped_progs]) michael@0: elif p not in skipped_progs: michael@0: progs.append(os.path.abspath(p)) michael@0: michael@0: return progs michael@0: michael@0: def main(): michael@0: parser = CPPUnittestOptions() michael@0: options, args = parser.parse_args() michael@0: if not args: michael@0: print >>sys.stderr, """Usage: %s [...]""" % sys.argv[0] michael@0: sys.exit(1) michael@0: if not options.xre_path: michael@0: print >>sys.stderr, """Error: --xre-path is required""" michael@0: sys.exit(1) michael@0: michael@0: progs = extract_unittests_from_args(args, options.manifest_file) michael@0: options.xre_path = os.path.abspath(options.xre_path) michael@0: tester = CPPUnitTests() michael@0: try: michael@0: result = tester.run_tests(progs, options.xre_path, options.symbols_path) michael@0: except Exception, e: michael@0: log.error(str(e)) michael@0: result = False michael@0: sys.exit(0 if result else 1) michael@0: michael@0: if __name__ == '__main__': michael@0: main() michael@0: