├── __init__.py ├── util ├── __init__.py ├── tool │ ├── __init__.py │ ├── ios.py │ ├── file.py │ ├── adict.py │ ├── btask.py │ ├── log.py │ ├── cli.py │ ├── taskcenter.py │ └── adb.py ├── decorator.py └── common.py ├── example ├── __init__.py ├── use_log.py ├── use_cli.py ├── use_adb.py ├── use_adict.py └── use_btask.py ├── requirements.txt ├── README.md ├── LICENSE └── .gitignore /__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | -------------------------------------------------------------------------------- /util/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | -------------------------------------------------------------------------------- /example/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | -------------------------------------------------------------------------------- /util/tool/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | PrettyTable 3 | paramiko 4 | Pillow 5 | colorama 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | util 2 | ---- 3 | 4 | 平时测试工作中用到的工具类库,在Windows平台下,Python 3.6中运行通过 5 | 6 | > 推荐使用Python3.6以上版本,IDE推荐Pycharm 7 | 8 | > 本项目不建议直接在项目中使用,欢迎参考指正~ 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/use_log.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | from util.tool import log 4 | 5 | if __name__ == '__main__': 6 | log.set_level_to_info() 7 | log.debug("debug msg") 8 | log.info("info msg") 9 | log.warn("warn msg") 10 | log.error("error msg") 11 | -------------------------------------------------------------------------------- /example/use_cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | from util.tool.cli import cli 4 | 5 | if __name__ == '__main__': 6 | 7 | @cli.add("hello方法") 8 | def hello(): 9 | print("hello") 10 | 11 | 12 | @cli.add("hi方法") 13 | def hi(): 14 | print("hi") 15 | 16 | 17 | cli.run() 18 | -------------------------------------------------------------------------------- /example/use_adb.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | from util.tool.adb import ADB 5 | from util.tool import log 6 | 7 | if __name__ == '__main__': 8 | 9 | log.set_level_to_info() 10 | 11 | adb = ADB() 12 | print(adb.android_version) 13 | print(adb.current_package_info) 14 | print(adb.resolution) 15 | -------------------------------------------------------------------------------- /example/use_adict.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | from util.tool.adict import Adict 4 | 5 | if __name__ == '__main__': 6 | t = {"name": "jianbing", 7 | "job": "tester", 8 | "skill": [{"python": 60}, {"java": 60}], 9 | } 10 | t2 = Adict.load_dict(t) 11 | print(t2) 12 | print(t2.name) 13 | print(t2.skill[0].python) 14 | -------------------------------------------------------------------------------- /example/use_btask.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import time 4 | from util.tool.btask import background_task, TaskService 5 | 6 | 7 | @background_task("a_background_job") 8 | def hi(name): 9 | while 1: 10 | print("hi, {}".format(name)) 11 | time.sleep(1) 12 | 13 | 14 | if __name__ == '__main__': 15 | hi("jianbing") 16 | 17 | time.sleep(5) 18 | TaskService.stop("a_background_job") 19 | print("stop") 20 | -------------------------------------------------------------------------------- /util/tool/ios.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | """ 4 | Created by jianbing on 2017-11-06 5 | """ 6 | import os 7 | import plistlib 8 | import traceback 9 | 10 | 11 | def get_bundle_identifier(file_path): 12 | for root, dirs, files in os.walk(file_path): 13 | for file in files: 14 | if file == 'Info.plist': 15 | with open(os.path.join(root, file), 'rb') as plist_file: 16 | plist = plistlib.loads(plist_file.read()) 17 | try: 18 | return plist['CFBundleIdentifier'], plist['CFBundleShortVersionString'], plist['CFBundleVersion'] 19 | except: 20 | traceback.print_exc() 21 | print(plist) 22 | return 23 | raise Exception("can not find Info.plist") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2017] [jianbing.g] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /util/tool/file.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import os 4 | import time 5 | import shutil 6 | 7 | 8 | class File: 9 | def __init__(self, file_path): 10 | self._file_path = file_path 11 | 12 | @property 13 | def path(self): 14 | return self._file_path 15 | 16 | @property 17 | def is_dir(self): 18 | return os.path.isdir(self._file_path) 19 | 20 | @property 21 | def suffix(self): 22 | if self.is_dir: 23 | return None 24 | return self._file_path.split(".")[-1] 25 | 26 | @property 27 | def exists(self): 28 | return os.path.exists(self._file_path) 29 | 30 | @property 31 | def basename(self): 32 | return os.path.basename(self._file_path) 33 | 34 | @property 35 | def modify_time(self): 36 | return time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(os.stat(self._file_path).st_mtime)) 37 | 38 | @property 39 | def size(self): 40 | return os.path.getsize(self._file_path) 41 | 42 | def move(self, dst): 43 | shutil.move(self._file_path, dst) 44 | 45 | def copy(self, dst): 46 | shutil.copy2(self._file_path, dst) 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | 64 | # pyenv 65 | .python-version 66 | 67 | # pycharm 68 | .idea/ 69 | 70 | 71 | # 本地测试 72 | local_test/ 73 | 74 | # 本地配置 75 | local_config.py 76 | -------------------------------------------------------------------------------- /util/tool/adict.py: -------------------------------------------------------------------------------- 1 | class Adict(dict): 2 | 3 | def __getattr__(self, name): 4 | try: 5 | return self[name] 6 | except KeyError: 7 | raise self.__attr_error(name) 8 | 9 | def __setattr__(self, name, value): 10 | self[name] = value 11 | 12 | def __delattr__(self, name): 13 | try: 14 | del self[name] 15 | except KeyError: 16 | raise self.__attr_error(name) 17 | 18 | def __attr_error(self, name): 19 | return AttributeError( 20 | "type object '{subclass_name}' has no attribute '{attr_name}'".format(subclass_name=type(self).__name__, 21 | attr_name=name)) 22 | 23 | def copy(self): 24 | return Adict(self) 25 | 26 | @staticmethod 27 | def load_dict(target_dict): 28 | import copy 29 | return Adict._do_load_dict(copy.deepcopy(target_dict)) 30 | 31 | @staticmethod 32 | def _do_load_dict(raw_dict): 33 | for key, value in raw_dict.items(): 34 | if isinstance(value, dict): 35 | raw_dict[key] = Adict(value) 36 | Adict._do_load_dict(raw_dict[key]) 37 | if isinstance(value, list): 38 | for index, i in enumerate(value): 39 | if isinstance(i, dict): 40 | value[index] = Adict(i) 41 | Adict._do_load_dict(value[index]) 42 | return Adict(raw_dict) 43 | 44 | 45 | -------------------------------------------------------------------------------- /util/tool/btask.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import functools 4 | import threading 5 | import ctypes 6 | """ 7 | 启动一个后台线程,随时可以结束掉它。 8 | """ 9 | 10 | 11 | class TaskService: 12 | _tasks = dict() 13 | 14 | @classmethod 15 | def register(cls, task_name, ident): 16 | cls._tasks[task_name] = ident 17 | 18 | @classmethod 19 | def have(cls, task_name): 20 | if task_name in cls._tasks: 21 | print("task {} is already exists".format(task_name)) 22 | return True 23 | return False 24 | 25 | @classmethod 26 | def stop(cls, task_name): 27 | if task_name not in cls._tasks: 28 | print("task {} is not exists".format(task_name)) 29 | return 30 | tid = cls._tasks[task_name] 31 | exc_type = SystemExit 32 | tid = ctypes.c_long(tid) 33 | res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exc_type)) 34 | del cls._tasks[task_name] 35 | 36 | if res == 0: 37 | raise ValueError("invalid thread id,the task may be already stop") 38 | elif res != 1: 39 | # """if it returns a number greater than one, you're in trouble, 40 | # and you should call it again with exc=NULL to revert the effect""" 41 | ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) 42 | raise SystemError("PyThreadState_SetAsyncExc failed") 43 | 44 | 45 | def background_task(task_name): 46 | def warp(func): 47 | @functools.wraps(func) 48 | def _wrap(*args, **kwargs): 49 | if TaskService.have(task_name): 50 | return 51 | try: 52 | t = threading.Thread(target=func, args=args, kwargs=kwargs) 53 | t.start() 54 | TaskService.register(task_name, t.ident) 55 | except Exception: 56 | import traceback 57 | traceback.print_exc() 58 | 59 | return _wrap 60 | 61 | return warp 62 | 63 | 64 | -------------------------------------------------------------------------------- /util/tool/log.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import os 4 | import sys 5 | import inspect 6 | import logging 7 | import logging.handlers 8 | from colorama import Fore, Style 9 | 10 | 11 | class _ColorLogger: 12 | def __init__(self): 13 | self._logger = logging.getLogger('color_logger') 14 | self._logger.setLevel(logging.INFO) 15 | handler = logging.StreamHandler(sys.stdout) 16 | handler.setFormatter(logging.Formatter('%(asctime)s %(message)s')) 17 | self._logger.addHandler(handler) 18 | 19 | def debug(self, msg): 20 | self._logger.debug("DEBUG " + str(msg) + 21 | " Func:{} Line:{} File:{}".format(*self._get_inspect_info()) + 22 | Style.RESET_ALL) 23 | 24 | def info(self, msg): 25 | self._logger.info(Fore.GREEN + "INFO " + str(msg) + 26 | " Func:{} Line:{} File:{}".format(*self._get_inspect_info()) + 27 | Style.RESET_ALL) 28 | 29 | def warn(self, msg): 30 | self._logger.warning(Fore.YELLOW + "WARNING " + str(msg) + 31 | " Func:{} Line:{} File:{}".format(*self._get_inspect_info()) + 32 | Style.RESET_ALL) 33 | 34 | def error(self, msg): 35 | self._logger.error(Fore.RED + "ERROR " + str(msg) + 36 | " Func:{} Line:{} File:{}".format(*self._get_inspect_info()) + 37 | Style.RESET_ALL) 38 | 39 | def set_level(self, level): 40 | self._logger.setLevel(level) 41 | 42 | def _get_inspect_info(self): 43 | stack = inspect.stack() 44 | return stack[3].function, stack[3].lineno, os.path.basename(stack[3].filename) 45 | 46 | 47 | _logger = _ColorLogger() 48 | 49 | 50 | def debug(msg): 51 | _logger.debug(msg) 52 | 53 | 54 | def info(msg): 55 | _logger.info(msg) 56 | 57 | 58 | def error(msg): 59 | _logger.error(msg) 60 | 61 | 62 | def warn(msg): 63 | _logger.warn(msg) 64 | 65 | 66 | def set_level(level): 67 | _logger.set_level(level) 68 | 69 | 70 | def set_level_to_debug(): 71 | _logger.set_level(logging.DEBUG) 72 | 73 | 74 | def set_level_to_info(): 75 | _logger.set_level(logging.INFO) 76 | 77 | 78 | def set_level_to_warn(): 79 | _logger.set_level(logging.WARN) 80 | 81 | 82 | def set_level_to_error(): 83 | _logger.set_level(logging.ERROR) 84 | -------------------------------------------------------------------------------- /util/tool/cli.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | 5 | from collections import OrderedDict 6 | from prettytable import PrettyTable 7 | import datetime 8 | 9 | 10 | class _Command: 11 | def __init__(self, func, title, hot_key=None, need_confirm=False): 12 | self.func = func 13 | self.title = title 14 | self.hot_key = hot_key 15 | self.need_confirm = need_confirm 16 | 17 | def confirm(self): 18 | if not self.need_confirm: 19 | return True 20 | choice = input("输入yes确认执行 {} :".format(self.title)) 21 | if choice == "yes": 22 | return True 23 | print("没有输入yes,没有通过确认") 24 | return False 25 | 26 | def run(self): 27 | self.func() 28 | 29 | 30 | class _CliTool: 31 | def __init__(self): 32 | self.cmds = OrderedDict() 33 | self.hotkeys = dict() 34 | 35 | def add_cmd(self, cmd): 36 | self.cmds[str(len(self.cmds) + 1)] = cmd 37 | 38 | if cmd.hot_key: 39 | if cmd.hot_key in self.hotkeys: 40 | print("\n热键冲突,{}和{}".format(cmd.title, self.hotkeys[cmd.hot_key].title)) 41 | else: 42 | self.hotkeys[cmd.hot_key] = cmd 43 | 44 | def show_cmds(self): 45 | 46 | table = PrettyTable(["ID", "指令", "热键"]) 47 | table.align["指令"] = "l" # 左对齐 48 | 49 | for cmd_id in self.cmds: 50 | if not self.cmds[cmd_id].hot_key: 51 | hot_key = '' 52 | else: 53 | hot_key = self.cmds[cmd_id].hot_key 54 | table.add_row([cmd_id, self.cmds[cmd_id].title, hot_key]) 55 | 56 | print(table) 57 | 58 | def choice_cmd(self): 59 | while 1: 60 | cmd_id = input("选择指令:") 61 | if cmd_id not in self.cmds and cmd_id not in self.hotkeys: 62 | print('输入的ID或者热键不存在,请重新选择') 63 | continue 64 | break 65 | return cmd_id 66 | 67 | def run(self): 68 | while True: 69 | try: 70 | self.show_cmds() 71 | cmd_id = self.choice_cmd() 72 | 73 | if cmd_id in self.cmds: 74 | cmd = self.cmds[cmd_id] 75 | else: 76 | cmd = self.hotkeys[cmd_id] 77 | 78 | if not cmd.confirm(): 79 | continue 80 | 81 | cmd.run() 82 | print('\n执行完成 {}\n'.format(datetime.datetime.now())) 83 | 84 | except KeyboardInterrupt: 85 | print('手动退出\n') 86 | except: 87 | import traceback 88 | traceback.print_exc() 89 | 90 | 91 | class cli: 92 | _cli_tool = _CliTool() 93 | 94 | @classmethod 95 | def add(cls, title, hot_key=None, need_confirm=False): 96 | def wrap(func): 97 | cls._cli_tool.add_cmd(_Command(func, title, hot_key, need_confirm)) 98 | 99 | return wrap 100 | 101 | @classmethod 102 | def run(cls): 103 | cls._cli_tool.run() 104 | -------------------------------------------------------------------------------- /util/tool/taskcenter.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import queue 5 | import threading 6 | import time 7 | 8 | 9 | class TaskCenter: 10 | """ 11 | 任务执行器,多线程方式 12 | """ 13 | 14 | def __init__(self, target, param_list, thread_num, allow_append_param=False): 15 | """初始化 16 | 17 | :param target: 任务函数 18 | :param param_list: 包含需要处理的全部参数的列表,多个参数的,使用tuple方式,如 [(1,2,3),(3,)],单个参数的,直接加入list 19 | :param thread_num: 任务线程数量 20 | :param allow_append_param:是否支持执行期间继续添加参数,如果True,则任务线程get不到参数时,会阻塞等待 21 | :return: 22 | """ 23 | self._target = target 24 | self._param_list = param_list 25 | self._thread_num = thread_num 26 | self._param_queue = queue.Queue() 27 | self._allow_append_param = allow_append_param 28 | self._thread_dict = dict() 29 | 30 | def _init_param_queue(self): 31 | """初始化参数列表 32 | 33 | :return: 34 | """ 35 | for i in self._param_list: 36 | self._param_queue.put(i) 37 | if not self._allow_append_param: 38 | self._add_finish_param() 39 | 40 | def _add_finish_param(self): 41 | """设置完成任务的标记,任务线程接收到None时,会结束线程 42 | 43 | :return: 44 | """ 45 | for i in range(self._thread_num): 46 | self._param_queue.put("__finish__") 47 | 48 | def _thread_func(self): 49 | while True: 50 | args = self._param_queue.get() 51 | print("参数列表剩余:{}组".format(self._param_queue.qsize())) 52 | if args is not "__finish__": 53 | if isinstance(args, tuple): 54 | self._target(*args) 55 | else: 56 | self._target(args) 57 | self._param_queue.task_done() 58 | else: 59 | break 60 | 61 | def start(self): 62 | self._init_param_queue() 63 | for i in range(1, self._thread_num+1): 64 | self._thread_dict[i] = threading.Thread(target=self._thread_func) 65 | self._thread_dict[i].start() 66 | 67 | def wait_to_finish(self): 68 | """调用后,会阻塞主线程,直到全部任务线程结束,不允许继续新增任务函数参数 69 | 70 | :return: 71 | """ 72 | self.finish_append_params() 73 | for i in self._thread_dict: 74 | self._thread_dict[i].join() 75 | 76 | def append_params(self, param_list): 77 | if self._allow_append_param: 78 | for i in param_list: 79 | self._param_queue.put(i) 80 | else: 81 | raise Exception("不可以添加新的参数列表") 82 | 83 | def finish_append_params(self): 84 | self._allow_append_param = False 85 | self._add_finish_param() 86 | 87 | 88 | if __name__ == '__main__': 89 | 90 | from util.decorator import retry 91 | 92 | def to_download(astr): 93 | print("{0}___________".format(astr)) 94 | time.sleep(1) 95 | 96 | start_time = time.time() 97 | 98 | params = [] 99 | for i in range(2): 100 | for ii in range(6): 101 | params.append("{0}_{1}".format(i,ii)) 102 | 103 | params.append(("hello world",)) 104 | print(len(params)) 105 | print(params) 106 | center = TaskCenter(target=retry()(to_download), param_list=params, thread_num=2) 107 | center.start() 108 | center.wait_to_finish() 109 | 110 | print('finish') 111 | print(time.time()-start_time) 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /util/decorator.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import threading 5 | import time 6 | import os 7 | import sys 8 | import platform 9 | import functools 10 | 11 | 12 | def chdir(dir_path=''): 13 | """自动调用os.chdir 14 | 15 | :param dir_path: 16 | :return: 17 | """ 18 | 19 | def _chdir(func): 20 | @functools.wraps(func) 21 | def __chdir(*args, **kwargs): 22 | os.chdir(dir_path) 23 | return func(*args, **kwargs) 24 | 25 | return __chdir 26 | 27 | return _chdir 28 | 29 | 30 | def retry(times=5): 31 | """一个装饰器,可以设置报错重试次数 32 | 33 | :param times: 最多重试次数 34 | :return: 35 | """ 36 | 37 | def _retry(func): 38 | @functools.wraps(func) 39 | def __retry(*args, **kwargs): 40 | retry_times = 0 41 | while retry_times <= times: 42 | try: 43 | res = func(*args, **kwargs) 44 | return res 45 | except Exception: 46 | print(sys.exc_info()[1]) 47 | retry_times += 1 48 | if retry_times <= times: 49 | print('1秒后重试第{0}次'.format(retry_times)) 50 | time.sleep(1) 51 | else: 52 | print('max try,can not fix') 53 | import traceback 54 | traceback.print_exc() 55 | return None 56 | 57 | return __retry 58 | 59 | return _retry 60 | 61 | 62 | def count_running_time(func): 63 | """装饰器函数,统计函数的运行耗时 64 | 65 | :param func: 66 | :return: 67 | """ 68 | 69 | @functools.wraps(func) 70 | def _count_running_time(*args, **kwargs): 71 | start = time.time() 72 | res = func(*args, **kwargs) 73 | print(('cost time :{:.3f}'.format(time.time() - start))) 74 | return res 75 | 76 | return _count_running_time 77 | 78 | 79 | def auto_next(func): 80 | """可以给协程用,自动next一次 81 | 82 | :param func: 83 | :return: 84 | """ 85 | 86 | @functools.wraps(func) 87 | def _auto_next(*args, **kwargs): 88 | g = func(*args, **kwargs) 89 | next(g) 90 | return g 91 | 92 | return _auto_next 93 | 94 | 95 | def check_adb(func): 96 | @functools.wraps(func) 97 | def _check_adb(*args, **kwargs): 98 | @cache_result() 99 | def get_adb_devices(): 100 | from util.common import run_cmd 101 | return run_cmd('adb devices') 102 | 103 | result = get_adb_devices() 104 | if (len(result)) < 2: 105 | print('当前没有连接上手机') 106 | return None 107 | return func(*args, **kwargs) 108 | 109 | return _check_adb 110 | 111 | 112 | def cache_result(times=60): 113 | def _wrap(func): 114 | @functools.wraps(func) 115 | def __wrap(*args, **kwargs): 116 | 117 | if hasattr(func, "__last_call_result__") and time.time() - func.__last_call_time__ < times: 118 | print(func.__last_call_result__) 119 | return func.__last_call_result__ 120 | else: 121 | result = func(*args, **kwargs) 122 | func.__last_call_result__ = result 123 | func.__last_call_time__ = time.time() 124 | return result 125 | 126 | return __wrap 127 | 128 | return _wrap 129 | 130 | 131 | def windows(obj): 132 | """如果非windows系统,抛出异常""" 133 | 134 | if not platform.platform().startswith('Windows'): 135 | raise Exception("仅支持在Windows下使用") 136 | return obj 137 | 138 | 139 | class Singleton: 140 | """单例模式的一种其实,其实Python最佳的单例方式还是通过模块来实现 141 | 142 | 用法如下: 143 | @Singleton 144 | class YourClass(object): 145 | """ 146 | 147 | def __init__(self, cls): 148 | self.__instance = None 149 | self.__cls = cls 150 | self._lock = threading.Lock() 151 | 152 | def __call__(self, *args, **kwargs): 153 | self._lock.acquire() 154 | if self.__instance is None: 155 | self.__instance = self.__cls(*args, **kwargs) 156 | self._lock.release() 157 | return self.__instance 158 | 159 | 160 | def simple_background_task(func): 161 | @functools.wraps(func) 162 | def _wrap(*args, **kwargs): 163 | threading.Thread(target=func, args=args, kwargs=kwargs).start() 164 | return 165 | 166 | return _wrap 167 | -------------------------------------------------------------------------------- /util/common.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | import hashlib 4 | import subprocess 5 | import zipfile 6 | import requests 7 | from util.decorator import * 8 | from util.tool.file import File 9 | import threading 10 | 11 | 12 | def delay(secs): 13 | """和sleep类似,多一个显示剩余sleep时间 14 | 15 | :param secs: 16 | :return: 17 | """ 18 | secs = int(secs) 19 | for i in reversed(list(range(secs))): 20 | sys.stdout.write("\rsleep {}s, left {}s".format(secs, i)) 21 | sys.stdout.flush() 22 | time.sleep(1) 23 | sys.stdout.write(",sleep over") 24 | sys.stdout.write("\n") 25 | 26 | 27 | def remove_bom(file_path): 28 | """移除utf-8文件的bom字节 29 | 30 | :param file_path: 31 | :return: 32 | """ 33 | '''''' 34 | bom = b'\xef\xbb\xbf' 35 | exist_bom = lambda s: True if s == bom else False 36 | 37 | f = open(file_path, 'rb') 38 | if exist_bom(f.read(3)): 39 | body = f.read() 40 | f.close() 41 | with open(file_path, 'wb') as f: 42 | f.write(body) 43 | 44 | 45 | def is_utf_bom(file_path): 46 | """判断文件是否是utf-8-bom 47 | 48 | :param file_path: 49 | :return: 50 | """ 51 | with open(file_path, 'rb') as f: 52 | if f.read(3) == b'\xef\xbb\xbf': 53 | return True 54 | return False 55 | 56 | 57 | def run_cmd(cmd, print_result=False, shell=True): 58 | """执行cmd命令,返回结果 59 | 60 | :param cmd: 61 | :param print_result: 是否打印,默认False 62 | :param shell: 63 | :return: stdout 64 | """ 65 | 66 | stdout, stderr = subprocess.Popen(cmd, shell=shell, stdout=subprocess.PIPE, 67 | stderr=subprocess.PIPE).communicate() # wait 如果输出量多,会死锁 68 | 69 | if print_result: 70 | print(stdout) 71 | 72 | if stderr: 73 | print(stderr) 74 | result = [i.decode('utf-8') for i in stdout.splitlines()] 75 | return result 76 | 77 | 78 | def run_in_new_thread(func, *args, **kwargs): 79 | threading.Thread(target=func, args=args, kwargs=kwargs).start() 80 | 81 | 82 | def get_local_ip(): 83 | """获取本地ip地址 84 | 85 | :return: 86 | """ 87 | import socket 88 | return socket.gethostbyname(socket.gethostname()) 89 | 90 | 91 | @windows 92 | def get_desktop_dir(): 93 | """获取桌面文件夹地址 94 | 95 | :return: 96 | """ 97 | return os.path.join("C:", os.environ['HOMEPATH'], 'Desktop') 98 | 99 | 100 | def max_n(a_list, num): 101 | """从一个列表里边,获取最大的n的值,如果num大于列表内的总数量,则返回整个列表 102 | 103 | :param a_list: 104 | :param num: 105 | :return: 106 | """ 107 | import heapq 108 | return heapq.nlargest(num, a_list) 109 | 110 | 111 | def profile_func(call_func_str): 112 | """分析函数的运行消耗 113 | 114 | def f(): 115 | d = AndroidDevice("192.168.1.120") 116 | d.swipe_position(650, 700, 50, 700, 30) 117 | d.swipe_position(130, 800, 850, 800, 50) 118 | 119 | profile_func("f()") 120 | 121 | :param call_func_str: 122 | :return: 123 | """ 124 | import cProfile 125 | cProfile.run(call_func_str, "prof.txt") 126 | import pstats 127 | p = pstats.Stats("prof.txt") 128 | p.sort_stats("time").print_stats() 129 | 130 | 131 | def get_file_size(file_path): 132 | """返回文件大小,单位是字节 133 | 134 | :param file_path: 135 | :return: 136 | """ 137 | return os.path.getsize(file_path) 138 | 139 | 140 | def get_files_by_suffix(path, suffixes=("txt", "xml"), traverse=True): 141 | """从path路径下,找出全部指定后缀名的文件,支持1层目录,或者整个目录遍历 142 | 143 | :param path: 根目录 144 | :param suffixes: 指定查找的文件后缀名 145 | :param traverse: 如果为False,只遍历一层目录 146 | :return: 147 | """ 148 | file_list = [] 149 | for root, dirs, files in os.walk(path): 150 | for file in files: 151 | file_suffix = os.path.splitext(file)[1][1:].lower() # 后缀名 152 | if file_suffix in suffixes: 153 | file_list.append(os.path.join(root, file)) 154 | if not traverse: 155 | return file_list 156 | 157 | return file_list 158 | 159 | 160 | def get_files_by_suffix_ex(path: str, suffixes: tuple = ("txt", "xml"), traverse: bool = True): 161 | """从path路径下,找出全部指定后缀名的文件,支持1层目录,或者整个目录遍历 162 | 163 | :param path: 根目录 164 | :param suffixes: 指定查找的文件后缀名 165 | :param traverse: 如果为False,只遍历一层目录 166 | :return: File对象列表 167 | """ 168 | file_list = [] 169 | for root, dirs, files in os.walk(path): 170 | for file in files: 171 | file_suffix = os.path.splitext(file)[1][1:].lower() # 后缀名 172 | if file_suffix in suffixes: 173 | file_list.append(File(os.path.join(root, file))) 174 | if not traverse: 175 | return file_list 176 | 177 | return file_list 178 | 179 | 180 | def zip_dir(dir_path, zip_filename): 181 | files_list = [] 182 | if os.path.isfile(dir_path): 183 | files_list.append(dir_path) 184 | else: 185 | for root, dirs, files in os.walk(dir_path): 186 | for name in files: 187 | files_list.append(os.path.join(root, name)) 188 | 189 | zf = zipfile.ZipFile(zip_filename, "w", zipfile.zlib.DEFLATED) 190 | for tar in files_list: 191 | zf.write(tar, tar[len(dir_path):]) 192 | zf.close() 193 | 194 | 195 | def unzip(file_path): 196 | """解压文件,解压到压缩包所在目录的同名文件夹中 197 | 198 | :param file_path:压缩包的绝对路径 199 | :return: 200 | """ 201 | folder = os.path.splitext(file_path)[0] # 建立和文件同名的文件夹 202 | if not os.path.exists(folder): 203 | os.mkdir(folder) 204 | zip_file = zipfile.ZipFile(file_path, 'r') 205 | zip_file.extractall(folder) 206 | zip_file.close() 207 | 208 | 209 | def format_timestamp(timestamp=time.time(), fmt='%Y-%m-%d-%H-%M-%S'): 210 | """时间戳转为指定的文本格式,默认是当前时间 211 | 212 | :param timestamp: 213 | :param fmt: 默认不需要填写,默认='%Y-%m-%d-%H-%M-%S'. 可以更改成自己想用的string格式. 比如 '%Y.%m.%d.%H.%M.%S' 214 | 215 | """ 216 | return time.strftime(fmt, time.localtime(timestamp)) 217 | 218 | 219 | def is_chinese(unicode_text): 220 | """检测unicode文本是否为中文 221 | 222 | :param unicode_text: 223 | :return: 224 | """ 225 | for uchar in unicode_text: 226 | if uchar >= '\u4e00' and not uchar > '\u9fa5': 227 | return True 228 | else: 229 | return False 230 | 231 | 232 | def java_timestamp_to_py(java_timestamp): 233 | """把java的时间戳转为python的,并打印出相应的时间 234 | 235 | :param java_timestamp: 236 | :return: 237 | """ 238 | py_time = int(java_timestamp / 1000) 239 | print((time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(py_time)))) 240 | 241 | 242 | def match_file(file1, file2): 243 | """对比两个文件的md5,看是否一致 244 | 245 | :param file1: 246 | :param file2: 247 | :return: 248 | """ 249 | 250 | with open(file1, encoding="utf-8") as file1, open(file2, encoding="utf-8") as file2: 251 | 252 | file1md5 = hashlib.md5() 253 | for temp in file1.readlines(): 254 | file1md5.update(temp.encode('utf-8')) 255 | 256 | file2md5 = hashlib.md5() 257 | for temp in file2.readlines(): 258 | file2md5.update(temp.encode('utf-8')) 259 | 260 | return True if file1md5.hexdigest() == file2md5.hexdigest() else False 261 | 262 | 263 | @retry(times=3) 264 | def get_url_file_size(url, proxies=None): 265 | """获取下载链接文件的大小,单位是KB 266 | 267 | :param proxies: 268 | :param url: 269 | :return: 270 | """ 271 | r = requests.head(url=url, proxies=proxies, timeout=3.0) 272 | 273 | while r.is_redirect: # 如果有重定向 274 | r = requests.head(url=r.headers['Location'], proxies=proxies, timeout=3.0) 275 | return int(r.headers['Content-Length']) / 1024 276 | 277 | 278 | def start_file(path): 279 | import os 280 | assert os.path.exists(path), "路径不存在:{}".format(path) 281 | assert os.path.isfile(path), "路径不能是文件夹".format(path) 282 | dir_path, file_name = os.path.split(path) 283 | 284 | def job(): 285 | cwd = os.getcwd() 286 | os.chdir(dir_path) 287 | run_cmd(r"start {}".format(file_name)) 288 | os.chdir(cwd) 289 | 290 | run_in_new_thread(job) 291 | 292 | 293 | @retry(times=5) 294 | def download_file(url, target_file, proxies=None, check_file=False, check_size=1000): 295 | """下载文件,通过requests库 296 | 297 | :param url: 目标url 298 | :param target_file:下载到本地的地址 299 | :param proxies: 代理 300 | :param check_file: 是否检查已经下载的文件的大小 301 | :param check_size: 最小文件大小(字节) 302 | :return: 303 | """ 304 | 305 | print('start to download {0}'.format(url)) 306 | 307 | r = requests.get(url, stream=True, proxies=proxies, timeout=5) 308 | 309 | with open(target_file, 'wb') as f: 310 | for chunk in r.iter_content(chunk_size=2048): 311 | f.write(chunk) 312 | if check_file: 313 | if get_file_size(target_file) < check_size: 314 | raise Exception("fileSize Error") 315 | print('finish download') 316 | 317 | 318 | @windows 319 | def is_port_used(port=5037, kill=False): 320 | cmd = 'netstat -ano | findstr {} | findstr LISTENING'.format(port) 321 | print(cmd) 322 | result = os.popen(cmd).read() 323 | print(result) 324 | pid = None 325 | if result != '': 326 | try: 327 | pid = result.split()[-1] 328 | 329 | result = os.popen('tasklist /FI "PID eq {0}'.format(pid)).read() 330 | """:type: str """ 331 | print(result) 332 | 333 | position = result.rfind('=====') 334 | program_name = result[position + 5:].split()[0] 335 | print("占用的程序是{}".format(program_name)) 336 | 337 | result = os.popen('wmic process where name="{0}" get executablepath'.format(program_name)).read() 338 | 339 | result = result.split() 340 | print("占用的程序所在位置:{}".format(result[1])) 341 | 342 | cmd = "explorer {0}".format(os.path.dirname(result[1])) 343 | run_cmd(cmd) # 打开所在文件夹 344 | 345 | except Exception: 346 | import traceback 347 | traceback.print_exc() 348 | finally: 349 | if kill: 350 | if not pid: 351 | raise Exception("pid is None") 352 | print(os.popen("taskkill /F /PID {0}".format(pid)).read()) # 结束进程 353 | 354 | else: 355 | print('{}端口没有被占用'.format(port)) 356 | 357 | 358 | def get_screen_scale(x: int, y: int): 359 | """通过屏幕分辨率,返回屏幕比例 360 | 361 | :param x: 362 | :param y: 363 | :return: 364 | """ 365 | x = int(x) 366 | y = int(y) 367 | scale = x / y 368 | if scale == 16 / 9: 369 | return 16, 9 370 | elif scale == 4 / 3: 371 | return 4, 3 372 | elif scale == 15 / 9: 373 | return 15, 9 374 | elif scale == 16 / 10: 375 | return 16, 10 376 | else: 377 | def gcd(a, b): 378 | if b == 0: 379 | return a 380 | else: 381 | return gcd(b, a % b) 382 | 383 | scale = gcd(x, y) 384 | print("没有找到合适的比例") 385 | return x / scale, y / scale 386 | 387 | 388 | def search_keyword_from_dirs(path, keyword, suffix=("txt", "xml"), traverse=True, length=100): 389 | files = get_files_by_suffix(path, suffix, traverse=traverse) 390 | print("发现{}个文件".format(len(files))) 391 | for file in files: 392 | try: 393 | with open(file, 'r', encoding='utf-8', errors='ignore') as f: 394 | 395 | content = f.read().lower() 396 | position = content.find(keyword.lower()) 397 | 398 | if position != -1: 399 | print("Find in {0}".format(file)) 400 | start = position - length if position - length > 0 else 0 401 | end = position + length if position + length < len(content) else len(content) 402 | print(content[start:end]) 403 | print("_" * 100) 404 | 405 | except Exception as e: 406 | print(e) 407 | print(file) 408 | print(("#" * 100)) 409 | print(("_" * 100)) 410 | -------------------------------------------------------------------------------- /util/tool/adb.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # -*- coding: UTF-8 -*- 3 | 4 | import os 5 | import re 6 | import subprocess 7 | import time 8 | import traceback 9 | from contextlib import contextmanager 10 | from PIL import Image 11 | from util.decorator import windows 12 | from util.tool import log 13 | from util.common import run_cmd, is_chinese, get_desktop_dir 14 | from functools import lru_cache 15 | 16 | 17 | @windows 18 | class ADB: 19 | def __init__(self, serial=None, adb_remote=None, chdir=""): 20 | 21 | self._serial = serial 22 | self._adb_remote = adb_remote 23 | self._adb_name = "adb.exe" 24 | self._findstr = "findstr" 25 | self._chidr = chdir 26 | 27 | self._func_data = dict() # 存储各函数运行时的临时数据 28 | self._init_adb() 29 | 30 | @contextmanager 31 | def _change_dir(self): 32 | cwd_backup = os.getcwd() 33 | if self._chidr: 34 | os.chdir(self._chidr) 35 | yield 36 | os.chdir(cwd_backup) 37 | 38 | def _init_adb(self): 39 | 40 | if self._adb_remote: 41 | with self._change_dir(): 42 | log.info(subprocess.check_output("{} connect {}".format(self._adb_name, self._adb_remote))) 43 | 44 | if not self._serial: 45 | devices_info = self.devices() 46 | log.debug(devices_info) 47 | 48 | if not devices_info: 49 | raise Exception("当前没有已连接的安卓设备") 50 | if len(devices_info) > 1: 51 | log.error(devices_info) 52 | print("当前通过数据线连接的安卓设备有") 53 | for i in devices_info: 54 | print(i) 55 | print("") 56 | raise Exception("同时有多个安卓设备连接着电脑,需要指定serialno.") 57 | else: 58 | self._serial = devices_info[0] 59 | 60 | # get_app_cpu_using 使用 61 | self._func_data['cpu_cost'] = None 62 | self._func_data['cpu_cost_update_time'] = None 63 | 64 | def is_connect(self): 65 | return self.serial in self.devices() 66 | 67 | def adb(self, *args): 68 | """adb命令执行入口 69 | 70 | :param args: 71 | :return: 72 | """ 73 | 74 | if self._serial: 75 | cmd = " ".join([self._adb_name, '-s', self._serial] + list(args)) 76 | else: 77 | cmd = " ".join([self._adb_name] + list(args)) 78 | with self._change_dir(): 79 | stdout, stderr = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, 80 | stderr=subprocess.PIPE).communicate() 81 | log.debug("cmd is {}".format(cmd)) 82 | log.debug("stdout is {}".format(stdout.strip())) 83 | log.debug("stderr is {}".format(stderr.decode('gbk'))) 84 | result = [i.decode() for i in stdout.splitlines()] 85 | return [i for i in result if i and not i.startswith("* daemon")] # 过滤掉空的行,以及adb启动消息 86 | 87 | def adb_shell(self, *args): 88 | """adb shell命令入口 89 | 90 | :param args: 91 | :return: 92 | """ 93 | args = ['shell'] + list(args) 94 | return self.adb(*args) 95 | 96 | def tap(self, x, y): 97 | self.adb_shell("input tap {} {}".format(x, y)) 98 | 99 | def swipe(self, sx, sy, ex, ey, steps=100): 100 | self.adb_shell("input swipe {} {} {} {} {}".format(sx, sy, ex, ey, steps)) 101 | 102 | def long_press(self, x, y, steps=1000): 103 | self.adb_shell("input swipe {} {} {} {} {}".format(x, y, x, y, steps)) 104 | 105 | def devices(self): 106 | result = self.adb('devices') 107 | if len(result) == 1: 108 | return [] 109 | log.debug(result) 110 | return [i.split()[0] for i in result if not i.startswith('List') and not i.startswith("adb")] 111 | 112 | @property 113 | @lru_cache() 114 | def resolution(self): 115 | """手机屏幕分辨率 116 | 117 | :return: 118 | """ 119 | result = self.adb_shell("wm size")[0] 120 | result = result[result.find('size: ') + 6:] # 1080x1800 121 | result = result.split('x') 122 | return [int(i) for i in result] 123 | 124 | @property 125 | @lru_cache() 126 | def orientation(self): 127 | for i in self.adb_shell('dumpsys', 'display'): 128 | index = i.find("orientation") 129 | if index != -1: 130 | return int(i[index + 12:index + 13]) 131 | raise Exception("找不到orientation") 132 | 133 | @property 134 | def adb_remote(self): 135 | return self._adb_remote 136 | 137 | @property 138 | @lru_cache() 139 | def version(self): 140 | """adb 版本信息 141 | 142 | :return: 143 | """ 144 | return self.adb('version')[0] 145 | 146 | @property 147 | def serial(self): 148 | return self._serial 149 | 150 | @property 151 | def android_version(self): 152 | return self.adb_shell('getprop ro.build.version.release')[0] 153 | 154 | @property 155 | def wlan_ip(self): 156 | """获取IP地址,基于 adb shell ifconfig 157 | 158 | :return: 159 | """ 160 | for i in self.adb_shell('ifconfig'): 161 | i = i.strip() 162 | if i.startswith("inet addr") and i.find("Bcast") != -1: 163 | return i[i.find("inet addr:") + len("inet addr:"): i.find("Bcast")].strip() 164 | 165 | def screenshot(self, screenshot_dir, info="N"): 166 | """screencap方式截图 167 | 168 | :param screenshot_dir: 169 | :param info: 附加信息 170 | :return: 171 | """ 172 | start_time = time.time() 173 | print("开始截图...") 174 | self.adb_shell("screencap -p /sdcard/screenshot.png") 175 | filename = '{0}-{1}.png'.format(time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(start_time)), info) 176 | self.adb('pull /sdcard/screenshot.png {}\{}'.format(screenshot_dir, filename)) 177 | print('截图已保存') 178 | return os.path.join(screenshot_dir, filename) 179 | 180 | def screenshot_ex(self, screenshot_dir, info='shot', compress=(0.5, 0.5)): 181 | """扩展screenshot函数,加入是否压缩和是否打开文件 182 | 183 | :param screenshot_dir: 184 | :param info: 185 | :param compress: 186 | :return: 187 | """ 188 | png_file = self.screenshot(screenshot_dir, info) 189 | print('screenshot is {0}'.format(os.path.normpath(png_file))) 190 | if compress: 191 | im = Image.open(png_file) 192 | im_size = im.size 193 | im = im.resize((int(im_size[0] * compress[0]), int(im_size[1] * compress[1])), Image.ANTIALIAS) # 尺寸减少一半 194 | # im = im.rotate(270, expand=1) # 旋转角度是逆时针的,如果没expand,会出现旋转后,部分图像丢失 195 | 196 | compress_file = png_file.replace(r".png", r"_small.png") 197 | im.save(compress_file) 198 | return compress_file 199 | 200 | def screenshot_by_minicap(self, screenshot_dir, file_name="", scale=1.0): 201 | 202 | start_time = time.time() 203 | w, h = self.resolution 204 | r = self.orientation 205 | params = '{x}x{y}@{rx}x{ry}/{r}'.format(x=w, y=h, rx=int(w * scale), ry=int(h * scale), r=r * 90) 206 | self.adb_shell( 207 | '"LD_LIBRARY_PATH=/data/local/tmp /data/local/tmp/minicap -s -P {} > /sdcard/minicap-screenshot.jpg"'.format( 208 | params)) 209 | if not file_name: 210 | file_name = '{0}.jpg'.format(time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(start_time))) 211 | self.adb('pull /sdcard/minicap-screenshot.jpg {}\{}'.format(screenshot_dir, file_name)) 212 | 213 | def get_app_mem_using(self, package_name=None): 214 | """获取内存占用 215 | 216 | :param package_name: app的包名 217 | :return: 返回app的内存总占用,单位MB 218 | """ 219 | try: 220 | if not package_name: 221 | package_name = self.current_package_name 222 | log.debug(package_name) 223 | result = self.adb_shell("dumpsys meminfo {}".format(package_name)) 224 | info = re.search('TOTAL\W+\d+', str(result)).group() 225 | result = info.split() 226 | return int(int(result[-1]) / 1000) 227 | except: 228 | import traceback 229 | traceback.print_exc() 230 | return 0 231 | 232 | def get_total_cpu_using(self): 233 | """获取手机当前CPU的总占用,不太准确,延迟很大 234 | 235 | :return: 236 | """ 237 | 238 | cmd = 'dumpsys cpuinfo |{} "TOTAL"'.format(self._findstr) 239 | result = self.adb_shell(cmd) 240 | assert len(result) == 1 241 | result = result[0] 242 | cpu = 0 243 | try: 244 | cpu = float(result[:result.find('%')].strip()) 245 | except: 246 | print(result) 247 | return cpu 248 | 249 | def get_app_cpu_using(self, pid=None): 250 | """采集当前运行的app的CPU占用,首次调用会延迟一秒统计出数据再返回 251 | 252 | :param pid: 253 | :return: 254 | """ 255 | 256 | if not pid: 257 | pid = self.current_pid 258 | cmd = 'cat /proc/{}/stat'.format(pid) 259 | now = time.time() 260 | try: 261 | cpu = sum([int(i) for i in self.adb_shell(cmd)[0].split()[13:17]]) 262 | 263 | if not self._func_data['cpu_cost']: 264 | self._func_data['cpu_cost'] = cpu 265 | self._func_data['cpu_cost_update_time'] = now 266 | time.sleep(1) 267 | return self.get_app_cpu_using(pid) 268 | else: 269 | cpu_use = cpu - self._func_data['cpu_cost'] 270 | self._func_data['cpu_cost'] = cpu 271 | self._func_data['cpu_cost_update_time'] = now 272 | result = float("{:.2f}".format(cpu_use / (now - self._func_data['cpu_cost_update_time']) / 10)) 273 | 274 | if result < 0: 275 | log.error("采集到的CPU占用数据异常:{}".format(result)) 276 | return 0 277 | return result 278 | except: 279 | traceback.print_exc() 280 | return 0 281 | 282 | @property 283 | def current_package_info(self): 284 | result = self.adb_shell('dumpsys activity activities | {} mResumedActivity'.format(self._findstr)) 285 | assert len(result) == 1, result 286 | return result[0].split()[-2].split("/") 287 | 288 | @property 289 | @lru_cache() 290 | def current_pid(self): 291 | result = self.adb_shell("ps|{} {}".format(self._findstr, self.current_package_name)) 292 | log.info(result) 293 | return result[0].split()[1] 294 | 295 | @property 296 | def current_package_name(self): 297 | """获取当前运行app包名 298 | 299 | :return: 300 | """ 301 | return self.current_package_info[0] 302 | 303 | @property 304 | def current_activity_name(self): 305 | """获取当前运行activity 306 | 307 | :return: 308 | """ 309 | return self.current_package_info[1] 310 | 311 | def pull_file(self, remote, local): 312 | """从手机导出文件到本地 eg pull_file("/sdcard/screenshot.png", "1.png") 313 | 314 | :param remote: 315 | :param local: 316 | :return: 317 | """ 318 | return self.adb('pull', remote, local) 319 | 320 | def push_file(self, local, remote): 321 | """上传文件到手机 322 | 323 | :param local: 324 | :param remote: 325 | :return: 326 | """ 327 | return self.adb('push', local, remote) 328 | 329 | @staticmethod 330 | def start_server(): 331 | log.debug('adb start-server') 332 | log.info(os.popen('adb.exe start-server').read()) 333 | 334 | @staticmethod 335 | def kill_server(): 336 | log.debug('adb kill-server') 337 | os.popen('adb.exe kill-server') 338 | 339 | @staticmethod 340 | def get_package_name_from_apk(apk_path): 341 | """从apk安装包中,获取包名 342 | 343 | :param apk_path: apk文件位置 344 | :return: 345 | """ 346 | return ADB.get_apk_info_from_apk_file(apk_path)[0] 347 | 348 | @staticmethod 349 | def get_apk_info_from_apk_file(apk_path): 350 | """从apk安装包中,获取包名,和版本信息 351 | 352 | :param apk_path: apk文件位置 353 | :return:从 package: name='com.funsplay.god.anzhi' versionCode='10300' versionName='1.3.0' 中获取信息 354 | """ 355 | result = "\n".join(run_cmd('aapt dump badging "{0}"'.format(apk_path))) 356 | package_name = result[result.index("name=\'") + 6:result.index("\' versionCode")] 357 | version_code = result[result.index("versionCode=\'") + 13:result.index("\' versionName")] 358 | version_name = result[result.index("versionName=\'") + 13:result.index("\' platformBuildVersionName")] 359 | return package_name, version_code, version_name 360 | 361 | def auto_install(self, path): 362 | """path可以是目录,自动安装目录下的全部apk,如果已经存在,则先卸载 363 | path也可以是具体apk路径 364 | 365 | :param path: 366 | :return: 367 | """ 368 | if not os.path.exists(path): 369 | print('不存在的路径:{}'.format(path)) 370 | return 371 | 372 | if os.path.isdir(path): 373 | print("当前连接的手机是:{0}".format(self._serial)) 374 | for filename in os.listdir(path): 375 | if os.path.splitext(filename)[1].lower() == '.apk': 376 | self.install(os.path.join(path, filename)) 377 | else: 378 | if os.path.splitext(path)[1].lower() == '.apk': 379 | self.install(path) 380 | else: 381 | print("文件后缀名不是apk") 382 | 383 | print('任务完成') 384 | 385 | def install(self, apk_path): 386 | print("发现apk文件:{0}".format(apk_path)) 387 | dir_path, filename = os.path.split(apk_path) 388 | rename = False 389 | raw_apk_path = apk_path 390 | if is_chinese(filename): 391 | print("apk文件名存在中文,进行重命名") 392 | new_apk_path = os.path.join(dir_path, "{}.apk".format(int(time.time()))) 393 | os.rename(raw_apk_path, new_apk_path) 394 | apk_path = new_apk_path 395 | rename = True 396 | 397 | package_name = self.get_package_name_from_apk(apk_path) 398 | print('apk安装包的包名是:{0}'.format(package_name)) 399 | 400 | if self.is_install(package_name): 401 | print('手机中已经安装了该应用,准备移除') 402 | self.uninstall(package_name) 403 | else: 404 | print('手机未安装该应用') 405 | 406 | print("开始安装:{0}".format(apk_path)) 407 | self.adb('install {}'.format(apk_path)) 408 | 409 | if rename: 410 | os.rename(apk_path, raw_apk_path) 411 | 412 | def is_install(self, package_name): 413 | """检查手机中是否已经安装了某个apk 414 | 415 | :param package_name: 应用的包名 416 | :return: 417 | """ 418 | return 'package:{0}'.format(package_name) in self.adb_shell("pm list package") 419 | 420 | def uninstall(self, package_name): 421 | """卸载apk 422 | 423 | :param package_name: 424 | :return: 425 | """ 426 | self.adb("uninstall", package_name) 427 | 428 | def backup_current_apk(self, path=get_desktop_dir()): 429 | """导出当前正在运行的apk到指定目录,默认是桌面 430 | 431 | :param path:导出目录 432 | :return: 433 | """ 434 | result = self.adb_shell("pm path", self.current_package_name) 435 | apk_path = result[0].strip().replace('package:', '') 436 | print('apk位置是:{0}'.format(apk_path)) 437 | print('开始导出apk') 438 | apk_name = "{}{}.apk".format(self.current_package_name, 439 | time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime(time.time()))) 440 | self.adb("pull {0} {1}\{2}".format(apk_path, path, apk_name)) 441 | print("备份完成") 442 | 443 | def start_monkey(self, pct_touch=100, pct_motion=0, throttle=200, v='-v -v', times=100, logfile=None): 444 | 445 | if pct_touch + pct_motion != 100: 446 | raise Exception("Monkey各行为的配比总和超过了100") 447 | 448 | cmd = [ 449 | 'monkey', 450 | '-p {}'.format(self.current_package_name), 451 | '--pct-touch {}'.format(pct_touch), 452 | '--pct-motion {}'.format(pct_motion), 453 | '--throttle {}'.format(throttle), 454 | '{}'.format(v), 455 | '{}'.format(times) 456 | ] 457 | 458 | if logfile: 459 | cmd.append(r'> {}'.format(logfile)) 460 | 461 | self.adb_shell(*iter(cmd)) 462 | 463 | def stop_monkey(self): 464 | result = self.adb_shell("ps|{} monkey".format(self._findstr)) 465 | if result: 466 | pid = result[0].split()[1] 467 | self.adb_shell('kill', pid) 468 | 469 | @staticmethod 470 | def raw_cmd(cmd): 471 | print('开始执行{}'.format(cmd)) 472 | os.system(cmd) 473 | print('执行完成') 474 | 475 | def start_app(self, component): 476 | log.info(self.adb_shell("am start -n {}".format(component))) 477 | 478 | def stop_app(self, package): 479 | log.info(self.adb_shell('am force-stop {}'.format(package))) 480 | --------------------------------------------------------------------------------