1#!/usr/bin/env python3 2# 3# Copyright 2016 - The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import base64 18import concurrent.futures 19import datetime 20import functools 21import json 22import logging 23import os 24import random 25import re 26import signal 27import string 28import subprocess 29import time 30import traceback 31import zipfile 32from concurrent.futures import ThreadPoolExecutor 33 34from acts import signals 35from acts.controllers import adb 36from acts.libs.proc import job 37 38# File name length is limited to 255 chars on some OS, so we need to make sure 39# the file names we output fits within the limit. 40MAX_FILENAME_LEN = 255 41 42 43class ActsUtilsError(Exception): 44 """Generic error raised for exceptions in ACTS utils.""" 45 46 47class NexusModelNames: 48 # TODO(angli): This will be fixed later by angli. 49 ONE = 'sprout' 50 N5 = 'hammerhead' 51 N5v2 = 'bullhead' 52 N6 = 'shamu' 53 N6v2 = 'angler' 54 N6v3 = 'marlin' 55 N5v3 = 'sailfish' 56 57 58class DozeModeStatus: 59 ACTIVE = "ACTIVE" 60 IDLE = "IDLE" 61 62 63ascii_letters_and_digits = string.ascii_letters + string.digits 64valid_filename_chars = "-_." + ascii_letters_and_digits 65 66models = ("sprout", "occam", "hammerhead", "bullhead", "razor", "razorg", 67 "shamu", "angler", "volantis", "volantisg", "mantaray", "fugu", 68 "ryu", "marlin", "sailfish") 69 70manufacture_name_to_model = { 71 "flo": "razor", 72 "flo_lte": "razorg", 73 "flounder": "volantis", 74 "flounder_lte": "volantisg", 75 "dragon": "ryu" 76} 77 78GMT_to_olson = { 79 "GMT-9": "America/Anchorage", 80 "GMT-8": "US/Pacific", 81 "GMT-7": "US/Mountain", 82 "GMT-6": "US/Central", 83 "GMT-5": "US/Eastern", 84 "GMT-4": "America/Barbados", 85 "GMT-3": "America/Buenos_Aires", 86 "GMT-2": "Atlantic/South_Georgia", 87 "GMT-1": "Atlantic/Azores", 88 "GMT+0": "Africa/Casablanca", 89 "GMT+1": "Europe/Amsterdam", 90 "GMT+2": "Europe/Athens", 91 "GMT+3": "Europe/Moscow", 92 "GMT+4": "Asia/Baku", 93 "GMT+5": "Asia/Oral", 94 "GMT+6": "Asia/Almaty", 95 "GMT+7": "Asia/Bangkok", 96 "GMT+8": "Asia/Hong_Kong", 97 "GMT+9": "Asia/Tokyo", 98 "GMT+10": "Pacific/Guam", 99 "GMT+11": "Pacific/Noumea", 100 "GMT+12": "Pacific/Fiji", 101 "GMT+13": "Pacific/Tongatapu", 102 "GMT-11": "Pacific/Midway", 103 "GMT-10": "Pacific/Honolulu" 104} 105 106 107def abs_path(path): 108 """Resolve the '.' and '~' in a path to get the absolute path. 109 110 Args: 111 path: The path to expand. 112 113 Returns: 114 The absolute path of the input path. 115 """ 116 return os.path.abspath(os.path.expanduser(path)) 117 118 119def create_dir(path): 120 """Creates a directory if it does not exist already. 121 122 Args: 123 path: The path of the directory to create. 124 """ 125 os.makedirs(path, exist_ok=True) 126 127 128def get_current_epoch_time(): 129 """Current epoch time in milliseconds. 130 131 Returns: 132 An integer representing the current epoch time in milliseconds. 133 """ 134 return int(round(time.time() * 1000)) 135 136 137def get_current_human_time(): 138 """Returns the current time in human readable format. 139 140 Returns: 141 The current time stamp in Month-Day-Year Hour:Min:Sec format. 142 """ 143 return time.strftime("%m-%d-%Y %H:%M:%S ") 144 145 146def epoch_to_human_time(epoch_time): 147 """Converts an epoch timestamp to human readable time. 148 149 This essentially converts an output of get_current_epoch_time to an output 150 of get_current_human_time 151 152 Args: 153 epoch_time: An integer representing an epoch timestamp in milliseconds. 154 155 Returns: 156 A time string representing the input time. 157 None if input param is invalid. 158 """ 159 if isinstance(epoch_time, int): 160 try: 161 d = datetime.datetime.fromtimestamp(epoch_time / 1000) 162 return d.strftime("%m-%d-%Y %H:%M:%S ") 163 except ValueError: 164 return None 165 166 167def get_timezone_olson_id(): 168 """Return the Olson ID of the local (non-DST) timezone. 169 170 Returns: 171 A string representing one of the Olson IDs of the local (non-DST) 172 timezone. 173 """ 174 tzoffset = int(time.timezone / 3600) 175 gmt = None 176 if tzoffset <= 0: 177 gmt = "GMT+{}".format(-tzoffset) 178 else: 179 gmt = "GMT-{}".format(tzoffset) 180 return GMT_to_olson[gmt] 181 182 183def find_files(paths, file_predicate): 184 """Locate files whose names and extensions match the given predicate in 185 the specified directories. 186 187 Args: 188 paths: A list of directory paths where to find the files. 189 file_predicate: A function that returns True if the file name and 190 extension are desired. 191 192 Returns: 193 A list of files that match the predicate. 194 """ 195 file_list = [] 196 if not isinstance(paths, list): 197 paths = [paths] 198 for path in paths: 199 p = abs_path(path) 200 for dirPath, subdirList, fileList in os.walk(p): 201 for fname in fileList: 202 name, ext = os.path.splitext(fname) 203 if file_predicate(name, ext): 204 file_list.append((dirPath, name, ext)) 205 return file_list 206 207 208def load_config(file_full_path, log_errors=True): 209 """Loads a JSON config file. 210 211 Returns: 212 A JSON object. 213 """ 214 with open(file_full_path, 'r') as f: 215 try: 216 return json.load(f) 217 except Exception as e: 218 if log_errors: 219 logging.error("Exception error to load %s: %s", f, e) 220 raise 221 222 223def load_file_to_base64_str(f_path): 224 """Loads the content of a file into a base64 string. 225 226 Args: 227 f_path: full path to the file including the file name. 228 229 Returns: 230 A base64 string representing the content of the file in utf-8 encoding. 231 """ 232 path = abs_path(f_path) 233 with open(path, 'rb') as f: 234 f_bytes = f.read() 235 base64_str = base64.b64encode(f_bytes).decode("utf-8") 236 return base64_str 237 238 239def dump_string_to_file(content, file_path, mode='w'): 240 """ Dump content of a string to 241 242 Args: 243 content: content to be dumped to file 244 file_path: full path to the file including the file name. 245 mode: file open mode, 'w' (truncating file) by default 246 :return: 247 """ 248 full_path = abs_path(file_path) 249 with open(full_path, mode) as f: 250 f.write(content) 251 252 253def list_of_dict_to_dict_of_dict(list_of_dicts, dict_key): 254 """Transforms a list of dicts to a dict of dicts. 255 256 For instance: 257 >>> list_of_dict_to_dict_of_dict([{'a': '1', 'b':'2'}, 258 >>> {'a': '3', 'b':'4'}], 259 >>> 'b') 260 261 returns: 262 263 >>> {'2': {'a': '1', 'b':'2'}, 264 >>> '4': {'a': '3', 'b':'4'}} 265 266 Args: 267 list_of_dicts: A list of dictionaries. 268 dict_key: The key in the inner dict to be used as the key for the 269 outer dict. 270 Returns: 271 A dict of dicts. 272 """ 273 return {d[dict_key]: d for d in list_of_dicts} 274 275 276def dict_purge_key_if_value_is_none(dictionary): 277 """Removes all pairs with value None from dictionary.""" 278 for k, v in dict(dictionary).items(): 279 if v is None: 280 del dictionary[k] 281 return dictionary 282 283 284def find_field(item_list, cond, comparator, target_field): 285 """Finds the value of a field in a dict object that satisfies certain 286 conditions. 287 288 Args: 289 item_list: A list of dict objects. 290 cond: A param that defines the condition. 291 comparator: A function that checks if an dict satisfies the condition. 292 target_field: Name of the field whose value to be returned if an item 293 satisfies the condition. 294 295 Returns: 296 Target value or None if no item satisfies the condition. 297 """ 298 for item in item_list: 299 if comparator(item, cond) and target_field in item: 300 return item[target_field] 301 return None 302 303 304def rand_ascii_str(length): 305 """Generates a random string of specified length, composed of ascii letters 306 and digits. 307 308 Args: 309 length: The number of characters in the string. 310 311 Returns: 312 The random string generated. 313 """ 314 letters = [random.choice(ascii_letters_and_digits) for i in range(length)] 315 return ''.join(letters) 316 317 318def rand_hex_str(length): 319 """Generates a random string of specified length, composed of hex digits 320 321 Args: 322 length: The number of characters in the string. 323 324 Returns: 325 The random string generated. 326 """ 327 letters = [random.choice(string.hexdigits) for i in range(length)] 328 return ''.join(letters) 329 330 331# Thead/Process related functions. 332def concurrent_exec(func, param_list): 333 """Executes a function with different parameters pseudo-concurrently. 334 335 This is basically a map function. Each element (should be an iterable) in 336 the param_list is unpacked and passed into the function. Due to Python's 337 GIL, there's no true concurrency. This is suited for IO-bound tasks. 338 339 Args: 340 func: The function that parforms a task. 341 param_list: A list of iterables, each being a set of params to be 342 passed into the function. 343 344 Returns: 345 A list of return values from each function execution. If an execution 346 caused an exception, the exception object will be the corresponding 347 result. 348 """ 349 with concurrent.futures.ThreadPoolExecutor(max_workers=30) as executor: 350 # Start the load operations and mark each future with its params 351 future_to_params = {executor.submit(func, *p): p for p in param_list} 352 return_vals = [] 353 for future in concurrent.futures.as_completed(future_to_params): 354 params = future_to_params[future] 355 try: 356 return_vals.append(future.result()) 357 except Exception as exc: 358 print("{} generated an exception: {}".format( 359 params, traceback.format_exc())) 360 return_vals.append(exc) 361 return return_vals 362 363 364def exe_cmd(*cmds): 365 """Executes commands in a new shell. 366 367 Args: 368 cmds: A sequence of commands and arguments. 369 370 Returns: 371 The output of the command run. 372 373 Raises: 374 OSError is raised if an error occurred during the command execution. 375 """ 376 cmd = ' '.join(cmds) 377 proc = subprocess.Popen( 378 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) 379 (out, err) = proc.communicate() 380 if not err: 381 return out 382 raise OSError(err) 383 384 385def require_sl4a(android_devices): 386 """Makes sure sl4a connection is established on the given AndroidDevice 387 objects. 388 389 Args: 390 android_devices: A list of AndroidDevice objects. 391 392 Raises: 393 AssertionError is raised if any given android device does not have SL4A 394 connection established. 395 """ 396 for ad in android_devices: 397 msg = "SL4A connection not established properly on %s." % ad.serial 398 assert ad.droid, msg 399 400 401def _assert_subprocess_running(proc): 402 """Checks if a subprocess has terminated on its own. 403 404 Args: 405 proc: A subprocess returned by subprocess.Popen. 406 407 Raises: 408 ActsUtilsError is raised if the subprocess has stopped. 409 """ 410 ret = proc.poll() 411 if ret is not None: 412 out, err = proc.communicate() 413 raise ActsUtilsError("Process %d has terminated. ret: %d, stderr: %s," 414 " stdout: %s" % (proc.pid, ret, err, out)) 415 416 417def start_standing_subprocess(cmd, check_health_delay=0, shell=True): 418 """Starts a long-running subprocess. 419 420 This is not a blocking call and the subprocess started by it should be 421 explicitly terminated with stop_standing_subprocess. 422 423 For short-running commands, you should use exe_cmd, which blocks. 424 425 You can specify a health check after the subprocess is started to make sure 426 it did not stop prematurely. 427 428 Args: 429 cmd: string, the command to start the subprocess with. 430 check_health_delay: float, the number of seconds to wait after the 431 subprocess starts to check its health. Default is 0, 432 which means no check. 433 434 Returns: 435 The subprocess that got started. 436 """ 437 proc = subprocess.Popen( 438 cmd, 439 stdout=subprocess.PIPE, 440 stderr=subprocess.PIPE, 441 shell=shell, 442 preexec_fn=os.setpgrp) 443 logging.debug("Start standing subprocess with cmd: %s", cmd) 444 if check_health_delay > 0: 445 time.sleep(check_health_delay) 446 _assert_subprocess_running(proc) 447 return proc 448 449 450def stop_standing_subprocess(proc, kill_signal=signal.SIGTERM): 451 """Stops a subprocess started by start_standing_subprocess. 452 453 Before killing the process, we check if the process is running, if it has 454 terminated, ActsUtilsError is raised. 455 456 Catches and ignores the PermissionError which only happens on Macs. 457 458 Args: 459 proc: Subprocess to terminate. 460 """ 461 pid = proc.pid 462 logging.debug("Stop standing subprocess %d", pid) 463 _assert_subprocess_running(proc) 464 try: 465 os.killpg(pid, kill_signal) 466 except PermissionError: 467 pass 468 469 470def wait_for_standing_subprocess(proc, timeout=None): 471 """Waits for a subprocess started by start_standing_subprocess to finish 472 or times out. 473 474 Propagates the exception raised by the subprocess.wait(.) function. 475 The subprocess.TimeoutExpired exception is raised if the process timed-out 476 rather then terminating. 477 478 If no exception is raised: the subprocess terminated on its own. No need 479 to call stop_standing_subprocess() to kill it. 480 481 If an exception is raised: the subprocess is still alive - it did not 482 terminate. Either call stop_standing_subprocess() to kill it, or call 483 wait_for_standing_subprocess() to keep waiting for it to terminate on its 484 own. 485 486 Args: 487 p: Subprocess to wait for. 488 timeout: An integer number of seconds to wait before timing out. 489 """ 490 proc.wait(timeout) 491 492 493def sync_device_time(ad): 494 """Sync the time of an android device with the current system time. 495 496 Both epoch time and the timezone will be synced. 497 498 Args: 499 ad: The android device to sync time on. 500 """ 501 ad.adb.shell("settings global put auto_time 0", ignore_status=True) 502 ad.adb.shell("settings global put auto_time_zone 0", ignore_status=True) 503 droid = ad.droid 504 droid.setTimeZone(get_timezone_olson_id()) 505 droid.setTime(get_current_epoch_time()) 506 507 508# Timeout decorator block 509class TimeoutError(Exception): 510 """Exception for timeout decorator related errors. 511 """ 512 pass 513 514 515def _timeout_handler(signum, frame): 516 """Handler function used by signal to terminate a timed out function. 517 """ 518 raise TimeoutError() 519 520 521def timeout(sec): 522 """A decorator used to add time out check to a function. 523 524 This only works in main thread due to its dependency on signal module. 525 Do NOT use it if the decorated funtion does not run in the Main thread. 526 527 Args: 528 sec: Number of seconds to wait before the function times out. 529 No timeout if set to 0 530 531 Returns: 532 What the decorated function returns. 533 534 Raises: 535 TimeoutError is raised when time out happens. 536 """ 537 538 def decorator(func): 539 @functools.wraps(func) 540 def wrapper(*args, **kwargs): 541 if sec: 542 signal.signal(signal.SIGALRM, _timeout_handler) 543 signal.alarm(sec) 544 try: 545 return func(*args, **kwargs) 546 except TimeoutError: 547 raise TimeoutError(("Function {} timed out after {} " 548 "seconds.").format(func.__name__, sec)) 549 finally: 550 signal.alarm(0) 551 552 return wrapper 553 554 return decorator 555 556 557def trim_model_name(model): 558 """Trim any prefix and postfix and return the android designation of the 559 model name. 560 561 e.g. "m_shamu" will be trimmed to "shamu". 562 563 Args: 564 model: model name to be trimmed. 565 566 Returns 567 Trimmed model name if one of the known model names is found. 568 None otherwise. 569 """ 570 # Directly look up first. 571 if model in models: 572 return model 573 if model in manufacture_name_to_model: 574 return manufacture_name_to_model[model] 575 # If not found, try trimming off prefix/postfix and look up again. 576 tokens = re.split("_|-", model) 577 for t in tokens: 578 if t in models: 579 return t 580 if t in manufacture_name_to_model: 581 return manufacture_name_to_model[t] 582 return None 583 584 585def force_airplane_mode(ad, new_state, timeout_value=60): 586 """Force the device to set airplane mode on or off by adb shell command. 587 588 Args: 589 ad: android device object. 590 new_state: Turn on airplane mode if True. 591 Turn off airplane mode if False. 592 timeout_value: max wait time for 'adb wait-for-device' 593 594 Returns: 595 True if success. 596 False if timeout. 597 """ 598 599 # Using timeout decorator. 600 # Wait for device with timeout. If after <timeout_value> seconds, adb 601 # is still waiting for device, throw TimeoutError exception. 602 @timeout(timeout_value) 603 def wait_for_device_with_timeout(ad): 604 ad.adb.wait_for_device() 605 606 try: 607 wait_for_device_with_timeout(ad) 608 ad.adb.shell("settings put global airplane_mode_on {}".format( 609 1 if new_state else 0)) 610 ad.adb.shell("am broadcast -a android.intent.action.AIRPLANE_MODE") 611 except TimeoutError: 612 # adb wait for device timeout 613 return False 614 return True 615 616 617def get_device_usb_charging_status(ad): 618 """ Returns the usb charging status of the device. 619 620 Args: 621 ad: android device object 622 623 Returns: 624 True if charging 625 False if not charging 626 """ 627 adb_shell_result = ad.adb.shell("dumpsys deviceidle get charging") 628 ad.log.info("Device Charging State: {}".format(adb_shell_result)) 629 return adb_shell_result == 'true' 630 631 632def disable_usb_charging(ad): 633 """ Unplug device from usb charging. 634 635 Args: 636 ad: android device object 637 638 Returns: 639 True if device is unplugged 640 False otherwise 641 """ 642 ad.adb.shell("dumpsys battery unplug") 643 if not get_device_usb_charging_status(ad): 644 return True 645 else: 646 ad.log.info("Could not disable USB charging") 647 return False 648 649 650def enable_usb_charging(ad): 651 """ Plug device to usb charging. 652 653 Args: 654 ad: android device object 655 656 Returns: 657 True if device is Plugged 658 False otherwise 659 """ 660 ad.adb.shell("dumpsys battery reset") 661 if get_device_usb_charging_status(ad): 662 return True 663 else: 664 ad.log.info("Could not enable USB charging") 665 return False 666 667 668def enable_doze(ad): 669 """Force the device into doze mode. 670 671 Args: 672 ad: android device object. 673 674 Returns: 675 True if device is in doze mode. 676 False otherwise. 677 """ 678 ad.adb.shell("dumpsys battery unplug") 679 ad.adb.shell("dumpsys deviceidle enable") 680 ad.adb.shell("dumpsys deviceidle force-idle") 681 ad.droid.goToSleepNow() 682 time.sleep(5) 683 adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep") 684 if not adb_shell_result.startswith(DozeModeStatus.IDLE): 685 info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result)) 686 print(info) 687 return False 688 return True 689 690 691def disable_doze(ad): 692 """Force the device not in doze mode. 693 694 Args: 695 ad: android device object. 696 697 Returns: 698 True if device is not in doze mode. 699 False otherwise. 700 """ 701 ad.adb.shell("dumpsys deviceidle disable") 702 ad.adb.shell("dumpsys battery reset") 703 adb_shell_result = ad.adb.shell("dumpsys deviceidle get deep") 704 if not adb_shell_result.startswith(DozeModeStatus.ACTIVE): 705 info = ("dumpsys deviceidle get deep: {}".format(adb_shell_result)) 706 print(info) 707 return False 708 return True 709 710 711def enable_doze_light(ad): 712 """Force the device into doze light mode. 713 714 Args: 715 ad: android device object. 716 717 Returns: 718 True if device is in doze light mode. 719 False otherwise. 720 """ 721 ad.adb.shell("dumpsys battery unplug") 722 ad.droid.goToSleepNow() 723 time.sleep(5) 724 ad.adb.shell("cmd deviceidle enable light") 725 ad.adb.shell("cmd deviceidle step light") 726 adb_shell_result = ad.adb.shell("dumpsys deviceidle get light") 727 if not adb_shell_result.startswith(DozeModeStatus.IDLE): 728 info = ("dumpsys deviceidle get light: {}".format(adb_shell_result)) 729 print(info) 730 return False 731 return True 732 733 734def disable_doze_light(ad): 735 """Force the device not in doze light mode. 736 737 Args: 738 ad: android device object. 739 740 Returns: 741 True if device is not in doze light mode. 742 False otherwise. 743 """ 744 ad.adb.shell("dumpsys battery reset") 745 ad.adb.shell("cmd deviceidle disable light") 746 adb_shell_result = ad.adb.shell("dumpsys deviceidle get light") 747 if not adb_shell_result.startswith(DozeModeStatus.ACTIVE): 748 info = ("dumpsys deviceidle get light: {}".format(adb_shell_result)) 749 print(info) 750 return False 751 return True 752 753 754def set_ambient_display(ad, new_state): 755 """Set "Ambient Display" in Settings->Display 756 757 Args: 758 ad: android device object. 759 new_state: new state for "Ambient Display". True or False. 760 """ 761 ad.adb.shell( 762 "settings put secure doze_enabled {}".format(1 if new_state else 0)) 763 764 765def set_adaptive_brightness(ad, new_state): 766 """Set "Adaptive Brightness" in Settings->Display 767 768 Args: 769 ad: android device object. 770 new_state: new state for "Adaptive Brightness". True or False. 771 """ 772 ad.adb.shell("settings put system screen_brightness_mode {}".format( 773 1 if new_state else 0)) 774 775 776def set_auto_rotate(ad, new_state): 777 """Set "Auto-rotate" in QuickSetting 778 779 Args: 780 ad: android device object. 781 new_state: new state for "Auto-rotate". True or False. 782 """ 783 ad.adb.shell("settings put system accelerometer_rotation {}".format( 784 1 if new_state else 0)) 785 786 787def set_location_service(ad, new_state): 788 """Set Location service on/off in Settings->Location 789 790 Args: 791 ad: android device object. 792 new_state: new state for "Location service". 793 If new_state is False, turn off location service. 794 If new_state if True, set location service to "High accuracy". 795 """ 796 ad.adb.shell("content insert --uri " 797 " content://com.google.settings/partner --bind " 798 "name:s:network_location_opt_in --bind value:s:1") 799 ad.adb.shell("content insert --uri " 800 " content://com.google.settings/partner --bind " 801 "name:s:use_location_for_services --bind value:s:1") 802 if new_state: 803 ad.adb.shell("settings put secure location_mode 3") 804 else: 805 ad.adb.shell("settings put secure location_mode 0") 806 807 808def set_mobile_data_always_on(ad, new_state): 809 """Set Mobile_Data_Always_On feature bit 810 811 Args: 812 ad: android device object. 813 new_state: new state for "mobile_data_always_on" 814 if new_state is False, set mobile_data_always_on disabled. 815 if new_state if True, set mobile_data_always_on enabled. 816 """ 817 ad.adb.shell("settings put global mobile_data_always_on {}".format( 818 1 if new_state else 0)) 819 820 821def bypass_setup_wizard(ad): 822 """Bypass the setup wizard on an input Android device 823 824 Args: 825 ad: android device object. 826 827 Returns: 828 True if Android device successfully bypassed the setup wizard. 829 False if failed. 830 """ 831 try: 832 ad.adb.shell("am start -n \"com.google.android.setupwizard/" 833 ".SetupWizardExitActivity\"") 834 logging.debug("No error during default bypass call.") 835 except adb.AdbError as adb_error: 836 if adb_error.stdout == "ADB_CMD_OUTPUT:0": 837 if adb_error.stderr and \ 838 not adb_error.stderr.startswith("Error type 3\n"): 839 logging.error( 840 "ADB_CMD_OUTPUT:0, but error is %s " % adb_error.stderr) 841 raise adb_error 842 logging.debug("Bypass wizard call received harmless error 3: " 843 "No setup to bypass.") 844 elif adb_error.stdout == "ADB_CMD_OUTPUT:255": 845 # Run it again as root. 846 ad.adb.root_adb() 847 logging.debug("Need root access to bypass setup wizard.") 848 try: 849 ad.adb.shell("am start -n \"com.google.android.setupwizard/" 850 ".SetupWizardExitActivity\"") 851 logging.debug("No error during rooted bypass call.") 852 except adb.AdbError as adb_error: 853 if adb_error.stdout == "ADB_CMD_OUTPUT:0": 854 if adb_error.stderr and \ 855 not adb_error.stderr.startswith("Error type 3\n"): 856 logging.error("Rooted ADB_CMD_OUTPUT:0, but error is " 857 "%s " % adb_error.stderr) 858 raise adb_error 859 logging.debug( 860 "Rooted bypass wizard call received harmless " 861 "error 3: No setup to bypass.") 862 863 # magical sleep to wait for the gservices override broadcast to complete 864 time.sleep(3) 865 866 provisioned_state = int( 867 ad.adb.shell("settings get global device_provisioned")) 868 if provisioned_state != 1: 869 logging.error("Failed to bypass setup wizard.") 870 return False 871 logging.debug("Setup wizard successfully bypassed.") 872 return True 873 874 875def parse_ping_ouput(ad, count, out, loss_tolerance=20): 876 """Ping Parsing util. 877 878 Args: 879 ad: Android Device Object. 880 count: Number of ICMP packets sent 881 out: shell output text of ping operation 882 loss_tolerance: Threshold after which flag test as false 883 Returns: 884 False: if packet loss is more than loss_tolerance% 885 True: if all good 886 """ 887 result = re.search( 888 r"(\d+) packets transmitted, (\d+) received, (\d+)% packet loss", out) 889 if not result: 890 ad.log.info("Ping failed with %s", out) 891 return False 892 893 packet_loss = int(result.group(3)) 894 packet_xmit = int(result.group(1)) 895 packet_rcvd = int(result.group(2)) 896 min_packet_xmit_rcvd = (100 - loss_tolerance) * 0.01 897 if (packet_loss > loss_tolerance 898 or packet_xmit < count * min_packet_xmit_rcvd 899 or packet_rcvd < count * min_packet_xmit_rcvd): 900 ad.log.error("%s, ping failed with loss more than tolerance %s%%", 901 result.group(0), loss_tolerance) 902 return False 903 ad.log.info("Ping succeed with %s", result.group(0)) 904 return True 905 906 907def adb_shell_ping(ad, 908 count=120, 909 dest_ip="www.google.com", 910 timeout=200, 911 loss_tolerance=20): 912 """Ping utility using adb shell. 913 914 Args: 915 ad: Android Device Object. 916 count: Number of ICMP packets to send 917 dest_ip: hostname or IP address 918 default www.google.com 919 timeout: timeout for icmp pings to complete. 920 """ 921 ping_cmd = "ping -W 1" 922 if count: 923 ping_cmd += " -c %d" % count 924 if dest_ip: 925 ping_cmd += " %s" % dest_ip 926 try: 927 ad.log.info("Starting ping test to %s using adb command %s", dest_ip, 928 ping_cmd) 929 out = ad.adb.shell(ping_cmd, timeout=timeout, ignore_status=True) 930 if not parse_ping_ouput(ad, count, out, loss_tolerance): 931 return False 932 return True 933 except Exception as e: 934 ad.log.warning("Ping Test to %s failed with exception %s", dest_ip, e) 935 return False 936 937 938def unzip_maintain_permissions(zip_path, extract_location): 939 """Unzip a .zip file while maintaining permissions. 940 941 Args: 942 zip_path: The path to the zipped file. 943 extract_location: the directory to extract to. 944 """ 945 with zipfile.ZipFile(zip_path, 'r') as zip_file: 946 for info in zip_file.infolist(): 947 _extract_file(zip_file, info, extract_location) 948 949 950def _extract_file(zip_file, zip_info, extract_location): 951 """Extracts a single entry from a ZipFile while maintaining permissions. 952 953 Args: 954 zip_file: A zipfile.ZipFile. 955 zip_info: A ZipInfo object from zip_file. 956 extract_location: The directory to extract to. 957 """ 958 out_path = zip_file.extract(zip_info.filename, path=extract_location) 959 perm = zip_info.external_attr >> 16 960 os.chmod(out_path, perm) 961 962 963def get_directory_size(path): 964 """Computes the total size of the files in a directory, including subdirectories. 965 966 Args: 967 path: The path of the directory. 968 Returns: 969 The size of the provided directory. 970 """ 971 total = 0 972 for dirpath, dirnames, filenames in os.walk(path): 973 for filename in filenames: 974 total += os.path.getsize(os.path.join(dirpath, filename)) 975 return total 976 977 978def get_process_uptime(process): 979 """Returns the runtime in [[dd-]hh:]mm:ss, or '' if not running.""" 980 pid = job.run('pidof %s' % process, ignore_status=True).stdout 981 runtime = '' 982 if pid: 983 runtime = job.run('ps -o etime= -p "%s"' % pid).stdout 984 return runtime 985 986 987def get_device_process_uptime(adb, process): 988 """Returns the uptime of a device process.""" 989 pid = adb.shell('pidof %s' % process, ignore_status=True) 990 runtime = '' 991 if pid: 992 runtime = adb.shell('ps -o etime= -p "%s"' % pid) 993 return runtime 994 995 996def wait_until(func, timeout_s, condition=True, sleep_s=1.0): 997 """Executes a function repeatedly until condition is met. 998 999 Args: 1000 func: The function pointer to execute. 1001 timeout_s: Amount of time (in seconds) to wait before raising an 1002 exception. 1003 condition: The ending condition of the WaitUntil loop. 1004 sleep_s: The amount of time (in seconds) to sleep between each function 1005 execution. 1006 1007 Returns: 1008 The time in seconds before detecting a successful condition. 1009 1010 Raises: 1011 TimeoutError: If the condition was never met and timeout is hit. 1012 """ 1013 start_time = time.time() 1014 end_time = start_time + timeout_s 1015 count = 0 1016 while True: 1017 count += 1 1018 if func() == condition: 1019 return time.time() - start_time 1020 if time.time() > end_time: 1021 break 1022 time.sleep(sleep_s) 1023 raise TimeoutError('Failed to complete function %s in %d seconds having ' 1024 'attempted %d times.' % (str(func), timeout_s, count)) 1025 1026 1027# Adapted from 1028# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python 1029# Available under the Creative Commons Attribution-ShareAlike License 1030def levenshtein(string1, string2): 1031 """Returns the Levenshtein distance of two strings. 1032 Uses Dynamic Programming approach, only keeping track of 1033 two rows of the DP table at a time. 1034 1035 Args: 1036 string1: String to compare to string2 1037 string2: String to compare to string1 1038 1039 Returns: 1040 distance: the Levenshtein distance between string1 and string2 1041 """ 1042 1043 if len(string1) < len(string2): 1044 return levenshtein(string2, string1) 1045 1046 if len(string2) == 0: 1047 return len(string1) 1048 1049 previous_row = range(len(string2) + 1) 1050 for i, char1 in enumerate(string1): 1051 current_row = [i + 1] 1052 for j, char2 in enumerate(string2): 1053 insertions = previous_row[j + 1] + 1 1054 deletions = current_row[j] + 1 1055 substitutions = previous_row[j] + (char1 != char2) 1056 current_row.append(min(insertions, deletions, substitutions)) 1057 previous_row = current_row 1058 1059 return previous_row[-1] 1060 1061 1062def string_similarity(s1, s2): 1063 """Returns a similarity measurement based on Levenshtein distance. 1064 1065 Args: 1066 s1: the string to compare to s2 1067 s2: the string to compare to s1 1068 1069 Returns: 1070 result: the similarity metric 1071 """ 1072 lev = levenshtein(s1, s2) 1073 try: 1074 lev_ratio = float(lev) / max(len(s1), len(s2)) 1075 result = (1.0 - lev_ratio) * 100 1076 except ZeroDivisionError: 1077 result = 100 if not s2 else 0 1078 return float(result) 1079 1080 1081def run_concurrent_actions_no_raise(*calls): 1082 """Concurrently runs all callables passed in using multithreading. 1083 1084 Example: 1085 1086 >>> def test_function_1(arg1, arg2): 1087 >>> return arg1, arg2 1088 >>> 1089 >>> def test_function_2(arg1, kwarg='kwarg'): 1090 >>> raise arg1(kwarg) 1091 >>> 1092 >>> run_concurrent_actions_no_raise( 1093 >>> lambda: test_function_1('arg1', 'arg2'), 1094 >>> lambda: test_function_2(IndexError, kwarg='kwarg'), 1095 >>> ) 1096 >>> # Output: 1097 >>> [('arg1', 'arg2'), IndexError('kwarg')] 1098 1099 Args: 1100 *calls: A *args list of argumentless callable objects to be called. Note 1101 that if a function has arguments it can be turned into an 1102 argumentless function via the lambda keyword or functools.partial. 1103 1104 Returns: 1105 An array of the returned values or exceptions received from calls, 1106 respective of the order given. 1107 """ 1108 with ThreadPoolExecutor(max_workers=len(calls)) as executor: 1109 futures = [executor.submit(call) for call in calls] 1110 1111 results = [] 1112 for future in futures: 1113 try: 1114 results.append(future.result()) 1115 except Exception as e: 1116 results.append(e) 1117 return results 1118 1119 1120def run_concurrent_actions(*calls): 1121 """Runs all callables passed in concurrently using multithreading. 1122 1123 Examples: 1124 1125 >>> def test_function_1(arg1, arg2): 1126 >>> print(arg1, arg2) 1127 >>> 1128 >>> def test_function_2(arg1, kwarg='kwarg'): 1129 >>> raise arg1(kwarg) 1130 >>> 1131 >>> run_concurrent_actions( 1132 >>> lambda: test_function_1('arg1', 'arg2'), 1133 >>> lambda: test_function_2(IndexError, kwarg='kwarg'), 1134 >>> ) 1135 >>> 'The above line raises IndexError("kwarg")' 1136 1137 Args: 1138 *calls: A *args list of argumentless callable objects to be called. Note 1139 that if a function has arguments it can be turned into an 1140 argumentless function via the lambda keyword or functools.partial. 1141 1142 Returns: 1143 An array of the returned values respective of the order of the calls 1144 argument. 1145 1146 Raises: 1147 If an exception is raised in any of the calls, the first exception 1148 caught will be raised. 1149 """ 1150 first_exception = None 1151 1152 class WrappedException(Exception): 1153 """Raised when a passed-in callable raises an exception.""" 1154 1155 def call_wrapper(call): 1156 nonlocal first_exception 1157 1158 try: 1159 return call() 1160 except Exception as e: 1161 logging.exception(e) 1162 # Note that there is a potential race condition between two 1163 # exceptions setting first_exception. Even if a locking mechanism 1164 # was added to prevent this from happening, it is still possible 1165 # that we capture the second exception as the first exception, as 1166 # the active thread can swap to the thread that raises the second 1167 # exception. There is no way to solve this with the tools we have 1168 # here, so we do not bother. The effects this issue has on the 1169 # system as a whole are negligible. 1170 if first_exception is None: 1171 first_exception = e 1172 raise WrappedException(e) 1173 1174 with ThreadPoolExecutor(max_workers=len(calls)) as executor: 1175 futures = [executor.submit(call_wrapper, call) for call in calls] 1176 1177 results = [] 1178 for future in futures: 1179 try: 1180 results.append(future.result()) 1181 except WrappedException: 1182 # We do not need to raise here, since first_exception will already 1183 # be set to the first exception raised by these callables. 1184 break 1185 1186 if first_exception: 1187 raise first_exception 1188 1189 return results 1190 1191 1192def test_concurrent_actions(*calls, failure_exceptions=(Exception,)): 1193 """Concurrently runs all passed in calls using multithreading. 1194 1195 If any callable raises an Exception found within failure_exceptions, the 1196 test case is marked as a failure. 1197 1198 Example: 1199 >>> def test_function_1(arg1, arg2): 1200 >>> print(arg1, arg2) 1201 >>> 1202 >>> def test_function_2(kwarg='kwarg'): 1203 >>> raise IndexError(kwarg) 1204 >>> 1205 >>> test_concurrent_actions( 1206 >>> lambda: test_function_1('arg1', 'arg2'), 1207 >>> lambda: test_function_2(kwarg='kwarg'), 1208 >>> failure_exceptions=IndexError 1209 >>> ) 1210 >>> 'raises signals.TestFailure due to IndexError being raised.' 1211 1212 Args: 1213 *calls: A *args list of argumentless callable objects to be called. Note 1214 that if a function has arguments it can be turned into an 1215 argumentless function via the lambda keyword or functools.partial. 1216 failure_exceptions: A tuple of all possible Exceptions that will mark 1217 the test as a FAILURE. Any exception that is not in this list will 1218 mark the tests as UNKNOWN. 1219 1220 Returns: 1221 An array of the returned values respective of the order of the calls 1222 argument. 1223 1224 Raises: 1225 signals.TestFailure if any call raises an Exception. 1226 """ 1227 try: 1228 return run_concurrent_actions(*calls) 1229 except signals.TestFailure: 1230 # Do not modify incoming test failures 1231 raise 1232 except failure_exceptions as e: 1233 raise signals.TestFailure(e) 1234