├── LICENSE ├── config.conf ├── mobileperf.png ├── mobileperf ├── __init__.py ├── android │ ├── __init__.py │ ├── cpu_top.py │ ├── dataworker.py │ ├── devicemonitor.py │ ├── excel.py │ ├── fd.py │ ├── fps.py │ ├── globaldata.py │ ├── logcat.py │ ├── meminfos.py │ ├── monkey.py │ ├── powerconsumption.py │ ├── report.py │ ├── startup.py │ ├── thread_num.py │ ├── tools │ │ ├── AdbWinApi.dll │ │ ├── AdbWinUsbApi.dll │ │ ├── __init__.py │ │ ├── adb.exe │ │ ├── androiddevice.py │ │ ├── platform-tools-latest-darwin │ │ │ └── platform-tools │ │ │ │ └── adb │ │ └── platform-tools-latest-linux │ │ │ └── platform-tools │ │ │ └── adb │ └── trafficstats.py ├── common │ ├── __init__.py │ ├── basemonitor.py │ ├── log.py │ └── utils.py ├── extlib │ ├── __init__.py │ └── xlsxwriter │ │ ├── __init__.py │ │ ├── app.py │ │ ├── chart.py │ │ ├── chart_area.py │ │ ├── chart_bar.py │ │ ├── chart_column.py │ │ ├── chart_doughnut.py │ │ ├── chart_line.py │ │ ├── chart_pie.py │ │ ├── chart_radar.py │ │ ├── chart_scatter.py │ │ ├── chart_stock.py │ │ ├── chartsheet.py │ │ ├── comments.py │ │ ├── compat_collections.py │ │ ├── compatibility.py │ │ ├── contenttypes.py │ │ ├── core.py │ │ ├── custom.py │ │ ├── drawing.py │ │ ├── exceptions.py │ │ ├── format.py │ │ ├── packager.py │ │ ├── relationships.py │ │ ├── shape.py │ │ ├── sharedstrings.py │ │ ├── styles.py │ │ ├── table.py │ │ ├── theme.py │ │ ├── utility.py │ │ ├── vml.py │ │ ├── workbook.py │ │ ├── worksheet.py │ │ └── xmlwriter.py └── pic │ ├── PC impact.png │ ├── anr.png │ ├── architecture.png │ ├── config.png │ ├── cpu table.png │ ├── cpu trend.png │ ├── dingding qr code.jpg │ ├── exception config.png │ ├── exception log.png │ ├── fps table.png │ ├── launchtime table.png │ ├── logcat file.png │ ├── mem hprof.png │ ├── mem table.png │ ├── mem trend.png │ ├── pid change table.png │ ├── power table.png │ ├── pss table.png │ ├── pss trend.png │ ├── test info.png │ ├── test result.png │ ├── tool compare.png │ └── traffic table.png ├── readme.md ├── run.bat ├── run.sh └── setup.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Alibaba 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. 22 | -------------------------------------------------------------------------------- /config.conf: -------------------------------------------------------------------------------- 1 | [Common] 2 | #test process,example: package=com.alibaba.ailabs.genie.contacts 3 | #support multi process,separate use; if contains child process,first must be main process 4 | #com.tencent.mm com.sina.weibo com.taobao.taobao 5 | package=com.taobao.taobao 6 | #collect frequency, int type,unit: second 7 | frequency=5 8 | #collect timeout ,int type ,unit:minute, for example:72 hours 4320 9 | timeout=4320 10 | #dumpheap frequency, int type,unit: minute 11 | dumpheap_freq=60 12 | #adb serialnum,adb devices result example WSKFSKBQLFA695D6 13 | serialnum=9e15838 14 | #except log tag,tools will check in logcat,save exception log in exception.log,multi tags separate use ; 15 | exceptionlog=fatal exception;has died 16 | #monkey test,true will enable,other disable 17 | monkey= 18 | #test results save path,forbidden space, default None,will save in mobileperf/results 19 | #example save_path=/Users/look/Desktop/project/mobileperf_output 20 | save_path= 21 | #device pull path,test end,tool pull path to PC,multi path separate use; 22 | phone_log_path=/data/anr 23 | #mailbox Reserved, no use 24 | mailbox=390125133@qq.com -------------------------------------------------------------------------------- /mobileperf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf.png -------------------------------------------------------------------------------- /mobileperf/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | 性能测试库 4 | ''' 5 | #python版本检查 6 | import sys 7 | if sys.version_info[0] != 3: 8 | raise RuntimeError(u'当前python版本为:%s, 请使用python3' % sys.version) 9 | -------------------------------------------------------------------------------- /mobileperf/android/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | Android平台性能测试库 4 | ''' -------------------------------------------------------------------------------- /mobileperf/android/devicemonitor.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import os 12 | import re 13 | 14 | import sys,csv 15 | import threading 16 | import random 17 | import time 18 | import traceback 19 | 20 | BaseDir=os.path.dirname(__file__) 21 | sys.path.append(os.path.join(BaseDir,'../..')) 22 | from mobileperf.common.log import logger 23 | from mobileperf.android.tools.androiddevice import AndroidDevice 24 | from mobileperf.android.globaldata import RuntimeData 25 | from mobileperf.common.utils import TimeUtils 26 | 27 | class DeviceMonitor(object): 28 | ''' 29 | 一个监控类,监控手机中的一些状态变化,目前监控应用是否卸载,获取前台正在活动的activity 30 | ''' 31 | def __init__(self, device_id, packagename, interval = 1.0,main_activity=[],activity_list=[],event=None,activity_queue = None): 32 | '''' 33 | :param list main_activity 指定模块的主入口 34 | :param list activity_list : 限制默认范围的activity列表,默认为空,则不限制 35 | ''' 36 | self.uninstall_flag = event 37 | self.device = AndroidDevice(device_id) 38 | self.packagename = packagename 39 | self.interval = interval 40 | self.main_activity = main_activity 41 | self.activity_list = activity_list 42 | self.stop_event = threading.Event() 43 | self.activity_queue = activity_queue 44 | self.current_activity = None 45 | 46 | 47 | def start(self, starttime): 48 | self.activity_monitor_thread = threading.Thread(target=self._activity_monitor_thread) 49 | self.activity_monitor_thread.start() 50 | logger.debug("DeviceMonitor activitymonitor has started...") 51 | 52 | # self.uninstaller_checker_thread = threading.Thread(target=self._uninstaller_checker_thread) 53 | # self.uninstaller_checker_thread.start() 54 | # logger.debug("DeviceMonitor uninstaller checker has started...") 55 | 56 | def stop(self): 57 | if self.activity_monitor_thread.isAlive(): 58 | self.stop_event.set() 59 | self.activity_monitor_thread.join(timeout=1) 60 | self.activity_monitor_thread = None 61 | if self.activity_queue: 62 | self.activity_queue.task_done() 63 | logger.debug("DeviceMonitor stopped!") 64 | 65 | 66 | def _activity_monitor_thread(self): 67 | activity_title = ("datetime", "current_activity") 68 | self.activity_file = os.path.join(RuntimeData.package_save_path, 'current_activity.csv') 69 | try: 70 | with open(self.activity_file, 'a+') as af: 71 | csv.writer(af, lineterminator='\n').writerow(activity_title) 72 | except Exception as e: 73 | logger.error("file not found: " + str(self.activity_file)) 74 | 75 | while not self.stop_event.is_set(): 76 | try: 77 | before = time.time() 78 | self.current_activity = self.device.adb.get_current_activity() 79 | collection_time = time.time() 80 | activity_list = [collection_time, self.current_activity] 81 | if self.activity_queue: 82 | logger.debug("activity monitor thread activity_list: " + str(activity_list)) 83 | self.activity_queue.put(activity_list) 84 | if self.current_activity: 85 | logger.debug("current activity: " + self.current_activity) 86 | if self.main_activity and self.activity_list: 87 | if self.current_activity not in self.activity_list: 88 | start_activity = self.packagename + "/" + self.main_activity[ 89 | random.randint(0, len(self.main_activity) - 1)] 90 | logger.debug("start_activity:" + start_activity) 91 | self.device.adb.start_activity(start_activity) 92 | activity_tuple=(TimeUtils.getCurrentTime(),self.current_activity) 93 | # 写文件 94 | try: 95 | with open(self.activity_file, 'a+',encoding="utf-8") as writer: 96 | writer_p = csv.writer(writer, lineterminator='\n') 97 | writer_p.writerow(activity_tuple) 98 | except RuntimeError as e: 99 | logger.error(e) 100 | time_consume = time.time() - before 101 | delta_inter = self.interval - time_consume 102 | logger.debug("get app activity time consumed: " + str(time_consume)) 103 | if delta_inter > 0 : 104 | time.sleep(delta_inter) 105 | except Exception as e: 106 | s = traceback.format_exc() 107 | logger.debug(s) # 将堆栈信息打印到log中 108 | if self.activity_queue: 109 | self.activity_queue.task_done() 110 | 111 | # 这个检查频率不用那么高 112 | def _uninstaller_checker_thread(self): 113 | ''' 114 | 这个方法用轮询的方式查询指定的应用是否被卸载,一旦卸载会往主线程发送一个卸载的信号,终止程序 115 | :return: 116 | ''' 117 | while not self.stop_event.is_set(): 118 | before = time.time() 119 | is_installed = self.device.adb.is_app_installed(self.packagename) 120 | if not is_installed: 121 | if self.uninstall_flag and isinstance(self.uninstall_flag, threading._Event): 122 | logger.debug("uninstall flag is set, as the app has checked uninstalled!") 123 | self.uninstall_flag.set() 124 | time_consume = time.time() - before 125 | delta_inter = self.interval*10 - time_consume 126 | logger.debug("check installed app: " + self.packagename +", time consumed: " + str(time_consume) + ", is installed: " + str(is_installed)) 127 | if delta_inter > 0: 128 | time.sleep(delta_inter) 129 | 130 | 131 | if __name__ == '__main__': 132 | monitor = DeviceMonitor("NVGILZSO99999999", "com.taobao.taobao", 2) 133 | monitor.start(time.time()) 134 | time.sleep(60) 135 | monitor.stop() 136 | -------------------------------------------------------------------------------- /mobileperf/android/excel.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | ''' 3 | @author: Juncheng Chen 4 | 5 | @copyright: 1999-2015 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: juncheng.cjc@outlook.com 10 | ''' 11 | import csv,os,sys 12 | 13 | BaseDir=os.path.dirname(__file__) 14 | sys.path.append(os.path.join(BaseDir,'../..')) 15 | from mobileperf.android.globaldata import RuntimeData 16 | from mobileperf.extlib import xlsxwriter 17 | from mobileperf.common.log import logger 18 | 19 | class Excel(object): 20 | 21 | def __init__(self, excel_file): 22 | self.excel_file = excel_file 23 | self.workbook = xlsxwriter.Workbook(excel_file) 24 | self.color_list = ["blue", "green", "red", "yellow","purple"] 25 | 26 | def add_sheet(self, sheet_name, x_axis, y_axis, headings, lines): 27 | worksheet = self.workbook.add_worksheet(sheet_name) 28 | worksheet.write_row('A1', headings) 29 | for i, line in enumerate(lines, 2): 30 | worksheet.write_row('A%d' % i, line) 31 | columns = len(headings) 32 | rows = len(lines) 33 | if columns > 1 and rows > 1: 34 | chart = self.workbook.add_chart({'type': 'line'}) 35 | for j in range(1, columns): 36 | chart.add_series({'name': [sheet_name, 0, j], 37 | 'categories': [sheet_name, 1, 0, rows, 0], 38 | 'values': [sheet_name, 1, j, rows, j]}) 39 | chart.set_title ({'name': sheet_name.replace('.', ' ').title()}) 40 | chart.set_x_axis({'name': x_axis}) 41 | chart.set_y_axis({'name': y_axis}) 42 | worksheet.insert_chart('B3', chart, {'x_scale': 2, 'y_scale': 2}) 43 | 44 | def save(self): 45 | self.workbook.close() 46 | 47 | def csv_to_xlsx(self, csv_file, sheet_name, x_axis,y_axis, y_fields=[]): 48 | ''' 49 | 把csv的数据存到excel中,并画曲线 50 | csv_file csv 文件路径 表格名 51 | sheet_name 图表名 52 | x_axis 横轴名 和 表中做横轴字段名 53 | y_axis 纵轴名 54 | y_fields 纵轴表中数据字段名 ,可以多个 55 | ''' 56 | filename = os.path.splitext(os.path.basename(csv_file))[0] 57 | logger.debug("filename:"+filename) 58 | worksheet = self.workbook.add_worksheet(filename) # 创建一个sheet表格 59 | with open(csv_file, 'r') as f: 60 | read = csv.reader(f) 61 | # 行数 62 | l = 0 63 | # 表头 64 | headings = [] 65 | for line in read: 66 | # print(line) 67 | r = 0 68 | for i in line: 69 | # print(i) 70 | if self.is_number(i): 71 | worksheet.write(l, r, float(i)) # 一个一个将单元格数据写入 72 | else: 73 | worksheet.write(l, r,i) 74 | r = r + 1 75 | if l==0: 76 | headings=line 77 | l = l + 1 78 | # 列数 79 | columns = len(headings) 80 | # 求出展示数据索引 81 | indexs=[] 82 | # 求出系列名所在索引 83 | series_index=[] 84 | for columu_name in y_fields: 85 | indexs.extend([i for i, v in enumerate(headings) if v == columu_name]) 86 | series_index.extend([i for i, v in enumerate(headings) if v == "package"]) 87 | logger.debug("series_index") 88 | logger.debug(series_index) 89 | if columns > 1 and l>2: 90 | chart = self.workbook.add_chart({'type': 'line'}) 91 | # 画图 92 | i =0 93 | for index in indexs: 94 | if "pid_cpu%" == headings[index] or "pid_pss(MB)" == headings[index]: 95 | chart.add_series({ 96 | # 这个是series 系列名 包名 97 | 'name': [filename,1,series_index[i]], 98 | 'categories': [filename, 1, 0, l - 1, 0], 99 | 'values': [filename, 1, index, l - 1, index], 100 | 'line':{'color': self.color_list[index%len(self.color_list)]} 101 | }) 102 | i = i+1 103 | else: 104 | chart.add_series({ 105 | 'name': [filename, 0, index], 106 | 'categories': [filename, 1, 0, l - 1, 0], 107 | 'values': [filename, 1, index, l - 1, index], 108 | 'line': {'color': self.color_list[index % len(self.color_list)]} 109 | }) 110 | # 图表名 111 | chart.set_title ({'name':sheet_name}) 112 | chart.set_x_axis({'name': x_axis}) 113 | chart.set_y_axis({'name': y_axis}) 114 | worksheet.insert_chart('L3', chart, {'x_scale': 2, 'y_scale': 2}) 115 | 116 | 117 | def is_number(self,s): 118 | try: 119 | float(s) 120 | return True 121 | except ValueError: 122 | pass 123 | 124 | try: 125 | import unicodedata 126 | unicodedata.numeric(s) 127 | return True 128 | except (TypeError, ValueError): 129 | pass 130 | 131 | return False 132 | 133 | if __name__ == '__main__': 134 | book_name = 'summary.xlsx' 135 | excel = Excel(book_name) 136 | # excel.csv_to_xlsx("mem_infos_10-42-38.csv","meminfo","datetime","mem(MB)",["pid_pss(MB)","pid_alloc_heap(MB)"]) 137 | excel.csv_to_xlsx("/Users/look/Desktop/project/mobileperf-mac/results/com.alibaba.ailabs.genie.launcher/2019_03_05_23_55_28/cpuinfo.csv", 138 | "pid_cpu","datetime","%",["pid_cpu%","total_pid_cpu%"]) 139 | excel.save() 140 | -------------------------------------------------------------------------------- /mobileperf/android/fd.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import csv 12 | import os 13 | import re 14 | import sys 15 | import threading 16 | import time 17 | import traceback 18 | 19 | BaseDir=os.path.dirname(__file__) 20 | sys.path.append(os.path.join(BaseDir,'../..')) 21 | 22 | from mobileperf.android.tools.androiddevice import AndroidDevice 23 | from mobileperf.common.utils import TimeUtils 24 | from mobileperf.common.log import logger 25 | from mobileperf.android.globaldata import RuntimeData 26 | 27 | class FdInfoPackageCollector(object): 28 | def __init__(self, device, pacakgename, interval=1.0, timeout =24 * 60 * 60,fd_queue = None): 29 | self.device = device 30 | self.packagename = pacakgename 31 | self._interval = interval 32 | self._timeout = timeout 33 | self._stop_event = threading.Event() 34 | self.fd_queue = fd_queue 35 | 36 | 37 | def start(self,start_time): 38 | logger.debug("INFO: FdInfoPackageCollector start... ") 39 | self.collect_fd_thread = threading.Thread(target=self._collect_fd_thread, args=(start_time,)) 40 | self.collect_fd_thread.start() 41 | 42 | def stop(self): 43 | logger.debug("INFO: FdInfoPackageCollector stop... ") 44 | if (self.collect_fd_thread.isAlive()): 45 | self._stop_event.set() 46 | self.collect_fd_thread.join(timeout=1) 47 | self.collect_fd_thread = None 48 | #结束的时候,发送一个任务完成的信号,以结束队列 49 | if self.fd_queue: 50 | self.fd_queue.task_done() 51 | 52 | def get_process_fd(self, process): 53 | pid = self.device.adb.get_pid_from_pck(self.packagename) 54 | global old_pid 55 | # pid发生变化 ,更新old_pid,这个时间间隔长 56 | if None == RuntimeData.old_pid or RuntimeData.old_pid!=pid: 57 | RuntimeData.old_pid = pid 58 | out = self.device.adb.run_shell_cmd('ls -lt /proc/%s/fd' % pid) 59 | collection_time = time.time() 60 | logger.debug("collection time in fd info is : " + str(collection_time)) 61 | if out: 62 | fd_num = len(out.split("\n")) 63 | return [collection_time,self.packagename,pid,fd_num] 64 | else: 65 | return [] 66 | 67 | def _collect_fd_thread(self, start_time): 68 | end_time = time.time() + self._timeout 69 | fd_list_titile = ("datatime", "packagename", "pid", "fd_num") 70 | fd_file = os.path.join(RuntimeData.package_save_path, 'fd_num.csv') 71 | try: 72 | with open(fd_file, 'a+') as df: 73 | csv.writer(df, lineterminator='\n').writerow(fd_list_titile) 74 | if self.fd_queue: 75 | fd_file_dic = {'fd_file': fd_file} 76 | self.fd_queue.put(fd_file_dic) 77 | except RuntimeError as e: 78 | logger.error(e) 79 | 80 | while not self._stop_event.is_set() and time.time() < end_time: 81 | try: 82 | before = time.time() 83 | logger.debug("-----------into _collect_fd_thread loop, thread is : " + str(threading.current_thread().name)) 84 | 85 | # 获取pakagename的fd信息 86 | fd_pck_info = self.get_process_fd(self.packagename) 87 | current_time = TimeUtils.getCurrentTime() 88 | if not fd_pck_info: 89 | continue 90 | else: 91 | logger.debug( 92 | "current time: " + current_time + ", processname: " +fd_pck_info[1]+ ", pid: " + str(fd_pck_info[2]) + 93 | " fd num: " + str(fd_pck_info[3])) 94 | if self.fd_queue: 95 | self.fd_queue.put(fd_pck_info) 96 | if not self.fd_queue:#为了本地单个文件运行 97 | try: 98 | with open(fd_file, 'a+',encoding="utf-8") as fd_writer: 99 | writer_p = csv.writer(fd_writer, lineterminator='\n') 100 | fd_pck_info[0] = current_time 101 | writer_p.writerow(fd_pck_info) 102 | except RuntimeError as e: 103 | logger.error(e) 104 | 105 | after = time.time() 106 | time_consume = after - before 107 | delta_inter = self._interval - time_consume 108 | logger.debug("time_consume for fd infos: " + str(time_consume)) 109 | if delta_inter > 0: 110 | time.sleep(delta_inter) 111 | except: 112 | logger.error("an exception hanpend in fdinfo thread, reason unkown!") 113 | s = traceback.format_exc() 114 | logger.debug(s) 115 | if self.fd_queue: 116 | self.fd_queue.task_done() 117 | 118 | class FdMonitor(object): 119 | def __init__(self, device_id, packagename, interval = 1.0,timeout=24*60*60, fd_queue = None): 120 | self.device = AndroidDevice(device_id) 121 | if not packagename: 122 | packagename = self.device.adb.get_foreground_process() 123 | self.fd_package_collector = FdInfoPackageCollector(self.device, packagename, interval, timeout,fd_queue) 124 | 125 | def start(self,start_time): 126 | self.start_time = start_time 127 | self.fd_package_collector.start(start_time) 128 | 129 | def stop(self): 130 | self.fd_package_collector.stop() 131 | 132 | def save(self): 133 | pass 134 | 135 | if __name__ == "__main__": 136 | monitor = FdMonitor("","com.yunos.tv.alitvasr",3) 137 | monitor.start(TimeUtils.getCurrentTime()) 138 | time.sleep(20) 139 | monitor.stop() 140 | # monitor.save() 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /mobileperf/android/globaldata.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import threading 12 | 13 | # 记录运行时需要共享的全局变量 14 | class RuntimeData(): 15 | # 记录pid变更前的pid 16 | old_pid = None 17 | packages = None 18 | package_save_path = None 19 | start_time = None 20 | exit_event = threading.Event() 21 | top_dir = None 22 | config_dic = {} 23 | -------------------------------------------------------------------------------- /mobileperf/android/logcat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | '''logcat监控器 12 | ''' 13 | import os,sys,csv 14 | import re 15 | import time 16 | 17 | BaseDir=os.path.dirname(__file__) 18 | sys.path.append(os.path.join(BaseDir,'../..')) 19 | 20 | from mobileperf.android.tools.androiddevice import AndroidDevice 21 | from mobileperf.common.basemonitor import Monitor 22 | from mobileperf.common.utils import TimeUtils,FileUtils 23 | from mobileperf.common.utils import ms2s 24 | from mobileperf.common.log import logger 25 | from mobileperf.android.globaldata import RuntimeData 26 | 27 | class LogcatMonitor(Monitor): 28 | '''logcat监控器 29 | ''' 30 | def __init__(self, device_id, package=None, **regx_config): 31 | '''构造器 32 | 33 | :param str device_id: 设备id 34 | :param list package : 监控的进程列表,列表为空时,监控所有进程 35 | :param dict regx_config : 日志匹配配置项{conf_id = regx},如:AutoMonitor=ur'AutoMonitor.*:(.*), cost=(\d+)' 36 | ''' 37 | super(LogcatMonitor, self).__init__(**regx_config) 38 | self.package = package # 监控的进程列表 39 | self.device_id = device_id 40 | self.device = AndroidDevice(device_id) # 设备 41 | self.running = False # logcat监控器的启动状态(启动/结束) 42 | self.launchtime = LaunchTime(self.device_id, self.package) 43 | self.exception_log_list = [] 44 | self.start_time = None 45 | 46 | self.append_log_line_num = 0 47 | self.file_log_line_num = 0 48 | self.log_file_create_time = None 49 | 50 | def start(self,start_time): 51 | '''启动logcat日志监控器 52 | ''' 53 | self.start_time = start_time 54 | # 注册启动日志处理回调函数为handle_lauchtime 55 | self.add_log_handle(self.launchtime.handle_launchtime) 56 | logger.debug("logcatmonitor start...") 57 | # 捕获所有进程日志 58 | # https://developer.android.com/studio/command-line/logcat #alternativeBuffers 59 | # 默认缓冲区 main system crash,输出全部缓冲区 60 | if not self.running: 61 | self.device.adb.start_logcat(RuntimeData.package_save_path, [], ' -b all') 62 | time.sleep(1) 63 | self.running = True 64 | 65 | def stop(self): 66 | '''结束logcat日志监控器 67 | ''' 68 | logger.debug("logcat monitor: stop...") 69 | self.remove_log_handle(self.launchtime.handle_launchtime) # 删除回调 70 | logger.debug("logcat monitor: stopped") 71 | if self.exception_log_list: 72 | self.remove_log_handle(self.handle_exception) 73 | self.device.adb.stop_logcat() 74 | self.running = False 75 | 76 | def parse(self, file_path): 77 | pass 78 | 79 | def set_exception_list(self,exception_log_list): 80 | self.exception_log_list = exception_log_list 81 | 82 | def add_log_handle(self, handle): 83 | '''添加实时日志处理器,每产生一条日志,就调用一次handle 84 | ''' 85 | self.device.adb._logcat_handle.append(handle) 86 | 87 | def remove_log_handle(self, handle): 88 | '''删除实时日志处理器 89 | ''' 90 | self.device.adb._logcat_handle.remove(handle) 91 | 92 | def handle_exception(self, log_line): 93 | ''' 94 | 这个方法在每次有log时回调 95 | :param log_line:最近一条的log 内容 96 | 异常日志写一个文件 97 | :return:void 98 | ''' 99 | 100 | for tag in self.exception_log_list: 101 | if tag in log_line: 102 | logger.debug("exception Info: " + log_line) 103 | tmp_file = os.path.join(RuntimeData.package_save_path, 'exception.log') 104 | with open(tmp_file, 'a+',encoding="utf-8") as f: 105 | f.write(log_line + '\n') 106 | # 这个路径 空格会有影响 107 | process_stack_log_file = os.path.join(RuntimeData.package_save_path, 'process_stack_%s_%s.log' % ( 108 | self.package, TimeUtils.getCurrentTimeUnderline())) 109 | # 如果进程挂了,pid会变 ,抓变后进程pid的堆栈没有意义 110 | # self.logmonitor.device.adb.get_process_stack(self.package,process_stack_log_file) 111 | if RuntimeData.old_pid: 112 | self.device.adb.get_process_stack_from_pid(RuntimeData.old_pid, process_stack_log_file) 113 | 114 | 115 | class LaunchTime(object): 116 | 117 | def __init__(self,deviceid, packagename = ""): 118 | # 列表的容积应该不用担心,与系统有一定关系,一般存几十万条数据没问题的 119 | self.launch_list = [("datetime","packagenme/activity","this_time(s)","total_time(s)","launchtype")] 120 | self.packagename = packagename 121 | 122 | def handle_launchtime(self, log_line): 123 | ''' 124 | 这个方法在每次一个启动时间的log产生时回调 125 | :param log_line:最近一条的log 内容 126 | :param tag:启动的方式,是normal的启动,还是自定义方式的启动:fullydrawnlaunch 127 | #如果监控到到fully drawn这样的log,则优先统计这种log,它表示了到起始界面自定义界面的启动时间 128 | :return:void 129 | ''' 130 | # logger.debug(log_line) 131 | # 08-28 10:57:30.229 18882 19137 D IC5: CLogProducer == > code = 0, uuid = 4FE71E350379C64611CCD905938C10CA, eventType = performance, eventName = am_activity_launch_timeme, \ 132 | # log_time = 2019-08-28 10:57:30.229, contextInfo = {"tag": "am_activity_launch_time", "start_time": "2019-08-28 10:57:16", 133 | # "activity_name_original": "com.android.settings\/.FallbackHome", 134 | # "activity_name": "com.android.settings#com.android.settings.FallbackHome", 135 | # "this_time": "916", "total_time": "916", "start_type": "code_start", 136 | # "gmt_create": "2019-08-28 10:57:16.742", "uploadtime": "2019-08-28 10:57:30.173", 137 | # "boottime": "2019-08-28 10:57:18.502", "firstupload": "2019-08-28 10:57:25.733"} 138 | ltag = "" 139 | if ("am_activity_launch_time" in log_line or "am_activity_fully_drawn_time" in log_line): 140 | # 最近增加的一条如果是启动时间相关的log,那么回调所有注册的_handle 141 | if "am_activity_launch_time" in log_line: 142 | ltag = "normal launch" 143 | elif "am_activity_fully_drawn_time" in log_line: 144 | ltag = "fullydrawn launch" 145 | logger.debug("launchtime log:"+log_line) 146 | if ltag: 147 | content = [] 148 | timestamp = time.time() 149 | content.append(TimeUtils.formatTimeStamp(timestamp)) 150 | temp_list = log_line.split()[-1].replace("[", "").replace("]", "").split(',')[2:5] 151 | for i in range(len(temp_list)): 152 | content.append(temp_list[i]) 153 | content.append(ltag) 154 | logger.debug("Launch Info: "+str(content)) 155 | if len(content) == 5: 156 | content = self.trim_value(content) 157 | if content: 158 | self.update_launch_list(content,timestamp) 159 | 160 | def trim_value(self, content): 161 | try: 162 | content[2] = ms2s(float(content[2]))#将this_time转化单位转化为s 163 | content[3] = ms2s(float(content[3]))#将total_time 转化为s 164 | except Exception as e: 165 | logger.error(e) 166 | return [] 167 | return content 168 | 169 | def update_launch_list(self, content,timestamp): 170 | # if self.packagename in content[1]: 171 | self.launch_list.append(content) 172 | tmp_file = os.path.join(RuntimeData.package_save_path, 'launch_logcat.csv') 173 | perf_data = {"task_id":"",'launch_time':[],'cpu':[],"mem":[], 174 | 'traffic':[], "fluency":[],'power':[],} 175 | dic = {"time": timestamp, 176 | "act_name": content[1], 177 | "this_time": content[2], 178 | "total_time": content[3], 179 | "launch_type": content[4]} 180 | perf_data['launch_time'].append(dic) 181 | # perf_queue.put(perf_data) 182 | 183 | with open(tmp_file,"a+",encoding="utf-8") as f: 184 | csvwriter = csv.writer(f, lineterminator='\n')#这种方式可以去除csv的空行 185 | logger.debug("save launchtime data to csv: " + str(self.launch_list)) 186 | csvwriter.writerows(self.launch_list) 187 | del self.launch_list[:] 188 | 189 | if __name__ == '__main__': 190 | logcat_monitor = LogcatMonitor("85I7UO4PFQCINJL7", "com.yunos.tv.alitvasr") 191 | # 如果有异常日志标志,才启动这个模块 192 | exceptionlog_list=["fatal exception","has died"] 193 | if exceptionlog_list: 194 | logcat_monitor.set_exception_list(exceptionlog_list) 195 | logcat_monitor.add_log_handle(logcat_monitor.handle_exception) 196 | start_time = TimeUtils.getCurrentTimeUnderline() 197 | RuntimeData.package_save_path = os.path.join(FileUtils.get_top_dir(), 'results', "com.yunos.tv.alitvasr", start_time) 198 | logcat_monitor.start(start_time) 199 | -------------------------------------------------------------------------------- /mobileperf/android/monkey.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import csv 12 | import os 13 | import re 14 | import sys 15 | import threading 16 | import time 17 | import random 18 | import traceback 19 | 20 | BaseDir = os.path.dirname(__file__) 21 | sys.path.append(os.path.join(BaseDir, '../..')) 22 | 23 | from mobileperf.android.tools.androiddevice import AndroidDevice 24 | from mobileperf.common.utils import TimeUtils,FileUtils 25 | from mobileperf.common.log import logger 26 | from mobileperf.android.globaldata import RuntimeData 27 | 28 | 29 | class Monkey(object): 30 | ''' 31 | monkey执行器 32 | ''' 33 | 34 | def __init__(self, device_id, package=None,timeout=1200000000): 35 | '''构造器 36 | 37 | :param str device_id: 设备id 38 | :param str process : monkey测试的包名 39 | :param timeout : monkey时长 单位 分钟 默认无穷大 40 | ''' 41 | self.package = package 42 | self.device = AndroidDevice(device_id) # 设备 43 | self.running = False # monkey监控器的启动状态(启动/结束) 44 | self.timeout = timeout 45 | self._stop_event = threading.Event() 46 | 47 | def start(self,start_time): 48 | '''启动monkey 49 | ''' 50 | self.start_time = start_time 51 | if not self.running: 52 | self.running = True 53 | # time.sleep(1) 54 | self.start_monkey(self.package,self.timeout) 55 | 56 | def stop(self): 57 | '''结束monkey 58 | ''' 59 | self.stop_monkey() 60 | 61 | def start_monkey(self, package,timeout): 62 | '''运行monkey进程 63 | ''' 64 | if hasattr(self, '_monkey_running') and self.running == True: 65 | logger.warn(u'monkey process have started,not need start') 66 | return 67 | self.monkey_cmd = 'monkey -p %s -v -v -v -s 1000 ' \ 68 | '--ignore-crashes --ignore-timeouts --ignore-security-exceptions ' \ 69 | '--kill-process-after-error --pct-appswitch 20 --pct-touch 40 ' \ 70 | '--pct-motion 10 --pct-trackball 0 --pct-anyevent 10 --pct-flip 0 --pct-pinchzoom 0 ' \ 71 | '--throttle 1000 %s' % (package, str(timeout)) 72 | self._log_pipe = self.device.adb.run_shell_cmd(self.monkey_cmd, sync=False) 73 | self._monkey_thread = threading.Thread(target=self._monkey_thread_func, args=[RuntimeData.package_save_path]) 74 | # self._monkey_thread.setDaemon(True) 75 | self._monkey_thread.start() 76 | 77 | 78 | def stop_monkey(self): 79 | self.running = False 80 | logger.debug("stop monkey") 81 | if hasattr(self, '_log_pipe'): 82 | if self._log_pipe.poll() == None: #判断logcat进程是否存在 83 | self._log_pipe.terminate() 84 | 85 | def _monkey_thread_func(self,save_dir): 86 | '''获取monkey线程,保存monkey日志,monkey Crash日志暂不处理,后续有需要再处理 87 | ''' 88 | self.append_log_line_num = 0 89 | self.file_log_line_num = 0 90 | self.log_file_create_time = None 91 | log_is_none = 0 92 | logs = [] 93 | logger.debug("monkey_thread_func") 94 | if RuntimeData.start_time is None: 95 | RuntimeData.start_time = TimeUtils.getCurrentTime() 96 | while self.running: 97 | try: 98 | log = self._log_pipe.stdout.readline().strip() 99 | if not isinstance(log, str): 100 | # 先编码为unicode 101 | try: 102 | log = str(log, "utf8") 103 | except Exception as e: 104 | log = repr(log) 105 | logger.error('str error:' + log) 106 | logger.error(e) 107 | if log: 108 | logs.append(log) 109 | self.append_log_line_num = self.append_log_line_num + 1 110 | self.file_log_line_num = self.file_log_line_num + 1 111 | # if self.append_log_line_num > 1000: 112 | if self.append_log_line_num > 100: 113 | if not self.log_file_create_time: 114 | self.log_file_create_time = TimeUtils.getCurrentTimeUnderline() 115 | log_file = os.path.join(save_dir, 116 | 'monkey_%s.log' % self.log_file_create_time) 117 | self.append_log_line_num = 0 118 | # 降低音量,避免音量过大,导致语音指令失败 119 | self.device.adb.run_shell_cmd("input keyevent 25") 120 | self.save(log_file, logs) 121 | logs = [] 122 | # 新建文件 123 | if self.file_log_line_num > 600000: 124 | # if self.file_log_line_num > 200: 125 | self.file_log_line_num = 0 126 | self.log_file_create_time = TimeUtils.getCurrentTimeUnderline() 127 | log_file = os.path.join(save_dir, 'monkey_%s.log' % self.log_file_create_time) 128 | self.save(log_file, logs) 129 | logs = [] 130 | else: 131 | log_is_none = log_is_none + 1 132 | if log_is_none % 1000 == 0: 133 | logger.info("log is none") 134 | if not self.device.adb.is_process_running("com.android.commands.monkey") and self.running: 135 | self.device.adb.kill_process("com.android.commands.monkey") 136 | self._log_pipe = self.device.adb.run_shell_cmd(self.monkey_cmd, sync=False) 137 | except: 138 | logger.error("an exception hanpend in monkey thread, reason unkown!") 139 | s = traceback.format_exc() 140 | logger.debug(s) 141 | 142 | def save(self, save_file_path, loglist): 143 | monkey_file = os.path.join(save_file_path) 144 | with open(monkey_file, 'a+', encoding="utf-8") as log_f: 145 | for log in loglist: 146 | log_f.write(log + "\n") 147 | 148 | 149 | if __name__ == "__main__": 150 | test_pacakge_list = ["com.alibaba.ailabs.genie.musicplayer","com.alibaba.ailabs.genie.contacts","com.alibaba.ailabs.genie.launcher", 151 | "com.alibaba.ailabs.genie.shopping","com.youku.iot"] 152 | device = AndroidDevice() 153 | # device.adb.kill_process("monkey") 154 | # for i in range(0, 10): 155 | # for package in test_pacakge_list: 156 | # monkey = Monkey("",package,1200000000) 157 | # monkey.start(TimeUtils.getCurrentTimeUnderline()) 158 | # time.sleep(60*60*2) 159 | # monkey.stop() 160 | start_time = TimeUtils.getCurrentTimeUnderline() 161 | logger.debug(start_time) 162 | RuntimeData.top_dir = FileUtils.get_top_dir() 163 | RuntimeData.package_save_path = os.path.join(RuntimeData.top_dir, 'results', "com.alibaba.ailabs.genie.contacts", start_time) 164 | main_activity = ["com.alibaba.ailabs.genie.contacts.MainActivity"] 165 | activity_list = ["com.alibaba.ailabs.genie.contacts.MainActivity", 166 | "com.alibaba.ailabs.genie.contacts.cmd.CmdDispatchActivity", 167 | "com.alibaba.ailabs.genie.contacts.cmd.transform.VoipToPstnActivity", 168 | "com.alibaba.ailabs.genie.contacts.add.AddContactsActivity"] 169 | monkey = Monkey("WST4DYVWKBFEV8Q4","com.alibaba.ailabs.genie.smartapp") 170 | monkey.start(start_time) 171 | -------------------------------------------------------------------------------- /mobileperf/android/powerconsumption.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | ''' 12 | 这个类搜集的是手机整机的电池方面的信息,包括电流,电量,电压,电池温度,充电情况等 13 | ''' 14 | import csv 15 | import os 16 | import re 17 | import sys 18 | import threading 19 | 20 | import time 21 | import traceback 22 | 23 | BaseDir=os.path.dirname(__file__) 24 | sys.path.append(os.path.join(BaseDir,'../..')) 25 | from mobileperf.android.tools.androiddevice import AndroidDevice 26 | from mobileperf.common.utils import TimeUtils 27 | from mobileperf.common.utils import transfer_temp 28 | from mobileperf.common.utils import mV2V 29 | from mobileperf.common.utils import uA2mA 30 | from mobileperf.common.log import logger 31 | from mobileperf.android.globaldata import RuntimeData 32 | 33 | class DevicePowerInfo(object): 34 | RE_BATTERY = re.compile(r'level: (\d+) voltage: (\d+) temp: (\d+)') 35 | RE_CURRENT = re.compile(r'current now: (\S?\d+)') 36 | 37 | def __init__(self, source=None): 38 | ''' 39 | :param source: dumpsys batteryproperties 40 | :param device: 41 | ''' 42 | self.source = source 43 | self.level = 0 #电池的电量一般以100为总刻度 44 | self.voltage = 0#电压 45 | self.temp = 0#温度 46 | self.current = 0#电流,这个电流值来自于设备的底层上报,准确性取决于具体的设备,可以作为参考 47 | self._parse() 48 | 49 | def _parse(self): 50 | # logger.debug("into _parse") 51 | if self.source: 52 | match = self.RE_BATTERY.search(self.source) 53 | if (match): 54 | self.level = match.group(1) 55 | self.voltage = match.group(2) 56 | self.temp = match.group(3) 57 | # logger.debug("into parse level: "+str(self.level) + ", voltage: " + str(self.voltage) + ", temp: " + str(self.temp)) 58 | 59 | match = self.RE_CURRENT.search(self.source) 60 | if (match): 61 | self.current = match.group(1) 62 | # logger.debug("into parse current: " + str(self.current)) 63 | 64 | def __repr__(self): 65 | return "DevicePowerInfo, " + "level:"+str(self.level) + ", voltage:" + str(self.voltage) + ", temperature:" + str(self.temp) + ", current:" + str(self.current) 66 | 67 | class PowerCollector(object): 68 | def __init__(self, device, interval=1.0,timeout=24*60 * 60,power_queue = None,): 69 | self.device = device 70 | self._interval = interval 71 | self._timeout = timeout 72 | self._stop_event = threading.Event() 73 | self.power_queue = power_queue 74 | 75 | def start(self,start_time): 76 | logger.debug("INFO: PowerCollector start...") 77 | self.collect_power_thread = threading.Thread(target=self._collect_power_thread,args=(start_time,)) 78 | self.collect_power_thread.start() 79 | 80 | def _get_battaryproperties(self): 81 | ''' 82 | :return: 返回电池的相关属性,电量,温度,电压,电流等 83 | ''' 84 | # android 5.0及以上的版本使用该命令获取电池的信息 85 | out = self.device.adb.run_shell_cmd("dumpsys batteryproperties") 86 | out.replace('\r', '') 87 | power_info = None 88 | if not out or(isinstance(out,str) and ("Can't find service") in out) : 89 | #4.0到4.4使用该命令获取电池的信息 90 | logger.debug("get battery info from dumpsys battery") 91 | reg = self.device.adb.run_shell_cmd("dumpsys battery") 92 | reg.replace('\r', '') 93 | power_info = DevicePowerInfo() 94 | power_dic = self._get_powerinfo_dic(reg) 95 | power_info.level = power_dic['level'] 96 | power_info.temp = power_dic['temperature'] 97 | power_info.voltage = power_dic['voltage'] 98 | current_flag = power_dic['current_flag'] 99 | if current_flag == -1: 100 | #在4.0的某些版本上通过dumpsys battery没有电流的信息,通过该命令获取 101 | power_info.current = self._cat_current() 102 | else: 103 | power_info.current = power_dic['current'] 104 | else: 105 | power_info = DevicePowerInfo(out) 106 | if power_info.voltage == '0':#三星的机型上测试会发现这个dump出来的电压,电量,等为0 ,不正确,重新获取下 107 | logger.debug(" power info from dumpsys properties is 0, trim it") 108 | reg = self.device.adb.run_shell_cmd("dumpsys battery") 109 | reg.replace('\r', '') 110 | power_dic = self._get_powerinfo_dic(reg) 111 | power_info.level = power_dic['level'] 112 | power_info.temp = power_dic['temperature'] 113 | power_info.voltage = power_dic['voltage'] 114 | logger.debug(power_info) 115 | return power_info 116 | 117 | def _cat_current(self): 118 | current = 0 119 | # cat /sys/class/power_supply/Battery/current_now Android9 上没权限 120 | reg = self.device.adb.run_shell_cmd('cat /sys/class/power_supply/battery/current_now') 121 | if isinstance(reg, str) and "No such file or directory"==reg: 122 | logger.debug("can't get current from file /sys/class/power_supply/battery/current_now") 123 | elif reg: 124 | current = reg 125 | return current 126 | 127 | 128 | def _get_powerinfo_dic(self, out): 129 | ''' 130 | :param out: 电池的dump信息 131 | :return: 返回电池信息,以字典的方式返回 132 | ''' 133 | dic = {} 134 | if out: 135 | level_l = re.findall(u'level:\s?(\d+)', out) 136 | temp_l = re.findall(u'temperature:\s?(\d+)', out) 137 | current_l = re.findall(u'current now:\s?(\d+)', out) 138 | vol_l = re.findall(u' voltage:\s?(\d+)', out) 139 | vol_ll = re.findall(u' voltage:\s?(\d+)',out) 140 | logger.debug(vol_ll) 141 | dic['level'] = level_l[0] if len(level_l) else 0 142 | dic['temperature'] = temp_l[0] if len(temp_l) else 0 143 | dic['current'] = current_l[0] if len(current_l) else 0 144 | dic['voltage'] = vol_l[0] if len(vol_l) else 0 145 | if len(current_l): 146 | dic['current'] = current_l[0] 147 | dic['current_flag'] = 1 148 | else: 149 | dic['current_flag'] = -1 150 | dic['current'] = 0 151 | return dic 152 | 153 | def _collect_power_thread(self,start_time): 154 | ''' 155 | 搜集电池信息的线程 156 | :return: 157 | ''' 158 | end_time = time.time() + self._timeout 159 | power_list_titile = ("datetime","level","voltage(V)","tempreture(C)","current(mA)") 160 | power_device_file = os.path.join(RuntimeData.package_save_path, 'powerinfo.csv') 161 | try: 162 | with open(power_device_file, 'a+') as df: 163 | csv.writer(df, lineterminator='\n').writerow(power_list_titile) 164 | if self.power_queue: 165 | power_file_dic = {'power_file':power_device_file} 166 | self.power_queue.put(power_file_dic) 167 | except RuntimeError as e: 168 | logger.error(e) 169 | while not self._stop_event.is_set() and time.time() < end_time: 170 | try: 171 | before = time.time() 172 | logger.debug("------------into _collect_power_thread loop thread is : " + str(threading.current_thread().name)) 173 | device_power_info = self._get_battaryproperties() 174 | 175 | if device_power_info.source == '': 176 | logger.debug("can't get power info , break!") 177 | break 178 | device_power_info = self.trim_data(device_power_info)#debug 179 | collection_time = time.time() 180 | logger.debug(" collection time in powerconsumption is : " + str(collection_time)) 181 | power_tmp_list = [collection_time, device_power_info.level, device_power_info.voltage, 182 | device_power_info.temp, device_power_info.current] 183 | 184 | if self.power_queue: 185 | self.power_queue.put(power_tmp_list) 186 | 187 | if not self.power_queue:#为了本地单个脚本运行 188 | power_tmp_list[0] = TimeUtils.formatTimeStamp(power_tmp_list[0]) 189 | try: 190 | with open(power_device_file,'a+',encoding="utf-8") as writer: 191 | writer_p = csv.writer(writer, lineterminator='\n') 192 | writer_p.writerow(power_tmp_list) 193 | except RuntimeError as e: 194 | logger.error(e) 195 | 196 | after = time.time() 197 | time_consume = after - before 198 | delta_inter = self._interval - time_consume 199 | if delta_inter > 0: 200 | time.sleep(delta_inter) 201 | except: 202 | logger.error("an exception hanpend in powerconsumption thread , reason unkown!") 203 | s = traceback.format_exc() 204 | logger.debug(s) 205 | if self.power_queue: 206 | self.power_queue.task_done() 207 | def trim_data(self, power_info): 208 | power_info.voltage = mV2V(float(power_info.voltage)) 209 | power_info.temp = transfer_temp(float(power_info.temp)) 210 | power_info.current = uA2mA(float(power_info.current)) 211 | return power_info 212 | 213 | def stop(self): 214 | ''' 215 | 终止power模块的数据采集工作 216 | :return: 217 | ''' 218 | logger.debug("INFO: PowerCollector stop...") 219 | if (self.collect_power_thread.isAlive()): 220 | self._stop_event.set() 221 | self.collect_power_thread.join(timeout=1) 222 | self.collect_power_thread = None 223 | #线程结束时,需要通知队列结束自己 224 | if self.power_queue: 225 | self.power_queue.task_done() 226 | 227 | class PowerMonitor(object): 228 | def __init__(self, device_id, interval = 1.0, timeout = 24 * 60 * 60,power_queue = None): 229 | self.device = AndroidDevice(device_id) 230 | self.power_collector = PowerCollector(self.device, interval,timeout,power_queue) 231 | 232 | def start(self,start_time): 233 | if not RuntimeData.package_save_path: 234 | RuntimeData.package_save_path = os.path.join(os.path.abspath(os.path.join(os.getcwd(), "../..")),'results',self.device.adb._device_id,start_time) 235 | if not os.path.exists(RuntimeData.package_save_path): 236 | os.makedirs(RuntimeData.package_save_path) 237 | self.start_time = start_time 238 | self.power_collector.start(start_time) 239 | logger.debug("INFO: PowerMonitor has started...") 240 | 241 | def stop(self): 242 | self.power_collector.stop() 243 | logger.debug("INFO: PowerMonitor has stopped...") 244 | 245 | def _get_power_collector(self): 246 | return self.power_collector 247 | 248 | def save(self): 249 | pass 250 | 251 | if __name__ == "__main__": 252 | monitor = PowerMonitor("DWT7N19306001517",5) 253 | monitor.start(TimeUtils.getCurrentTimeUnderline()) 254 | time.sleep(10) 255 | monitor.stop() 256 | # monitor.save() 257 | -------------------------------------------------------------------------------- /mobileperf/android/report.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import os 12 | from datetime import datetime 13 | 14 | from mobileperf.android.excel import Excel 15 | from mobileperf.common.log import logger 16 | from mobileperf.common.utils import TimeUtils 17 | 18 | class Report(object): 19 | def __init__(self, csv_dir, packages=[]): 20 | os.chdir(csv_dir) 21 | # 需要画曲线的csv文件名 22 | self.summary_csf_file={"cpuinfo.csv":{"table_name":"pid_cpu", 23 | "x_axis":"datatime", 24 | "y_axis":"%", 25 | "values":["pid_cpu%","total_pid_cpu%"]}, 26 | "meminfo.csv":{"table_name":"pid_pss", 27 | "x_axis":"datatime", 28 | "y_axis":"mem(MB)", 29 | "values":["pid_pss(MB)","total_pss(MB)"]}, 30 | "pid_change.csv": {"table_name": "pid", 31 | "x_axis": "datatime", 32 | "y_axis": "pid_num", 33 | "values": ["pid"]}, 34 | } 35 | self.packages = packages 36 | if len(self.packages)>0: 37 | for package in self.packages: 38 | pss_detail_dic ={"table_name":"pss_detail", 39 | "x_axis":"datatime", 40 | "y_axis":"mem(MB)", 41 | "values":["pss","java_heap","native_heap","system"] 42 | } 43 | # 文件名太长会导致写excel失败 44 | self.summary_csf_file["pss_%s.csv"%package.split(".")[-1].replace(":","_")]= pss_detail_dic 45 | logger.debug(self.packages) 46 | logger.debug(self.summary_csf_file) 47 | logger.info('create report for %s' % csv_dir) 48 | file_names = self.filter_file_names(csv_dir) 49 | logger.debug('%s' % file_names) 50 | if file_names: 51 | book_name = 'summary_%s.xlsx' % TimeUtils.getCurrentTimeUnderline() 52 | excel = Excel(book_name) 53 | for file_name in file_names: 54 | logger.debug('get csv %s to excel' % file_name) 55 | values = self.summary_csf_file[file_name] 56 | excel.csv_to_xlsx(file_name,values["table_name"],values["x_axis"],values["y_axis"],values["values"]) 57 | logger.info('wait to save %s' % book_name) 58 | excel.save() 59 | # 60 | def filter_file_names(self, device): 61 | csv_files = [] 62 | logger.debug(device) 63 | for f in os.listdir(device): 64 | if os.path.isfile(os.path.join(device, f)) and os.path.basename(f) in self.summary_csf_file.keys(): 65 | logger.debug(os.path.join(device, f)) 66 | csv_files.append(f) 67 | return csv_files 68 | #return [f for f in os.listdir(device) if os.path.isfile(os.path.join(device, f)) and os.path.basename(f) in self.summary_csf_file.keys()] 69 | 70 | if __name__ == '__main__': 71 | # 根据csv生成excel汇总文件 72 | from mobileperf.android.globaldata import RuntimeData 73 | RuntimeData.packages = ["com.alibaba.ailabs.genie.smartapp","com.alibaba.ailabs.genie.smartapp:core","com.alibaba.ailabs.genie.smartapp:business"] 74 | RuntimeData.package_save_path = "/Users/look/Downloads/mobileperf-turandot-shicun-2-13/results/com.alibaba.ailabs.genie.smartapp/2020_02_13_22_58_14" 75 | report = Report(RuntimeData.package_save_path,RuntimeData.packages) 76 | report.filter_file_names(RuntimeData.package_save_path) 77 | -------------------------------------------------------------------------------- /mobileperf/android/thread_num.py: -------------------------------------------------------------------------------- 1 | #encoding:utf-8 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import csv 12 | import os 13 | import re 14 | import sys 15 | import threading 16 | import time 17 | import traceback 18 | 19 | BaseDir=os.path.dirname(__file__) 20 | sys.path.append(os.path.join(BaseDir,'../..')) 21 | 22 | from mobileperf.android.tools.androiddevice import AndroidDevice 23 | from mobileperf.common.utils import TimeUtils 24 | from mobileperf.common.log import logger 25 | from mobileperf.android.globaldata import RuntimeData 26 | 27 | 28 | class ThreadNumPackageCollector(object): 29 | def __init__(self, device, pacakgename, interval=1.0,timeout =24 * 60 * 60, thread_queue = None): 30 | self.device = device 31 | self.packagename = pacakgename 32 | self._interval = interval 33 | self._timeout = timeout 34 | self._stop_event = threading.Event() 35 | self.thread_queue = thread_queue 36 | 37 | 38 | def start(self,start_time): 39 | logger.debug("INFO: ThreadNum PackageCollector start... ") 40 | self.collect_thread_num_thread = threading.Thread(target=self._collect_thread_num_thread, args=(start_time,)) 41 | self.collect_thread_num_thread.start() 42 | 43 | def stop(self): 44 | logger.debug("INFO: ThreadNumPackageCollector stop... ") 45 | if (self.collect_thread_num_thread.isAlive()): 46 | self._stop_event.set() 47 | self.collect_thread_num_thread.join(timeout=1) 48 | self.collect_thread_num_thread = None 49 | #结束的时候,发送一个任务完成的信号,以结束队列 50 | if self.thread_queue: 51 | self.thread_queue.task_done() 52 | 53 | def get_process_thread_num(self, process): 54 | pid = self.device.adb.get_pid_from_pck(self.packagename) 55 | out = self.device.adb.run_shell_cmd('ls -lt /proc/%s/task' % pid) 56 | collection_time = time.time() 57 | logger.debug("collection time in thread_num info is : " + str(collection_time)) 58 | if out: 59 | # logger.debug("thread num out:"+out) 60 | thread_num = len(out.split("\n")) 61 | return [collection_time,self.packagename,pid,thread_num] 62 | else: 63 | return [] 64 | 65 | def _collect_thread_num_thread(self, start_time): 66 | end_time = time.time() + self._timeout 67 | thread_list_titile = ( 68 | "datatime", "packagename", "pid", "thread_num") 69 | thread_num_file = os.path.join(RuntimeData.package_save_path, 'thread_num.csv') 70 | try: 71 | with open(thread_num_file, 'a+') as df: 72 | csv.writer(df, lineterminator='\n').writerow(thread_list_titile) 73 | if self.thread_queue: 74 | thread_file_dic = {'thread_file': thread_num_file} 75 | self.thread_queue.put(thread_file_dic) 76 | except RuntimeError as e: 77 | logger.error(e) 78 | 79 | while not self._stop_event.is_set() and time.time() < end_time: 80 | try: 81 | before = time.time() 82 | logger.debug("-----------into _collect_thread_num_thread loop, thread is : " + str(threading.current_thread().name)) 83 | 84 | # 获取pakagename的thread num信息 85 | thread_pck_info = self.get_process_thread_num(self.packagename) 86 | logger.debug(thread_pck_info) 87 | current_time = TimeUtils.getCurrentTime() 88 | if not thread_pck_info: 89 | continue 90 | else: 91 | logger.debug( 92 | "current time: " + current_time + ", processname: " + thread_pck_info[1]+ ", pid: " + str(thread_pck_info[2]) + 93 | " thread num: " + str(thread_pck_info[3])) 94 | if self.thread_queue: 95 | self.thread_queue.put(thread_pck_info) 96 | if not self.thread_queue:#为了本地单个文件运行 97 | try: 98 | with open(thread_num_file, 'a+',encoding="utf-8") as thread_writer: 99 | writer_p = csv.writer(thread_writer, lineterminator='\n') 100 | thread_pck_info[0] = current_time 101 | writer_p.writerow(thread_pck_info) 102 | except RuntimeError as e: 103 | logger.error(e) 104 | 105 | after = time.time() 106 | time_consume = after - before 107 | delta_inter = self._interval - time_consume 108 | logger.debug("time_consume for thread num infos: " + str(time_consume)) 109 | if delta_inter > 0: 110 | time.sleep(delta_inter) 111 | except: 112 | logger.error("an exception hanpend in thread num thread, reason unkown!") 113 | s = traceback.format_exc() 114 | logger.debug(s) 115 | if self.thread_queue: 116 | self.thread_queue.task_done() 117 | 118 | class ThreadNumMonitor(object): 119 | def __init__(self, device_id, packagename, interval = 1.0, timeout=24*60*60,thread_queue = None): 120 | self.device = AndroidDevice(device_id) 121 | if not packagename: 122 | packagename = self.device.adb.get_foreground_process() 123 | self.thread_package_collector = ThreadNumPackageCollector(self.device, packagename, interval, timeout,thread_queue) 124 | 125 | def start(self,start_time): 126 | self.start_time = start_time 127 | self.thread_package_collector.start(start_time) 128 | 129 | def stop(self): 130 | self.thread_package_collector.stop() 131 | 132 | def save(self): 133 | pass 134 | 135 | if __name__ == "__main__": 136 | monitor = ThreadNumMonitor("","com.yunos.tv.alitvasr",3) 137 | monitor.start(TimeUtils.getCurrentTime()) 138 | time.sleep(20) 139 | monitor.stop() 140 | # monitor.save() 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /mobileperf/android/tools/AdbWinApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/android/tools/AdbWinApi.dll -------------------------------------------------------------------------------- /mobileperf/android/tools/AdbWinUsbApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/android/tools/AdbWinUsbApi.dll -------------------------------------------------------------------------------- /mobileperf/android/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/android/tools/__init__.py -------------------------------------------------------------------------------- /mobileperf/android/tools/adb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/android/tools/adb.exe -------------------------------------------------------------------------------- /mobileperf/android/tools/platform-tools-latest-darwin/platform-tools/adb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/android/tools/platform-tools-latest-darwin/platform-tools/adb -------------------------------------------------------------------------------- /mobileperf/android/tools/platform-tools-latest-linux/platform-tools/adb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/android/tools/platform-tools-latest-linux/platform-tools/adb -------------------------------------------------------------------------------- /mobileperf/common/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/common/__init__.py -------------------------------------------------------------------------------- /mobileperf/common/basemonitor.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | ''' Monitor 基础能力 12 | ''' 13 | 14 | import logging 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class Monitor(object): 20 | '''性能测试数据采集能力基类 21 | ''' 22 | 23 | def __init__(self, **kwargs): 24 | '''构造器 25 | 26 | :param dict kwargs: 配置项 27 | ''' 28 | self.config = kwargs # 配置项 29 | self.matched_data = {} # 采集到匹配的性能数据 30 | 31 | def start(self): 32 | '''子类中实现该接口,开始采集性能数据''' 33 | logger.warn("请在%s类中实现start方法" % type(self)) 34 | 35 | def clear(self): 36 | '''清空monitor保存的数据''' 37 | self.matched_data = {} 38 | 39 | def stop(self): 40 | '''子类中实现该接口,结束采集性能数据,如果后期需要解析性能数据,需要保存数据文件''' 41 | logger.warn("请在%s类中实现stop方法" % type(self)) 42 | 43 | 44 | def save(self): 45 | '''保存数据 46 | ''' 47 | logger.warn("请在%s类中实现save方法" % type(self)) 48 | 49 | if __name__ == '__main__': 50 | pass -------------------------------------------------------------------------------- /mobileperf/common/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | import os,time 12 | import sys 13 | import re 14 | import logging 15 | import logging.handlers 16 | from logging.handlers import TimedRotatingFileHandler 17 | 18 | BaseDir=os.path.dirname(__file__) 19 | sys.path.append(os.path.join(BaseDir,'../..')) 20 | 21 | from mobileperf.common.utils import FileUtils 22 | logger = logging.getLogger('mobileperf') 23 | logger.setLevel(logging.DEBUG) 24 | fmt = logging.Formatter('[%(asctime)s]%(levelname)s:%(name)s:%(module)s:%(message)s') 25 | streamhandler=logging.StreamHandler(sys.stdout) 26 | streamhandler.setFormatter(fmt) 27 | # 调试时改为DEBUG 发布改为 INFO 28 | streamhandler.setLevel(logging.DEBUG) 29 | dir = os.path.join(FileUtils.get_top_dir(), 'logs') 30 | FileUtils.makedir(dir) 31 | log_file = os.path.join(dir,"mobileperf_log") 32 | log_file_handler = TimedRotatingFileHandler(filename=log_file, when="D", interval=1, backupCount=3) 33 | log_file_handler.suffix = "%Y-%m-%d_%H-%M-%S.log" 34 | log_file_handler.extMatch = re.compile(r"^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}.log$") 35 | log_file_handler.setFormatter(fmt) 36 | log_file_handler.setLevel(logging.DEBUG) 37 | 38 | logger.addHandler(streamhandler) 39 | logger.addHandler(log_file_handler) 40 | 41 | if __name__=="__main__": 42 | logger.debug("测试3!") -------------------------------------------------------------------------------- /mobileperf/common/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | 12 | '''性能相关的工具 13 | ''' 14 | import os 15 | import time,re 16 | import types 17 | import logging 18 | import zipfile 19 | 20 | BaseDir=os.path.dirname(os.path.abspath(__file__)) 21 | # BaseDir=os.getcwd() 22 | 23 | class TimeUtils(object): 24 | UnderLineFormatter = "%Y_%m_%d_%H_%M_%S" 25 | NormalFormatter = "%Y-%m-%d %H-%M-%S" 26 | ColonFormatter = "%Y-%m-%d %H:%M:%S" 27 | 28 | # 文件路径要用这个,mac有空格,很麻烦 29 | @staticmethod 30 | def getCurrentTimeUnderline(): 31 | return time.strftime(TimeUtils.UnderLineFormatter, time.localtime(time.time())) 32 | 33 | @staticmethod 34 | def getCurrentTime(): 35 | return time.strftime(TimeUtils.NormalFormatter, time.localtime(time.time())) 36 | 37 | @staticmethod 38 | def formatTimeStamp(timestamp): 39 | return time.strftime(TimeUtils.NormalFormatter, time.localtime(timestamp)) 40 | 41 | @staticmethod 42 | def getTimeStamp(time_str,format): 43 | timeArray = time.strptime(time_str, format) 44 | # 转换成时间戳 45 | return time.mktime(timeArray) 46 | 47 | @staticmethod 48 | def is_between_times(timestamp,begin_timestamp,end_timestamp): 49 | if begin_timestamp < timestamp and timestamp < end_timestamp: 50 | return True 51 | else: 52 | return False 53 | 54 | 55 | @staticmethod 56 | def get_interval(begin_timestamp,end_timestamp): 57 | ''' 58 | :param begin_timestamp: 59 | :param end_timestamp: 60 | :return:求起止时间戳之间的时间间隔 ,返回H,浮点数 61 | ''' 62 | interval = end_timestamp-begin_timestamp 63 | return round(float(interval)/(60*60)) 64 | 65 | 66 | class FileUtils(object): 67 | 68 | @staticmethod 69 | def makedir(dir): 70 | if (not os.path.exists(dir)): 71 | os.makedirs(dir) 72 | 73 | @staticmethod 74 | def get_top_dir(): 75 | dir = os.path.dirname(BaseDir) 76 | path = os.path.dirname(dir) 77 | print("path:%s"%path) 78 | return path 79 | 80 | @staticmethod 81 | def get_files(path=None, pattern=None): 82 | ''' 83 | :param path: 目录 84 | :param pattern: 正则 85 | :return: 递归遍历目录,返回匹配的文件路径 86 | ''' 87 | file_list = [] 88 | if path is None or not os.path.exists(path): #没有路径返回空 89 | return file_list 90 | for oneFile in os.listdir(path): 91 | if os.path.isfile(os.path.join(path, oneFile)): 92 | match = False 93 | if pattern: 94 | match = re.match(pattern, oneFile, flags=0) 95 | if match: 96 | file_list.append(os.path.join(path, oneFile)) #是文件 且匹配正则成功 则添加文件 97 | elif os.path.isdir(os.path.join(path, oneFile)): 98 | file_list.extend(FileUtils.get_files(os.path.join(path, oneFile), pattern)) #是文件夹 遍历下一级目录下 符合正则的文件 并追加 99 | return file_list 100 | 101 | @staticmethod 102 | def get_FileSize(filePath): 103 | ''' 104 | 获取文件的大小,结果保留4位小数,单位为MB 105 | :param filePath: 106 | :return: 107 | ''' 108 | fsize = os.path.getsize(filePath) 109 | fsize = fsize / float(1024 * 1024) 110 | return round(fsize, 4) 111 | 112 | @staticmethod 113 | def get_FileAccessTime(filePath): 114 | '''获取文件的访问时间''' 115 | t = os.path.getatime(filePath) 116 | return t 117 | 118 | @staticmethod 119 | def get_FileCreateTime(filePath): 120 | '''获取文件的创建时间''' 121 | t = os.path.getctime(filePath) 122 | return t 123 | 124 | @staticmethod 125 | def get_FileModifyTime(filePath): 126 | '''获取文件的修改时间''' 127 | t = os.path.getmtime(filePath) 128 | return t 129 | 130 | class ZipUtils(object): 131 | 132 | @staticmethod 133 | def zip_dir(dirname,zipfilename): 134 | filelist = [] 135 | if os.path.isfile(dirname): 136 | filelist.append(dirname) 137 | else: 138 | for root, dirs, files in os.walk(dirname): 139 | for name in files: 140 | filelist.append(os.path.join(root, name)) 141 | 142 | zf = zipfile.ZipFile(zipfilename, "w", zipfile.zlib.DEFLATED) 143 | for tar in filelist: 144 | arcname = tar[len(dirname):] 145 | # print tar 146 | # print arcname 147 | zf.write(tar, arcname) 148 | zf.close() 149 | 150 | 151 | 152 | def ms2s(value): 153 | return round(value/1000.0, 2) 154 | 155 | def transfer_temp(temp): 156 | return round(temp/10.0,1) 157 | 158 | def mV2V(v): 159 | return round(v/1000.0,2) 160 | 161 | def uA2mA(c): 162 | return round(c/1000.0,2) 163 | 164 | if __name__ == '__main__': 165 | ZipUtils.zip_dir("/Users/look/Desktop/project/mobileperf-turandot/mobileperf/common/test","test.zip") 166 | 167 | -------------------------------------------------------------------------------- /mobileperf/extlib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/extlib/__init__.py -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.1.2' 2 | __VERSION__ = __version__ 3 | from .workbook import Workbook 4 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/app.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # App - A class for writing the Excel XLSX App file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | # Package imports. 9 | from . import xmlwriter 10 | 11 | 12 | class App(xmlwriter.XMLwriter): 13 | """ 14 | A class for writing the Excel XLSX App file. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | 31 | super(App, self).__init__() 32 | 33 | self.part_names = [] 34 | self.heading_pairs = [] 35 | self.properties = {} 36 | 37 | def _add_part_name(self, part_name): 38 | # Add the name of a workbook Part such as 'Sheet1' or 'Print_Titles'. 39 | self.part_names.append(part_name) 40 | 41 | def _add_heading_pair(self, heading_pair): 42 | # Add the name of a workbook Heading Pair such as 'Worksheets', 43 | # 'Charts' or 'Named Ranges'. 44 | 45 | # Ignore empty pairs such as chartsheets. 46 | if not heading_pair[1]: 47 | return 48 | 49 | self.heading_pairs.append(('lpstr', heading_pair[0])) 50 | self.heading_pairs.append(('i4', heading_pair[1])) 51 | 52 | def _set_properties(self, properties): 53 | # Set the document properties. 54 | self.properties = properties 55 | 56 | ########################################################################### 57 | # 58 | # Private API. 59 | # 60 | ########################################################################### 61 | 62 | def _assemble_xml_file(self): 63 | # Assemble and write the XML file. 64 | 65 | # Write the XML declaration. 66 | self._xml_declaration() 67 | 68 | self._write_properties() 69 | self._write_application() 70 | self._write_doc_security() 71 | self._write_scale_crop() 72 | self._write_heading_pairs() 73 | self._write_titles_of_parts() 74 | self._write_manager() 75 | self._write_company() 76 | self._write_links_up_to_date() 77 | self._write_shared_doc() 78 | self._write_hyperlink_base() 79 | self._write_hyperlinks_changed() 80 | self._write_app_version() 81 | 82 | self._xml_end_tag('Properties') 83 | 84 | # Close the file. 85 | self._xml_close() 86 | 87 | ########################################################################### 88 | # 89 | # XML methods. 90 | # 91 | ########################################################################### 92 | 93 | def _write_properties(self): 94 | # Write the element. 95 | schema = 'http://schemas.openxmlformats.org/officeDocument/2006/' 96 | xmlns = schema + 'extended-properties' 97 | xmlns_vt = schema + 'docPropsVTypes' 98 | 99 | attributes = [ 100 | ('xmlns', xmlns), 101 | ('xmlns:vt', xmlns_vt), 102 | ] 103 | 104 | self._xml_start_tag('Properties', attributes) 105 | 106 | def _write_application(self): 107 | # Write the element. 108 | self._xml_data_element('Application', 'Microsoft Excel') 109 | 110 | def _write_doc_security(self): 111 | # Write the element. 112 | self._xml_data_element('DocSecurity', '0') 113 | 114 | def _write_scale_crop(self): 115 | # Write the element. 116 | self._xml_data_element('ScaleCrop', 'false') 117 | 118 | def _write_heading_pairs(self): 119 | # Write the element. 120 | self._xml_start_tag('HeadingPairs') 121 | self._write_vt_vector('variant', self.heading_pairs) 122 | self._xml_end_tag('HeadingPairs') 123 | 124 | def _write_titles_of_parts(self): 125 | # Write the element. 126 | parts_data = [] 127 | 128 | self._xml_start_tag('TitlesOfParts') 129 | 130 | for part_name in self.part_names: 131 | parts_data.append(('lpstr', part_name)) 132 | 133 | self._write_vt_vector('lpstr', parts_data) 134 | 135 | self._xml_end_tag('TitlesOfParts') 136 | 137 | def _write_vt_vector(self, base_type, vector_data): 138 | # Write the element. 139 | attributes = [ 140 | ('size', len(vector_data)), 141 | ('baseType', base_type), 142 | ] 143 | 144 | self._xml_start_tag('vt:vector', attributes) 145 | 146 | for vt_data in vector_data: 147 | if base_type == 'variant': 148 | self._xml_start_tag('vt:variant') 149 | 150 | self._write_vt_data(vt_data) 151 | 152 | if base_type == 'variant': 153 | self._xml_end_tag('vt:variant') 154 | 155 | self._xml_end_tag('vt:vector') 156 | 157 | def _write_vt_data(self, vt_data): 158 | # Write the elements such as and . 159 | self._xml_data_element("vt:%s" % vt_data[0], vt_data[1]) 160 | 161 | def _write_company(self): 162 | company = self.properties.get('company', '') 163 | 164 | self._xml_data_element('Company', company) 165 | 166 | def _write_manager(self): 167 | # Write the element. 168 | if 'manager' not in self.properties: 169 | return 170 | 171 | self._xml_data_element('Manager', self.properties['manager']) 172 | 173 | def _write_links_up_to_date(self): 174 | # Write the element. 175 | self._xml_data_element('LinksUpToDate', 'false') 176 | 177 | def _write_shared_doc(self): 178 | # Write the element. 179 | self._xml_data_element('SharedDoc', 'false') 180 | 181 | def _write_hyperlink_base(self): 182 | # Write the element. 183 | hyperlink_base = self.properties.get('hyperlink_base') 184 | 185 | if hyperlink_base is None: 186 | return 187 | 188 | self._xml_data_element('HyperlinkBase', hyperlink_base) 189 | 190 | def _write_hyperlinks_changed(self): 191 | # Write the element. 192 | self._xml_data_element('HyperlinksChanged', 'false') 193 | 194 | def _write_app_version(self): 195 | # Write the element. 196 | self._xml_data_element('AppVersion', '12.0000') 197 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_area.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartArea - A class for writing the Excel XLSX Area charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | 10 | 11 | class ChartArea(chart.Chart): 12 | """ 13 | A class for writing the Excel XLSX Area charts. 14 | 15 | 16 | """ 17 | 18 | ########################################################################### 19 | # 20 | # Public API. 21 | # 22 | ########################################################################### 23 | 24 | def __init__(self, options=None): 25 | """ 26 | Constructor. 27 | 28 | """ 29 | super(ChartArea, self).__init__() 30 | 31 | if options is None: 32 | options = {} 33 | 34 | self.subtype = options.get('subtype') 35 | 36 | if not self.subtype: 37 | self.subtype = 'standard' 38 | 39 | self.cross_between = 'midCat' 40 | self.show_crosses = 0 41 | 42 | # Override and reset the default axis values. 43 | if self.subtype == 'percent_stacked': 44 | self.y_axis['defaults']['num_format'] = '0%' 45 | 46 | # Set the available data label positions for this chart type. 47 | self.label_position_default = 'center' 48 | self.label_positions = {'center': 'ctr'} 49 | 50 | self.set_y_axis({}) 51 | 52 | ########################################################################### 53 | # 54 | # Private API. 55 | # 56 | ########################################################################### 57 | 58 | def _write_chart_type(self, args): 59 | # Override the virtual superclass method with a chart specific method. 60 | # Write the c:areaChart element. 61 | self._write_area_chart(args) 62 | 63 | ########################################################################### 64 | # 65 | # XML methods. 66 | # 67 | ########################################################################### 68 | # 69 | def _write_area_chart(self, args): 70 | # Write the element. 71 | 72 | if args['primary_axes']: 73 | series = self._get_primary_axes_series() 74 | else: 75 | series = self._get_secondary_axes_series() 76 | 77 | if not len(series): 78 | return 79 | 80 | subtype = self.subtype 81 | 82 | if subtype == 'percent_stacked': 83 | subtype = 'percentStacked' 84 | 85 | self._xml_start_tag('c:areaChart') 86 | 87 | # Write the c:grouping element. 88 | self._write_grouping(subtype) 89 | 90 | # Write the series elements. 91 | for data in series: 92 | self._write_ser(data) 93 | 94 | # Write the c:dropLines element. 95 | self._write_drop_lines() 96 | 97 | # Write the c:axId elements 98 | self._write_axis_ids(args) 99 | 100 | self._xml_end_tag('c:areaChart') 101 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_bar.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartBar - A class for writing the Excel XLSX Bar charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | from warnings import warn 10 | 11 | 12 | class ChartBar(chart.Chart): 13 | """ 14 | A class for writing the Excel XLSX Bar charts. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self, options=None): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | super(ChartBar, self).__init__() 31 | 32 | if options is None: 33 | options = {} 34 | 35 | self.subtype = options.get('subtype') 36 | 37 | if not self.subtype: 38 | self.subtype = 'clustered' 39 | 40 | self.cat_axis_position = 'l' 41 | self.val_axis_position = 'b' 42 | self.horiz_val_axis = 0 43 | self.horiz_cat_axis = 1 44 | self.show_crosses = 0 45 | 46 | # Override and reset the default axis values. 47 | self.x_axis['defaults']['major_gridlines'] = {'visible': 1} 48 | self.y_axis['defaults']['major_gridlines'] = {'visible': 0} 49 | 50 | if self.subtype == 'percent_stacked': 51 | self.x_axis['defaults']['num_format'] = '0%' 52 | 53 | # Set the available data label positions for this chart type. 54 | self.label_position_default = 'outside_end' 55 | self.label_positions = { 56 | 'center': 'ctr', 57 | 'inside_base': 'inBase', 58 | 'inside_end': 'inEnd', 59 | 'outside_end': 'outEnd'} 60 | 61 | self.set_x_axis({}) 62 | self.set_y_axis({}) 63 | 64 | def combine(self, chart=None): 65 | """ 66 | Create a combination chart with a secondary chart. 67 | 68 | Note: Override parent method to add an extra check that is required 69 | for Bar charts to ensure that their combined chart is on a secondary 70 | axis. 71 | 72 | Args: 73 | chart: The secondary chart to combine with the primary chart. 74 | 75 | Returns: 76 | Nothing. 77 | 78 | """ 79 | if chart is None: 80 | return 81 | 82 | if not chart.is_secondary: 83 | warn('Charts combined with Bar charts must be on a secondary axis') 84 | 85 | self.combined = chart 86 | 87 | ########################################################################### 88 | # 89 | # Private API. 90 | # 91 | ########################################################################### 92 | 93 | def _write_chart_type(self, args): 94 | # Override the virtual superclass method with a chart specific method. 95 | if args['primary_axes']: 96 | # Reverse X and Y axes for Bar charts. 97 | tmp = self.y_axis 98 | self.y_axis = self.x_axis 99 | self.x_axis = tmp 100 | 101 | if self.y2_axis['position'] == 'r': 102 | self.y2_axis['position'] = 't' 103 | 104 | # Write the c:barChart element. 105 | self._write_bar_chart(args) 106 | 107 | def _write_bar_chart(self, args): 108 | # Write the element. 109 | 110 | if args['primary_axes']: 111 | series = self._get_primary_axes_series() 112 | else: 113 | series = self._get_secondary_axes_series() 114 | 115 | if not len(series): 116 | return 117 | 118 | subtype = self.subtype 119 | if subtype == 'percent_stacked': 120 | subtype = 'percentStacked' 121 | 122 | # Set a default overlap for stacked charts. 123 | if 'stacked' in self.subtype: 124 | if self.series_overlap_1 is None: 125 | self.series_overlap_1 = 100 126 | 127 | self._xml_start_tag('c:barChart') 128 | 129 | # Write the c:barDir element. 130 | self._write_bar_dir() 131 | 132 | # Write the c:grouping element. 133 | self._write_grouping(subtype) 134 | 135 | # Write the c:ser elements. 136 | for data in series: 137 | self._write_ser(data) 138 | 139 | # Write the c:gapWidth element. 140 | if args['primary_axes']: 141 | self._write_gap_width(self.series_gap_1) 142 | else: 143 | self._write_gap_width(self.series_gap_2) 144 | 145 | # Write the c:overlap element. 146 | if args['primary_axes']: 147 | self._write_overlap(self.series_overlap_1) 148 | else: 149 | self._write_overlap(self.series_overlap_2) 150 | 151 | # Write the c:axId elements 152 | self._write_axis_ids(args) 153 | 154 | self._xml_end_tag('c:barChart') 155 | 156 | ########################################################################### 157 | # 158 | # XML methods. 159 | # 160 | ########################################################################### 161 | 162 | def _write_bar_dir(self): 163 | # Write the element. 164 | val = 'bar' 165 | 166 | attributes = [('val', val)] 167 | 168 | self._xml_empty_tag('c:barDir', attributes) 169 | 170 | def _write_err_dir(self, val): 171 | # Overridden from Chart class since it is not used in Bar charts. 172 | pass 173 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_column.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartColumn - A class for writing the Excel XLSX Column charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | 10 | 11 | class ChartColumn(chart.Chart): 12 | """ 13 | A class for writing the Excel XLSX Column charts. 14 | 15 | 16 | """ 17 | 18 | ########################################################################### 19 | # 20 | # Public API. 21 | # 22 | ########################################################################### 23 | 24 | def __init__(self, options=None): 25 | """ 26 | Constructor. 27 | 28 | """ 29 | super(ChartColumn, self).__init__() 30 | 31 | if options is None: 32 | options = {} 33 | 34 | self.subtype = options.get('subtype') 35 | 36 | if not self.subtype: 37 | self.subtype = 'clustered' 38 | 39 | self.horiz_val_axis = 0 40 | 41 | if self.subtype == 'percent_stacked': 42 | self.y_axis['defaults']['num_format'] = '0%' 43 | 44 | # Set the available data label positions for this chart type. 45 | self.label_position_default = 'outside_end' 46 | self.label_positions = { 47 | 'center': 'ctr', 48 | 'inside_base': 'inBase', 49 | 'inside_end': 'inEnd', 50 | 'outside_end': 'outEnd'} 51 | 52 | self.set_y_axis({}) 53 | 54 | ########################################################################### 55 | # 56 | # Private API. 57 | # 58 | ########################################################################### 59 | 60 | def _write_chart_type(self, args): 61 | # Override the virtual superclass method with a chart specific method. 62 | 63 | # Write the c:barChart element. 64 | self._write_bar_chart(args) 65 | 66 | def _write_bar_chart(self, args): 67 | # Write the element. 68 | 69 | if args['primary_axes']: 70 | series = self._get_primary_axes_series() 71 | else: 72 | series = self._get_secondary_axes_series() 73 | 74 | if not len(series): 75 | return 76 | 77 | subtype = self.subtype 78 | if subtype == 'percent_stacked': 79 | subtype = 'percentStacked' 80 | 81 | # Set a default overlap for stacked charts. 82 | if 'stacked' in self.subtype: 83 | if self.series_overlap_1 is None: 84 | self.series_overlap_1 = 100 85 | 86 | self._xml_start_tag('c:barChart') 87 | 88 | # Write the c:barDir element. 89 | self._write_bar_dir() 90 | 91 | # Write the c:grouping element. 92 | self._write_grouping(subtype) 93 | 94 | # Write the c:ser elements. 95 | for data in series: 96 | self._write_ser(data) 97 | 98 | # Write the c:gapWidth element. 99 | if args['primary_axes']: 100 | self._write_gap_width(self.series_gap_1) 101 | else: 102 | self._write_gap_width(self.series_gap_2) 103 | 104 | # Write the c:overlap element. 105 | if args['primary_axes']: 106 | self._write_overlap(self.series_overlap_1) 107 | else: 108 | self._write_overlap(self.series_overlap_2) 109 | 110 | # Write the c:axId elements 111 | self._write_axis_ids(args) 112 | 113 | self._xml_end_tag('c:barChart') 114 | 115 | ########################################################################### 116 | # 117 | # XML methods. 118 | # 119 | ########################################################################### 120 | 121 | def _write_bar_dir(self): 122 | # Write the element. 123 | val = 'col' 124 | 125 | attributes = [('val', val)] 126 | 127 | self._xml_empty_tag('c:barDir', attributes) 128 | 129 | def _write_err_dir(self, val): 130 | # Overridden from Chart class since it is not used in Column charts. 131 | pass 132 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_doughnut.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartDoughnut - A class for writing the Excel XLSX Doughnut charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from warnings import warn 9 | from . import chart_pie 10 | 11 | 12 | class ChartDoughnut(chart_pie.ChartPie): 13 | """ 14 | A class for writing the Excel XLSX Doughnut charts. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self, options=None): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | super(ChartDoughnut, self).__init__() 31 | 32 | if options is None: 33 | options = {} 34 | 35 | self.vary_data_color = 1 36 | self.rotation = 0 37 | self.hole_size = 50 38 | 39 | def set_hole_size(self, size): 40 | """ 41 | Set the Doughnut chart hole size. 42 | 43 | Args: 44 | size: 10 <= size <= 90. 45 | 46 | Returns: 47 | Nothing. 48 | 49 | """ 50 | if size is None: 51 | return 52 | 53 | # Ensure the size is in Excel's range. 54 | if size < 10 or size > 90: 55 | warn("Chart hole size %d outside Excel range: 10 <= size <= 90" 56 | % size) 57 | return 58 | 59 | self.hole_size = int(size) 60 | 61 | ########################################################################### 62 | # 63 | # Private API. 64 | # 65 | ########################################################################### 66 | 67 | def _write_chart_type(self, args): 68 | # Override the virtual superclass method with a chart specific method. 69 | # Write the c:doughnutChart element. 70 | self._write_doughnut_chart(args) 71 | 72 | ########################################################################### 73 | # 74 | # XML methods. 75 | # 76 | ########################################################################### 77 | 78 | def _write_doughnut_chart(self, args): 79 | # Write the element. Over-ridden method to remove 80 | # axis_id code since Doughnut charts don't require val and cat axes. 81 | self._xml_start_tag('c:doughnutChart') 82 | 83 | # Write the c:varyColors element. 84 | self._write_vary_colors() 85 | 86 | # Write the series elements. 87 | for data in self.series: 88 | self._write_ser(data) 89 | 90 | # Write the c:firstSliceAng element. 91 | self._write_first_slice_ang() 92 | 93 | # Write the c:holeSize element. 94 | self._write_c_hole_size() 95 | 96 | self._xml_end_tag('c:doughnutChart') 97 | 98 | def _write_c_hole_size(self): 99 | # Write the element. 100 | attributes = [('val', self.hole_size)] 101 | 102 | self._xml_empty_tag('c:holeSize', attributes) 103 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_line.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartLine - A class for writing the Excel XLSX Line charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | 10 | 11 | class ChartLine(chart.Chart): 12 | """ 13 | A class for writing the Excel XLSX Line charts. 14 | 15 | 16 | """ 17 | 18 | ########################################################################### 19 | # 20 | # Public API. 21 | # 22 | ########################################################################### 23 | 24 | def __init__(self, options=None): 25 | """ 26 | Constructor. 27 | 28 | """ 29 | super(ChartLine, self).__init__() 30 | 31 | if options is None: 32 | options = {} 33 | 34 | self.default_marker = {'type': 'none'} 35 | self.smooth_allowed = True 36 | 37 | # Set the available data label positions for this chart type. 38 | self.label_position_default = 'right' 39 | self.label_positions = { 40 | 'center': 'ctr', 41 | 'right': 'r', 42 | 'left': 'l', 43 | 'above': 't', 44 | 'below': 'b', 45 | # For backward compatibility. 46 | 'top': 't', 47 | 'bottom': 'b'} 48 | 49 | ########################################################################### 50 | # 51 | # Private API. 52 | # 53 | ########################################################################### 54 | 55 | def _write_chart_type(self, args): 56 | # Override the virtual superclass method with a chart specific method. 57 | # Write the c:lineChart element. 58 | self._write_line_chart(args) 59 | 60 | ########################################################################### 61 | # 62 | # XML methods. 63 | # 64 | ########################################################################### 65 | 66 | def _write_line_chart(self, args): 67 | # Write the element. 68 | 69 | if args['primary_axes']: 70 | series = self._get_primary_axes_series() 71 | else: 72 | series = self._get_secondary_axes_series() 73 | 74 | if not len(series): 75 | return 76 | 77 | self._xml_start_tag('c:lineChart') 78 | 79 | # Write the c:grouping element. 80 | self._write_grouping('standard') 81 | 82 | # Write the series elements. 83 | for data in series: 84 | self._write_ser(data) 85 | 86 | # Write the c:dropLines element. 87 | self._write_drop_lines() 88 | 89 | # Write the c:hiLowLines element. 90 | self._write_hi_low_lines() 91 | 92 | # Write the c:upDownBars element. 93 | self._write_up_down_bars() 94 | 95 | # Write the c:marker element. 96 | self._write_marker_value() 97 | 98 | # Write the c:axId elements 99 | self._write_axis_ids(args) 100 | 101 | self._xml_end_tag('c:lineChart') 102 | 103 | def _write_d_pt_point(self, index, point): 104 | # Write an individual element. Override the parent method to 105 | # add markers. 106 | 107 | self._xml_start_tag('c:dPt') 108 | 109 | # Write the c:idx element. 110 | self._write_idx(index) 111 | 112 | self._xml_start_tag('c:marker') 113 | 114 | # Write the c:spPr element. 115 | self._write_sp_pr(point) 116 | 117 | self._xml_end_tag('c:marker') 118 | 119 | self._xml_end_tag('c:dPt') 120 | 121 | def _write_marker_value(self): 122 | # Write the element without a sub-element. 123 | attributes = [('val', 1)] 124 | 125 | self._xml_empty_tag('c:marker', attributes) 126 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_pie.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartPie - A class for writing the Excel XLSX Pie charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from warnings import warn 9 | from . import chart 10 | 11 | 12 | class ChartPie(chart.Chart): 13 | """ 14 | A class for writing the Excel XLSX Pie charts. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self, options=None): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | super(ChartPie, self).__init__() 31 | 32 | if options is None: 33 | options = {} 34 | 35 | self.vary_data_color = 1 36 | self.rotation = 0 37 | 38 | # Set the available data label positions for this chart type. 39 | self.label_position_default = 'best_fit' 40 | self.label_positions = { 41 | 'center': 'ctr', 42 | 'inside_end': 'inEnd', 43 | 'outside_end': 'outEnd', 44 | 'best_fit': 'bestFit'} 45 | 46 | def set_rotation(self, rotation): 47 | """ 48 | Set the Pie/Doughnut chart rotation: the angle of the first slice. 49 | 50 | Args: 51 | rotation: First segment angle: 0 <= rotation <= 360. 52 | 53 | Returns: 54 | Nothing. 55 | 56 | """ 57 | if rotation is None: 58 | return 59 | 60 | # Ensure the rotation is in Excel's range. 61 | if rotation < 0 or rotation > 360: 62 | warn("Chart rotation %d outside Excel range: 0 <= rotation <= 360" 63 | % rotation) 64 | return 65 | 66 | self.rotation = int(rotation) 67 | 68 | ########################################################################### 69 | # 70 | # Private API. 71 | # 72 | ########################################################################### 73 | 74 | def _write_chart_type(self, args): 75 | # Override the virtual superclass method with a chart specific method. 76 | # Write the c:pieChart element. 77 | self._write_pie_chart(args) 78 | 79 | ########################################################################### 80 | # 81 | # XML methods. 82 | # 83 | ########################################################################### 84 | 85 | def _write_pie_chart(self, args): 86 | # Write the element. Over-ridden method to remove 87 | # axis_id code since Pie charts don't require val and cat axes. 88 | self._xml_start_tag('c:pieChart') 89 | 90 | # Write the c:varyColors element. 91 | self._write_vary_colors() 92 | 93 | # Write the series elements. 94 | for data in self.series: 95 | self._write_ser(data) 96 | 97 | # Write the c:firstSliceAng element. 98 | self._write_first_slice_ang() 99 | 100 | self._xml_end_tag('c:pieChart') 101 | 102 | def _write_plot_area(self): 103 | # Over-ridden method to remove the cat_axis() and val_axis() code 104 | # since Pie charts don't require those axes. 105 | # 106 | # Write the element. 107 | 108 | self._xml_start_tag('c:plotArea') 109 | 110 | # Write the c:layout element. 111 | self._write_layout(self.plotarea.get('layout'), 'plot') 112 | 113 | # Write the subclass chart type element. 114 | self._write_chart_type(None) 115 | 116 | # Write the c:spPr element for the plotarea formatting. 117 | self._write_sp_pr(self.plotarea) 118 | 119 | self._xml_end_tag('c:plotArea') 120 | 121 | def _write_legend(self): 122 | # Over-ridden method to add to legend. 123 | # Write the element. 124 | legend = self.legend 125 | position = legend.get('position', 'right') 126 | font = legend.get('font') 127 | delete_series = [] 128 | overlay = 0 129 | 130 | if (legend.get('delete_series') 131 | and type(legend['delete_series']) is list): 132 | delete_series = legend['delete_series'] 133 | 134 | if position.startswith('overlay_'): 135 | position = position.replace('overlay_', '') 136 | overlay = 1 137 | 138 | allowed = { 139 | 'right': 'r', 140 | 'left': 'l', 141 | 'top': 't', 142 | 'bottom': 'b', 143 | 'top_right': 'tr', 144 | } 145 | 146 | if position == 'none': 147 | return 148 | 149 | if position not in allowed: 150 | return 151 | 152 | position = allowed[position] 153 | 154 | self._xml_start_tag('c:legend') 155 | 156 | # Write the c:legendPos element. 157 | self._write_legend_pos(position) 158 | 159 | # Remove series labels from the legend. 160 | for index in delete_series: 161 | # Write the c:legendEntry element. 162 | self._write_legend_entry(index) 163 | 164 | # Write the c:layout element. 165 | self._write_layout(legend.get('layout'), 'legend') 166 | 167 | # Write the c:overlay element. 168 | if overlay: 169 | self._write_overlay() 170 | 171 | # Write the c:spPr element. 172 | self._write_sp_pr(legend) 173 | 174 | # Write the c:txPr element. Over-ridden. 175 | self._write_tx_pr_legend(None, font) 176 | 177 | self._xml_end_tag('c:legend') 178 | 179 | def _write_tx_pr_legend(self, horiz, font): 180 | # Write the element for legends. 181 | 182 | if font and font.get('rotation'): 183 | rotation = font['rotation'] 184 | else: 185 | rotation = None 186 | 187 | self._xml_start_tag('c:txPr') 188 | 189 | # Write the a:bodyPr element. 190 | self._write_a_body_pr(rotation, horiz) 191 | 192 | # Write the a:lstStyle element. 193 | self._write_a_lst_style() 194 | 195 | # Write the a:p element. 196 | self._write_a_p_legend(font) 197 | 198 | self._xml_end_tag('c:txPr') 199 | 200 | def _write_a_p_legend(self, font): 201 | # Write the element for legends. 202 | 203 | self._xml_start_tag('a:p') 204 | 205 | # Write the a:pPr element. 206 | self._write_a_p_pr_legend(font) 207 | 208 | # Write the a:endParaRPr element. 209 | self._write_a_end_para_rpr() 210 | 211 | self._xml_end_tag('a:p') 212 | 213 | def _write_a_p_pr_legend(self, font): 214 | # Write the element for legends. 215 | attributes = [('rtl', 0)] 216 | 217 | self._xml_start_tag('a:pPr', attributes) 218 | 219 | # Write the a:defRPr element. 220 | self._write_a_def_rpr(font) 221 | 222 | self._xml_end_tag('a:pPr') 223 | 224 | def _write_vary_colors(self): 225 | # Write the element. 226 | attributes = [('val', 1)] 227 | 228 | self._xml_empty_tag('c:varyColors', attributes) 229 | 230 | def _write_first_slice_ang(self): 231 | # Write the element. 232 | attributes = [('val', self.rotation)] 233 | 234 | self._xml_empty_tag('c:firstSliceAng', attributes) 235 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_radar.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartRadar - A class for writing the Excel XLSX Radar charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | 10 | 11 | class ChartRadar(chart.Chart): 12 | """ 13 | A class for writing the Excel XLSX Radar charts. 14 | 15 | 16 | """ 17 | 18 | ########################################################################### 19 | # 20 | # Public API. 21 | # 22 | ########################################################################### 23 | 24 | def __init__(self, options=None): 25 | """ 26 | Constructor. 27 | 28 | """ 29 | super(ChartRadar, self).__init__() 30 | 31 | if options is None: 32 | options = {} 33 | 34 | self.subtype = options.get('subtype') 35 | 36 | if not self.subtype: 37 | self.subtype = 'marker' 38 | self.default_marker = {'type': 'none'} 39 | 40 | # Override and reset the default axis values. 41 | self.x_axis['defaults']['major_gridlines'] = {'visible': 1} 42 | self.set_x_axis({}) 43 | 44 | # Set the available data label positions for this chart type. 45 | self.label_position_default = 'center' 46 | self.label_positions = {'center': 'ctr'} 47 | 48 | # Hardcode major_tick_mark for now until there is an accessor. 49 | self.y_axis['major_tick_mark'] = 'cross' 50 | 51 | ########################################################################### 52 | # 53 | # Private API. 54 | # 55 | ########################################################################### 56 | 57 | def _write_chart_type(self, args): 58 | # Write the c:radarChart element. 59 | self._write_radar_chart(args) 60 | 61 | ########################################################################### 62 | # 63 | # XML methods. 64 | # 65 | ########################################################################### 66 | 67 | def _write_radar_chart(self, args): 68 | # Write the element. 69 | 70 | if args['primary_axes']: 71 | series = self._get_primary_axes_series() 72 | else: 73 | series = self._get_secondary_axes_series() 74 | 75 | if not len(series): 76 | return 77 | 78 | self._xml_start_tag('c:radarChart') 79 | 80 | # Write the c:radarStyle element. 81 | self._write_radar_style() 82 | 83 | # Write the series elements. 84 | for data in series: 85 | self._write_ser(data) 86 | 87 | # Write the c:axId elements 88 | self._write_axis_ids(args) 89 | 90 | self._xml_end_tag('c:radarChart') 91 | 92 | def _write_radar_style(self): 93 | # Write the element. 94 | val = 'marker' 95 | 96 | if self.subtype == 'filled': 97 | val = 'filled' 98 | 99 | attributes = [('val', val)] 100 | 101 | self._xml_empty_tag('c:radarStyle', attributes) 102 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_scatter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartScatter - A class for writing the Excel XLSX Scatter charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | from warnings import warn 10 | 11 | 12 | class ChartScatter(chart.Chart): 13 | """ 14 | A class for writing the Excel XLSX Scatter charts. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self, options=None): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | super(ChartScatter, self).__init__() 31 | 32 | if options is None: 33 | options = {} 34 | 35 | self.subtype = options.get('subtype') 36 | 37 | if not self.subtype: 38 | self.subtype = 'marker_only' 39 | 40 | self.cross_between = 'midCat' 41 | self.horiz_val_axis = 0 42 | self.val_axis_position = 'b' 43 | self.smooth_allowed = True 44 | self.requires_category = True 45 | 46 | # Set the available data label positions for this chart type. 47 | self.label_position_default = 'right' 48 | self.label_positions = { 49 | 'center': 'ctr', 50 | 'right': 'r', 51 | 'left': 'l', 52 | 'above': 't', 53 | 'below': 'b', 54 | # For backward compatibility. 55 | 'top': 't', 56 | 'bottom': 'b'} 57 | 58 | def combine(self, chart=None): 59 | """ 60 | Create a combination chart with a secondary chart. 61 | 62 | Note: Override parent method to add a warning. 63 | 64 | Args: 65 | chart: The secondary chart to combine with the primary chart. 66 | 67 | Returns: 68 | Nothing. 69 | 70 | """ 71 | if chart is None: 72 | return 73 | 74 | warn('Combined chart not currently supported with scatter chart ' 75 | 'as the primary chart') 76 | 77 | ########################################################################### 78 | # 79 | # Private API. 80 | # 81 | ########################################################################### 82 | 83 | def _write_chart_type(self, args): 84 | # Override the virtual superclass method with a chart specific method. 85 | # Write the c:scatterChart element. 86 | self._write_scatter_chart(args) 87 | 88 | ########################################################################### 89 | # 90 | # XML methods. 91 | # 92 | ########################################################################### 93 | 94 | def _write_scatter_chart(self, args): 95 | # Write the element. 96 | 97 | if args['primary_axes']: 98 | series = self._get_primary_axes_series() 99 | else: 100 | series = self._get_secondary_axes_series() 101 | 102 | if not len(series): 103 | return 104 | 105 | style = 'lineMarker' 106 | subtype = self.subtype 107 | 108 | # Set the user defined chart subtype. 109 | if subtype == 'marker_only': 110 | style = 'lineMarker' 111 | 112 | if subtype == 'straight_with_markers': 113 | style = 'lineMarker' 114 | 115 | if subtype == 'straight': 116 | style = 'lineMarker' 117 | self.default_marker = {'type': 'none'} 118 | 119 | if subtype == 'smooth_with_markers': 120 | style = 'smoothMarker' 121 | 122 | if subtype == 'smooth': 123 | style = 'smoothMarker' 124 | self.default_marker = {'type': 'none'} 125 | 126 | # Add default formatting to the series data. 127 | self._modify_series_formatting() 128 | 129 | self._xml_start_tag('c:scatterChart') 130 | 131 | # Write the c:scatterStyle element. 132 | self._write_scatter_style(style) 133 | 134 | # Write the series elements. 135 | for data in series: 136 | self._write_ser(data) 137 | 138 | # Write the c:axId elements 139 | self._write_axis_ids(args) 140 | 141 | self._xml_end_tag('c:scatterChart') 142 | 143 | def _write_ser(self, series): 144 | # Over-ridden to write c:xVal/c:yVal instead of c:cat/c:val elements. 145 | # Write the element. 146 | 147 | index = self.series_index 148 | self.series_index += 1 149 | 150 | self._xml_start_tag('c:ser') 151 | 152 | # Write the c:idx element. 153 | self._write_idx(index) 154 | 155 | # Write the c:order element. 156 | self._write_order(index) 157 | 158 | # Write the series name. 159 | self._write_series_name(series) 160 | 161 | # Write the c:spPr element. 162 | self._write_sp_pr(series) 163 | 164 | # Write the c:marker element. 165 | self._write_marker(series.get('marker')) 166 | 167 | # Write the c:dPt element. 168 | self._write_d_pt(series.get('points')) 169 | 170 | # Write the c:dLbls element. 171 | self._write_d_lbls(series.get('labels')) 172 | 173 | # Write the c:trendline element. 174 | self._write_trendline(series.get('trendline')) 175 | 176 | # Write the c:errBars element. 177 | self._write_error_bars(series.get('error_bars')) 178 | 179 | # Write the c:xVal element. 180 | self._write_x_val(series) 181 | 182 | # Write the c:yVal element. 183 | self._write_y_val(series) 184 | 185 | # Write the c:smooth element. 186 | if 'smooth' in self.subtype and series['smooth'] is None: 187 | # Default is on for smooth scatter charts. 188 | self._write_c_smooth(True) 189 | else: 190 | self._write_c_smooth(series['smooth']) 191 | 192 | self._xml_end_tag('c:ser') 193 | 194 | def _write_plot_area(self): 195 | # Over-ridden to have 2 valAx elements for scatter charts instead 196 | # of catAx/valAx. 197 | # 198 | # Write the element. 199 | self._xml_start_tag('c:plotArea') 200 | 201 | # Write the c:layout element. 202 | self._write_layout(self.plotarea.get('layout'), 'plot') 203 | 204 | # Write the subclass chart elements for primary and secondary axes. 205 | self._write_chart_type({'primary_axes': 1}) 206 | self._write_chart_type({'primary_axes': 0}) 207 | 208 | # Write c:catAx and c:valAx elements for series using primary axes. 209 | self._write_cat_val_axis({'x_axis': self.x_axis, 210 | 'y_axis': self.y_axis, 211 | 'axis_ids': self.axis_ids, 212 | 'position': 'b', 213 | }) 214 | 215 | tmp = self.horiz_val_axis 216 | self.horiz_val_axis = 1 217 | 218 | self._write_val_axis({'x_axis': self.x_axis, 219 | 'y_axis': self.y_axis, 220 | 'axis_ids': self.axis_ids, 221 | 'position': 'l', 222 | }) 223 | 224 | self.horiz_val_axis = tmp 225 | 226 | # Write c:valAx and c:catAx elements for series using secondary axes 227 | self._write_cat_val_axis({'x_axis': self.x2_axis, 228 | 'y_axis': self.y2_axis, 229 | 'axis_ids': self.axis2_ids, 230 | 'position': 'b', 231 | }) 232 | self.horiz_val_axis = 1 233 | self._write_val_axis({'x_axis': self.x2_axis, 234 | 'y_axis': self.y2_axis, 235 | 'axis_ids': self.axis2_ids, 236 | 'position': 'l', 237 | }) 238 | 239 | # Write the c:spPr element for the plotarea formatting. 240 | self._write_sp_pr(self.plotarea) 241 | 242 | self._xml_end_tag('c:plotArea') 243 | 244 | def _write_x_val(self, series): 245 | # Write the element. 246 | formula = series.get('categories') 247 | data_id = series.get('cat_data_id') 248 | data = self.formula_data[data_id] 249 | 250 | self._xml_start_tag('c:xVal') 251 | 252 | # Check the type of cached data. 253 | data_type = self._get_data_type(data) 254 | 255 | # TODO. Can a scatter plot have non-numeric data. 256 | if data_type == 'str': 257 | # Write the c:numRef element. 258 | self._write_str_ref(formula, data, data_type) 259 | else: 260 | # Write the c:numRef element. 261 | self._write_num_ref(formula, data, data_type) 262 | 263 | self._xml_end_tag('c:xVal') 264 | 265 | def _write_y_val(self, series): 266 | # Write the element. 267 | formula = series.get('values') 268 | data_id = series.get('val_data_id') 269 | data = self.formula_data[data_id] 270 | 271 | self._xml_start_tag('c:yVal') 272 | 273 | # Unlike Cat axes data should only be numeric. 274 | # Write the c:numRef element. 275 | self._write_num_ref(formula, data, 'num') 276 | 277 | self._xml_end_tag('c:yVal') 278 | 279 | def _write_scatter_style(self, val): 280 | # Write the element. 281 | attributes = [('val', val)] 282 | 283 | self._xml_empty_tag('c:scatterStyle', attributes) 284 | 285 | def _modify_series_formatting(self): 286 | # Add default formatting to the series data unless it has already been 287 | # specified by the user. 288 | subtype = self.subtype 289 | 290 | # The default scatter style "markers only" requires a line type. 291 | if subtype == 'marker_only': 292 | 293 | # Go through each series and define default values. 294 | for series in self.series: 295 | 296 | # Set a line type unless there is already a user defined type. 297 | if not series['line']['defined']: 298 | series['line'] = {'width': 2.25, 299 | 'none': 1, 300 | 'defined': 1, 301 | } 302 | 303 | def _write_d_pt_point(self, index, point): 304 | # Write an individual element. Override the parent method to 305 | # add markers. 306 | 307 | self._xml_start_tag('c:dPt') 308 | 309 | # Write the c:idx element. 310 | self._write_idx(index) 311 | 312 | self._xml_start_tag('c:marker') 313 | 314 | # Write the c:spPr element. 315 | self._write_sp_pr(point) 316 | 317 | self._xml_end_tag('c:marker') 318 | 319 | self._xml_end_tag('c:dPt') 320 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chart_stock.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ChartStock - A class for writing the Excel XLSX Stock charts. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import chart 9 | 10 | 11 | class ChartStock(chart.Chart): 12 | """ 13 | A class for writing the Excel XLSX Stock charts. 14 | 15 | """ 16 | 17 | ########################################################################### 18 | # 19 | # Public API. 20 | # 21 | ########################################################################### 22 | 23 | def __init__(self, options=None): 24 | """ 25 | Constructor. 26 | 27 | """ 28 | super(ChartStock, self).__init__() 29 | 30 | if options is None: 31 | options = {} 32 | 33 | self.show_crosses = 0 34 | self.hi_low_lines = {} 35 | self.date_category = True 36 | 37 | # Override and reset the default axis values. 38 | self.x_axis['defaults']['num_format'] = 'dd/mm/yyyy' 39 | self.x2_axis['defaults']['num_format'] = 'dd/mm/yyyy' 40 | 41 | # Set the available data label positions for this chart type. 42 | self.label_position_default = 'right' 43 | self.label_positions = { 44 | 'center': 'ctr', 45 | 'right': 'r', 46 | 'left': 'l', 47 | 'above': 't', 48 | 'below': 'b', 49 | # For backward compatibility. 50 | 'top': 't', 51 | 'bottom': 'b'} 52 | 53 | self.set_x_axis({}) 54 | self.set_x2_axis({}) 55 | 56 | ########################################################################### 57 | # 58 | # Private API. 59 | # 60 | ########################################################################### 61 | 62 | def _write_chart_type(self, args): 63 | # Override the virtual superclass method with a chart specific method. 64 | # Write the c:stockChart element. 65 | self._write_stock_chart(args) 66 | 67 | ########################################################################### 68 | # 69 | # XML methods. 70 | # 71 | ########################################################################### 72 | 73 | def _write_stock_chart(self, args): 74 | # Write the element. 75 | # Overridden to add hi_low_lines(). 76 | 77 | if args['primary_axes']: 78 | series = self._get_primary_axes_series() 79 | else: 80 | series = self._get_secondary_axes_series() 81 | 82 | if not len(series): 83 | return 84 | 85 | # Add default formatting to the series data. 86 | self._modify_series_formatting() 87 | 88 | self._xml_start_tag('c:stockChart') 89 | 90 | # Write the series elements. 91 | for data in series: 92 | self._write_ser(data) 93 | 94 | # Write the c:dropLines element. 95 | self._write_drop_lines() 96 | 97 | # Write the c:hiLowLines element. 98 | if args.get('primary_axes'): 99 | self._write_hi_low_lines() 100 | 101 | # Write the c:upDownBars element. 102 | self._write_up_down_bars() 103 | 104 | # Write the c:axId elements 105 | self._write_axis_ids(args) 106 | 107 | self._xml_end_tag('c:stockChart') 108 | 109 | def _modify_series_formatting(self): 110 | # Add default formatting to the series data. 111 | 112 | index = 0 113 | 114 | for series in self.series: 115 | if index % 4 != 3: 116 | if not series['line']['defined']: 117 | series['line'] = {'width': 2.25, 118 | 'none': 1, 119 | 'defined': 1} 120 | 121 | if series['marker'] is None: 122 | if index % 4 == 2: 123 | series['marker'] = {'type': 'dot', 'size': 3} 124 | else: 125 | series['marker'] = {'type': 'none'} 126 | 127 | index += 1 128 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/chartsheet.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Chartsheet - A class for writing the Excel XLSX Worksheet file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import worksheet 9 | from .drawing import Drawing 10 | 11 | 12 | class Chartsheet(worksheet.Worksheet): 13 | """ 14 | A class for writing the Excel XLSX Chartsheet file. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | 31 | super(Chartsheet, self).__init__() 32 | 33 | self.is_chartsheet = True 34 | self.drawing = None 35 | self.chart = None 36 | self.charts = [] 37 | self.zoom_scale_normal = 0 38 | self.orientation = 0 39 | self.protection = False 40 | 41 | def set_chart(self, chart): 42 | """ 43 | Set the chart object for the chartsheet. 44 | Args: 45 | chart: Chart object. 46 | Returns: 47 | chart: A reference to the chart object. 48 | """ 49 | chart.embedded = False 50 | chart.protection = self.protection 51 | self.chart = chart 52 | self.charts.append([0, 0, chart, 0, 0, 1, 1]) 53 | return chart 54 | 55 | def protect(self, password='', options=None): 56 | """ 57 | Set the password and protection options of the worksheet. 58 | 59 | Args: 60 | password: An optional password string. 61 | options: A dictionary of worksheet objects to protect. 62 | 63 | Returns: 64 | Nothing. 65 | 66 | """ 67 | # This method is overridden from parent worksheet class. 68 | 69 | # Chartsheets only allow a reduced set of protect options. 70 | copy = {} 71 | 72 | if not options: 73 | options = {} 74 | 75 | if options.get('objects') is None: 76 | copy['objects'] = False 77 | else: 78 | # Objects are default on for chartsheets, so reverse state. 79 | copy['objects'] = not options['objects'] 80 | 81 | if options.get('content') is None: 82 | copy['content'] = True 83 | else: 84 | copy['content'] = options['content'] 85 | 86 | copy['sheet'] = False 87 | copy['scenarios'] = True 88 | 89 | # If objects and content are both off then the chartsheet isn't 90 | # protected, unless it has a password. 91 | if password == '' and copy['objects'] and not copy['content']: 92 | return 93 | 94 | if self.chart: 95 | self.chart.protection = True 96 | else: 97 | self.protection = True 98 | 99 | # Call the parent method. 100 | super(Chartsheet, self).protect(password, copy) 101 | 102 | ########################################################################### 103 | # 104 | # Private API. 105 | # 106 | ########################################################################### 107 | def _assemble_xml_file(self): 108 | # Assemble and write the XML file. 109 | 110 | # Write the XML declaration. 111 | self._xml_declaration() 112 | 113 | # Write the root worksheet element. 114 | self._write_chartsheet() 115 | 116 | # Write the worksheet properties. 117 | self._write_sheet_pr() 118 | 119 | # Write the sheet view properties. 120 | self._write_sheet_views() 121 | 122 | # Write the sheetProtection element. 123 | self._write_sheet_protection() 124 | 125 | # Write the printOptions element. 126 | self._write_print_options() 127 | 128 | # Write the worksheet page_margins. 129 | self._write_page_margins() 130 | 131 | # Write the worksheet page setup. 132 | self._write_page_setup() 133 | 134 | # Write the headerFooter element. 135 | self._write_header_footer() 136 | 137 | # Write the drawing element. 138 | self._write_drawings() 139 | 140 | # Close the worksheet tag. 141 | self._xml_end_tag('chartsheet') 142 | 143 | # Close the file. 144 | self._xml_close() 145 | 146 | def _prepare_chart(self, index, chart_id, drawing_id): 147 | # Set up chart/drawings. 148 | 149 | self.chart.id = chart_id - 1 150 | 151 | self.drawing = Drawing() 152 | self.drawing.orientation = self.orientation 153 | 154 | self.external_drawing_links.append(['/drawing', 155 | '../drawings/drawing' 156 | + str(drawing_id) 157 | + '.xml']) 158 | 159 | self.drawing_links.append(['/chart', 160 | '../charts/chart' 161 | + str(chart_id) 162 | + '.xml']) 163 | 164 | ########################################################################### 165 | # 166 | # XML methods. 167 | # 168 | ########################################################################### 169 | 170 | def _write_chartsheet(self): 171 | # Write the element. This is the root element. 172 | 173 | schema = 'http://schemas.openxmlformats.org/' 174 | xmlns = schema + 'spreadsheetml/2006/main' 175 | xmlns_r = schema + 'officeDocument/2006/relationships' 176 | 177 | attributes = [ 178 | ('xmlns', xmlns), 179 | ('xmlns:r', xmlns_r)] 180 | 181 | self._xml_start_tag('chartsheet', attributes) 182 | 183 | def _write_sheet_pr(self): 184 | # Write the element for Sheet level properties. 185 | attributes = [] 186 | 187 | if self.filter_on: 188 | attributes.append(('filterMode', 1)) 189 | 190 | if (self.fit_page or self.tab_color): 191 | self._xml_start_tag('sheetPr', attributes) 192 | self._write_tab_color() 193 | self._write_page_set_up_pr() 194 | self._xml_end_tag('sheetPr') 195 | else: 196 | self._xml_empty_tag('sheetPr', attributes) 197 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/comments.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Comments - A class for writing the Excel XLSX Worksheet file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | import re 9 | 10 | from . import xmlwriter 11 | from .utility import xl_rowcol_to_cell 12 | 13 | 14 | class Comments(xmlwriter.XMLwriter): 15 | """ 16 | A class for writing the Excel XLSX Comments file. 17 | 18 | 19 | """ 20 | 21 | ########################################################################### 22 | # 23 | # Public API. 24 | # 25 | ########################################################################### 26 | 27 | def __init__(self): 28 | """ 29 | Constructor. 30 | 31 | """ 32 | 33 | super(Comments, self).__init__() 34 | self.author_ids = {} 35 | 36 | ########################################################################### 37 | # 38 | # Private API. 39 | # 40 | ########################################################################### 41 | 42 | def _assemble_xml_file(self, comments_data=[]): 43 | # Assemble and write the XML file. 44 | 45 | # Write the XML declaration. 46 | self._xml_declaration() 47 | 48 | # Write the comments element. 49 | self._write_comments() 50 | 51 | # Write the authors element. 52 | self._write_authors(comments_data) 53 | 54 | # Write the commentList element. 55 | self._write_comment_list(comments_data) 56 | 57 | self._xml_end_tag('comments') 58 | 59 | # Close the file. 60 | self._xml_close() 61 | 62 | ########################################################################### 63 | # 64 | # XML methods. 65 | # 66 | ########################################################################### 67 | 68 | def _write_comments(self): 69 | # Write the element. 70 | xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' 71 | 72 | attributes = [('xmlns', xmlns)] 73 | 74 | self._xml_start_tag('comments', attributes) 75 | 76 | def _write_authors(self, comment_data): 77 | # Write the element. 78 | author_count = 0 79 | 80 | self._xml_start_tag('authors') 81 | 82 | for comment in comment_data: 83 | author = comment[3] 84 | 85 | if author is not None and author not in self.author_ids: 86 | # Store the author id. 87 | self.author_ids[author] = author_count 88 | author_count += 1 89 | 90 | # Write the author element. 91 | self._write_author(author) 92 | 93 | self._xml_end_tag('authors') 94 | 95 | def _write_author(self, data): 96 | # Write the element. 97 | self._xml_data_element('author', data) 98 | 99 | def _write_comment_list(self, comment_data): 100 | # Write the element. 101 | self._xml_start_tag('commentList') 102 | 103 | for comment in comment_data: 104 | row = comment[0] 105 | col = comment[1] 106 | text = comment[2] 107 | author = comment[3] 108 | font_name = comment[6] 109 | font_size = comment[7] 110 | font_family = comment[8] 111 | 112 | # Look up the author id. 113 | author_id = None 114 | if author is not None: 115 | author_id = self.author_ids[author] 116 | 117 | # Write the comment element. 118 | font = (font_name, font_size, font_family) 119 | self._write_comment(row, col, text, author_id, font) 120 | 121 | self._xml_end_tag('commentList') 122 | 123 | def _write_comment(self, row, col, text, author_id, font): 124 | # Write the element. 125 | ref = xl_rowcol_to_cell(row, col) 126 | 127 | attributes = [('ref', ref)] 128 | 129 | if author_id is not None: 130 | attributes.append(('authorId', author_id)) 131 | 132 | self._xml_start_tag('comment', attributes) 133 | 134 | # Write the text element. 135 | self._write_text(text, font) 136 | 137 | self._xml_end_tag('comment') 138 | 139 | def _write_text(self, text, font): 140 | # Write the element. 141 | self._xml_start_tag('text') 142 | 143 | # Write the text r element. 144 | self._write_text_r(text, font) 145 | 146 | self._xml_end_tag('text') 147 | 148 | def _write_text_r(self, text, font): 149 | # Write the element. 150 | self._xml_start_tag('r') 151 | 152 | # Write the rPr element. 153 | self._write_r_pr(font) 154 | 155 | # Write the text r element. 156 | self._write_text_t(text) 157 | 158 | self._xml_end_tag('r') 159 | 160 | def _write_text_t(self, text): 161 | # Write the text element. 162 | attributes = [] 163 | 164 | if re.search(r'^\s', text) or re.search(r'\s$', text): 165 | attributes.append(('xml:space', 'preserve')) 166 | 167 | self._xml_data_element('t', text, attributes) 168 | 169 | def _write_r_pr(self, font): 170 | # Write the element. 171 | self._xml_start_tag('rPr') 172 | 173 | # Write the sz element. 174 | self._write_sz(font[1]) 175 | 176 | # Write the color element. 177 | self._write_color() 178 | 179 | # Write the rFont element. 180 | self._write_r_font(font[0]) 181 | 182 | # Write the family element. 183 | self._write_family(font[2]) 184 | 185 | self._xml_end_tag('rPr') 186 | 187 | def _write_sz(self, font_size): 188 | # Write the element. 189 | attributes = [('val', font_size)] 190 | 191 | self._xml_empty_tag('sz', attributes) 192 | 193 | def _write_color(self): 194 | # Write the element. 195 | attributes = [('indexed', 81)] 196 | 197 | self._xml_empty_tag('color', attributes) 198 | 199 | def _write_r_font(self, font_name): 200 | # Write the element. 201 | attributes = [('val', font_name)] 202 | 203 | self._xml_empty_tag('rFont', attributes) 204 | 205 | def _write_family(self, font_family): 206 | # Write the element. 207 | attributes = [('val', font_family)] 208 | 209 | self._xml_empty_tag('family', attributes) 210 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/compat_collections.py: -------------------------------------------------------------------------------- 1 | """ 2 | From the GC3Pie project: https://code.google.com/p/gc3pie/ 3 | 4 | A backport of the Python standard `collections` package, providing 5 | `namedtuple` and `defaultdict` also on Python 2.4 and 2.5. 6 | 7 | This package actually imports your Python `collections`, and adds 8 | its own versions of `namedtuple` and `defaultdict` only if they are 9 | missing. 10 | """ 11 | 12 | from collections import * 13 | import sys 14 | 15 | try: 16 | defaultdict 17 | except NameError: 18 | class defaultdict(dict): 19 | """ 20 | A backport of `defaultdict` to Python 2.4 21 | See http://docs.python.org/library/collections.html 22 | """ 23 | def __new__(cls, default_factory=None): 24 | return dict.__new__(cls) 25 | 26 | def __init__(self, default_factory): 27 | self.default_factory = default_factory 28 | 29 | def __missing__(self, key): 30 | try: 31 | return self.default_factory() 32 | except: 33 | raise KeyError("Key '%s' not in dictionary" % key) 34 | 35 | def __getitem__(self, key): 36 | if not dict.__contains__(self, key): 37 | dict.__setitem__(self, key, self.__missing__(key)) 38 | return dict.__getitem__(self, key) 39 | 40 | 41 | try: 42 | namedtuple 43 | except NameError: 44 | # Use Raymond Hettinger's original `namedtuple` package. 45 | # 46 | # Source originally taken from: 47 | # http://code.activestate.com/recipes/500261-named-tuples/ 48 | from operator import itemgetter as _itemgetter 49 | from keyword import iskeyword as _iskeyword 50 | import sys as _sys 51 | 52 | def namedtuple(typename, field_names, verbose=False, rename=False): 53 | """Returns a new subclass of tuple with named fields. 54 | 55 | >>> Point = namedtuple('Point', 'x y') 56 | >>> Point.__doc__ # docstring for the new class 57 | 'Point(x, y)' 58 | >>> p = Point(11, y=22) # instantiate with positional args or keywords 59 | >>> p[0] + p[1] # indexable like a plain tuple 60 | 33 61 | >>> x, y = p # unpack like a regular tuple 62 | >>> x, y 63 | (11, 22) 64 | >>> p.x + p.y # fields also accessible by name 65 | 33 66 | >>> d = p._asdict() # convert to a dictionary 67 | >>> d['x'] 68 | 11 69 | >>> Point(**d) # convert from a dictionary 70 | Point(x=11, y=22) 71 | >>> p._replace(x=100) # _replace() is like str.replace() but targets named fields 72 | Point(x=100, y=22) 73 | 74 | """ 75 | 76 | # Parse and validate the field names. Validation serves two purposes, 77 | # generating informative error messages and preventing template injection attacks. 78 | if isinstance(field_names, basestring): 79 | field_names = field_names.replace(',', ' ').split() # names separated by whitespace and/or commas 80 | field_names = tuple(map(str, field_names)) 81 | if rename: 82 | names = list(field_names) 83 | seen = set() 84 | for i, name in enumerate(names): 85 | if (not min(c.isalnum() or c == '_' for c in name) 86 | or _iskeyword(name) 87 | or not name or name[0].isdigit() 88 | or name.startswith('_') 89 | or name in seen): 90 | names[i] = '_%d' % i 91 | 92 | seen.add(name) 93 | field_names = tuple(names) 94 | for name in (typename,) + field_names: 95 | if not min(c.isalnum() or c == '_' for c in name): 96 | raise ValueError('Type names and field names can only contain alphanumeric characters and underscores: %r' % name) 97 | if _iskeyword(name): 98 | raise ValueError('Type names and field names cannot be a keyword: %r' % name) 99 | if name[0].isdigit(): 100 | raise ValueError('Type names and field names cannot start with a number: %r' % name) 101 | seen_names = set() 102 | for name in field_names: 103 | if name.startswith('_') and not rename: 104 | raise ValueError('Field names cannot start with an underscore: %r' % name) 105 | if name in seen_names: 106 | raise ValueError('Encountered duplicate field name: %r' % name) 107 | seen_names.add(name) 108 | 109 | # Create and fill-in the class template 110 | numfields = len(field_names) 111 | argtxt = repr(field_names).replace("'", "")[1:-1] # tuple repr without parens or quotes 112 | reprtxt = ', '.join('%s=%%r' % name for name in field_names) 113 | template = '''class %(typename)s(tuple): 114 | '%(typename)s(%(argtxt)s)' \n 115 | __slots__ = () \n 116 | _fields = %(field_names)r \n 117 | def __new__(_cls, %(argtxt)s): 118 | return _tuple.__new__(_cls, (%(argtxt)s)) \n 119 | @classmethod 120 | def _make(cls, iterable, new=tuple.__new__, len=len): 121 | 'Make a new %(typename)s object from a sequence or iterable' 122 | result = new(cls, iterable) 123 | if len(result) != %(numfields)d: 124 | raise TypeError('Expected %(numfields)d arguments, got %%d' %% len(result)) 125 | return result \n 126 | def __repr__(self): 127 | return '%(typename)s(%(reprtxt)s)' %% self \n 128 | def _asdict(self): 129 | 'Return a new dict which maps field names to their values' 130 | return dict(zip(self._fields, self)) \n 131 | def _replace(_self, **kwds): 132 | 'Return a new %(typename)s object replacing specified fields with new values' 133 | result = _self._make(map(kwds.pop, %(field_names)r, _self)) 134 | if kwds: 135 | raise ValueError('Got unexpected field names: %%r' %% kwds.keys()) 136 | return result \n 137 | def __getnewargs__(self): 138 | return tuple(self) \n\n''' % locals() 139 | for i, name in enumerate(field_names): 140 | template += ' %s = _property(_itemgetter(%d))\n' % (name, i) 141 | if verbose: 142 | print(template) 143 | 144 | # Execute the template string in a temporary namespace 145 | namespace = dict(_itemgetter=_itemgetter, __name__='namedtuple_%s' % typename, 146 | _property=property, _tuple=tuple) 147 | try: 148 | exec(template) in namespace 149 | except SyntaxError: 150 | e = sys.exc_info()[1] 151 | raise SyntaxError(str(e) + ':\n' + template) 152 | result = namespace[typename] 153 | 154 | # For pickling to work, the __module__ variable needs to be set to the frame 155 | # where the named tuple is created. Bypass this step in environments where 156 | # sys._getframe is not defined (Jython for example) or sys._getframe is not 157 | # defined for arguments greater than 0 (IronPython). 158 | try: 159 | result.__module__ = _sys._getframe(1).f_globals.get('__name__', '__main__') 160 | except (AttributeError, ValueError): 161 | pass 162 | 163 | return result 164 | 165 | 166 | if __name__ == '__main__': 167 | # verify that instances can be pickled 168 | from cPickle import loads, dumps 169 | Point = namedtuple('Point', 'x, y', True) 170 | p = Point(x=10, y=20) 171 | assert p == loads(dumps(p, -1)) 172 | 173 | # test and demonstrate ability to override methods 174 | class Point(namedtuple('Point', 'x y')): 175 | @property 176 | def hypot(self): 177 | return (self.x ** 2 + self.y ** 2) ** 0.5 178 | 179 | def __str__(self): 180 | return 'Point: x=%6.3f y=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot) 181 | 182 | for p in Point(3, 4), Point(14, 5), Point(9. / 7, 6): 183 | print(p) 184 | 185 | class Point(namedtuple('Point', 'x y')): 186 | 'Point class with optimized _make() and _replace() without error-checking' 187 | _make = classmethod(tuple.__new__) 188 | 189 | def _replace(self, _map=map, **kwds): 190 | return self._make(_map(kwds.get, ('x', 'y'), self)) 191 | 192 | print(Point(11, 22)._replace(x=100)) 193 | 194 | import doctest 195 | TestResults = namedtuple('TestResults', 'failed attempted') 196 | print(TestResults(*doctest.testmod())) 197 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/compatibility.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Python 2/3 compatibility functions for XlsxWriter. 4 | # 5 | # Copyright (c), 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | import sys 9 | from decimal import Decimal 10 | 11 | try: 12 | # For compatibility between Python 2 and 3. 13 | from StringIO import StringIO 14 | except ImportError: 15 | from io import StringIO 16 | 17 | try: 18 | # For Python 2.6+. 19 | from fractions import Fraction 20 | except ImportError: 21 | Fraction = float 22 | 23 | try: 24 | # For Python 2.6+. 25 | from collections import defaultdict 26 | from collections import namedtuple 27 | except ImportError: 28 | # For Python 2.5 support. 29 | from .compat_collections import defaultdict 30 | from .compat_collections import namedtuple 31 | 32 | # Types to check in Python 2/3. 33 | if sys.version_info[0] == 2: 34 | int_types = (int, long) 35 | num_types = (float, int, long, Decimal, Fraction) 36 | str_types = basestring 37 | else: 38 | int_types = (int) 39 | num_types = (float, int, Decimal, Fraction) 40 | str_types = str 41 | 42 | 43 | if sys.version_info < (2, 6, 0): 44 | from StringIO import StringIO as BytesIO 45 | else: 46 | from io import BytesIO as BytesIO 47 | 48 | 49 | def force_unicode(string): 50 | """Return string as a native string""" 51 | if sys.version_info[0] == 2: 52 | if isinstance(string, unicode): 53 | return string.encode('utf-8') 54 | return string 55 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/contenttypes.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # ContentTypes - A class for writing the Excel XLSX ContentTypes file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | import copy 9 | from . import xmlwriter 10 | 11 | # Long namespace strings used in the class. 12 | app_package = 'application/vnd.openxmlformats-package.' 13 | app_document = 'application/vnd.openxmlformats-officedocument.' 14 | 15 | defaults = [ 16 | ['rels', app_package + 'relationships+xml'], 17 | ['xml', 'application/xml'], 18 | ] 19 | 20 | overrides = [ 21 | ['/docProps/app.xml', app_document + 'extended-properties+xml'], 22 | ['/docProps/core.xml', app_package + 'core-properties+xml'], 23 | ['/xl/styles.xml', app_document + 'spreadsheetml.styles+xml'], 24 | ['/xl/theme/theme1.xml', app_document + 'theme+xml'], 25 | ['/xl/workbook.xml', app_document + 'spreadsheetml.sheet.main+xml'], 26 | ] 27 | 28 | 29 | class ContentTypes(xmlwriter.XMLwriter): 30 | """ 31 | A class for writing the Excel XLSX ContentTypes file. 32 | 33 | 34 | """ 35 | 36 | ########################################################################### 37 | # 38 | # Public API. 39 | # 40 | ########################################################################### 41 | 42 | def __init__(self): 43 | """ 44 | Constructor. 45 | 46 | """ 47 | 48 | super(ContentTypes, self).__init__() 49 | 50 | # Copy the defaults in case we need to change them. 51 | self.defaults = copy.deepcopy(defaults) 52 | self.overrides = copy.deepcopy(overrides) 53 | 54 | ########################################################################### 55 | # 56 | # Private API. 57 | # 58 | ########################################################################### 59 | 60 | def _assemble_xml_file(self): 61 | # Assemble and write the XML file. 62 | 63 | # Write the XML declaration. 64 | self._xml_declaration() 65 | 66 | self._write_types() 67 | self._write_defaults() 68 | self._write_overrides() 69 | 70 | self._xml_end_tag('Types') 71 | 72 | # Close the file. 73 | self._xml_close() 74 | 75 | def _add_default(self, default): 76 | # Add elements to the ContentTypes defaults. 77 | self.defaults.append(default) 78 | 79 | def _add_override(self, override): 80 | # Add elements to the ContentTypes overrides. 81 | self.overrides.append(override) 82 | 83 | def _add_worksheet_name(self, worksheet_name): 84 | # Add the name of a worksheet to the ContentTypes overrides. 85 | worksheet_name = "/xl/worksheets/" + worksheet_name + ".xml" 86 | 87 | self._add_override((worksheet_name, 88 | app_document + 'spreadsheetml.worksheet+xml')) 89 | 90 | def _add_chartsheet_name(self, chartsheet_name): 91 | # Add the name of a chartsheet to the ContentTypes overrides. 92 | chartsheet_name = "/xl/chartsheets/" + chartsheet_name + ".xml" 93 | 94 | self._add_override((chartsheet_name, 95 | app_document + 'spreadsheetml.chartsheet+xml')) 96 | 97 | def _add_chart_name(self, chart_name): 98 | # Add the name of a chart to the ContentTypes overrides. 99 | chart_name = "/xl/charts/" + chart_name + ".xml" 100 | 101 | self._add_override((chart_name, app_document + 'drawingml.chart+xml')) 102 | 103 | def _add_drawing_name(self, drawing_name): 104 | # Add the name of a drawing to the ContentTypes overrides. 105 | drawing_name = "/xl/drawings/" + drawing_name + ".xml" 106 | 107 | self._add_override((drawing_name, app_document + 'drawing+xml')) 108 | 109 | def _add_vml_name(self): 110 | # Add the name of a VML drawing to the ContentTypes defaults. 111 | self._add_default(('vml', app_document + 'vmlDrawing')) 112 | 113 | def _add_comment_name(self, comment_name): 114 | # Add the name of a comment to the ContentTypes overrides. 115 | comment_name = "/xl/" + comment_name + ".xml" 116 | 117 | self._add_override((comment_name, 118 | app_document + 'spreadsheetml.comments+xml')) 119 | 120 | def _add_shared_strings(self): 121 | # Add the sharedStrings link to the ContentTypes overrides. 122 | self._add_override(('/xl/sharedStrings.xml', 123 | app_document + 'spreadsheetml.sharedStrings+xml')) 124 | 125 | def _add_calc_chain(self): 126 | # Add the calcChain link to the ContentTypes overrides. 127 | self._add_override(('/xl/calcChain.xml', 128 | app_document + 'spreadsheetml.calcChain+xml')) 129 | 130 | def _add_image_types(self, image_types): 131 | # Add the image default types. 132 | for image_type in image_types: 133 | extension = image_type 134 | 135 | if image_type in ('wmf', 'emf'): 136 | image_type = 'x-' + image_type 137 | 138 | self._add_default((extension, 'image/' + image_type)) 139 | 140 | def _add_table_name(self, table_name): 141 | # Add the name of a table to the ContentTypes overrides. 142 | table_name = "/xl/tables/" + table_name + ".xml" 143 | 144 | self._add_override((table_name, 145 | app_document + 'spreadsheetml.table+xml')) 146 | 147 | def _add_vba_project(self): 148 | # Add a vbaProject to the ContentTypes defaults. 149 | 150 | # Change the workbook.xml content-type from xlsx to xlsm. 151 | for i, override in enumerate(self.overrides): 152 | if override[0] == '/xl/workbook.xml': 153 | self.overrides[i][1] = 'application/vnd.ms-excel.' \ 154 | 'sheet.macroEnabled.main+xml' 155 | 156 | self._add_default(('bin', 'application/vnd.ms-office.vbaProject')) 157 | 158 | def _add_custom_properties(self): 159 | # Add the custom properties to the ContentTypes overrides. 160 | self._add_override(('/docProps/custom.xml', 161 | app_document + 'custom-properties+xml')) 162 | 163 | ########################################################################### 164 | # 165 | # XML methods. 166 | # 167 | ########################################################################### 168 | 169 | def _write_defaults(self): 170 | # Write out all of the types. 171 | 172 | for extension, content_type in self.defaults: 173 | self._xml_empty_tag('Default', 174 | [('Extension', extension), 175 | ('ContentType', content_type)]) 176 | 177 | def _write_overrides(self): 178 | # Write out all of the types. 179 | for part_name, content_type in self.overrides: 180 | self._xml_empty_tag('Override', 181 | [('PartName', part_name), 182 | ('ContentType', content_type)]) 183 | 184 | def _write_types(self): 185 | # Write the element. 186 | xmlns = 'http://schemas.openxmlformats.org/package/2006/content-types' 187 | 188 | attributes = [('xmlns', xmlns,)] 189 | self._xml_start_tag('Types', attributes) 190 | 191 | def _write_default(self, extension, content_type): 192 | # Write the element. 193 | attributes = [ 194 | ('Extension', extension), 195 | ('ContentType', content_type), 196 | ] 197 | 198 | self._xml_empty_tag('Default', attributes) 199 | 200 | def _write_override(self, part_name, content_type): 201 | # Write the element. 202 | attributes = [ 203 | ('PartName', part_name), 204 | ('ContentType', content_type), 205 | ] 206 | 207 | self._xml_empty_tag('Override', attributes) 208 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/core.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Core - A class for writing the Excel XLSX Worksheet file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | # Standard packages. 9 | from datetime import datetime 10 | 11 | # Package imports. 12 | from . import xmlwriter 13 | 14 | 15 | class Core(xmlwriter.XMLwriter): 16 | """ 17 | A class for writing the Excel XLSX Core file. 18 | 19 | 20 | """ 21 | 22 | ########################################################################### 23 | # 24 | # Public API. 25 | # 26 | ########################################################################### 27 | 28 | def __init__(self): 29 | """ 30 | Constructor. 31 | 32 | """ 33 | 34 | super(Core, self).__init__() 35 | 36 | self.properties = {} 37 | 38 | ########################################################################### 39 | # 40 | # Private API. 41 | # 42 | ########################################################################### 43 | 44 | def _assemble_xml_file(self): 45 | # Assemble and write the XML file. 46 | 47 | # Write the XML declaration. 48 | self._xml_declaration() 49 | 50 | self._write_cp_core_properties() 51 | self._write_dc_title() 52 | self._write_dc_subject() 53 | self._write_dc_creator() 54 | self._write_cp_keywords() 55 | self._write_dc_description() 56 | self._write_cp_last_modified_by() 57 | self._write_dcterms_created() 58 | self._write_dcterms_modified() 59 | self._write_cp_category() 60 | self._write_cp_content_status() 61 | 62 | self._xml_end_tag('cp:coreProperties') 63 | 64 | # Close the file. 65 | self._xml_close() 66 | 67 | def _set_properties(self, properties): 68 | # Set the document properties. 69 | self.properties = properties 70 | 71 | def _datetime_to_iso8601_date(self, date): 72 | # Convert to a ISO 8601 style "2010-01-01T00:00:00Z" date. 73 | if not date: 74 | date = datetime.utcnow() 75 | 76 | return date.strftime("%Y-%m-%dT%H:%M:%SZ") 77 | 78 | ########################################################################### 79 | # 80 | # XML methods. 81 | # 82 | ########################################################################### 83 | 84 | def _write_cp_core_properties(self): 85 | # Write the element. 86 | 87 | xmlns_cp = ('http://schemas.openxmlformats.org/package/2006/' + 88 | 'metadata/core-properties') 89 | xmlns_dc = 'http://purl.org/dc/elements/1.1/' 90 | xmlns_dcterms = 'http://purl.org/dc/terms/' 91 | xmlns_dcmitype = 'http://purl.org/dc/dcmitype/' 92 | xmlns_xsi = 'http://www.w3.org/2001/XMLSchema-instance' 93 | 94 | attributes = [ 95 | ('xmlns:cp', xmlns_cp), 96 | ('xmlns:dc', xmlns_dc), 97 | ('xmlns:dcterms', xmlns_dcterms), 98 | ('xmlns:dcmitype', xmlns_dcmitype), 99 | ('xmlns:xsi', xmlns_xsi), 100 | ] 101 | 102 | self._xml_start_tag('cp:coreProperties', attributes) 103 | 104 | def _write_dc_creator(self): 105 | # Write the element. 106 | data = self.properties.get('author', '') 107 | 108 | self._xml_data_element('dc:creator', data) 109 | 110 | def _write_cp_last_modified_by(self): 111 | # Write the element. 112 | data = self.properties.get('author', '') 113 | 114 | self._xml_data_element('cp:lastModifiedBy', data) 115 | 116 | def _write_dcterms_created(self): 117 | # Write the element. 118 | date = self.properties.get('created', datetime.utcnow()) 119 | 120 | xsi_type = 'dcterms:W3CDTF' 121 | 122 | date = self._datetime_to_iso8601_date(date) 123 | 124 | attributes = [('xsi:type', xsi_type,)] 125 | 126 | self._xml_data_element('dcterms:created', date, attributes) 127 | 128 | def _write_dcterms_modified(self): 129 | # Write the element. 130 | date = self.properties.get('created', datetime.utcnow()) 131 | 132 | xsi_type = 'dcterms:W3CDTF' 133 | 134 | date = self._datetime_to_iso8601_date(date) 135 | 136 | attributes = [('xsi:type', xsi_type,)] 137 | 138 | self._xml_data_element('dcterms:modified', date, attributes) 139 | 140 | def _write_dc_title(self): 141 | # Write the element. 142 | if 'title' in self.properties: 143 | data = self.properties['title'] 144 | else: 145 | return 146 | 147 | self._xml_data_element('dc:title', data) 148 | 149 | def _write_dc_subject(self): 150 | # Write the element. 151 | if 'subject' in self.properties: 152 | data = self.properties['subject'] 153 | else: 154 | return 155 | 156 | self._xml_data_element('dc:subject', data) 157 | 158 | def _write_cp_keywords(self): 159 | # Write the element. 160 | if 'keywords' in self.properties: 161 | data = self.properties['keywords'] 162 | else: 163 | return 164 | 165 | self._xml_data_element('cp:keywords', data) 166 | 167 | def _write_dc_description(self): 168 | # Write the element. 169 | if 'comments' in self.properties: 170 | data = self.properties['comments'] 171 | else: 172 | return 173 | 174 | self._xml_data_element('dc:description', data) 175 | 176 | def _write_cp_category(self): 177 | # Write the element. 178 | if 'category' in self.properties: 179 | data = self.properties['category'] 180 | else: 181 | return 182 | 183 | self._xml_data_element('cp:category', data) 184 | 185 | def _write_cp_content_status(self): 186 | # Write the element. 187 | if 'status' in self.properties: 188 | data = self.properties['status'] 189 | else: 190 | return 191 | 192 | self._xml_data_element('cp:contentStatus', data) 193 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/custom.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Custom - A class for writing the Excel XLSX Custom Property file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | # Package imports. 9 | from . import xmlwriter 10 | 11 | 12 | class Custom(xmlwriter.XMLwriter): 13 | """ 14 | A class for writing the Excel XLSX Custom Workbook Property file. 15 | 16 | 17 | """ 18 | 19 | ########################################################################### 20 | # 21 | # Public API. 22 | # 23 | ########################################################################### 24 | 25 | def __init__(self): 26 | """ 27 | Constructor. 28 | 29 | """ 30 | 31 | super(Custom, self).__init__() 32 | 33 | self.properties = [] 34 | self.pid = 1 35 | 36 | def _set_properties(self, properties): 37 | # Set the document properties. 38 | self.properties = properties 39 | 40 | ########################################################################### 41 | # 42 | # Private API. 43 | # 44 | ########################################################################### 45 | 46 | def _assemble_xml_file(self): 47 | # Assemble and write the XML file. 48 | 49 | # Write the XML declaration. 50 | self._xml_declaration() 51 | 52 | self._write_properties() 53 | 54 | self._xml_end_tag('Properties') 55 | 56 | # Close the file. 57 | self._xml_close() 58 | 59 | ########################################################################### 60 | # 61 | # XML methods. 62 | # 63 | ########################################################################### 64 | 65 | def _write_properties(self): 66 | # Write the element. 67 | schema = 'http://schemas.openxmlformats.org/officeDocument/2006/' 68 | xmlns = schema + 'custom-properties' 69 | xmlns_vt = schema + 'docPropsVTypes' 70 | 71 | attributes = [ 72 | ('xmlns', xmlns), 73 | ('xmlns:vt', xmlns_vt), 74 | ] 75 | 76 | self._xml_start_tag('Properties', attributes) 77 | 78 | for custom_property in self.properties: 79 | # Write the property element. 80 | self._write_property(custom_property) 81 | 82 | def _write_property(self, custom_property): 83 | # Write the element. 84 | 85 | fmtid = '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}' 86 | 87 | name, value, property_type = custom_property 88 | self.pid += 1 89 | 90 | attributes = [ 91 | ('fmtid', fmtid), 92 | ('pid', self.pid), 93 | ('name', name), 94 | ] 95 | 96 | self._xml_start_tag('property', attributes) 97 | 98 | if property_type == 'number_int': 99 | # Write the vt:i4 element. 100 | self._write_vt_i4(value) 101 | elif property_type == 'number': 102 | # Write the vt:r8 element. 103 | self._write_vt_r8(value) 104 | elif property_type == 'date': 105 | # Write the vt:filetime element. 106 | self._write_vt_filetime(value) 107 | elif property_type == 'bool': 108 | # Write the vt:bool element. 109 | self._write_vt_bool(value) 110 | else: 111 | # Write the vt:lpwstr element. 112 | self._write_vt_lpwstr(value) 113 | 114 | self._xml_end_tag('property') 115 | 116 | def _write_vt_lpwstr(self, value): 117 | # Write the element. 118 | self._xml_data_element('vt:lpwstr', value) 119 | 120 | def _write_vt_filetime(self, value): 121 | # Write the element. 122 | self._xml_data_element('vt:filetime', value) 123 | 124 | def _write_vt_i4(self, value): 125 | # Write the element. 126 | self._xml_data_element('vt:i4', value) 127 | 128 | def _write_vt_r8(self, value): 129 | # Write the element. 130 | self._xml_data_element('vt:r8', value) 131 | 132 | def _write_vt_bool(self, value): 133 | # Write the element. 134 | 135 | if value: 136 | value = 'true' 137 | else: 138 | value = 'false' 139 | 140 | self._xml_data_element('vt:bool', value) 141 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/exceptions.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Exceptions - A class for XlsxWriter exceptions. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | 9 | class XlsxWriterException(Exception): 10 | """Base exception for XlsxWriter.""" 11 | 12 | 13 | class XlsxInputError(XlsxWriterException): 14 | """Base exception for all input data related errors.""" 15 | 16 | 17 | class XlsxFileError(XlsxWriterException): 18 | """Base exception for all file related errors.""" 19 | 20 | 21 | class EmptyChartSeries(XlsxInputError): 22 | """Chart must contain at least one data series.""" 23 | 24 | 25 | class DuplicateTableName(XlsxInputError): 26 | """Worksheet table name already exists.""" 27 | 28 | 29 | class InvalidWorksheetName(XlsxInputError): 30 | """Worksheet name is too long or contains restricted characters.""" 31 | 32 | 33 | class DuplicateWorksheetName(XlsxInputError): 34 | """Worksheet name already exists.""" 35 | 36 | 37 | class UndefinedImageSize(XlsxFileError): 38 | """No size data found in image file.""" 39 | 40 | 41 | class UnsupportedImageFormat(XlsxFileError): 42 | """Unsupported image file format.""" 43 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/relationships.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Relationships - A class for writing the Excel XLSX Worksheet file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | # Package imports. 9 | from . import xmlwriter 10 | 11 | # Long namespace strings used in the class. 12 | schema_root = 'http://schemas.openxmlformats.org' 13 | package_schema = schema_root + '/package/2006/relationships' 14 | document_schema = schema_root + '/officeDocument/2006/relationships' 15 | 16 | 17 | class Relationships(xmlwriter.XMLwriter): 18 | """ 19 | A class for writing the Excel XLSX Relationships file. 20 | 21 | 22 | """ 23 | 24 | ########################################################################### 25 | # 26 | # Public API. 27 | # 28 | ########################################################################### 29 | 30 | def __init__(self): 31 | """ 32 | Constructor. 33 | 34 | """ 35 | 36 | super(Relationships, self).__init__() 37 | 38 | self.relationships = [] 39 | self.id = 1 40 | 41 | ########################################################################### 42 | # 43 | # Private API. 44 | # 45 | ########################################################################### 46 | 47 | def _assemble_xml_file(self): 48 | # Assemble and write the XML file. 49 | 50 | # Write the XML declaration. 51 | self._xml_declaration() 52 | 53 | self._write_relationships() 54 | 55 | # Close the file. 56 | self._xml_close() 57 | 58 | def _add_document_relationship(self, rel_type, target, target_mode=None): 59 | # Add container relationship to XLSX .rels xml files. 60 | rel_type = document_schema + rel_type 61 | 62 | self.relationships.append((rel_type, target, target_mode)) 63 | 64 | def _add_package_relationship(self, rel_type, target): 65 | # Add container relationship to XLSX .rels xml files. 66 | rel_type = package_schema + rel_type 67 | 68 | self.relationships.append((rel_type, target, None)) 69 | 70 | def _add_ms_package_relationship(self, rel_type, target): 71 | # Add container relationship to XLSX .rels xml files. Uses MS schema. 72 | schema = 'http://schemas.microsoft.com/office/2006/relationships' 73 | rel_type = schema + rel_type 74 | 75 | self.relationships.append((rel_type, target, None)) 76 | 77 | def _add_worksheet_relationship(self, rel_type, target, target_mode=None): 78 | # Add worksheet relationship to sheet.rels xml files. 79 | rel_type = document_schema + rel_type 80 | 81 | self.relationships.append((rel_type, target, target_mode)) 82 | 83 | ########################################################################### 84 | # 85 | # XML methods. 86 | # 87 | ########################################################################### 88 | 89 | def _write_relationships(self): 90 | # Write the element. 91 | attributes = [('xmlns', package_schema,)] 92 | 93 | self._xml_start_tag('Relationships', attributes) 94 | 95 | for relationship in self.relationships: 96 | self._write_relationship(relationship) 97 | 98 | self._xml_end_tag('Relationships') 99 | 100 | def _write_relationship(self, relationship): 101 | # Write the element. 102 | rel_type, target, target_mode = relationship 103 | 104 | attributes = [ 105 | ('Id', 'rId' + str(self.id)), 106 | ('Type', rel_type), 107 | ('Target', target), 108 | ] 109 | 110 | self.id += 1 111 | 112 | if target_mode: 113 | attributes.append(('TargetMode', target_mode)) 114 | 115 | self._xml_empty_tag('Relationship', attributes) 116 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/shape.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Shape - A class for to represent Excel XLSX shape objects. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | import copy 8 | from warnings import warn 9 | 10 | 11 | class Shape(object): 12 | """ 13 | A class for to represent Excel XLSX shape objects. 14 | 15 | 16 | """ 17 | 18 | ########################################################################### 19 | # 20 | # Public API. 21 | # 22 | ########################################################################### 23 | 24 | def __init__(self, shape_type, name, options): 25 | """ 26 | Constructor. 27 | 28 | """ 29 | super(Shape, self).__init__() 30 | self.name = name 31 | self.shape_type = shape_type 32 | self.connect = 0 33 | self.drawing = 0 34 | self.edit_as = '' 35 | self.id = 0 36 | self.text = '' 37 | self.stencil = 1 38 | self.element = -1 39 | self.start = None 40 | self.start_index = None 41 | self.end = None 42 | self.end_index = None 43 | self.adjustments = [] 44 | self.start_side = '' 45 | self.end_side = '' 46 | self.flip_h = 0 47 | self.flip_v = 0 48 | self.rotation = 0 49 | self.textbox = False 50 | 51 | self.align = None 52 | self.fill = None 53 | self.font = None 54 | self.format = None 55 | self.line = None 56 | 57 | self._set_options(options) 58 | 59 | ########################################################################### 60 | # 61 | # Private API. 62 | # 63 | ########################################################################### 64 | 65 | def _set_options(self, options): 66 | 67 | self.align = self._get_align_properties(options.get('align')) 68 | self.fill = self._get_fill_properties(options.get('fill')) 69 | self.font = self._get_font_properties(options.get('font')) 70 | self.gradient = self._get_gradient_properties(options.get('gradient')) 71 | self.line = self._get_line_properties(options.get('line')) 72 | 73 | if options.get('border'): 74 | self.line = self._get_line_properties(options['border']) 75 | 76 | # Gradient fill overrides solid fill. 77 | if self.gradient: 78 | self.fill = None 79 | 80 | ########################################################################### 81 | # 82 | # Static methods for processing chart/shape style properties. 83 | # 84 | ########################################################################### 85 | 86 | @staticmethod 87 | def _get_line_properties(line): 88 | # Convert user line properties to the structure required internally. 89 | 90 | if not line: 91 | return {'defined': False} 92 | 93 | # Copy the user defined properties since they will be modified. 94 | line = copy.deepcopy(line) 95 | 96 | dash_types = { 97 | 'solid': 'solid', 98 | 'round_dot': 'sysDot', 99 | 'square_dot': 'sysDash', 100 | 'dash': 'dash', 101 | 'dash_dot': 'dashDot', 102 | 'long_dash': 'lgDash', 103 | 'long_dash_dot': 'lgDashDot', 104 | 'long_dash_dot_dot': 'lgDashDotDot', 105 | 'dot': 'dot', 106 | 'system_dash_dot': 'sysDashDot', 107 | 'system_dash_dot_dot': 'sysDashDotDot', 108 | } 109 | 110 | # Check the dash type. 111 | dash_type = line.get('dash_type') 112 | 113 | if dash_type is not None: 114 | if dash_type in dash_types: 115 | line['dash_type'] = dash_types[dash_type] 116 | else: 117 | warn("Unknown dash type '%s'" % dash_type) 118 | return 119 | 120 | line['defined'] = True 121 | 122 | return line 123 | 124 | @staticmethod 125 | def _get_fill_properties(fill): 126 | # Convert user fill properties to the structure required internally. 127 | 128 | if not fill: 129 | return {'defined': False} 130 | 131 | # Copy the user defined properties since they will be modified. 132 | fill = copy.deepcopy(fill) 133 | 134 | fill['defined'] = True 135 | 136 | return fill 137 | 138 | @staticmethod 139 | def _get_pattern_properties(pattern): 140 | # Convert user defined pattern to the structure required internally. 141 | 142 | if not pattern: 143 | return 144 | 145 | # Copy the user defined properties since they will be modified. 146 | pattern = copy.deepcopy(pattern) 147 | 148 | if not pattern.get('pattern'): 149 | warn("Pattern must include 'pattern'") 150 | return 151 | 152 | if not pattern.get('fg_color'): 153 | warn("Pattern must include 'fg_color'") 154 | return 155 | 156 | types = { 157 | 'percent_5': 'pct5', 158 | 'percent_10': 'pct10', 159 | 'percent_20': 'pct20', 160 | 'percent_25': 'pct25', 161 | 'percent_30': 'pct30', 162 | 'percent_40': 'pct40', 163 | 'percent_50': 'pct50', 164 | 'percent_60': 'pct60', 165 | 'percent_70': 'pct70', 166 | 'percent_75': 'pct75', 167 | 'percent_80': 'pct80', 168 | 'percent_90': 'pct90', 169 | 'light_downward_diagonal': 'ltDnDiag', 170 | 'light_upward_diagonal': 'ltUpDiag', 171 | 'dark_downward_diagonal': 'dkDnDiag', 172 | 'dark_upward_diagonal': 'dkUpDiag', 173 | 'wide_downward_diagonal': 'wdDnDiag', 174 | 'wide_upward_diagonal': 'wdUpDiag', 175 | 'light_vertical': 'ltVert', 176 | 'light_horizontal': 'ltHorz', 177 | 'narrow_vertical': 'narVert', 178 | 'narrow_horizontal': 'narHorz', 179 | 'dark_vertical': 'dkVert', 180 | 'dark_horizontal': 'dkHorz', 181 | 'dashed_downward_diagonal': 'dashDnDiag', 182 | 'dashed_upward_diagonal': 'dashUpDiag', 183 | 'dashed_horizontal': 'dashHorz', 184 | 'dashed_vertical': 'dashVert', 185 | 'small_confetti': 'smConfetti', 186 | 'large_confetti': 'lgConfetti', 187 | 'zigzag': 'zigZag', 188 | 'wave': 'wave', 189 | 'diagonal_brick': 'diagBrick', 190 | 'horizontal_brick': 'horzBrick', 191 | 'weave': 'weave', 192 | 'plaid': 'plaid', 193 | 'divot': 'divot', 194 | 'dotted_grid': 'dotGrid', 195 | 'dotted_diamond': 'dotDmnd', 196 | 'shingle': 'shingle', 197 | 'trellis': 'trellis', 198 | 'sphere': 'sphere', 199 | 'small_grid': 'smGrid', 200 | 'large_grid': 'lgGrid', 201 | 'small_check': 'smCheck', 202 | 'large_check': 'lgCheck', 203 | 'outlined_diamond': 'openDmnd', 204 | 'solid_diamond': 'solidDmnd', 205 | } 206 | 207 | # Check for valid types. 208 | if not pattern['pattern'] in types: 209 | warn("unknown pattern type '%s'" % pattern['pattern']) 210 | return 211 | else: 212 | pattern['pattern'] = types[pattern['pattern']] 213 | 214 | # Specify a default background color. 215 | pattern['bg_color'] = pattern.get('bg_color', '#FFFFFF') 216 | 217 | return pattern 218 | 219 | @staticmethod 220 | def _get_gradient_properties(gradient): 221 | # Convert user defined gradient to the structure required internally. 222 | 223 | if not gradient: 224 | return 225 | 226 | # Copy the user defined properties since they will be modified. 227 | gradient = copy.deepcopy(gradient) 228 | 229 | types = { 230 | 'linear': 'linear', 231 | 'radial': 'circle', 232 | 'rectangular': 'rect', 233 | 'path': 'shape' 234 | } 235 | 236 | # Check the colors array exists and is valid. 237 | if 'colors' not in gradient or type(gradient['colors']) != list: 238 | warn("Gradient must include colors list") 239 | return 240 | 241 | # Check the colors array has the required number of entries. 242 | if not 2 <= len(gradient['colors']) <= 10: 243 | warn("Gradient colors list must at least 2 values " 244 | "and not more than 10") 245 | return 246 | 247 | if 'positions' in gradient: 248 | # Check the positions array has the right number of entries. 249 | if len(gradient['positions']) != len(gradient['colors']): 250 | warn("Gradient positions not equal to number of colors") 251 | return 252 | 253 | # Check the positions are in the correct range. 254 | for pos in gradient['positions']: 255 | if not 0 <= pos <= 100: 256 | warn("Gradient position must be in the range " 257 | "0 <= position <= 100") 258 | return 259 | else: 260 | # Use the default gradient positions. 261 | if len(gradient['colors']) == 2: 262 | gradient['positions'] = [0, 100] 263 | 264 | elif len(gradient['colors']) == 3: 265 | gradient['positions'] = [0, 50, 100] 266 | 267 | elif len(gradient['colors']) == 4: 268 | gradient['positions'] = [0, 33, 66, 100] 269 | 270 | else: 271 | warn("Must specify gradient positions") 272 | return 273 | 274 | angle = gradient.get('angle') 275 | if angle: 276 | if not 0 <= angle < 360: 277 | warn("Gradient angle must be in the range " 278 | "0 <= angle < 360") 279 | return 280 | else: 281 | gradient['angle'] = 90 282 | 283 | # Check for valid types. 284 | gradient_type = gradient.get('type') 285 | 286 | if gradient_type is not None: 287 | 288 | if gradient_type in types: 289 | gradient['type'] = types[gradient_type] 290 | else: 291 | warn("Unknown gradient type '%s" % gradient_type) 292 | return 293 | else: 294 | gradient['type'] = 'linear' 295 | 296 | return gradient 297 | 298 | @staticmethod 299 | def _get_font_properties(options): 300 | # Convert user defined font values into private dict values. 301 | if options is None: 302 | options = {} 303 | 304 | font = { 305 | 'name': options.get('name'), 306 | 'color': options.get('color'), 307 | 'size': options.get('size', 11), 308 | 'bold': options.get('bold'), 309 | 'italic': options.get('italic'), 310 | 'underline': options.get('underline'), 311 | 'pitch_family': options.get('pitch_family'), 312 | 'charset': options.get('charset'), 313 | 'baseline': options.get('baseline', -1), 314 | 'rotation': options.get('rotation'), 315 | 'lang': options.get('lang', 'en-US'), 316 | } 317 | 318 | # Convert font size units. 319 | if font['size']: 320 | font['size'] = int(font['size'] * 100) 321 | 322 | # Convert rotation into 60,000ths of a degree. 323 | if font['rotation']: 324 | font['rotation'] = 60000 * int(font['rotation']) 325 | 326 | return font 327 | 328 | @staticmethod 329 | def _get_font_style_attributes(font): 330 | # _get_font_style_attributes. 331 | attributes = [] 332 | 333 | if not font: 334 | return attributes 335 | 336 | if font.get('size'): 337 | attributes.append(('sz', font['size'])) 338 | 339 | if font.get('bold') is not None: 340 | attributes.append(('b', 0 + font['bold'])) 341 | 342 | if font.get('italic') is not None: 343 | attributes.append(('i', 0 + font['italic'])) 344 | 345 | if font.get('underline') is not None: 346 | attributes.append(('u', 'sng')) 347 | 348 | if font.get('baseline') != -1: 349 | attributes.append(('baseline', font['baseline'])) 350 | 351 | return attributes 352 | 353 | @staticmethod 354 | def _get_font_latin_attributes(font): 355 | # _get_font_latin_attributes. 356 | attributes = [] 357 | 358 | if not font: 359 | return attributes 360 | 361 | if font['name'] is not None: 362 | attributes.append(('typeface', font['name'])) 363 | 364 | if font['pitch_family'] is not None: 365 | attributes.append(('pitchFamily', font['pitch_family'])) 366 | 367 | if font['charset'] is not None: 368 | attributes.append(('charset', font['charset'])) 369 | 370 | return attributes 371 | 372 | @staticmethod 373 | def _get_align_properties(align): 374 | # Convert user defined align to the structure required internally. 375 | if not align: 376 | return {'defined': False} 377 | 378 | # Copy the user defined properties since they will be modified. 379 | align = copy.deepcopy(align) 380 | 381 | if 'vertical' in align: 382 | align_type = align['vertical'] 383 | 384 | align_types = { 385 | 'top': 'top', 386 | 'middle': 'middle', 387 | 'bottom': 'bottom', 388 | } 389 | 390 | if align_type in align_types: 391 | align['vertical'] = align_types[align_type] 392 | else: 393 | warn("Unknown alignment type '%s'" % align_type) 394 | return {'defined': False} 395 | 396 | if 'horizontal' in align: 397 | align_type = align['horizontal'] 398 | 399 | align_types = { 400 | 'left': 'left', 401 | 'center': 'center', 402 | 'right': 'right', 403 | } 404 | 405 | if align_type in align_types: 406 | align['horizontal'] = align_types[align_type] 407 | else: 408 | warn("Unknown alignment type '%s'" % align_type) 409 | return {'defined': False} 410 | 411 | align['defined'] = True 412 | 413 | return align 414 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/sharedstrings.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # SharedStrings - A class for writing the Excel XLSX sharedStrings file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | # Standard packages. 9 | import re 10 | import sys 11 | 12 | # Package imports. 13 | from . import xmlwriter 14 | 15 | 16 | class SharedStrings(xmlwriter.XMLwriter): 17 | """ 18 | A class for writing the Excel XLSX sharedStrings file. 19 | 20 | """ 21 | 22 | ########################################################################### 23 | # 24 | # Public API. 25 | # 26 | ########################################################################### 27 | 28 | def __init__(self): 29 | """ 30 | Constructor. 31 | 32 | """ 33 | 34 | super(SharedStrings, self).__init__() 35 | 36 | self.string_table = None 37 | 38 | ########################################################################### 39 | # 40 | # Private API. 41 | # 42 | ########################################################################### 43 | 44 | def _assemble_xml_file(self): 45 | # Assemble and write the XML file. 46 | 47 | # Write the XML declaration. 48 | self._xml_declaration() 49 | 50 | # Write the sst element. 51 | self._write_sst() 52 | 53 | # Write the sst strings. 54 | self._write_sst_strings() 55 | 56 | # Close the sst tag. 57 | self._xml_end_tag('sst') 58 | 59 | # Close the file. 60 | self._xml_close() 61 | 62 | ########################################################################### 63 | # 64 | # XML methods. 65 | # 66 | ########################################################################### 67 | 68 | def _write_sst(self): 69 | # Write the element. 70 | xmlns = 'http://schemas.openxmlformats.org/spreadsheetml/2006/main' 71 | 72 | attributes = [ 73 | ('xmlns', xmlns), 74 | ('count', self.string_table.count), 75 | ('uniqueCount', self.string_table.unique_count), 76 | ] 77 | 78 | self._xml_start_tag('sst', attributes) 79 | 80 | def _write_sst_strings(self): 81 | # Write the sst string elements. 82 | 83 | for string in (self.string_table._get_strings()): 84 | self._write_si(string) 85 | 86 | def _write_si(self, string): 87 | # Write the element. 88 | attributes = [] 89 | 90 | # Excel escapes control characters with _xHHHH_ and also escapes any 91 | # literal strings of that type by encoding the leading underscore. 92 | # So "\0" -> _x0000_ and "_x0000_" -> _x005F_x0000_. 93 | # The following substitutions deal with those cases. 94 | 95 | # Escape the escape. 96 | string = re.sub('(_x[0-9a-fA-F]{4}_)', r'_x005F\1', string) 97 | 98 | # Convert control character to the _xHHHH_ escape. 99 | string = re.sub(r'([\x00-\x08\x0B-\x1F])', 100 | lambda match: "_x%04X_" % 101 | ord(match.group(1)), string) 102 | 103 | # Escape Unicode non-characters FFFE and FFFF. 104 | if sys.version_info[0] == 2: 105 | non_char1 = unichr(0xFFFE) 106 | non_char2 = unichr(0xFFFF) 107 | else: 108 | non_char1 = "\uFFFE" 109 | non_char2 = "\uFFFF" 110 | 111 | string = re.sub(non_char1, '_xFFFE_', string) 112 | string = re.sub(non_char2, '_xFFFF_', string) 113 | 114 | # Add attribute to preserve leading or trailing whitespace. 115 | if re.search(r'^\s', string) or re.search(r'\s$', string): 116 | attributes.append(('xml:space', 'preserve')) 117 | 118 | # Write any rich strings without further tags. 119 | if re.search('^', string) and re.search('$', string): 120 | self._xml_rich_si_element(string) 121 | else: 122 | self._xml_si_element(string, attributes) 123 | 124 | 125 | # A metadata class to store Excel strings between worksheets. 126 | class SharedStringTable(object): 127 | """ 128 | A class to track Excel shared strings between worksheets. 129 | 130 | """ 131 | 132 | def __init__(self): 133 | self.count = 0 134 | self.unique_count = 0 135 | self.string_table = {} 136 | self.string_array = [] 137 | 138 | def _get_shared_string_index(self, string): 139 | """" Get the index of the string in the Shared String table. """ 140 | if string not in self.string_table: 141 | # String isn't already stored in the table so add it. 142 | index = self.unique_count 143 | self.string_table[string] = index 144 | self.count += 1 145 | self.unique_count += 1 146 | return index 147 | else: 148 | # String exists in the table. 149 | index = self.string_table[string] 150 | self.count += 1 151 | return index 152 | 153 | def _get_shared_string(self, index): 154 | """" Get a shared string from the index. """ 155 | return self.string_array[index] 156 | 157 | def _sort_string_data(self): 158 | """" Sort the shared string data and convert from dict to list. """ 159 | self.string_array = sorted(self.string_table, 160 | key=self.string_table.__getitem__) 161 | self.string_table = {} 162 | 163 | def _get_strings(self): 164 | """" Return the sorted string list. """ 165 | return self.string_array 166 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/table.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Table - A class for writing the Excel XLSX Worksheet file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | from . import xmlwriter 9 | 10 | 11 | class Table(xmlwriter.XMLwriter): 12 | """ 13 | A class for writing the Excel XLSX Table file. 14 | 15 | 16 | """ 17 | 18 | ########################################################################### 19 | # 20 | # Public API. 21 | # 22 | ########################################################################### 23 | 24 | def __init__(self): 25 | """ 26 | Constructor. 27 | 28 | """ 29 | 30 | super(Table, self).__init__() 31 | 32 | self.properties = {} 33 | 34 | ########################################################################### 35 | # 36 | # Private API. 37 | # 38 | ########################################################################### 39 | 40 | def _assemble_xml_file(self): 41 | # Assemble and write the XML file. 42 | 43 | # Write the XML declaration. 44 | self._xml_declaration() 45 | 46 | # Write the table element. 47 | self._write_table() 48 | 49 | # Write the autoFilter element. 50 | self._write_auto_filter() 51 | 52 | # Write the tableColumns element. 53 | self._write_table_columns() 54 | 55 | # Write the tableStyleInfo element. 56 | self._write_table_style_info() 57 | 58 | # Close the table tag. 59 | self._xml_end_tag('table') 60 | 61 | # Close the file. 62 | self._xml_close() 63 | 64 | def _set_properties(self, properties): 65 | # Set the document properties. 66 | self.properties = properties 67 | 68 | ########################################################################### 69 | # 70 | # XML methods. 71 | # 72 | ########################################################################### 73 | 74 | def _write_table(self): 75 | # Write the element. 76 | schema = 'http://schemas.openxmlformats.org/' 77 | xmlns = schema + 'spreadsheetml/2006/main' 78 | table_id = self.properties['id'] 79 | name = self.properties['name'] 80 | display_name = self.properties['name'] 81 | ref = self.properties['range'] 82 | totals_row_shown = self.properties['totals_row_shown'] 83 | header_row_count = self.properties['header_row_count'] 84 | 85 | attributes = [ 86 | ('xmlns', xmlns), 87 | ('id', table_id), 88 | ('name', name), 89 | ('displayName', display_name), 90 | ('ref', ref), 91 | ] 92 | 93 | if not header_row_count: 94 | attributes.append(('headerRowCount', 0)) 95 | 96 | if totals_row_shown: 97 | attributes.append(('totalsRowCount', 1)) 98 | else: 99 | attributes.append(('totalsRowShown', 0)) 100 | 101 | self._xml_start_tag('table', attributes) 102 | 103 | def _write_auto_filter(self): 104 | # Write the element. 105 | autofilter = self.properties.get('autofilter', 0) 106 | 107 | if not autofilter: 108 | return 109 | 110 | attributes = [('ref', autofilter,)] 111 | 112 | self._xml_empty_tag('autoFilter', attributes) 113 | 114 | def _write_table_columns(self): 115 | # Write the element. 116 | columns = self.properties['columns'] 117 | 118 | count = len(columns) 119 | 120 | attributes = [('count', count)] 121 | 122 | self._xml_start_tag('tableColumns', attributes) 123 | 124 | for col_data in columns: 125 | # Write the tableColumn element. 126 | self._write_table_column(col_data) 127 | 128 | self._xml_end_tag('tableColumns') 129 | 130 | def _write_table_column(self, col_data): 131 | # Write the element. 132 | attributes = [ 133 | ('id', col_data['id']), 134 | ('name', col_data['name']), 135 | ] 136 | 137 | if col_data.get('total_string'): 138 | attributes.append(('totalsRowLabel', col_data['total_string'])) 139 | elif col_data.get('total_function'): 140 | attributes.append(('totalsRowFunction', 141 | col_data['total_function'])) 142 | 143 | if 'format' in col_data and col_data['format'] is not None: 144 | attributes.append(('dataDxfId', col_data['format'])) 145 | 146 | if col_data.get('formula'): 147 | self._xml_start_tag('tableColumn', attributes) 148 | 149 | # Write the calculatedColumnFormula element. 150 | self._write_calculated_column_formula(col_data['formula']) 151 | 152 | self._xml_end_tag('tableColumn') 153 | else: 154 | self._xml_empty_tag('tableColumn', attributes) 155 | 156 | def _write_table_style_info(self): 157 | # Write the element. 158 | props = self.properties 159 | 160 | name = props['style'] 161 | show_first_column = 0 + props['show_first_col'] 162 | show_last_column = 0 + props['show_last_col'] 163 | show_row_stripes = 0 + props['show_row_stripes'] 164 | show_column_stripes = 0 + props['show_col_stripes'] 165 | 166 | attributes = [ 167 | ('name', name), 168 | ('showFirstColumn', show_first_column), 169 | ('showLastColumn', show_last_column), 170 | ('showRowStripes', show_row_stripes), 171 | ('showColumnStripes', show_column_stripes), 172 | ] 173 | 174 | self._xml_empty_tag('tableStyleInfo', attributes) 175 | 176 | def _write_calculated_column_formula(self, formula): 177 | # Write the element. 178 | self._xml_data_element('calculatedColumnFormula', formula) 179 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/theme.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Theme - A class for writing the Excel XLSX Worksheet file. 4 | # 5 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 6 | # 7 | 8 | # Standard packages. 9 | import codecs 10 | import sys 11 | 12 | # Standard packages in Python 2/3 compatibility mode. 13 | from .compatibility import StringIO 14 | 15 | 16 | class Theme(object): 17 | """ 18 | A class for writing the Excel XLSX Theme file. 19 | 20 | 21 | """ 22 | 23 | ########################################################################### 24 | # 25 | # Public API. 26 | # 27 | ########################################################################### 28 | 29 | def __init__(self): 30 | """ 31 | Constructor. 32 | 33 | """ 34 | super(Theme, self).__init__() 35 | self.fh = None 36 | self.internal_fh = False 37 | 38 | ########################################################################### 39 | # 40 | # Private API. 41 | # 42 | ########################################################################### 43 | 44 | def _assemble_xml_file(self): 45 | # Assemble and write the XML file. 46 | self._write_theme_file() 47 | if self.internal_fh: 48 | self.fh.close() 49 | 50 | def _set_xml_writer(self, filename): 51 | # Set the XML writer filehandle for the object. 52 | if isinstance(filename, StringIO): 53 | self.internal_fh = False 54 | self.fh = filename 55 | else: 56 | self.internal_fh = True 57 | self.fh = codecs.open(filename, 'w', 'utf-8') 58 | 59 | ########################################################################### 60 | # 61 | # XML methods. 62 | # 63 | ########################################################################### 64 | 65 | def _write_theme_file(self): 66 | # Write a default theme.xml file. 67 | 68 | # The theme is encoded to allow Python 2.5/Jython support. 69 | default_theme = """\n""" 70 | 71 | if sys.version_info < (3, 0, 0): 72 | default_theme = default_theme.decode('unicode-escape') 73 | 74 | self.fh.write(default_theme) 75 | -------------------------------------------------------------------------------- /mobileperf/extlib/xlsxwriter/xmlwriter.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # XMLwriter - A base class for XlsxWriter classes. 4 | # 5 | # Used in conjunction with XlsxWriter. 6 | # 7 | # Copyright 2013-2018, John McNamara, jmcnamara@cpan.org 8 | # 9 | 10 | # Standard packages. 11 | import re 12 | import codecs 13 | 14 | # Standard packages in Python 2/3 compatibility mode. 15 | from .compatibility import StringIO 16 | 17 | 18 | class XMLwriter(object): 19 | """ 20 | Simple XML writer class. 21 | 22 | """ 23 | 24 | def __init__(self): 25 | self.fh = None 26 | self.escapes = re.compile('["&<>\n]') 27 | self.internal_fh = False 28 | 29 | def _set_filehandle(self, filehandle): 30 | # Set the writer filehandle directly. Mainly for testing. 31 | self.fh = filehandle 32 | self.internal_fh = False 33 | 34 | def _set_xml_writer(self, filename): 35 | # Set the XML writer filehandle for the object. 36 | if isinstance(filename, StringIO): 37 | self.internal_fh = False 38 | self.fh = filename 39 | else: 40 | self.internal_fh = True 41 | self.fh = codecs.open(filename, 'w', 'utf-8') 42 | 43 | def _xml_close(self): 44 | # Close the XML filehandle if we created it. 45 | if self.internal_fh: 46 | self.fh.close() 47 | 48 | def _xml_declaration(self): 49 | # Write the XML declaration. 50 | self.fh.write( 51 | """\n""") 52 | 53 | def _xml_start_tag(self, tag, attributes=[]): 54 | # Write an XML start tag with optional attributes. 55 | for key, value in attributes: 56 | value = self._escape_attributes(value) 57 | tag += ' %s="%s"' % (key, value) 58 | 59 | self.fh.write("<%s>" % tag) 60 | 61 | def _xml_start_tag_unencoded(self, tag, attributes=[]): 62 | # Write an XML start tag with optional, unencoded, attributes. 63 | # This is a minor speed optimization for elements that don't 64 | # need encoding. 65 | for key, value in attributes: 66 | tag += ' %s="%s"' % (key, value) 67 | 68 | self.fh.write("<%s>" % tag) 69 | 70 | def _xml_end_tag(self, tag): 71 | # Write an XML end tag. 72 | self.fh.write("" % tag) 73 | 74 | def _xml_empty_tag(self, tag, attributes=[]): 75 | # Write an empty XML tag with optional attributes. 76 | for key, value in attributes: 77 | value = self._escape_attributes(value) 78 | tag += ' %s="%s"' % (key, value) 79 | 80 | self.fh.write("<%s/>" % tag) 81 | 82 | def _xml_empty_tag_unencoded(self, tag, attributes=[]): 83 | # Write an empty XML tag with optional, unencoded, attributes. 84 | # This is a minor speed optimization for elements that don't 85 | # need encoding. 86 | for key, value in attributes: 87 | tag += ' %s="%s"' % (key, value) 88 | 89 | self.fh.write("<%s/>" % tag) 90 | 91 | def _xml_data_element(self, tag, data, attributes=[]): 92 | # Write an XML element containing data with optional attributes. 93 | end_tag = tag 94 | 95 | for key, value in attributes: 96 | value = self._escape_attributes(value) 97 | tag += ' %s="%s"' % (key, value) 98 | 99 | data = self._escape_data(data) 100 | self.fh.write("<%s>%s" % (tag, data, end_tag)) 101 | 102 | def _xml_string_element(self, index, attributes=[]): 103 | # Optimized tag writer for cell string elements in the inner loop. 104 | attr = '' 105 | 106 | for key, value in attributes: 107 | value = self._escape_attributes(value) 108 | attr += ' %s="%s"' % (key, value) 109 | 110 | self.fh.write("""%d""" % (attr, index)) 111 | 112 | def _xml_si_element(self, string, attributes=[]): 113 | # Optimized tag writer for shared strings elements. 114 | attr = '' 115 | 116 | for key, value in attributes: 117 | value = self._escape_attributes(value) 118 | attr += ' %s="%s"' % (key, value) 119 | 120 | string = self._escape_data(string) 121 | 122 | self.fh.write("""%s""" % (attr, string)) 123 | 124 | def _xml_rich_si_element(self, string): 125 | # Optimized tag writer for shared strings rich string elements. 126 | 127 | self.fh.write("""%s""" % string) 128 | 129 | def _xml_number_element(self, number, attributes=[]): 130 | # Optimized tag writer for cell number elements in the inner loop. 131 | attr = '' 132 | 133 | for key, value in attributes: 134 | value = self._escape_attributes(value) 135 | attr += ' %s="%s"' % (key, value) 136 | 137 | self.fh.write("""%.16g""" % (attr, number)) 138 | 139 | def _xml_formula_element(self, formula, result, attributes=[]): 140 | # Optimized tag writer for cell formula elements in the inner loop. 141 | attr = '' 142 | 143 | for key, value in attributes: 144 | value = self._escape_attributes(value) 145 | attr += ' %s="%s"' % (key, value) 146 | 147 | self.fh.write("""%s%s""" 148 | % (attr, self._escape_data(formula), 149 | self._escape_data(result))) 150 | 151 | def _xml_inline_string(self, string, preserve, attributes=[]): 152 | # Optimized tag writer for inlineStr cell elements in the inner loop. 153 | attr = '' 154 | t_attr = '' 155 | 156 | # Set the attribute to preserve whitespace. 157 | if preserve: 158 | t_attr = ' xml:space="preserve"' 159 | 160 | for key, value in attributes: 161 | value = self._escape_attributes(value) 162 | attr += ' %s="%s"' % (key, value) 163 | 164 | string = self._escape_data(string) 165 | 166 | self.fh.write("""%s""" % 167 | (attr, t_attr, string)) 168 | 169 | def _xml_rich_inline_string(self, string, attributes=[]): 170 | # Optimized tag writer for rich inlineStr in the inner loop. 171 | attr = '' 172 | 173 | for key, value in attributes: 174 | value = self._escape_attributes(value) 175 | attr += ' %s="%s"' % (key, value) 176 | 177 | self.fh.write("""%s""" % 178 | (attr, string)) 179 | 180 | def _escape_attributes(self, attribute): 181 | # Escape XML characters in attributes. 182 | try: 183 | if not self.escapes.search(attribute): 184 | return attribute 185 | except TypeError: 186 | return attribute 187 | 188 | attribute = re.sub('[&]', '&', attribute) 189 | attribute = re.sub('["]', '"', attribute) 190 | attribute = re.sub('[<]', '<', attribute) 191 | attribute = re.sub('[>]', '>', attribute) 192 | attribute = re.sub('[\n]', ' ', attribute) 193 | 194 | return attribute 195 | 196 | def _escape_data(self, data): 197 | # Escape XML characters in data sections of tags. Note, this 198 | # is different from _escape_attributes() in that double quotes 199 | # are not escaped by Excel. 200 | try: 201 | if not self.escapes.search(data): 202 | return data 203 | except TypeError: 204 | return data 205 | 206 | data = re.sub('[&]', '&', data) 207 | data = re.sub('[<]', '<', data) 208 | data = re.sub('[>]', '>', data) 209 | 210 | return data 211 | -------------------------------------------------------------------------------- /mobileperf/pic/PC impact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/PC impact.png -------------------------------------------------------------------------------- /mobileperf/pic/anr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/anr.png -------------------------------------------------------------------------------- /mobileperf/pic/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/architecture.png -------------------------------------------------------------------------------- /mobileperf/pic/config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/config.png -------------------------------------------------------------------------------- /mobileperf/pic/cpu table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/cpu table.png -------------------------------------------------------------------------------- /mobileperf/pic/cpu trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/cpu trend.png -------------------------------------------------------------------------------- /mobileperf/pic/dingding qr code.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/dingding qr code.jpg -------------------------------------------------------------------------------- /mobileperf/pic/exception config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/exception config.png -------------------------------------------------------------------------------- /mobileperf/pic/exception log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/exception log.png -------------------------------------------------------------------------------- /mobileperf/pic/fps table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/fps table.png -------------------------------------------------------------------------------- /mobileperf/pic/launchtime table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/launchtime table.png -------------------------------------------------------------------------------- /mobileperf/pic/logcat file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/logcat file.png -------------------------------------------------------------------------------- /mobileperf/pic/mem hprof.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/mem hprof.png -------------------------------------------------------------------------------- /mobileperf/pic/mem table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/mem table.png -------------------------------------------------------------------------------- /mobileperf/pic/mem trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/mem trend.png -------------------------------------------------------------------------------- /mobileperf/pic/pid change table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/pid change table.png -------------------------------------------------------------------------------- /mobileperf/pic/power table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/power table.png -------------------------------------------------------------------------------- /mobileperf/pic/pss table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/pss table.png -------------------------------------------------------------------------------- /mobileperf/pic/pss trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/pss trend.png -------------------------------------------------------------------------------- /mobileperf/pic/test info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/test info.png -------------------------------------------------------------------------------- /mobileperf/pic/test result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/test result.png -------------------------------------------------------------------------------- /mobileperf/pic/tool compare.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/tool compare.png -------------------------------------------------------------------------------- /mobileperf/pic/traffic table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alibaba/mobileperf/481ad2849768a5c560d2f9a2b4dfc7fb1a6aa8cc/mobileperf/pic/traffic table.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mobileperf 2 | # [US English] 3 | 4 | mobileperf is a Python PC tool that can collect Android performance data: cpu,memory,fps,logcat log,traffic,process thread number,process launch log.mobileperf also support monkey test. 5 | 6 | ## Features 7 | 8 | * Support most versions of Android OS from 5.0 to 10.0 9 | * No need root device,no need integrate SDK 10 | * Support Mac, linux, windows 11 | * Good stability, can run continuously for more than 72 hours 12 | * a little PC resource occupation, consume PC memory about 12M 13 | 14 | ## Getting started 15 | 16 | - Python3,recommend python3.7 17 | - adb,ensure system path contains adb 18 | 19 | - edit config file in mobileperf root dir,example config.conf 20 | - run ,in mobileperf root dir,mac or linux execute sh run.sh ,windows double click run.bat,end test wait timeout or click ctrl+C 21 | 22 | # [简体中文] 23 | 24 | mobileperf is python PC 工具,可以收集Android性能数据: cpu 内存 流畅度fps logcat日志 流量 进程线程数 进程启动日志,mobileperf也支持原生monkey test 25 | 26 | ## 特性 27 | 28 | - 支持Android5.0-10.0,兼容性好 29 | - 无需root设备,无需集成SDK,非侵入式,使用成本低 30 | - 支持mac linux windows 31 | - 稳定性好,能连续运行72小时以上 32 | - 少量占用PC资源,消耗PC内存约12M左右 33 | 34 | ## 使用方法 35 | 36 | - 安装python3.7 [python下载链接](https://www.python.org/downloads/),加入到环境变量中,执行python --version,确保是python3 37 | - 安装adb,确保adb devices能找到设备 38 | - 修改配置文件,示例参考根目录下config.conf 39 | 40 | - 运行,mac、linux 在mobileperf工具根目录下执行sh run.sh,windows 双击run.bat,结束测试,等待设置测试时长到或按Ctrl+C 41 | -------------------------------------------------------------------------------- /run.bat: -------------------------------------------------------------------------------- 1 | python mobileperf\android\startup.py 2 | pause -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | python3 mobileperf/android/startup.py 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | ''' 3 | @author: look 4 | 5 | @copyright: 1999-2020 Alibaba.com. All rights reserved. 6 | 7 | @license: Apache Software License 2.0 8 | 9 | @contact: 390125133@qq.com 10 | ''' 11 | 12 | from setuptools import find_packages, setup 13 | 14 | setup( 15 | name='mobileperf', 16 | version='1.0.0', 17 | author='look', 18 | maintainer='look', 19 | author_email='390125133@qq.com', 20 | install_requires=[ 21 | "requests", 22 | "urllib3", 23 | ], 24 | description="Python Android mobile perf (support Python3)" 25 | ) --------------------------------------------------------------------------------