rungtests.py (12697B)
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 7 import argparse 8 import json 9 import os 10 import pathlib 11 import re 12 import sys 13 14 import mozcrash 15 import mozinfo 16 import mozlog 17 import mozprocess 18 from mozfile import load_source 19 from mozrunner.utils import get_stack_fixer_function 20 21 HERE = os.path.abspath(os.path.dirname(__file__)) 22 23 log = mozlog.unstructured.getLogger("gtest") 24 25 26 class GTests: 27 # Time (seconds) in which process will be killed if it produces no output. 28 TEST_PROC_NO_OUTPUT_TIMEOUT = 300 29 30 def gtest_timeout_value(self): 31 # Time (seconds) to wait for test process to complete 32 if mozinfo.info["tsan"]: 33 return 3600 34 else: 35 return 2400 36 37 def merge_perfherder_data(self, perfherder_data): 38 grouped = {} 39 40 for data in perfherder_data: 41 framework_name = data.get("framework", {}).get("name") 42 suites_by_name = grouped.setdefault(framework_name, {}) 43 for suite in data.get("suites", []): 44 suite_name = suite.get("name") 45 suite_data = suites_by_name.setdefault( 46 suite_name, {"name": suite_name, "subtests": []} 47 ) 48 suite_data["subtests"].extend(suite.get("subtests", [])) 49 50 results = {} 51 for framework_name, suites in grouped.items(): 52 results[framework_name] = { 53 "framework": {"name": framework_name}, 54 "suites": list(suites.values()), 55 } 56 57 return results 58 59 def run_gtest( 60 self, 61 prog, 62 xre_path, 63 cwd, 64 symbols_path=None, 65 utility_path=None, 66 enable_inc_origin_init=False, 67 filter_set=None, 68 ): 69 """ 70 Run a single C++ unit test program. 71 72 Arguments: 73 * prog: The path to the test program to run. 74 * env: The environment to use for running the program. 75 * cwd: The directory to run tests from (support files will be found 76 in this direcotry). 77 * symbols_path: A path to a directory containing Breakpad-formatted 78 symbol files for producing stack traces on crash. 79 * utility_path: A path to a directory containing utility programs. 80 currently used to locate a stack fixer to provide 81 symbols symbols for assertion stacks. 82 83 Return True if the program exits with a zero status, False otherwise. 84 """ 85 self.xre_path = xre_path 86 env = self.build_environment(enable_inc_origin_init, filter_set) 87 perfherder_data = [] 88 PERFHERDER_MATCHER = re.compile(r"PERFHERDER_DATA:\s*(\{.*\})\s*$") 89 log.info("Running gtest") 90 91 if cwd and not os.path.isdir(cwd): 92 os.makedirs(cwd) 93 94 stack_fixer = None 95 if utility_path: 96 stack_fixer = get_stack_fixer_function(utility_path, symbols_path) 97 98 GTests.run_gtest.timed_out = False 99 100 def output_line_handler(proc, line): 101 if stack_fixer: 102 print(stack_fixer(line)) 103 else: 104 print(line) 105 106 match = PERFHERDER_MATCHER.search(line) 107 if match: 108 data = json.loads(match.group(1)) 109 perfherder_data.append(data) 110 111 def proc_timeout_handler(proc): 112 GTests.run_gtest.timed_out = True 113 log.testFail( 114 "gtest | timed out after %d seconds", self.gtest_timeout_value() 115 ) 116 mozcrash.kill_and_get_minidump(proc.pid, cwd, utility_path) 117 118 def output_timeout_handler(proc): 119 GTests.run_gtest.timed_out = True 120 log.testFail( 121 "gtest | timed out after %d seconds without output", 122 GTests.TEST_PROC_NO_OUTPUT_TIMEOUT, 123 ) 124 mozcrash.kill_and_get_minidump(proc.pid, cwd, utility_path) 125 126 proc = mozprocess.run_and_wait( 127 [prog, "-unittest", "--gtest_death_test_style=threadsafe"], 128 cwd=cwd, 129 env=env, 130 output_line_handler=output_line_handler, 131 timeout=self.gtest_timeout_value(), 132 timeout_handler=proc_timeout_handler, 133 output_timeout=GTests.TEST_PROC_NO_OUTPUT_TIMEOUT, 134 output_timeout_handler=output_timeout_handler, 135 ) 136 137 if perfherder_data and "MOZ_AUTOMATION" in os.environ: 138 upload_dir = pathlib.Path(os.getenv("MOZ_UPLOAD_DIR")) 139 upload_dir.mkdir(parents=True, exist_ok=True) 140 merged_perfherder_data = self.merge_perfherder_data(perfherder_data) 141 for framework_name, data in merged_perfherder_data.items(): 142 file_name = ( 143 "perfherder-data-gtest.json" 144 if len(merged_perfherder_data) == 1 145 else f"perfherder-data-gtest-{framework_name}.json" 146 ) 147 out_path = upload_dir / file_name 148 with out_path.open("w", encoding="utf-8") as f: 149 json.dump(data, f) 150 151 log.info("gtest | process wait complete, returncode=%s" % proc.returncode) 152 if mozcrash.check_for_crashes(cwd, symbols_path, test_name="gtest"): 153 # mozcrash will output the log failure line for us. 154 return False 155 if GTests.run_gtest.timed_out: 156 return False 157 result = proc.returncode == 0 158 if not result: 159 log.testFail("gtest | test failed with return code %d", proc.returncode) 160 return result 161 162 def build_core_environment(self, env={}): 163 """ 164 Add environment variables likely to be used across all platforms, including remote systems. 165 """ 166 env["MOZ_XRE_DIR"] = self.xre_path 167 env["MOZ_GMP_PATH"] = os.pathsep.join( 168 os.path.join(self.xre_path, p, "1.0") 169 for p in ("gmp-fake", "gmp-fakeopenh264") 170 ) 171 env["XPCOM_DEBUG_BREAK"] = "stack-and-abort" 172 env["MOZ_CRASHREPORTER_NO_REPORT"] = "1" 173 env["MOZ_CRASHREPORTER"] = "1" 174 env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1" 175 env["MOZ_RUN_GTEST"] = "1" 176 # Normally we run with GTest default output, override this to use the TBPL test format. 177 env["MOZ_TBPL_PARSER"] = "1" 178 179 if not mozinfo.has_sandbox: 180 # Bug 1082193 - This is horrible. Our linux build boxes run CentOS 6, 181 # which is too old to support sandboxing. Disable sandbox for gtests 182 # on machines which don't support sandboxing until they can be 183 # upgraded, or gtests are run on test machines instead. 184 env["MOZ_DISABLE_GMP_SANDBOX"] = "1" 185 186 return env 187 188 def build_environment(self, enable_inc_origin_init, filter_set): 189 """ 190 Create and return a dictionary of all the appropriate env variables 191 and values. On a remote system, we overload this to set different 192 values and are missing things like os.environ and PATH. 193 """ 194 if not os.path.isdir(self.xre_path): 195 raise Exception("xre_path does not exist: %s", self.xre_path) 196 env = dict(os.environ) 197 env = self.build_core_environment(env) 198 env["PERFHERDER_ALERTING_ENABLED"] = "1" 199 pathvar = "" 200 if mozinfo.os == "linux": 201 pathvar = "LD_LIBRARY_PATH" 202 # disable alerts for unstable tests (Bug 1369807) 203 del env["PERFHERDER_ALERTING_ENABLED"] 204 elif mozinfo.os == "mac": 205 pathvar = "DYLD_LIBRARY_PATH" 206 elif mozinfo.os == "win": 207 pathvar = "PATH" 208 if pathvar: 209 if pathvar in env: 210 env[pathvar] = "%s%s%s" % (self.xre_path, os.pathsep, env[pathvar]) 211 else: 212 env[pathvar] = self.xre_path 213 214 symbolizer_path = None 215 if mozinfo.info["asan"]: 216 symbolizer_path = "ASAN_SYMBOLIZER_PATH" 217 elif mozinfo.info["tsan"]: 218 symbolizer_path = "TSAN_SYMBOLIZER_PATH" 219 220 if symbolizer_path is not None: 221 # Use llvm-symbolizer for ASan/TSan if available/required 222 if symbolizer_path in env and os.path.isfile(env[symbolizer_path]): 223 llvmsym = env[symbolizer_path] 224 else: 225 llvmsym = os.path.join( 226 self.xre_path, "llvm-symbolizer" + mozinfo.info["bin_suffix"] 227 ) 228 if os.path.isfile(llvmsym): 229 env[symbolizer_path] = llvmsym 230 log.info("Using LLVM symbolizer at %s", llvmsym) 231 else: 232 # This should be |testFail| instead of |info|. See bug 1050891. 233 log.info("Failed to find LLVM symbolizer at %s", llvmsym) 234 235 # webrender needs gfx.webrender.all=true, gtest doesn't use prefs 236 env["MOZ_WEBRENDER"] = "1" 237 env["MOZ_ACCELERATED"] = "1" 238 239 if enable_inc_origin_init: 240 env["MOZ_ENABLE_INC_ORIGIN_INIT"] = "1" 241 else: 242 env["MOZ_ENABLE_INC_ORIGIN_INIT"] = "0" 243 244 if filter_set is not None: 245 filter_sets_mod_path = os.path.join(HERE, "gtest_filter_sets.py") 246 load_source("gtest_filter_sets", filter_sets_mod_path) 247 248 import gtest_filter_sets 249 250 gtest_filter_for_filter_set = gtest_filter_sets.get(filter_set) 251 if gtest_filter_for_filter_set: 252 env["GTEST_FILTER"] = gtest_filter_for_filter_set 253 log.info("Using gtest filter for %s", filter_set) 254 else: 255 log.info("Failed to get gtest filter for %s", filter_set) 256 257 return env 258 259 260 class gtestOptions(argparse.ArgumentParser): 261 def __init__(self): 262 super().__init__() 263 264 self.add_argument( 265 "--cwd", 266 dest="cwd", 267 default=os.getcwd(), 268 help="absolute path to directory from which to run the binary", 269 ) 270 self.add_argument( 271 "--xre-path", 272 dest="xre_path", 273 default=None, 274 help="absolute path to directory containing XRE (probably xulrunner)", 275 ) 276 self.add_argument( 277 "--symbols-path", 278 dest="symbols_path", 279 default=None, 280 help="absolute path to directory containing breakpad " 281 "symbols, or the URL of a zip file containing " 282 "symbols", 283 ) 284 self.add_argument( 285 "--utility-path", 286 dest="utility_path", 287 default=None, 288 help="path to a directory containing utility program binaries", 289 ) 290 self.add_argument( 291 "--enable-inc-origin-init", 292 action="store_true", 293 dest="enable_inc_origin_init", 294 default=False, 295 help="enabling of incremental origin initialization in Gecko", 296 ) 297 self.add_argument( 298 "--filter-set", 299 dest="filter_set", 300 default=None, 301 help="predefined gtest filter", 302 ) 303 self.add_argument("args", nargs=argparse.REMAINDER) 304 305 306 def update_mozinfo(): 307 """walk up directories to find mozinfo.json update the info""" 308 path = os.path.abspath(os.path.realpath(os.path.dirname(__file__))) 309 dirs = set() 310 while path != os.path.expanduser("~"): 311 if path in dirs: 312 break 313 dirs.add(path) 314 path = os.path.split(path)[0] 315 mozinfo.find_and_update_from_json(*dirs) 316 317 318 def main(): 319 parser = gtestOptions() 320 options = parser.parse_args() 321 args = options.args 322 if not args: 323 print("Usage: %s <binary>" % sys.argv[0]) 324 sys.exit(1) 325 if not options.xre_path: 326 print("Error: --xre-path is required") 327 sys.exit(1) 328 if not options.utility_path: 329 print("Warning: --utility-path is required to process assertion stacks") 330 331 update_mozinfo() 332 prog = os.path.abspath(args[0]) 333 options.xre_path = os.path.abspath(options.xre_path) 334 tester = GTests() 335 try: 336 result = tester.run_gtest( 337 prog, 338 options.xre_path, 339 options.cwd, 340 symbols_path=options.symbols_path, 341 utility_path=options.utility_path, 342 enable_inc_origin_init=options.enable_inc_origin_init, 343 filter_set=options.filter_set, 344 ) 345 except Exception as e: 346 log.error(str(e)) 347 result = False 348 exit_code = 0 if result else 1 349 log.info("rungtests.py exits with code %s" % exit_code) 350 sys.exit(exit_code) 351 352 353 if __name__ == "__main__": 354 main()