remotecppunittests.py (11484B)
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 os 8 import posixpath 9 import subprocess 10 import sys 11 import traceback 12 from zipfile import ZipFile 13 14 import mozcrash 15 import mozfile 16 import mozinfo 17 import mozlog 18 import runcppunittests as cppunittests 19 from mozdevice import ADBDeviceFactory, ADBProcessError, ADBTimeoutError 20 21 try: 22 from mozbuild.base import MozbuildObject 23 24 build_obj = MozbuildObject.from_environment() 25 except ImportError: 26 build_obj = None 27 28 29 class RemoteCPPUnitTests(cppunittests.CPPUnitTests): 30 def __init__(self, options, progs): 31 cppunittests.CPPUnitTests.__init__(self) 32 self.options = options 33 self.device = ADBDeviceFactory( 34 adb=options.adb_path or "adb", 35 device=options.device_serial, 36 test_root=options.remote_test_root, 37 ) 38 self.remote_test_root = posixpath.join(self.device.test_root, "cppunittests") 39 self.remote_bin_dir = posixpath.join(self.remote_test_root, "b") 40 self.remote_tmp_dir = posixpath.join(self.remote_test_root, "tmp") 41 self.remote_home_dir = posixpath.join(self.remote_test_root, "h") 42 if options.setup: 43 self.setup_bin(progs) 44 45 def setup_bin(self, progs): 46 self.device.rm(self.remote_test_root, force=True, recursive=True) 47 self.device.mkdir(self.remote_home_dir, parents=True) 48 self.device.mkdir(self.remote_tmp_dir) 49 self.device.mkdir(self.remote_bin_dir) 50 self.push_libs() 51 self.push_progs(progs) 52 self.device.chmod(self.remote_bin_dir, recursive=True) 53 54 def push_libs(self): 55 if self.options.local_apk: 56 with mozfile.TemporaryDirectory() as tmpdir: 57 apk_contents = ZipFile(self.options.local_apk) 58 59 for info in apk_contents.infolist(): 60 if info.filename.endswith(".so"): 61 print("Pushing %s.." % info.filename, file=sys.stderr) 62 remote_file = posixpath.join( 63 self.remote_bin_dir, os.path.basename(info.filename) 64 ) 65 apk_contents.extract(info, tmpdir) 66 local_file = os.path.join(tmpdir, info.filename) 67 with open(local_file, "rb") as f: 68 # Decompress xz-compressed file. 69 if f.read(5)[1:] == "7zXZ": 70 cmd = ["xz", "-df", "--suffix", ".so", local_file] 71 subprocess.check_output(cmd) 72 # xz strips the ".so" file suffix. 73 os.rename(local_file[:-3], local_file) 74 self.device.push(local_file, remote_file) 75 76 elif self.options.local_lib: 77 for path in os.listdir(self.options.local_lib): 78 if path.endswith(".so"): 79 print(f"Pushing {path}..", file=sys.stderr) 80 remote_file = posixpath.join(self.remote_bin_dir, path) 81 local_file = os.path.join(self.options.local_lib, path) 82 self.device.push(local_file, remote_file) 83 # Additional libraries may be found in a sub-directory such as 84 # "lib/armeabi-v7a" 85 for subdir in ["assets", "lib"]: 86 local_arm_lib = os.path.join(self.options.local_lib, subdir) 87 if os.path.isdir(local_arm_lib): 88 for root, dirs, paths in os.walk(local_arm_lib): 89 for path in paths: 90 if path.endswith(".so"): 91 print(f"Pushing {path}..", file=sys.stderr) 92 remote_file = posixpath.join(self.remote_bin_dir, path) 93 local_file = os.path.join(root, path) 94 self.device.push(local_file, remote_file) 95 96 def push_progs(self, progs): 97 for local_file in progs: 98 remote_file = posixpath.join( 99 self.remote_bin_dir, os.path.basename(local_file) 100 ) 101 self.device.push(local_file, remote_file) 102 103 def build_environment(self): 104 env = self.build_core_environment({}) 105 env["LD_LIBRARY_PATH"] = self.remote_bin_dir 106 env["TMPDIR"] = self.remote_tmp_dir 107 env["HOME"] = self.remote_home_dir 108 env["MOZ_XRE_DIR"] = self.remote_bin_dir 109 if self.options.add_env: 110 for envdef in self.options.add_env: 111 envdef_parts = envdef.split("=", 1) 112 if len(envdef_parts) == 2: 113 env[envdef_parts[0]] = envdef_parts[1] 114 elif len(envdef_parts) == 1: 115 env[envdef_parts[0]] = "" 116 else: 117 self.log.warning("invalid --addEnv option skipped: %s" % envdef) 118 119 return env 120 121 def run_one_test( 122 self, 123 prog, 124 env, 125 symbols_path=None, 126 utility_path=None, 127 interactive=False, 128 timeout_factor=1, 129 ): 130 """ 131 Run a single C++ unit test program remotely. 132 133 Arguments: 134 * prog: The path to the test program to run. 135 * env: The environment to use for running the program. 136 * symbols_path: A path to a directory containing Breakpad-formatted 137 symbol files for producing stack traces on crash. 138 * timeout_factor: An optional test-specific timeout multiplier. 139 140 Return True if the program exits with a zero status, False otherwise. 141 """ 142 basename = os.path.basename(prog) 143 remote_bin = posixpath.join(self.remote_bin_dir, basename) 144 self.log.test_start(basename) 145 test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor 146 147 try: 148 output = self.device.shell_output( 149 remote_bin, env=env, cwd=self.remote_home_dir, timeout=test_timeout 150 ) 151 returncode = 0 152 except ADBTimeoutError: 153 raise 154 except ADBProcessError as e: 155 output = e.adb_process.stdout 156 returncode = e.adb_process.exitcode 157 158 self.log.process_output(basename, "\n%s" % output, command=[remote_bin]) 159 with mozfile.TemporaryDirectory() as tempdir: 160 self.device.pull(self.remote_home_dir, tempdir) 161 if mozcrash.check_for_crashes(tempdir, symbols_path, test_name=basename): 162 self.log.test_end(basename, status="CRASH", expected="PASS") 163 return False 164 result = returncode == 0 165 if not result: 166 self.log.test_end( 167 basename, 168 status="FAIL", 169 expected="PASS", 170 message=("test failed with return code %s" % returncode), 171 ) 172 else: 173 self.log.test_end(basename, status="PASS", expected="PASS") 174 return result 175 176 177 class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions): 178 def __init__(self): 179 cppunittests.CPPUnittestOptions.__init__(self) 180 defaults = {} 181 182 self.add_option( 183 "--deviceSerial", 184 action="store", 185 type="string", 186 dest="device_serial", 187 help="adb serial number of remote device. This is required " 188 "when more than one device is connected to the host. " 189 "Use 'adb devices' to see connected devices.", 190 ) 191 defaults["device_serial"] = None 192 193 self.add_option( 194 "--adbPath", 195 action="store", 196 type="string", 197 dest="adb_path", 198 help="Path to adb binary.", 199 ) 200 defaults["adb_path"] = None 201 202 self.add_option( 203 "--noSetup", 204 action="store_false", 205 dest="setup", 206 help="Do not copy any files to device (to be used only if " 207 "device is already setup).", 208 ) 209 defaults["setup"] = True 210 211 self.add_option( 212 "--localLib", 213 action="store", 214 type="string", 215 dest="local_lib", 216 help="Location of libraries to push -- preferably stripped.", 217 ) 218 defaults["local_lib"] = None 219 220 self.add_option( 221 "--apk", 222 action="store", 223 type="string", 224 dest="local_apk", 225 help="Local path to Firefox for Android APK.", 226 ) 227 defaults["local_apk"] = None 228 229 self.add_option( 230 "--localBinDir", 231 action="store", 232 type="string", 233 dest="local_bin", 234 help="Local path to bin directory.", 235 ) 236 defaults["local_bin"] = build_obj.bindir if build_obj is not None else None 237 238 self.add_option( 239 "--remoteTestRoot", 240 action="store", 241 type="string", 242 dest="remote_test_root", 243 help="Remote directory to use as test root " 244 "(eg. /data/local/tmp/test_root).", 245 ) 246 247 # /data/local/tmp/test_root is used because it is usually not 248 # possible to set +x permissions on binaries on /mnt/sdcard 249 # and since scope storage on Android 10 causes permission 250 # errors on the sdcard. 251 defaults["remote_test_root"] = "/data/local/tmp/test_root" 252 253 self.add_option( 254 "--addEnv", 255 action="append", 256 type="string", 257 dest="add_env", 258 help="additional remote environment variable definitions " 259 '(eg. --addEnv "somevar=something")', 260 ) 261 defaults["add_env"] = None 262 263 self.set_defaults(**defaults) 264 265 266 def run_test_harness(options, args): 267 options.xre_path = os.path.abspath(options.xre_path) 268 cppunittests.update_mozinfo() 269 progs = cppunittests.extract_unittests_from_args( 270 args, mozinfo.info, options.manifest_path 271 ) 272 tester = RemoteCPPUnitTests(options, [item[0] for item in progs]) 273 result = tester.run_tests( 274 progs, 275 options.xre_path, 276 options.symbols_path, 277 ) 278 return result 279 280 281 def main(): 282 parser = RemoteCPPUnittestOptions() 283 mozlog.commandline.add_logging_group(parser) 284 options, args = parser.parse_args() 285 if not args: 286 print( 287 """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0], 288 file=sys.stderr, 289 ) 290 sys.exit(1) 291 if options.local_lib is not None and not os.path.isdir(options.local_lib): 292 print( 293 """Error: --localLib directory %s not found""" % options.local_lib, 294 file=sys.stderr, 295 ) 296 sys.exit(1) 297 if options.local_apk is not None and not os.path.isfile(options.local_apk): 298 print("""Error: --apk file %s not found""" % options.local_apk, file=sys.stderr) 299 sys.exit(1) 300 if not options.xre_path: 301 print("""Error: --xre-path is required""", file=sys.stderr) 302 sys.exit(1) 303 304 log = mozlog.commandline.setup_logging( 305 "remotecppunittests", options, {"tbpl": sys.stdout} 306 ) 307 try: 308 result = run_test_harness(options, args) 309 except Exception as e: 310 log.error(str(e)) 311 traceback.print_exc() 312 result = False 313 sys.exit(0 if result else 1) 314 315 316 if __name__ == "__main__": 317 main()