1#!/usr/bin/env python 2# 3# This Source Code Form is subject to the terms of the Mozilla Public 4# License, v. 2.0. If a copy of the MPL was not distributed with this 5# file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 7from __future__ import absolute_import, print_function, with_statement 8import sys 9import os 10from optparse import OptionParser 11from os import environ as env 12import manifestparser 13import mozprocess 14import mozinfo 15import mozcrash 16import mozfile 17import mozlog 18import mozrunner.utils 19 20SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) 21 22# Export directory js/src for tests that need it. 23env["CPP_UNIT_TESTS_DIR_JS_SRC"] = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "..")) 24 25 26class CPPUnitTests(object): 27 # Time (seconds) to wait for test process to complete 28 TEST_PROC_TIMEOUT = 900 29 # Time (seconds) in which process will be killed if it produces no output. 30 TEST_PROC_NO_OUTPUT_TIMEOUT = 300 31 32 def run_one_test( 33 self, prog, env, symbols_path=None, interactive=False, timeout_factor=1 34 ): 35 """ 36 Run a single C++ unit test program. 37 38 Arguments: 39 * prog: The path to the test program to run. 40 * env: The environment to use for running the program. 41 * symbols_path: A path to a directory containing Breakpad-formatted 42 symbol files for producing stack traces on crash. 43 * timeout_factor: An optional test-specific timeout multiplier. 44 45 Return True if the program exits with a zero status, False otherwise. 46 """ 47 basename = os.path.basename(prog) 48 self.log.test_start(basename) 49 with mozfile.TemporaryDirectory() as tempdir: 50 if interactive: 51 # For tests run locally, via mach, print output directly 52 proc = mozprocess.ProcessHandler( 53 [prog], 54 cwd=tempdir, 55 env=env, 56 storeOutput=False, 57 universal_newlines=True, 58 ) 59 else: 60 proc = mozprocess.ProcessHandler( 61 [prog], 62 cwd=tempdir, 63 env=env, 64 storeOutput=True, 65 processOutputLine=lambda _: None, 66 universal_newlines=True, 67 ) 68 # TODO: After bug 811320 is fixed, don't let .run() kill the process, 69 # instead use a timeout in .wait() and then kill to get a stack. 70 test_timeout = CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor 71 proc.run( 72 timeout=test_timeout, 73 outputTimeout=CPPUnitTests.TEST_PROC_NO_OUTPUT_TIMEOUT, 74 ) 75 proc.wait() 76 if proc.output: 77 if self.fix_stack: 78 procOutput = [self.fix_stack(l) for l in proc.output] 79 else: 80 procOutput = proc.output 81 82 output = "\n%s" % "\n".join(procOutput) 83 self.log.process_output(proc.pid, output, command=[prog]) 84 if proc.timedOut: 85 message = "timed out after %d seconds" % CPPUnitTests.TEST_PROC_TIMEOUT 86 self.log.test_end( 87 basename, status="TIMEOUT", expected="PASS", message=message 88 ) 89 return False 90 if mozcrash.check_for_crashes(tempdir, symbols_path, test_name=basename): 91 self.log.test_end(basename, status="CRASH", expected="PASS") 92 return False 93 result = proc.proc.returncode == 0 94 if not result: 95 self.log.test_end( 96 basename, 97 status="FAIL", 98 expected="PASS", 99 message=("test failed with return code %d" % proc.proc.returncode), 100 ) 101 else: 102 self.log.test_end(basename, status="PASS", expected="PASS") 103 return result 104 105 def build_core_environment(self, env, enable_webrender): 106 """ 107 Add environment variables likely to be used across all platforms, including remote systems. 108 """ 109 env["MOZ_XRE_DIR"] = self.xre_path 110 # TODO: switch this to just abort once all C++ unit tests have 111 # been fixed to enable crash reporting 112 env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" 113 env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" 114 env["MOZ_CRASHREPORTER"] = "1" 115 116 if enable_webrender: 117 env["MOZ_WEBRENDER"] = "1" 118 env["MOZ_ACCELERATED"] = "1" 119 else: 120 env["MOZ_WEBRENDER"] = "0" 121 122 return env 123 124 def build_environment(self, enable_webrender=False): 125 """ 126 Create and return a dictionary of all the appropriate env variables and values. 127 On a remote system, we overload this to set different values and are missing things 128 like os.environ and PATH. 129 """ 130 if not os.path.isdir(self.xre_path): 131 raise Exception("xre_path does not exist: %s", self.xre_path) 132 env = dict(os.environ) 133 env = self.build_core_environment(env, enable_webrender) 134 pathvar = "" 135 libpath = self.xre_path 136 if mozinfo.os == "linux": 137 pathvar = "LD_LIBRARY_PATH" 138 elif mozinfo.os == "mac": 139 applibpath = os.path.join(os.path.dirname(libpath), "MacOS") 140 if os.path.exists(applibpath): 141 # Set the library load path to Contents/MacOS if we're run from 142 # the app bundle. 143 libpath = applibpath 144 pathvar = "DYLD_LIBRARY_PATH" 145 elif mozinfo.os == "win": 146 pathvar = "PATH" 147 if pathvar: 148 if pathvar in env: 149 env[pathvar] = "%s%s%s" % (libpath, os.pathsep, env[pathvar]) 150 else: 151 env[pathvar] = libpath 152 153 if mozinfo.info["asan"]: 154 # Use llvm-symbolizer for ASan if available/required 155 llvmsym = os.path.join( 156 self.xre_path, "llvm-symbolizer" + mozinfo.info["bin_suffix"] 157 ) 158 if os.path.isfile(llvmsym): 159 env["ASAN_SYMBOLIZER_PATH"] = llvmsym 160 self.log.info("ASan using symbolizer at %s" % llvmsym) 161 else: 162 self.log.info("Failed to find ASan symbolizer at %s" % llvmsym) 163 164 # dom/media/webrtc/transport tests statically link in NSS, which 165 # causes ODR violations. See bug 1215679. 166 assert "ASAN_OPTIONS" not in env 167 env["ASAN_OPTIONS"] = "detect_leaks=0:detect_odr_violation=0" 168 169 return env 170 171 def run_tests( 172 self, 173 programs, 174 xre_path, 175 symbols_path=None, 176 utility_path=None, 177 enable_webrender=False, 178 interactive=False, 179 ): 180 """ 181 Run a set of C++ unit test programs. 182 183 Arguments: 184 * programs: An iterable containing (test path, test timeout factor) tuples 185 * xre_path: A path to a directory containing a XUL Runtime Environment. 186 * symbols_path: A path to a directory containing Breakpad-formatted 187 symbol files for producing stack traces on crash. 188 * utility_path: A path to a directory containing utility programs 189 (xpcshell et al) 190 191 Returns True if all test programs exited with a zero status, False 192 otherwise. 193 """ 194 self.xre_path = xre_path 195 self.log = mozlog.get_default_logger() 196 if utility_path: 197 self.fix_stack = mozrunner.utils.get_stack_fixer_function( 198 utility_path, symbols_path 199 ) 200 self.log.suite_start(programs, name="cppunittest") 201 env = self.build_environment(enable_webrender) 202 pass_count = 0 203 fail_count = 0 204 for prog in programs: 205 test_path = prog[0] 206 timeout_factor = prog[1] 207 single_result = self.run_one_test( 208 test_path, env, symbols_path, interactive, timeout_factor 209 ) 210 if single_result: 211 pass_count += 1 212 else: 213 fail_count += 1 214 self.log.suite_end() 215 216 # Mozharness-parseable summary formatting. 217 self.log.info("Result summary:") 218 self.log.info("cppunittests INFO | Passed: %d" % pass_count) 219 self.log.info("cppunittests INFO | Failed: %d" % fail_count) 220 return fail_count == 0 221 222 223class CPPUnittestOptions(OptionParser): 224 def __init__(self): 225 OptionParser.__init__(self) 226 self.add_option( 227 "--xre-path", 228 action="store", 229 type="string", 230 dest="xre_path", 231 default=None, 232 help="absolute path to directory containing XRE (probably xulrunner)", 233 ) 234 self.add_option( 235 "--symbols-path", 236 action="store", 237 type="string", 238 dest="symbols_path", 239 default=None, 240 help="absolute path to directory containing breakpad symbols, or " 241 "the URL of a zip file containing symbols", 242 ) 243 self.add_option( 244 "--manifest-path", 245 action="store", 246 type="string", 247 dest="manifest_path", 248 default=None, 249 help="path to test manifest, if different from the path to test binaries", 250 ) 251 self.add_option( 252 "--utility-path", 253 action="store", 254 type="string", 255 dest="utility_path", 256 default=None, 257 help="path to directory containing utility programs", 258 ) 259 self.add_option( 260 "--enable-webrender", 261 action="store_true", 262 dest="enable_webrender", 263 default=False, 264 help="Enable the WebRender compositor in Gecko", 265 ) 266 267 268def extract_unittests_from_args(args, environ, manifest_path): 269 """Extract unittests from args, expanding directories as needed""" 270 mp = manifestparser.TestManifest(strict=True) 271 tests = [] 272 binary_path = None 273 274 if manifest_path: 275 mp.read(manifest_path) 276 binary_path = os.path.abspath(args[0]) 277 else: 278 for p in args: 279 if os.path.isdir(p): 280 try: 281 mp.read(os.path.join(p, "cppunittest.ini")) 282 except IOError: 283 files = [os.path.abspath(os.path.join(p, x)) for x in os.listdir(p)] 284 tests.extend( 285 (f, 1) for f in files if os.access(f, os.R_OK | os.X_OK) 286 ) 287 else: 288 tests.append((os.path.abspath(p), 1)) 289 290 # we skip the existence check here because not all tests are built 291 # for all platforms (and it will fail on Windows anyway) 292 active_tests = mp.active_tests(exists=False, disabled=False, **environ) 293 suffix = ".exe" if mozinfo.isWin else "" 294 if binary_path: 295 tests.extend( 296 [ 297 ( 298 os.path.join(binary_path, test["relpath"] + suffix), 299 int(test.get("requesttimeoutfactor", 1)), 300 ) 301 for test in active_tests 302 ] 303 ) 304 else: 305 tests.extend( 306 [ 307 (test["path"] + suffix, int(test.get("requesttimeoutfactor", 1))) 308 for test in active_tests 309 ] 310 ) 311 312 # skip and warn for any tests in the manifest that are not found 313 final_tests = [] 314 log = mozlog.get_default_logger() 315 for test in tests: 316 if os.path.isfile(test[0]): 317 final_tests.append(test) 318 else: 319 log.warning("test file not found: %s - skipped" % test[0]) 320 321 return final_tests 322 323 324def update_mozinfo(): 325 """walk up directories to find mozinfo.json update the info""" 326 path = SCRIPT_DIR 327 dirs = set() 328 while path != os.path.expanduser("~"): 329 if path in dirs: 330 break 331 dirs.add(path) 332 path = os.path.split(path)[0] 333 mozinfo.find_and_update_from_json(*dirs) 334 335 336def run_test_harness(options, args): 337 update_mozinfo() 338 progs = extract_unittests_from_args(args, mozinfo.info, options.manifest_path) 339 options.xre_path = os.path.abspath(options.xre_path) 340 options.utility_path = os.path.abspath(options.utility_path) 341 tester = CPPUnitTests() 342 result = tester.run_tests( 343 progs, 344 options.xre_path, 345 options.symbols_path, 346 options.utility_path, 347 options.enable_webrender, 348 ) 349 350 return result 351 352 353def main(): 354 parser = CPPUnittestOptions() 355 mozlog.commandline.add_logging_group(parser) 356 options, args = parser.parse_args() 357 if not args: 358 print( 359 """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0], 360 file=sys.stderr, 361 ) 362 sys.exit(1) 363 if not options.xre_path: 364 print("""Error: --xre-path is required""", file=sys.stderr) 365 sys.exit(1) 366 if options.manifest_path and len(args) > 1: 367 print( 368 "Error: multiple arguments not supported with --test-manifest", 369 file=sys.stderr, 370 ) 371 sys.exit(1) 372 log = mozlog.commandline.setup_logging( 373 "cppunittests", options, {"tbpl": sys.stdout} 374 ) 375 try: 376 result = run_test_harness(options, args) 377 except Exception as e: 378 log.error(str(e)) 379 result = False 380 381 sys.exit(0 if result else 1) 382 383 384if __name__ == "__main__": 385 main() 386