android.py (10137B)
1 # This Source Code Form is subject to the terms of the Mozilla Public 2 # License, v. 2.0. If a copy of the MPL was not distributed with this 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 """ Drives an android device. 5 """ 6 import os 7 import posixpath 8 import tempfile 9 import contextlib 10 import time 11 import logging 12 13 import attr 14 from arsenic.services import Geckodriver, free_port, subprocess_based_service 15 from mozdevice import ADBDeviceFactory, ADBError 16 17 from condprof.util import write_yml_file, logger, DEFAULT_PREFS, BaseEnv 18 19 20 # XXX most of this code should migrate into mozdevice - see Bug 1574849 21 class AndroidDevice: 22 def __init__( 23 self, 24 app_name, 25 marionette_port=2828, 26 verbose=False, 27 remote_test_root="/sdcard/test_root/", 28 ): 29 self.app_name = app_name 30 31 # XXX make that an option 32 if "fenix" in app_name: 33 self.activity = "org.mozilla.fenix.IntentReceiverActivity" 34 else: 35 self.activity = "org.mozilla.geckoview_example.GeckoViewActivity" 36 self.verbose = verbose 37 self.device = None 38 self.marionette_port = marionette_port 39 self.profile = None 40 self.remote_profile = None 41 self.log_file = None 42 self.remote_test_root = remote_test_root 43 self._adb_fh = None 44 45 def _set_adb_logger(self, log_file): 46 self.log_file = log_file 47 if self.log_file is None: 48 return 49 logger.info("Setting ADB log file to %s" % self.log_file) 50 adb_logger = logging.getLogger("adb") 51 adb_logger.setLevel(logging.DEBUG) 52 self._adb_fh = logging.FileHandler(self.log_file) 53 self._adb_fh.setLevel(logging.DEBUG) 54 adb_logger.addHandler(self._adb_fh) 55 56 def _unset_adb_logger(self): 57 if self._adb_fh is None: 58 return 59 logging.getLogger("adb").removeHandler(self._adb_fh) 60 self._adb_fh = None 61 62 def clear_logcat(self, timeout=None, buffers=[]): 63 if not self.device: 64 return 65 self.device.clear_logcat(timeout, buffers) 66 67 def get_logcat(self): 68 if not self.device: 69 return None 70 # we don't want to have ADBCommand dump the command 71 # in the debug stream so we reduce its verbosity here 72 # temporarely 73 old_verbose = self.device._verbose 74 self.device._verbose = False 75 try: 76 return self.device.get_logcat() 77 finally: 78 self.device._verbose = old_verbose 79 80 def prepare(self, profile, logfile): 81 self._set_adb_logger(logfile) 82 try: 83 # See android_emulator_pgo.py run_tests for more 84 # details on why test_root must be /sdcard/test_root 85 # for android pgo due to Android 4.3. 86 self.device = ADBDeviceFactory( 87 verbose=self.verbose, logger_name="adb", test_root=self.remote_test_root 88 ) 89 except Exception: 90 logger.error("Cannot initialize device") 91 raise 92 device = self.device 93 self.profile = profile 94 95 # checking that the app is installed 96 if not device.is_app_installed(self.app_name): 97 raise Exception("%s is not installed" % self.app_name) 98 99 # debug flag 100 logger.info("Setting %s as the debug app on the phone" % self.app_name) 101 device.shell( 102 "am set-debug-app --persistent %s" % self.app_name, 103 stdout_callback=logger.info, 104 ) 105 106 # creating the profile on the device 107 logger.info("Creating the profile on the device") 108 109 remote_profile = posixpath.join(self.remote_test_root, "profile") 110 logger.info("The profile on the phone will be at %s" % remote_profile) 111 112 device.rm(remote_profile, force=True, recursive=True) 113 logger.info("Pushing %s on the phone" % self.profile) 114 device.push(profile, remote_profile) 115 device.chmod(remote_profile, recursive=True) 116 self.profile = profile 117 self.remote_profile = remote_profile 118 119 # creating the yml file 120 yml_data = { 121 "args": ["-marionette", "-profile", self.remote_profile], 122 "prefs": DEFAULT_PREFS, 123 "env": {"LOG_VERBOSE": 1, "R_LOG_LEVEL": 6, "MOZ_LOG": ""}, 124 } 125 126 yml_name = "%s-geckoview-config.yaml" % self.app_name 127 yml_on_host = posixpath.join(tempfile.mkdtemp(), yml_name) 128 write_yml_file(yml_on_host, yml_data) 129 tmp_on_device = posixpath.join("/data", "local", "tmp") 130 if not device.exists(tmp_on_device): 131 raise IOError("%s does not exists on the device" % tmp_on_device) 132 yml_on_device = posixpath.join(tmp_on_device, yml_name) 133 try: 134 device.rm(yml_on_device, force=True, recursive=True) 135 device.push(yml_on_host, yml_on_device) 136 device.chmod(yml_on_device, recursive=True) 137 except Exception: 138 logger.info("could not create the yaml file on device. Permission issue?") 139 raise 140 141 # command line 'extra' args not used with geckoview apps; instead we use 142 # an on-device config.yml file 143 intent = "android.intent.action.VIEW" 144 device.stop_application(self.app_name) 145 device.launch_application( 146 self.app_name, self.activity, intent, extras=None, url="about:blank" 147 ) 148 if not device.process_exist(self.app_name): 149 raise Exception("Could not start %s" % self.app_name) 150 151 logger.info("Creating socket forwarding on port %d" % self.marionette_port) 152 device.forward( 153 local="tcp:%d" % self.marionette_port, 154 remote="tcp:%d" % self.marionette_port, 155 ) 156 157 # we don't have a clean way for now to check that GV or Fenix 158 # is ready to handle our tests. So here we just wait 30s 159 logger.info("Sleeping for 30s") 160 time.sleep(30) 161 162 def stop_browser(self): 163 logger.info("Stopping %s" % self.app_name) 164 try: 165 self.device.stop_application(self.app_name) 166 except ADBError: 167 logger.info("Could not stop the application using force-stop") 168 169 time.sleep(5) 170 if self.device.process_exist(self.app_name): 171 logger.info("%s still running, trying SIGKILL" % self.app_name) 172 num_tries = 0 173 while self.device.process_exist(self.app_name) and num_tries < 5: 174 try: 175 self.device.pkill(self.app_name) 176 except ADBError: 177 pass 178 num_tries += 1 179 time.sleep(1) 180 logger.info("%s stopped" % self.app_name) 181 182 def collect_profile(self): 183 logger.info("Collecting profile from %s" % self.remote_profile) 184 self.device.pull(self.remote_profile, self.profile) 185 186 def close(self): 187 self._unset_adb_logger() 188 if self.device is None: 189 return 190 try: 191 self.device.remove_forwards("tcp:%d" % self.marionette_port) 192 except ADBError: 193 logger.warning("Could not remove forward port") 194 195 196 # XXX redundant, remove 197 @contextlib.contextmanager 198 def device( 199 app_name, marionette_port=2828, verbose=True, remote_test_root="/sdcard/test_root/" 200 ): 201 device_ = AndroidDevice( 202 app_name, marionette_port, verbose, remote_test_root=remote_test_root 203 ) 204 try: 205 yield device_ 206 finally: 207 device_.close() 208 209 210 @attr.s 211 class AndroidGeckodriver(Geckodriver): 212 async def start(self): 213 port = free_port() 214 await self._check_version() 215 logger.info("Running Webdriver on port %d" % port) 216 logger.info("Running Marionette on port 2828") 217 pargs = [ 218 self.binary, 219 "--log", 220 "trace", 221 "--port", 222 str(port), 223 "--marionette-port", 224 "2828", 225 ] 226 logger.info("Connecting on Android device") 227 pargs.append("--connect-existing") 228 return await subprocess_based_service( 229 pargs, f"http://localhost:{port}", self.log_file 230 ) 231 232 233 class AndroidEnv(BaseEnv): 234 @contextlib.contextmanager 235 def get_device(self, *args, **kw): 236 with device(self.firefox, *args, **kw) as d: 237 self.device = d 238 yield self.device 239 240 def get_target_platform(self): 241 app = self.firefox.split("org.mozilla.")[-1] 242 if self.device_name is None: 243 return app 244 return "%s-%s" % (self.device_name, app) 245 246 def dump_logs(self): 247 logger.info("Dumping Android logs") 248 try: 249 logcat = self.device.get_logcat() 250 if logcat: 251 # local path, not using posixpath 252 logfile = os.path.join(self.archive, "logcat.log") 253 logger.info("Writing logcat at %s" % logfile) 254 with open(logfile, "wb") as f: 255 for line in logcat: 256 f.write(line.encode("utf8", errors="replace") + b"\n") 257 else: 258 logger.info("logcat came back empty") 259 except Exception: 260 logger.error("Could not extract the logcat", exc_info=True) 261 262 @contextlib.contextmanager 263 def get_browser(self): 264 yield 265 266 def get_browser_args(self, headless, prefs=None): 267 # XXX merge with DesktopEnv.get_browser_args 268 options = ["--allow-downgrade"] 269 if headless: 270 options.append("-headless") 271 if prefs is None: 272 prefs = {} 273 return {"moz:firefoxOptions": {"args": options, "prefs": prefs}} 274 275 def prepare(self, logfile): 276 self.device.prepare(self.profile, logfile) 277 278 def get_browser_version(self): 279 return self.target_platform + "-XXXneedtograbversion" 280 281 def get_geckodriver(self, log_file): 282 return AndroidGeckodriver(binary=self.geckodriver, log_file=log_file) 283 284 def check_session(self, session): 285 async def fake_close(*args): 286 pass 287 288 session.close = fake_close 289 290 def collect_profile(self): 291 self.device.collect_profile() 292 293 def stop_browser(self): 294 self.device.stop_browser()