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 8import os 9import posixpath 10import sys 11import subprocess 12import traceback 13from zipfile import ZipFile 14import runcppunittests as cppunittests 15import mozcrash 16import mozfile 17import mozinfo 18import mozlog 19from mozdevice import ADBDeviceFactory, ADBProcessError, ADBTimeoutError 20 21try: 22 from mozbuild.base import MozbuildObject 23 24 build_obj = MozbuildObject.from_environment() 25except ImportError: 26 build_obj = None 27 28 29class 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("Pushing {}..".format(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("Pushing {}..".format(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, enable_webrender=False): 104 env = self.build_core_environment({}, enable_webrender) 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, prog, env, symbols_path=None, interactive=False, timeout_factor=1 123 ): 124 """ 125 Run a single C++ unit test program remotely. 126 127 Arguments: 128 * prog: The path to the test program to run. 129 * env: The environment to use for running the program. 130 * symbols_path: A path to a directory containing Breakpad-formatted 131 symbol files for producing stack traces on crash. 132 * timeout_factor: An optional test-specific timeout multiplier. 133 134 Return True if the program exits with a zero status, False otherwise. 135 """ 136 basename = os.path.basename(prog) 137 remote_bin = posixpath.join(self.remote_bin_dir, basename) 138 self.log.test_start(basename) 139 test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor 140 141 try: 142 output = self.device.shell_output( 143 remote_bin, env=env, cwd=self.remote_home_dir, timeout=test_timeout 144 ) 145 returncode = 0 146 except ADBTimeoutError: 147 raise 148 except ADBProcessError as e: 149 output = e.adb_process.stdout 150 returncode = e.adb_process.exitcode 151 152 self.log.process_output(basename, "\n%s" % output, command=[remote_bin]) 153 with mozfile.TemporaryDirectory() as tempdir: 154 self.device.pull(self.remote_home_dir, tempdir) 155 if mozcrash.check_for_crashes(tempdir, symbols_path, test_name=basename): 156 self.log.test_end(basename, status="CRASH", expected="PASS") 157 return False 158 result = returncode == 0 159 if not result: 160 self.log.test_end( 161 basename, 162 status="FAIL", 163 expected="PASS", 164 message=("test failed with return code %s" % returncode), 165 ) 166 else: 167 self.log.test_end(basename, status="PASS", expected="PASS") 168 return result 169 170 171class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions): 172 def __init__(self): 173 cppunittests.CPPUnittestOptions.__init__(self) 174 defaults = {} 175 176 self.add_option( 177 "--deviceSerial", 178 action="store", 179 type="string", 180 dest="device_serial", 181 help="adb serial number of remote device. This is required " 182 "when more than one device is connected to the host. " 183 "Use 'adb devices' to see connected devices.", 184 ) 185 defaults["device_serial"] = None 186 187 self.add_option( 188 "--adbPath", 189 action="store", 190 type="string", 191 dest="adb_path", 192 help="Path to adb binary.", 193 ) 194 defaults["adb_path"] = None 195 196 self.add_option( 197 "--noSetup", 198 action="store_false", 199 dest="setup", 200 help="Do not copy any files to device (to be used only if " 201 "device is already setup).", 202 ) 203 defaults["setup"] = True 204 205 self.add_option( 206 "--localLib", 207 action="store", 208 type="string", 209 dest="local_lib", 210 help="Location of libraries to push -- preferably stripped.", 211 ) 212 defaults["local_lib"] = None 213 214 self.add_option( 215 "--apk", 216 action="store", 217 type="string", 218 dest="local_apk", 219 help="Local path to Firefox for Android APK.", 220 ) 221 defaults["local_apk"] = None 222 223 self.add_option( 224 "--localBinDir", 225 action="store", 226 type="string", 227 dest="local_bin", 228 help="Local path to bin directory.", 229 ) 230 defaults["local_bin"] = build_obj.bindir if build_obj is not None else None 231 232 self.add_option( 233 "--remoteTestRoot", 234 action="store", 235 type="string", 236 dest="remote_test_root", 237 help="Remote directory to use as test root " 238 "(eg. /data/local/tmp/test_root).", 239 ) 240 241 # /data/local/tmp/test_root is used because it is usually not 242 # possible to set +x permissions on binaries on /mnt/sdcard 243 # and since scope storage on Android 10 causes permission 244 # errors on the sdcard. 245 defaults["remote_test_root"] = "/data/local/tmp/test_root" 246 247 self.add_option( 248 "--addEnv", 249 action="append", 250 type="string", 251 dest="add_env", 252 help="additional remote environment variable definitions " 253 '(eg. --addEnv "somevar=something")', 254 ) 255 defaults["add_env"] = None 256 257 self.set_defaults(**defaults) 258 259 260def run_test_harness(options, args): 261 options.xre_path = os.path.abspath(options.xre_path) 262 cppunittests.update_mozinfo() 263 progs = cppunittests.extract_unittests_from_args( 264 args, mozinfo.info, options.manifest_path 265 ) 266 tester = RemoteCPPUnitTests(options, [item[0] for item in progs]) 267 result = tester.run_tests( 268 progs, 269 options.xre_path, 270 options.symbols_path, 271 enable_webrender=options.enable_webrender, 272 ) 273 return result 274 275 276def main(): 277 parser = RemoteCPPUnittestOptions() 278 mozlog.commandline.add_logging_group(parser) 279 options, args = parser.parse_args() 280 if not args: 281 print( 282 """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0], 283 file=sys.stderr, 284 ) 285 sys.exit(1) 286 if options.local_lib is not None and not os.path.isdir(options.local_lib): 287 print( 288 """Error: --localLib directory %s not found""" % options.local_lib, 289 file=sys.stderr, 290 ) 291 sys.exit(1) 292 if options.local_apk is not None and not os.path.isfile(options.local_apk): 293 print("""Error: --apk file %s not found""" % options.local_apk, file=sys.stderr) 294 sys.exit(1) 295 if not options.xre_path: 296 print("""Error: --xre-path is required""", file=sys.stderr) 297 sys.exit(1) 298 299 log = mozlog.commandline.setup_logging( 300 "remotecppunittests", options, {"tbpl": sys.stdout} 301 ) 302 try: 303 result = run_test_harness(options, args) 304 except Exception as e: 305 log.error(str(e)) 306 traceback.print_exc() 307 result = False 308 sys.exit(0 if result else 1) 309 310 311if __name__ == "__main__": 312 main() 313