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