first commit
This commit is contained in:
BIN
__pycache__/permissions.cpython-312.pyc
Normal file
BIN
__pycache__/permissions.cpython-312.pyc
Normal file
Binary file not shown.
0
appium_automation.log
Normal file
0
appium_automation.log
Normal file
BIN
globals/__pycache__/alarm.cpython-312.pyc
Normal file
BIN
globals/__pycache__/alarm.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/apis.cpython-312.pyc
Normal file
BIN
globals/__pycache__/apis.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/driver_utils.cpython-312.pyc
Normal file
BIN
globals/__pycache__/driver_utils.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/ex_apis.cpython-312.pyc
Normal file
BIN
globals/__pycache__/ex_apis.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/global_variable.cpython-312.pyc
Normal file
BIN
globals/__pycache__/global_variable.cpython-312.pyc
Normal file
Binary file not shown.
BIN
globals/__pycache__/ids.cpython-312.pyc
Normal file
BIN
globals/__pycache__/ids.cpython-312.pyc
Normal file
Binary file not shown.
88
globals/alarm.py
Normal file
88
globals/alarm.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
def push_audio_to_phone(device_id, local_audio_path, phone_audio_path): # 移除默认值
|
||||
"""
|
||||
将本地音频文件推送到手机指定路径
|
||||
"""
|
||||
# 检查本地文件是否存在
|
||||
if not os.path.exists(local_audio_path):
|
||||
logging.error(f"本地音频文件不存在: {local_audio_path}")
|
||||
return False
|
||||
|
||||
# 执行ADB推送命令
|
||||
cmd = ["adb", "-s", device_id, "push", local_audio_path, phone_audio_path]
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info(f"音频文件已推送到手机: {phone_audio_path}")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"推送音频文件失败: {result.stderr.strip()}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.error("推送音频文件超时")
|
||||
return False
|
||||
|
||||
def play_audio_on_phone(device_id, phone_audio_path):
|
||||
"""
|
||||
控制手机播放指定路径的音频文件
|
||||
"""
|
||||
# 检查文件是否存在于手机
|
||||
check_cmd = ["adb", "-s", device_id, "shell", "ls", phone_audio_path]
|
||||
check_result = subprocess.run(check_cmd, capture_output=True, text=True)
|
||||
|
||||
if check_result.returncode != 0:
|
||||
logging.error(f"音频文件在手机上不存在: {phone_audio_path}")
|
||||
return False
|
||||
|
||||
# 使用am命令播放音频
|
||||
cmd = [
|
||||
"adb", "-s", device_id, "shell",
|
||||
"am", "start",
|
||||
"-a", "android.intent.action.VIEW",
|
||||
"-t", "audio/*",
|
||||
"-d", f"file://{phone_audio_path}"
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info(f"已成功触发手机播放音频: {phone_audio_path}")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"播放音频失败: {result.stderr.strip()}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.error("播放音频命令超时")
|
||||
return False
|
||||
|
||||
def play_system_alarm(device_id):
|
||||
"""
|
||||
播放系统内置的警报声
|
||||
"""
|
||||
try:
|
||||
# 方法1:使用系统铃声URI
|
||||
cmd = [
|
||||
"adb", "-s", device_id, "shell",
|
||||
"am", "start",
|
||||
"-a", "android.intent.action.VIEW",
|
||||
"-d", "content://settings/system/alarm_alert"
|
||||
]
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info("成功触发系统警报声")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"播放系统警报失败: {result.stderr.strip()}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"播放系统警报时出错: {str(e)}")
|
||||
return False
|
||||
479
globals/apis.py
Normal file
479
globals/apis.py
Normal file
@@ -0,0 +1,479 @@
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import socket
|
||||
from typing import Optional, Dict, Any
|
||||
import globals.global_variable as global_variable
|
||||
|
||||
def send_tcp_command(command="StartMultiple", host="127.0.0.1", port=8888, timeout=10):
|
||||
"""
|
||||
使用TCP协议发送命令到指定地址和端口
|
||||
|
||||
参数:
|
||||
command: 要发送的命令字符串(默认:"StartMultiple")
|
||||
host: 目标主机地址(默认:"127.0.0.1")
|
||||
port: 目标端口(默认:8888)
|
||||
timeout: 连接超时时间(秒,默认:10)
|
||||
|
||||
返回:
|
||||
成功返回服务器响应(字符串),失败返回None
|
||||
"""
|
||||
# 创建TCP套接字
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
try:
|
||||
# 设置超时时间
|
||||
sock.settimeout(timeout)
|
||||
|
||||
# 连接到目标服务器
|
||||
sock.connect((host, port))
|
||||
logging.info(f"已成功连接到 {host}:{port}")
|
||||
|
||||
# 发送命令(注意:需要根据服务器要求的编码格式发送,这里用UTF-8)
|
||||
sock.sendall(command.encode('utf-8'))
|
||||
logging.info(f"已发送命令: {command}")
|
||||
|
||||
# 接收服务器响应(缓冲区大小1024字节,可根据实际情况调整)
|
||||
response = sock.recv(1024)
|
||||
if response:
|
||||
response_str = response.decode('utf-8')
|
||||
logging.info(f"收到响应: {response_str}")
|
||||
return response_str
|
||||
else:
|
||||
logging.info("未收到服务器响应")
|
||||
return None
|
||||
|
||||
except ConnectionRefusedError:
|
||||
logging.info(f"连接被拒绝,请检查 {host}:{port} 是否开启服务")
|
||||
return None
|
||||
except socket.timeout:
|
||||
logging.info(f"连接超时({timeout}秒)")
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.info(f"发送命令时发生错误: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_breakpoint_list():
|
||||
"""
|
||||
获取需要处理的断点列表
|
||||
"""
|
||||
# 请求参数
|
||||
params = {
|
||||
'user_name': global_variable.GLOBAL_USERNAME
|
||||
}
|
||||
|
||||
# 请求地址
|
||||
url = "https://engineering.yuxindazhineng.com/index/index/get_name_all"
|
||||
|
||||
try:
|
||||
# 发送GET请求
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
|
||||
# 检查请求是否成功
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
|
||||
# 检查接口返回状态
|
||||
if result.get('code') == 0:
|
||||
data = result.get('data', [])
|
||||
logging.info("成功获取断点列表,数据条数:", len(data))
|
||||
|
||||
# 打印断点信息
|
||||
# for item in data:
|
||||
# logging.info(f"线路编码: {item.get('line_num')}, "
|
||||
# f"线路名称: {item.get('line_name')}, "
|
||||
# f"状态: {item.get('status')}, "
|
||||
# f"用户: {item.get('name')}")
|
||||
|
||||
return data
|
||||
else:
|
||||
logging.info(f"接口返回错误: {result.get('code')}")
|
||||
return [{"id": 37,
|
||||
"user_name": "wangshun",
|
||||
"name": "wangshun",
|
||||
"line_num": "L193588",
|
||||
"line_name": "CDWZQ-2标-155号路基左线-461221-461570-155左-平原",
|
||||
"status": 3
|
||||
}]
|
||||
else:
|
||||
logging.info(f"请求失败,状态码: {response.status_code}")
|
||||
return []
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.info(f"请求异常: {e}")
|
||||
return []
|
||||
except ValueError as e:
|
||||
logging.info(f"JSON解析错误: {e}")
|
||||
return []
|
||||
|
||||
def get_measurement_task():
|
||||
"""
|
||||
获取测量任务
|
||||
返回: 如果有状态为1的数据返回任务信息,否则返回None
|
||||
"""
|
||||
try:
|
||||
url = "https://engineering.yuxindazhineng.com/index/index/getOne"
|
||||
|
||||
# 获取用户名
|
||||
user_name = global_variable.GLOBAL_USERNAME
|
||||
if not user_name:
|
||||
logging.error("未设置用户名,无法获取测量任务")
|
||||
return None
|
||||
|
||||
# 构造请求参数
|
||||
data = {
|
||||
"user_name": user_name
|
||||
}
|
||||
|
||||
logging.info(f"请求参数: user_name={user_name}")
|
||||
response = requests.post(url, data=data, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
logging.info(f"接口返回数据: {data}")
|
||||
|
||||
if data.get('code') == 0 and data.get('data'):
|
||||
task_data = data['data']
|
||||
if task_data.get('status') == 1:
|
||||
logging.info(f"获取到测量任务: {task_data}")
|
||||
return task_data
|
||||
else:
|
||||
logging.info("获取到的任务状态不为1,不执行测量")
|
||||
return None
|
||||
else:
|
||||
logging.warning("未获取到有效任务数据")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取测量任务失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_end_with_num():
|
||||
"""
|
||||
根据线路编码获取测量任务
|
||||
返回: 如果有状态为1的数据返回任务信息,否则返回None
|
||||
"""
|
||||
try:
|
||||
url = "https://engineering.yuxindazhineng.com/index/index/getOne3"
|
||||
|
||||
# 获取用户名
|
||||
user_name = global_variable.GLOBAL_USERNAME
|
||||
line_num = global_variable.GLOBAL_LINE_NUM
|
||||
if not line_num:
|
||||
logging.error("未设置线路编码,无法获取测量任务")
|
||||
return None
|
||||
if not user_name:
|
||||
logging.error("未设置用户名,无法获取测量任务")
|
||||
return None
|
||||
|
||||
# 构造请求参数
|
||||
data = {
|
||||
"user_name": user_name,
|
||||
"line_num": line_num
|
||||
}
|
||||
|
||||
# logging.info(f"请求参数: user_name={user_name}, line_num={line_num}")
|
||||
response = requests.post(url, data=data, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
logging.info(f"接口返回数据: {data}")
|
||||
|
||||
if data.get('code') == 0 and data.get('data'):
|
||||
task_data = data['data']
|
||||
if task_data.get('status') == 3:
|
||||
logging.info(f"获取到测量任务: {task_data}")
|
||||
return task_data
|
||||
else:
|
||||
logging.info("获取到的任务状态不为3,不执行测量")
|
||||
return None
|
||||
else:
|
||||
# logging.warning("未获取到有效任务数据")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"获取测量任务失败: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def get_work_conditions_by_linecode(linecode: str) -> Optional[Dict[str, Dict]]:
|
||||
"""
|
||||
通过线路编码获取工况信息
|
||||
|
||||
Args:
|
||||
linecode: 线路编码,如 "L118134"
|
||||
|
||||
Returns:
|
||||
返回字典,格式为 {point_id: {"sjName": "", "workinfoname": "", "work_type": ""}}
|
||||
如果请求失败返回None
|
||||
"""
|
||||
url="http://www.yuxindazhineng.com:3002/api/comprehensive_data/get_settlement_by_linecode"
|
||||
max_retries = 3 # 最大重试次数
|
||||
retry_count = 0 # 当前重试计数
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
# 准备请求参数
|
||||
payload = {"linecode": linecode}
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
||||
}
|
||||
|
||||
logging.info(f"发送POST请求到: {url}")
|
||||
logging.info(f"请求参数: {payload}")
|
||||
|
||||
# 发送POST请求
|
||||
response = requests.post(
|
||||
url,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
# 检查响应状态
|
||||
if response.status_code != 200:
|
||||
logging.error(f"HTTP请求失败,状态码: {response.status_code}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
|
||||
continue # 继续重试
|
||||
|
||||
# 解析响应数据
|
||||
try:
|
||||
result = response.json()
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"JSON解析失败: {str(e)}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
|
||||
continue # 继续重试
|
||||
|
||||
|
||||
# 检查API返回码
|
||||
if result.get('code') != 0:
|
||||
logging.error(f"API返回错误: {result.get('message', '未知错误')}")
|
||||
return None
|
||||
|
||||
# 提取数据
|
||||
data_list = result.get('data', [])
|
||||
if not data_list:
|
||||
logging.warning("未找到工况数据")
|
||||
return {}
|
||||
|
||||
# 处理数据,提取所需字段
|
||||
work_conditions = {}
|
||||
for item in data_list:
|
||||
point_id = item.get('aname')
|
||||
if point_id:
|
||||
work_conditions[point_id] = {
|
||||
"sjName": item.get('sjName', ''),
|
||||
"workinfoname": item.get('next_workinfo', ''),
|
||||
"work_type": item.get('work_type', '')
|
||||
}
|
||||
|
||||
logging.info(f"成功提取 {len(work_conditions)} 个测点的工况信息")
|
||||
return work_conditions
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"网络请求异常: {str(e)}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"JSON解析失败: {str(e)}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
|
||||
except Exception as e:
|
||||
logging.error(f"获取工况信息时发生未知错误: {str(e)}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"准备重试... (剩余 {max_retries - retry_count} 次)")
|
||||
# 达到最大重试次数仍失败
|
||||
logging.error(f"已达到最大重试次数 ({max_retries} 次),请求失败")
|
||||
return None
|
||||
|
||||
def get_user_max_variation(username: str) -> Optional[int]:
|
||||
"""
|
||||
调用POST接口根据用户名获取用户的max_variation信息
|
||||
|
||||
Args:
|
||||
username: 目标用户名,如 "chzq02-02guoyu"
|
||||
|
||||
Returns:
|
||||
成功:返回用户的max_variation整数值
|
||||
失败:返回None
|
||||
"""
|
||||
# 接口基础配置
|
||||
api_url = "http://www.yuxindazhineng.com:3002/api/accounts/get"
|
||||
timeout = 30 # 超时时间(避免请求长时间阻塞)
|
||||
|
||||
# 1. 准备请求参数与头部
|
||||
# 接口要求的POST参数(JSON格式)
|
||||
payload = {"username": username}
|
||||
# 请求头部:指定JSON格式,模拟浏览器UA避免被接口拦截
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
try:
|
||||
# 2. 发送POST请求
|
||||
|
||||
response = requests.post(
|
||||
url=api_url,
|
||||
json=payload, # 自动将字典转为JSON字符串,无需手动json.dumps()
|
||||
headers=headers,
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# 3. 检查HTTP响应状态(200表示请求成功到达服务器)
|
||||
response.raise_for_status() # 若状态码非200(如404、500),直接抛出HTTPError
|
||||
logging.info(f"接口请求成功,HTTP状态码:{response.status_code}")
|
||||
|
||||
# 4. 解析JSON响应(处理文档中提到的"网页解析失败"风险)
|
||||
try:
|
||||
response_data = response.json()
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"接口返回数据非JSON格式,解析失败:{str(e)}")
|
||||
logging.error(f"接口原始返回内容:{response.text[:500]}") # 打印前500字符便于排查
|
||||
return None
|
||||
|
||||
# 5. 检查接口业务逻辑是否成功(按需求中"code=0表示查询成功")
|
||||
if response_data.get("code") != 0:
|
||||
logging.error(f"接口查询失败,业务错误信息:{response_data.get('message', '未知错误')}")
|
||||
return None
|
||||
|
||||
# 6. 验证返回数据结构并提取max_variation
|
||||
data_list = response_data.get("data", [])
|
||||
if not data_list:
|
||||
logging.warning(f"查询到用户名 {username},但未返回账号数据")
|
||||
return None
|
||||
|
||||
# 检查第一条数据是否包含max_variation
|
||||
first_user = data_list[0]
|
||||
if "max_variation" not in first_user:
|
||||
logging.warning(f"用户 {username} 的返回数据中缺少 max_variation 字段")
|
||||
return None
|
||||
|
||||
max_variation = first_user["max_variation"]
|
||||
logging.info(f"成功查询到用户 {username} 的 max_variation:{max_variation}")
|
||||
|
||||
# 7. 直接返回max_variation的值
|
||||
return max_variation
|
||||
|
||||
# 处理请求过程中的异常(网络问题、超时等)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logging.error(f"接口请求异常(网络/超时/服务器不可达):{str(e)}")
|
||||
# 若为连接错误,提示检查文档中提到的"不支持的网页类型"或域名有效性
|
||||
if "ConnectionRefusedError" in str(e) or "Failed to establish a new connection" in str(e):
|
||||
logging.error(f"建议排查:1. 接口域名 {api_url} 是否可访问;2. 服务器是否正常运行;3. 端口3002是否开放")
|
||||
return None
|
||||
|
||||
# 处理其他未知异常
|
||||
except Exception as e:
|
||||
logging.error(f"获取用户 {username} 的 max_variation 时发生未知错误:{str(e)}")
|
||||
return None
|
||||
|
||||
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
|
||||
def get_line_info_and_save_global(user_name: str) -> bool:
|
||||
"""
|
||||
调用get_name_all接口,提取status=3的line_num和line_name存入全局字典
|
||||
:param user_name: 接口请求参数,如"wangshun"
|
||||
:return: 执行成功返回True,失败/异常返回False
|
||||
"""
|
||||
# 接口基础配置
|
||||
api_url = "https://engineering.yuxindazhineng.com/index/index/get_name_all"
|
||||
request_params = {"user_name": user_name} # GET请求参数
|
||||
timeout = 10 # 请求超时时间(秒),避免卡进程
|
||||
|
||||
try:
|
||||
# 1. 发送GET请求
|
||||
response = requests.get(
|
||||
url=api_url,
|
||||
params=request_params, # GET参数用params传递,自动拼接到URL后,规范且防乱码
|
||||
timeout=timeout,
|
||||
verify=False # 禁用SSL验证,适配HTTPS接口
|
||||
)
|
||||
|
||||
# 2. 校验HTTP状态码(先确保请求本身成功)
|
||||
if response.status_code != 200:
|
||||
logging.error(f"接口请求失败,HTTP状态码异常:{response.status_code},响应内容:{response.text}")
|
||||
return False
|
||||
|
||||
# 3. 解析JSON响应(接口返回是JSON格式,需解析为字典)
|
||||
try:
|
||||
response_data = response.json()
|
||||
except Exception as e:
|
||||
logging.error(f"接口返回内容非合法JSON,无法解析:{response.text},错误:{str(e)}")
|
||||
return False
|
||||
|
||||
# 4. 校验业务状态码(接口约定:code=0成功,-1失败)
|
||||
business_code = response_data.get("code")
|
||||
if business_code == 0:
|
||||
logging.info("接口业务请求成功,开始解析数据")
|
||||
elif business_code == -1:
|
||||
logging.error(f"接口业务请求失败,业务状态码code=-1,返回数据:{response_data}")
|
||||
return False
|
||||
else:
|
||||
logging.warning(f"接口返回未知业务状态码:{business_code},请确认接口文档")
|
||||
return False
|
||||
|
||||
# 5. 提取data字段,校验数据是否存在
|
||||
api_data_list = response_data.get("data")
|
||||
if not api_data_list:
|
||||
logging.warning("接口业务成功,但data字段为空或无数据")
|
||||
return False
|
||||
|
||||
# 6. 校验data是否为列表类型
|
||||
if not isinstance(api_data_list, list):
|
||||
logging.error(f"data字段不是列表类型,实际类型:{type(api_data_list)},内容:{api_data_list}")
|
||||
return False
|
||||
|
||||
found_valid_data = False
|
||||
|
||||
# 7. 遍历列表,提取所有status=3的数据
|
||||
for item in api_data_list:
|
||||
# 确保每个item是字典
|
||||
if not isinstance(item, dict):
|
||||
logging.warning(f"列表中的元素不是字典类型,跳过:{item}")
|
||||
continue
|
||||
|
||||
# 获取字段值
|
||||
data_status = item.get("status")
|
||||
line_num = item.get("line_num")
|
||||
line_name = item.get("line_name")
|
||||
|
||||
# 校验status是否为3,且目标字段非空
|
||||
if data_status == 3 and line_num and line_name:
|
||||
# # 存入全局字典:key=line_num,value=line_name
|
||||
# global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_num] = line_name
|
||||
# 存入全局字典:key=line_name,value=line_num
|
||||
global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT[line_name] = line_num
|
||||
|
||||
# 如果line_name不在列表中,则添加
|
||||
if line_name not in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST:
|
||||
global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST.append(line_name)
|
||||
|
||||
logging.info(f"找到status=3的线路信息:line_num={line_num}, line_name={line_name}")
|
||||
found_valid_data = True
|
||||
|
||||
if found_valid_data:
|
||||
logging.info(f"成功提取所有status=3的线路信息,当前全局字典数据:{global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT}")
|
||||
return True
|
||||
else:
|
||||
logging.warning("data列表中未找到任何status=3且字段完整的线路信息")
|
||||
return False
|
||||
|
||||
# 捕获所有请求相关异常(超时、连接失败、网络异常等)
|
||||
except requests.exceptions.Timeout:
|
||||
logging.error(f"调用get_name_all接口超时,超时时间:{timeout}秒,请求参数:{request_params}")
|
||||
return False
|
||||
except requests.exceptions.ConnectionError:
|
||||
logging.error(f"调用get_name_all接口连接失败,检查网络或接口地址是否正确:{api_url}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"调用get_name_all接口时发生未知异常:{str(e)}", exc_info=True) # exc_info=True打印异常堆栈,方便排查
|
||||
return False
|
||||
763
globals/driver_utils.py
Normal file
763
globals/driver_utils.py
Normal file
@@ -0,0 +1,763 @@
|
||||
import logging
|
||||
import time
|
||||
import subprocess
|
||||
import traceback
|
||||
import socket
|
||||
import os
|
||||
import requests
|
||||
from appium import webdriver
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from appium.webdriver.appium_service import AppiumService
|
||||
from appium.options.android import UiAutomator2Options
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException, NoSuchElementException, InvalidSessionIdException, WebDriverException
|
||||
import globals.global_variable as global_variable
|
||||
|
||||
|
||||
def init_appium_driver(device_id, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"):
|
||||
"""
|
||||
初始化Appium驱动的全局函数
|
||||
|
||||
参数:
|
||||
device_id: 设备ID
|
||||
app_package: 应用包名,默认为"com.bjjw.cjgc"
|
||||
app_activity: 应用启动Activity,默认为".activity.LoginActivity"
|
||||
|
||||
返回:
|
||||
(driver, wait): (WebDriver实例, WebDriverWait实例),如果初始化失败则抛出异常
|
||||
"""
|
||||
logging.info(f"设备 {device_id} 开始初始化Appium驱动")
|
||||
|
||||
# 创建并配置Appium选项
|
||||
options = UiAutomator2Options()
|
||||
options.platform_name = "Android"
|
||||
options.device_name = device_id
|
||||
options.app_package = app_package
|
||||
options.app_activity = app_activity
|
||||
options.automation_name = "UiAutomator2"
|
||||
options.no_reset = True
|
||||
options.auto_grant_permissions = True
|
||||
options.new_command_timeout = 28800
|
||||
options.udid = device_id
|
||||
|
||||
# 增加uiautomator2服务器启动超时时间
|
||||
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
|
||||
# 增加连接超时设置
|
||||
options.set_capability('connection_timeout', 120000) # 120秒
|
||||
|
||||
try:
|
||||
# 连接Appium服务器
|
||||
driver_url = "http://127.0.0.1:4723/wd/hub"
|
||||
logging.info(f"设备 {device_id} 正在连接Appium服务器: {driver_url}")
|
||||
driver = webdriver.Remote(driver_url, options=options)
|
||||
logging.info(f"设备 {device_id} Appium服务器连接成功")
|
||||
|
||||
# 初始化等待对象
|
||||
wait = WebDriverWait(driver, 20)
|
||||
logging.info(f"设备 {device_id} WebDriverWait初始化成功")
|
||||
|
||||
# 等待应用稳定
|
||||
time.sleep(2)
|
||||
|
||||
logging.info(f"设备 {device_id} Appium驱动初始化完成")
|
||||
return driver, wait
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {device_id} 初始化驱动失败: {str(e)}")
|
||||
logging.error(f"错误类型: {type(e).__name__}")
|
||||
logging.error(f"错误堆栈: {traceback.format_exc()}")
|
||||
# 如果驱动已创建,尝试关闭
|
||||
if 'driver' in locals() and driver:
|
||||
try:
|
||||
driver.quit()
|
||||
except:
|
||||
pass
|
||||
raise
|
||||
|
||||
def check_session_valid(driver, device_id=None):
|
||||
"""
|
||||
检查当前会话是否有效
|
||||
|
||||
参数:
|
||||
driver: WebDriver实例
|
||||
device_id: 设备ID(可选)
|
||||
|
||||
返回:
|
||||
bool: 会话有效返回True,否则返回False
|
||||
"""
|
||||
if device_id is None:
|
||||
device_id = global_variable.GLOBAL_DEVICE_ID
|
||||
device_str = f"设备 {device_id} " if device_id else ""
|
||||
|
||||
if not driver:
|
||||
logging.debug(f"{device_str}驱动实例为空")
|
||||
return False
|
||||
|
||||
try:
|
||||
# 首先检查driver是否有session_id属性
|
||||
if not hasattr(driver, 'session_id') or not driver.session_id:
|
||||
logging.debug(f"{device_str}驱动缺少有效的session_id")
|
||||
return False
|
||||
|
||||
# 尝试获取当前上下文
|
||||
current_context = driver.current_context
|
||||
logging.debug(f"{device_str}会话检查通过,当前上下文: {current_context}")
|
||||
return True
|
||||
|
||||
except InvalidSessionIdException:
|
||||
logging.debug(f"{device_str}会话已失效")
|
||||
return False
|
||||
|
||||
except WebDriverException as e:
|
||||
error_msg = str(e).lower()
|
||||
|
||||
# 明确的会话失效错误
|
||||
if any(phrase in error_msg for phrase in [
|
||||
"session is either terminated or not started",
|
||||
"could not proxy command to the remote server",
|
||||
"socket hang up",
|
||||
"connection refused",
|
||||
"max retries exceeded"
|
||||
]):
|
||||
logging.debug(f"{device_str}会话连接错误: {error_msg[:100]}")
|
||||
return False
|
||||
else:
|
||||
logging.debug(f"{device_str}WebDriver异常但可能不是会话失效: {error_msg[:100]}")
|
||||
return True
|
||||
|
||||
except (ConnectionError, ConnectionRefusedError, ConnectionResetError) as e:
|
||||
logging.debug(f"{device_str}网络连接错误: {str(e)}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
# 检查是否是连接相关错误
|
||||
if any(phrase in error_msg.lower() for phrase in [
|
||||
"10054", "10061", "connection", "connect", "refused", "urllib3"
|
||||
]):
|
||||
logging.debug(f"{device_str}连接相关异常: {error_msg[:100]}")
|
||||
return False
|
||||
else:
|
||||
logging.debug(f"{device_str}检查会话时出现其他异常: {error_msg[:100]}")
|
||||
return True # 对于真正的未知异常,保守返回True
|
||||
|
||||
|
||||
def reconnect_driver(device_id, old_driver=None, app_package="com.bjjw.cjgc", app_activity=".activity.LoginActivity"):
|
||||
"""
|
||||
重新连接Appium驱动,不重新启动应用
|
||||
|
||||
参数:
|
||||
device_id: 设备ID
|
||||
old_driver: 旧的WebDriver实例(可选)
|
||||
app_package: 应用包名
|
||||
app_activity: 应用启动Activity
|
||||
|
||||
返回:
|
||||
(driver, wait): 新的WebDriver和WebDriverWait实例
|
||||
"""
|
||||
# 使用传入的device_id或从全局变量获取
|
||||
if not device_id:
|
||||
device_id = global_variable.GLOBAL_DEVICE_ID
|
||||
|
||||
# 修复device_id参数类型问题并使用全局设备ID作为备用
|
||||
actual_device_id = device_id
|
||||
|
||||
# 检查device_id是否为有效的字符串格式
|
||||
if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)):
|
||||
# 尝试从old_driver获取设备ID
|
||||
if old_driver and hasattr(old_driver, 'capabilities'):
|
||||
capability_device_id = old_driver.capabilities.get('udid')
|
||||
if capability_device_id:
|
||||
actual_device_id = capability_device_id
|
||||
logging.warning(f"检测到device_id参数无效,已从old_driver中提取设备ID: {actual_device_id}")
|
||||
|
||||
# 如果仍然没有有效的设备ID,使用全局变量
|
||||
if not actual_device_id or (isinstance(actual_device_id, str) and ("session=" in actual_device_id or len(actual_device_id.strip()) == 0)):
|
||||
actual_device_id = global_variable.GLOBAL_DEVICE_ID
|
||||
logging.warning(f"无法获取有效设备ID,使用全局变量GLOBAL_DEVICE_ID: {actual_device_id}")
|
||||
|
||||
device_id = actual_device_id # 使用修正后的设备ID
|
||||
logging.info(f"设备 {device_id} 开始重新连接驱动(不重启应用)")
|
||||
|
||||
# # 首先安全关闭旧驱动
|
||||
# if old_driver:
|
||||
# safe_quit_driver(old_driver, device_id)
|
||||
|
||||
max_reconnect_attempts = 3
|
||||
reconnect_delay = 5 # 秒
|
||||
|
||||
for attempt in range(max_reconnect_attempts):
|
||||
try:
|
||||
logging.info(f"设备 {device_id} 第{attempt + 1}次尝试重新连接")
|
||||
|
||||
# 确保Appium服务器运行
|
||||
if not ensure_appium_server_running():
|
||||
logging.warning(f"设备 {device_id} Appium服务器未运行,尝试启动")
|
||||
time.sleep(reconnect_delay)
|
||||
continue
|
||||
|
||||
# 创建并配置Appium选项 - 重点:设置 autoLaunch=False 不自动启动应用
|
||||
options = UiAutomator2Options()
|
||||
options.platform_name = "Android"
|
||||
options.device_name = device_id
|
||||
options.app_package = app_package
|
||||
options.app_activity = app_activity
|
||||
options.automation_name = "UiAutomator2"
|
||||
options.no_reset = True
|
||||
options.auto_grant_permissions = True
|
||||
options.new_command_timeout = 3600
|
||||
options.udid = device_id
|
||||
|
||||
# 关键设置:不自动启动应用
|
||||
options.set_capability('autoLaunch', False)
|
||||
options.set_capability('skipUnlock', True)
|
||||
options.set_capability('skipServerInstallation', True)
|
||||
options.set_capability('skipDeviceInitialization', True)
|
||||
|
||||
# 增加uiautomator2服务器启动超时时间
|
||||
options.set_capability('uiautomator2ServerLaunchTimeout', 60000) # 60秒
|
||||
# 增加连接超时设置
|
||||
options.set_capability('connection_timeout', 120000) # 120秒
|
||||
|
||||
# 连接Appium服务器
|
||||
logging.info(f"设备 {device_id} 正在连接Appium服务器(不启动应用)")
|
||||
driver = webdriver.Remote("http://localhost:4723", options=options)
|
||||
logging.info(f"设备 {device_id} Appium服务器连接成功")
|
||||
|
||||
# 初始化等待对象
|
||||
wait = WebDriverWait(driver, 20)
|
||||
logging.info(f"设备 {device_id} WebDriverWait初始化成功")
|
||||
|
||||
# 不启动应用,直接附加到当前运行的应用
|
||||
try:
|
||||
# 获取当前运行的应用
|
||||
current_package = driver.current_package
|
||||
logging.info(f"设备 {device_id} 当前运行的应用: {current_package}")
|
||||
|
||||
# 如果当前运行的不是目标应用,尝试切换到目标应用
|
||||
if current_package != app_package:
|
||||
logging.info(f"设备 {device_id} 当前应用不是目标应用,尝试启动目标应用")
|
||||
launch_app_manually(driver, app_package, app_activity)
|
||||
else:
|
||||
logging.info(f"设备 {device_id} 已成功连接到运行中的目标应用")
|
||||
except Exception as attach_error:
|
||||
logging.warning(f"设备 {device_id} 获取当前应用信息失败: {str(attach_error)}")
|
||||
# 即使获取当前应用失败,也继续使用连接
|
||||
|
||||
# 验证新会话是否有效
|
||||
if check_session_valid(driver, device_id):
|
||||
logging.info(f"设备 {device_id} 重新连接成功")
|
||||
return driver, wait
|
||||
else:
|
||||
logging.warning(f"设备 {device_id} 新创建的会话无效,将重试")
|
||||
safe_quit_driver(driver, device_id)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {device_id} 第{attempt + 1}次重新连接失败: {str(e)}")
|
||||
if attempt < max_reconnect_attempts - 1:
|
||||
wait_time = reconnect_delay * (attempt + 1)
|
||||
logging.info(f"设备 {device_id} 将在{wait_time}秒后重试重新连接")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logging.error(f"设备 {device_id} 所有重新连接尝试均失败")
|
||||
# 所有尝试都失败后才关闭旧驱动
|
||||
if old_driver:
|
||||
safe_quit_driver(old_driver, device_id)
|
||||
raise
|
||||
|
||||
# 所有尝试都失败
|
||||
raise Exception(f"设备 {device_id} 重新连接失败,已尝试{max_reconnect_attempts}次")
|
||||
|
||||
|
||||
|
||||
# def ensure_appium_server_running(port=4723):
|
||||
# """使用完整的环境变量启动Appium,解决路径缺失问题"""
|
||||
# try:
|
||||
# # 获取当前用户的环境变量
|
||||
# env = os.environ.copy()
|
||||
|
||||
# # ========== 仅修改这部分:Windows下Node.js/Appium常见路径 ==========
|
||||
# additional_paths = [
|
||||
# # Windows默认Node.js安装路径(64位)
|
||||
# os.path.join("C:\\", "Program Files\\nodejs"),
|
||||
# # Windows32位Node.js路径
|
||||
# os.path.join("C:\\", "Program Files (x86)\\nodejs"),
|
||||
# # npm全局安装路径(Windows核心,appium一般装在这里)
|
||||
# os.path.expanduser("~\\AppData\\Roaming\\npm"),
|
||||
# # 系统默认路径(防止基础命令缺失)
|
||||
# os.path.join("C:\\", "Windows\\System32"),
|
||||
# os.path.join("C:\\", "Windows\\System"),
|
||||
# # 自定义Node.js/npm路径(可选,根据你的实际安装路径加)
|
||||
# # "D:\\Program Files\\nodejs", # 若你装在D盘,解开注释并修改
|
||||
# ]
|
||||
# # ========== Windows路径修改结束 ==========
|
||||
|
||||
# # 更新PATH环境变量(跨系统通用,os.pathsep自动适配Windows的;和macOS的:)
|
||||
# current_path = env.get('PATH', '')
|
||||
# new_path = current_path + os.pathsep + os.pathsep.join(additional_paths)
|
||||
# env['PATH'] = new_path
|
||||
|
||||
# # 构建启动命令(和原函数一致,无修改)
|
||||
# appium_cmd = f"appium -p {port} --log-level error"
|
||||
|
||||
# # 使用完整环境启动(跨系统通用,Windows下正常执行)
|
||||
# process = subprocess.Popen(
|
||||
# appium_cmd,
|
||||
# shell=True, # Windows下字符串命令必须开启,和macOS一致
|
||||
# env=env, # 传入补全后的Windows环境变量(核心)
|
||||
# stdout=subprocess.PIPE, # 捕获输出,控制台不刷屏
|
||||
# stderr=subprocess.PIPE,
|
||||
# text=True # 输出为字符串,无需手动解码(跨系统通用)
|
||||
# )
|
||||
|
||||
# logging.info(f"Appium启动进程已创建,PID: {process.pid}")
|
||||
# # 等待并校验服务启动成功(需确保wait_for_appium_start已定义,timeout有值)
|
||||
# return wait_for_appium_start(port)
|
||||
|
||||
# except Exception as e:
|
||||
# logging.error(f"Windows下启动Appium时出错: {str(e)}")
|
||||
# return False
|
||||
|
||||
def is_port_in_use(port):
|
||||
"""检查端口是否被占用"""
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
return s.connect_ex(('127.0.0.1', port)) == 0
|
||||
|
||||
def kill_system_process(process_name):
|
||||
"""杀掉系统进程"""
|
||||
try:
|
||||
# Windows
|
||||
subprocess.run(f"taskkill /F /IM {process_name}", shell=True, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def start_appium_server(port=4723):
|
||||
"""启动 Appium 服务,强制指定路径兼容性"""
|
||||
# 1. 先尝试清理可能占用的 node 进程
|
||||
if is_port_in_use(port):
|
||||
logging.warning(f"端口 {port} 被占用,尝试清理 node.exe...")
|
||||
kill_system_process("node.exe")
|
||||
time.sleep(2)
|
||||
|
||||
# 2. 构造启动命令
|
||||
# 注意:这里增加了 --base-path /wd/hub 解决 404 问题
|
||||
# --allow-cors 允许跨域,有时候能解决连接问题
|
||||
appium_cmd = f"appium -p {port} --base-path /wd/hub --allow-cors"
|
||||
|
||||
logging.info(f"正在启动 Appium: {appium_cmd}")
|
||||
try:
|
||||
# 使用 shell=True 在 Windows 上更稳定
|
||||
# creationflags=subprocess.CREATE_NEW_CONSOLE 可以让它在后台运行不弹出窗口
|
||||
subprocess.Popen(appium_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
logging.info("Appium 启动命令已发送,等待服务就绪...")
|
||||
except Exception as e:
|
||||
logging.error(f"启动 Appium 进程失败: {e}")
|
||||
|
||||
def check_server_status(port):
|
||||
"""检测服务器状态,兼容 Appium 1.x 和 2.x 路径"""
|
||||
base_url = f"http://127.0.0.1:{port}"
|
||||
check_paths = ["/wd/hub/status", "/status"] # 优先检查 /wd/hub
|
||||
|
||||
for path in check_paths:
|
||||
try:
|
||||
url = f"{base_url}{path}"
|
||||
response = requests.get(url, timeout=1)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
def ensure_appium_server_running(port=4723):
|
||||
"""确保 Appium 服务器正在运行,如果没运行则启动它"""
|
||||
|
||||
# 1. 第一次快速检测
|
||||
if check_server_status(port):
|
||||
logging.info(f"Appium 服务已在端口 {port} 运行")
|
||||
return True
|
||||
|
||||
# 2. 如果没运行,启动它
|
||||
logging.warning(f"Appium 未在端口 {port} 运行,准备启动...")
|
||||
start_appium_server(port)
|
||||
|
||||
# 3. 循环等待启动成功(最多等待 20 秒)
|
||||
max_retries = 20
|
||||
for i in range(max_retries):
|
||||
if check_server_status(port):
|
||||
logging.info("Appium 服务启动成功并已就绪!")
|
||||
return True
|
||||
|
||||
time.sleep(1)
|
||||
if i % 5 == 0:
|
||||
logging.info(f"等待 Appium 启动中... ({i}/{max_retries})")
|
||||
|
||||
logging.error("Appium 服务启动超时!请检查 appium 命令是否在命令行可直接运行。")
|
||||
return False
|
||||
|
||||
# ... (保留 init_appium_driver, safe_quit_driver 等其他函数) ...
|
||||
|
||||
def wait_for_appium_start(port, timeout=10):
|
||||
"""
|
||||
检测指定端口的Appium服务是否真正启动并可用
|
||||
:param port: Appium服务端口
|
||||
:param timeout: 最大等待时间(秒)
|
||||
:return: 服务就绪返回True,超时/失败返回False
|
||||
"""
|
||||
# Appium官方状态查询接口
|
||||
check_url = f"http://localhost:{port}/wd/hub/status"
|
||||
# 检测开始时间
|
||||
start_check_time = time.time()
|
||||
logging.info(f"开始检测Appium服务是否就绪,端口:{port},最大等待{timeout}秒")
|
||||
|
||||
while time.time() - start_check_time < timeout:
|
||||
try:
|
||||
# 发送HTTP请求,超时1秒(避免单次检测卡太久)
|
||||
response = requests.get(
|
||||
url=check_url,
|
||||
timeout=1,
|
||||
verify=False # 本地接口,禁用SSL验证
|
||||
)
|
||||
# 接口返回200(HTTP成功状态码),且JSON中status=0(Appium服务正常)
|
||||
if response.status_code == 200 and response.json().get("status") == 0:
|
||||
logging.info(f"Appium服务检测成功,端口{port}已就绪")
|
||||
return True
|
||||
except Exception as e:
|
||||
# 捕获所有异常(连接拒绝、超时、JSON解析失败等),说明服务未就绪
|
||||
logging.debug(f"本次检测Appium服务未就绪:{str(e)}") # 调试日志,不刷屏
|
||||
|
||||
# 检测失败,休眠1秒后重试
|
||||
time.sleep(1)
|
||||
|
||||
# 循环结束→超时
|
||||
logging.error(f"检测超时!{timeout}秒内Appium服务端口{port}仍未就绪")
|
||||
return False
|
||||
|
||||
def safe_quit_driver(driver, device_id=None):
|
||||
"""
|
||||
安全关闭驱动的全局函数
|
||||
|
||||
参数:
|
||||
driver: WebDriver实例
|
||||
device_id: 设备ID(可选)
|
||||
"""
|
||||
if device_id is None:
|
||||
device_id = global_variable.GLOBAL_DEVICE_ID
|
||||
device_str = f"设备 {device_id} " if device_id else ""
|
||||
logging.info(f"{device_str}开始关闭驱动")
|
||||
|
||||
if not driver:
|
||||
logging.info(f"{device_str}没有可关闭的驱动实例")
|
||||
return
|
||||
|
||||
# 检查driver是否为WebDriver实例或是否有quit方法
|
||||
if not hasattr(driver, 'quit'):
|
||||
logging.warning(f"{device_str}驱动对象类型无效,不具有quit方法: {type(driver).__name__}")
|
||||
return
|
||||
|
||||
max_quit_attempts = 3
|
||||
for attempt in range(max_quit_attempts):
|
||||
try:
|
||||
logging.info(f"{device_str}尝试关闭驱动 (尝试 {attempt + 1}/{max_quit_attempts})")
|
||||
driver.quit()
|
||||
logging.info(f"{device_str}驱动已成功关闭")
|
||||
return
|
||||
except Exception as e:
|
||||
logging.error(f"{device_str}关闭驱动时出错 (尝试 {attempt + 1}/{max_quit_attempts}): {str(e)}")
|
||||
if attempt < max_quit_attempts - 1:
|
||||
# 等待一段时间后重试
|
||||
wait_time = 2
|
||||
logging.info(f"{device_str}将在 {wait_time} 秒后重试")
|
||||
time.sleep(wait_time)
|
||||
else:
|
||||
logging.critical(f"{device_str}尝试多次关闭驱动失败,可能导致资源泄漏")
|
||||
|
||||
|
||||
def is_app_launched(driver, package_name="com.bjjw.cjgc"):
|
||||
"""
|
||||
检查应用是否已启动
|
||||
|
||||
参数:
|
||||
driver: WebDriver实例
|
||||
package_name: 应用包名,默认为"com.bjjw.cjgc"
|
||||
|
||||
返回:
|
||||
bool: 如果应用已启动则返回True,否则返回False
|
||||
"""
|
||||
try:
|
||||
# 通过检查当前活动的包名来确认应用是否已启动
|
||||
current_package = driver.current_package
|
||||
return current_package == package_name
|
||||
except Exception as e:
|
||||
logging.error(f"检查应用启动状态时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def launch_app_manually(driver, device_id, package_name="com.bjjw.cjgc", activity=".activity.LoginActivity"):
|
||||
"""
|
||||
手动启动应用
|
||||
|
||||
参数:
|
||||
driver: WebDriver实例
|
||||
package_name: 应用包名,默认为"com.bjjw.cjgc"
|
||||
activity: 应用启动Activity,默认为".activity.LoginActivity"
|
||||
"""
|
||||
try:
|
||||
if not device_id:
|
||||
device_id = global_variable.GLOBAL_DEVICE_ID
|
||||
# 尝试从driver获取设备ID
|
||||
if driver and hasattr(driver, 'capabilities'):
|
||||
device_id = driver.capabilities.get('udid')
|
||||
device_str = f"设备 {device_id} " if device_id else ""
|
||||
else:
|
||||
device_str = ""
|
||||
|
||||
logging.info(f"{device_str}尝试手动启动应用: {package_name}/{activity}")
|
||||
|
||||
# 首先使用ADB命令退出应用
|
||||
if device_id:
|
||||
try:
|
||||
# 使用ADB命令强制停止应用
|
||||
stop_cmd = [
|
||||
"adb", "-s", device_id,
|
||||
"shell", "am", "force-stop",
|
||||
package_name
|
||||
]
|
||||
stop_result = subprocess.run(stop_cmd, capture_output=True, text=True, timeout=15)
|
||||
if stop_result.returncode == 0:
|
||||
logging.info(f"{device_str}已使用ADB命令成功退出应用")
|
||||
else:
|
||||
logging.warning(f"{device_str}ADB退出应用失败: {stop_result.stderr}")
|
||||
except Exception as stop_error:
|
||||
logging.warning(f"{device_str}退出应用时出错: {str(stop_error)}")
|
||||
|
||||
# 首先尝试使用driver的execute_script方法启动应用
|
||||
try:
|
||||
if driver:
|
||||
driver.execute_script("mobile: startActivity", {
|
||||
"intent": f"{package_name}/{activity}"
|
||||
})
|
||||
logging.info(f"{device_str}已使用Appium startActivity命令启动应用")
|
||||
except Exception as inner_e:
|
||||
logging.warning(f"{device_str}使用Appium startActivity命令失败: {str(inner_e)},尝试使用ADB命令")
|
||||
|
||||
# 如果device_id可用,使用ADB命令启动应用
|
||||
if device_id:
|
||||
cmd = [
|
||||
"adb", "-s", device_id,
|
||||
"shell", "am", "start",
|
||||
"-n", f"{package_name}/{activity}"
|
||||
]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info(f"{device_str}已使用ADB命令成功启动应用")
|
||||
else:
|
||||
logging.error(f"{device_str}ADB启动应用失败: {result.stderr}")
|
||||
else:
|
||||
logging.warning("无法获取设备ID,无法使用ADB命令启动应用")
|
||||
|
||||
# 设置屏幕永不休眠
|
||||
if device_id:
|
||||
try:
|
||||
# 使用ADB命令设置屏幕永不休眠
|
||||
screen_timeout_cmd = [
|
||||
"adb", "-s", device_id,
|
||||
"shell", "settings", "put", "system", "screen_off_timeout", "0"
|
||||
]
|
||||
timeout_result = subprocess.run(screen_timeout_cmd, capture_output=True, text=True, timeout=15)
|
||||
if timeout_result.returncode == 0:
|
||||
logging.info(f"{device_str}已成功设置屏幕永不休眠")
|
||||
else:
|
||||
logging.warning(f"{device_str}设置屏幕永不休眠失败: {timeout_result.stderr}")
|
||||
except Exception as timeout_error:
|
||||
logging.warning(f"{device_str}设置屏幕永不休眠时出错: {str(timeout_error)}")
|
||||
|
||||
# 等待应用启动
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
logging.error(f"手动启动应用时出错: {str(e)}")
|
||||
logging.error(f"错误堆栈: {traceback.format_exc()}")
|
||||
|
||||
def go_main_click_tabber_button(driver, device_id, tabber_button_text, max_retries=3):
|
||||
"""
|
||||
跳转到主页面并点击对应的导航菜单按钮(带重试机制)
|
||||
|
||||
参数:
|
||||
driver: WebDriver实例
|
||||
device_id: 设备ID
|
||||
tabber_button_text: 导航菜单按钮的文本
|
||||
max_retries: 最大重试次数
|
||||
|
||||
返回:
|
||||
bool: 成功返回True,失败返回False
|
||||
"""
|
||||
retry_count = 0
|
||||
|
||||
while retry_count < max_retries:
|
||||
try:
|
||||
logging.info(f"设备 {device_id} 第 {retry_count + 1} 次尝试执行导航操作")
|
||||
|
||||
if not check_session_valid(driver, device_id):
|
||||
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
|
||||
try:
|
||||
# 重新连接,获取新的driver
|
||||
new_driver, _ = reconnect_driver(device_id, driver)
|
||||
driver = new_driver # 更新driver引用
|
||||
logging.info(f"设备 {device_id} 驱动重连成功")
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {device_id} 驱动重连失败: {str(e)}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
time.sleep(2)
|
||||
continue
|
||||
else:
|
||||
return False
|
||||
|
||||
# 检查当前是否已经在主页面
|
||||
current_activity = driver.current_activity
|
||||
logging.info(f"设备 {device_id} 当前Activity: {current_activity}")
|
||||
|
||||
if ".activity.MainActivity" in current_activity:
|
||||
logging.info(f"设备 {device_id} 已在主页面")
|
||||
else:
|
||||
logging.info(f"设备 {device_id} 当前不在主页面,循环点击返回按钮,直到在主页面")
|
||||
|
||||
max_back_presses = 10 # 最大返回键次数
|
||||
back_press_count = 0
|
||||
|
||||
while ".activity.MainActivity" not in current_activity and back_press_count < max_back_presses:
|
||||
try:
|
||||
if not check_session_valid(driver, device_id):
|
||||
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
|
||||
if not reconnect_driver(device_id, driver):
|
||||
logging.error(f"设备 {device_id} 驱动重连失败")
|
||||
# 点击返回按钮
|
||||
driver.back()
|
||||
back_press_count += 1
|
||||
time.sleep(1)
|
||||
|
||||
# 更新当前Activity
|
||||
current_activity = driver.current_activity
|
||||
logging.info(f"设备 {device_id} 点击返回按钮 {back_press_count} 次后,当前Activity: {current_activity}")
|
||||
|
||||
except Exception as inner_e:
|
||||
logging.warning(f"设备 {device_id} 点击返回按钮时出错: {str(inner_e)}")
|
||||
break
|
||||
|
||||
# 检查是否成功回到主页面
|
||||
if ".activity.MainActivity" not in current_activity:
|
||||
logging.warning(f"设备 {device_id} 无法回到主页面,当前Activity: {current_activity}")
|
||||
# 不立即返回,继续重试逻辑
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"设备 {device_id} 等待2秒后重试...")
|
||||
time.sleep(2)
|
||||
continue
|
||||
else:
|
||||
logging.error(f"设备 {device_id} 达到最大重试次数,无法回到主页面")
|
||||
return False
|
||||
|
||||
# 现在已经在主页面,点击指定的导航菜单按钮
|
||||
# logging.info(f"设备 {device_id} 已在主页面,尝试点击导航菜单按钮: {tabber_button_text}")
|
||||
|
||||
try:
|
||||
tabber_button = driver.find_element(AppiumBy.ID, tabber_button_text)
|
||||
# 点击按钮
|
||||
tabber_button.click()
|
||||
logging.info(f"设备 {device_id} 已成功点击导航菜单按钮: {tabber_button_text}")
|
||||
|
||||
# 等待页面加载
|
||||
time.sleep(2)
|
||||
|
||||
# 验证操作是否成功
|
||||
# 可以添加一些验证逻辑,比如检查是否跳转到目标页面
|
||||
new_activity = driver.current_activity
|
||||
logging.info(f"设备 {device_id} 点击后当前Activity: {new_activity}")
|
||||
|
||||
return True
|
||||
|
||||
except TimeoutException:
|
||||
logging.error(f"设备 {device_id} 等待导航菜单按钮 '{tabber_button_text}' 超时")
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {device_id} 点击导航菜单按钮 '{tabber_button_text}' 时出错: {str(e)}")
|
||||
|
||||
# 检查会话有效性并尝试重连
|
||||
if not check_session_valid(driver, device_id):
|
||||
logging.warning(f"设备 {device_id} 会话无效,尝试重新连接驱动...")
|
||||
if reconnect_driver(device_id, driver):
|
||||
logging.info(f"设备 {device_id} 驱动重连成功,继续重试")
|
||||
# 重连成功后继续循环
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
time.sleep(2)
|
||||
continue
|
||||
else:
|
||||
logging.error(f"设备 {device_id} 驱动重连失败")
|
||||
return False
|
||||
else:
|
||||
# 会话有效但点击失败,可能是页面元素问题
|
||||
logging.warning(f"设备 {device_id} 会话有效但点击失败,可能是页面加载问题")
|
||||
|
||||
# 如果点击按钮失败,增加重试计数
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"设备 {device_id} 点击按钮失败,等待2秒后第 {retry_count + 1} 次重试...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {device_id} 第 {retry_count + 1} 次尝试时出错: {str(e)}")
|
||||
retry_count += 1
|
||||
if retry_count < max_retries:
|
||||
logging.info(f"设备 {device_id} 等待2秒后重试...")
|
||||
time.sleep(2)
|
||||
else:
|
||||
logging.error(f"设备 {device_id} 达到最大重试次数 {max_retries},导航失败")
|
||||
return False
|
||||
|
||||
return False
|
||||
|
||||
def check_connection_error(exception):
|
||||
"""检查是否为连接拒绝错误"""
|
||||
error_str = str(exception)
|
||||
connection_errors = [
|
||||
'远程主机强迫关闭了一个现有的连接',
|
||||
'由于目标计算机积极拒绝,无法连接',
|
||||
'ConnectionResetError',
|
||||
'NewConnectionError',
|
||||
'10054',
|
||||
'10061'
|
||||
]
|
||||
return any(error in error_str for error in connection_errors)
|
||||
|
||||
def restart_appium_server(port=4723):
|
||||
"""重启Appium服务器"""
|
||||
try:
|
||||
# 杀死可能存在的Appium进程
|
||||
subprocess.run(['taskkill', '/f', '/im', 'node.exe'],
|
||||
capture_output=True, shell=True)
|
||||
time.sleep(2)
|
||||
|
||||
# 启动Appium服务器
|
||||
# appium_command = f'appium -p {port}'
|
||||
appium_command = f"appium -p {port} --base-path /wd/hub"
|
||||
subprocess.Popen(appium_command, shell=True)
|
||||
|
||||
# 等待Appium启动
|
||||
time.sleep(10)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"重启Appium服务器失败: {str(e)}")
|
||||
return False
|
||||
|
||||
def is_appium_running(port=4723):
|
||||
"""检查Appium服务器是否在运行"""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
result = s.connect_ex(('127.0.0.1', port))
|
||||
return result == 0
|
||||
except:
|
||||
return False
|
||||
298
globals/ex_apis.py
Normal file
298
globals/ex_apis.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# external_apis.py
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
from typing import Dict, Tuple, Optional
|
||||
import time
|
||||
|
||||
class WeatherAPI:
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
# 使用腾讯天气API
|
||||
self.base_url = "https://wis.qq.com/weather/common"
|
||||
|
||||
def parse_city(self, city_string: str) -> Tuple[str, str, str]:
|
||||
"""
|
||||
解析城市字符串,返回省份、城市、区县
|
||||
|
||||
参数:
|
||||
city_string: 完整的地址字符串
|
||||
|
||||
返回:
|
||||
(province, city, county)
|
||||
"""
|
||||
# 匹配省份或自治区
|
||||
province_regex = r"(.*?)(省|自治区)"
|
||||
# 匹配城市或州
|
||||
city_regex = r"(.*?省|.*?自治区)(.*?市|.*?州)"
|
||||
# 匹配区、县或镇
|
||||
county_regex = r"(.*?市|.*?州)(.*?)(区|县|镇)"
|
||||
|
||||
province = ""
|
||||
city = ""
|
||||
county = ""
|
||||
|
||||
import re
|
||||
|
||||
# 先尝试匹配省份或自治区
|
||||
province_match = re.search(province_regex, city_string)
|
||||
if province_match:
|
||||
province = province_match.group(1).strip()
|
||||
|
||||
# 然后尝试匹配城市或州
|
||||
city_match = re.search(city_regex, city_string)
|
||||
if city_match:
|
||||
city = city_match.group(2).strip()
|
||||
else:
|
||||
# 如果没有匹配到城市,则可能是直辖市或者直接是区/县
|
||||
city = city_string
|
||||
|
||||
# 最后尝试匹配区、县或镇
|
||||
county_match = re.search(county_regex, city_string)
|
||||
if county_match:
|
||||
county = county_match.group(2).strip()
|
||||
# 如果有区、县或镇,那么前面的城市部分需要重新解析
|
||||
if city_match:
|
||||
city = city_match.group(2).strip()
|
||||
|
||||
# 特殊情况处理,去除重复的省市名称
|
||||
if city and province and city.startswith(province):
|
||||
city = city.replace(province, "").strip()
|
||||
if county and city and county.startswith(city):
|
||||
county = county.replace(city, "").strip()
|
||||
|
||||
# 去除后缀
|
||||
city = city.rstrip('市州')
|
||||
if county:
|
||||
county = county.rstrip('区县镇')
|
||||
|
||||
# self.logger.info(f"解析结果 - 省份: {province}, 城市: {city}, 区县: {county}")
|
||||
return province, city, county
|
||||
|
||||
def get_weather_by_qq_api(self, province: str, city: str, county: str) -> Optional[Dict]:
|
||||
"""
|
||||
使用腾讯天气API获取天气信息
|
||||
|
||||
参数:
|
||||
province: 省份
|
||||
city: 城市
|
||||
county: 区县
|
||||
|
||||
返回:
|
||||
天气信息字典 or None
|
||||
"""
|
||||
try:
|
||||
params = {
|
||||
'source': 'pc',
|
||||
'weather_type': 'observe',
|
||||
'province': province,
|
||||
'city': city,
|
||||
'county': county
|
||||
}
|
||||
|
||||
response = requests.get(self.base_url, params=params, timeout=10)
|
||||
response.raise_for_status()
|
||||
|
||||
data = response.json()
|
||||
|
||||
if data.get('status') == 200:
|
||||
observe_data = data.get('data', {}).get('observe', {})
|
||||
|
||||
return {
|
||||
'weather': observe_data.get('weather', ''),
|
||||
'temperature': observe_data.get('degree', ''),
|
||||
'pressure': observe_data.get('pressure', '1013')
|
||||
}
|
||||
else:
|
||||
self.logger.error(f"腾讯天气API错误: {data.get('message')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"腾讯天气API调用失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def normalize_weather_text(self, weather_text: str) -> str:
|
||||
"""
|
||||
将天气描述标准化为: 晴;阴;雨;雪;风;其他
|
||||
|
||||
参数:
|
||||
weather_text: 原始天气描述
|
||||
|
||||
返回:
|
||||
标准化后的天气文本
|
||||
"""
|
||||
if not weather_text:
|
||||
return '其他'
|
||||
|
||||
weather_text_lower = weather_text.lower()
|
||||
|
||||
# 晴
|
||||
if any(word in weather_text_lower for word in ['晴', 'sunny', 'clear']):
|
||||
return '晴'
|
||||
# 阴
|
||||
elif any(word in weather_text_lower for word in ['阴', '多云', 'cloudy', 'overcast']):
|
||||
return '阴'
|
||||
# 雨
|
||||
elif any(word in weather_text_lower for word in ['雨', 'rain', 'drizzle', 'shower']):
|
||||
return '雨'
|
||||
# 雪
|
||||
elif any(word in weather_text_lower for word in ['雪', 'snow']):
|
||||
return '雪'
|
||||
# 风
|
||||
elif any(word in weather_text_lower for word in ['风', 'wind']):
|
||||
return '风'
|
||||
# 其他
|
||||
else:
|
||||
return '其他'
|
||||
|
||||
def adjust_pressure(self, pressure: float) -> float:
|
||||
"""
|
||||
调整气压值:低于700时,填700-750之间随机一个;高于700就按实际情况填
|
||||
|
||||
参数:
|
||||
pressure: 原始气压值
|
||||
|
||||
返回:
|
||||
调整后的气压值
|
||||
"""
|
||||
try:
|
||||
pressure_float = float(pressure)
|
||||
if pressure_float < 700:
|
||||
adjusted_pressure = random.randint(700, 750)
|
||||
self.logger.info(f"气压值 {pressure_float} 低于700,调整为: {adjusted_pressure:.1f}")
|
||||
return round(adjusted_pressure, 1)
|
||||
else:
|
||||
self.logger.info(f"使用实际气压值: {pressure_float}")
|
||||
return round(pressure_float, 1)
|
||||
except (ValueError, TypeError):
|
||||
self.logger.warning(f"气压值格式错误: {pressure},使用默认值720")
|
||||
return round(random.randint(700, 750), 1)
|
||||
|
||||
def get_weather_by_address(self, address: str, max_retries: int = 2) -> Optional[Dict]:
|
||||
"""
|
||||
根据地址获取天气信息
|
||||
|
||||
参数:
|
||||
address: 地址字符串
|
||||
max_retries: 最大重试次数
|
||||
|
||||
返回:
|
||||
{
|
||||
'weather': '晴/阴/雨/雪/风/其他',
|
||||
'temperature': 温度值,
|
||||
'pressure': 气压值
|
||||
} or None
|
||||
"""
|
||||
# self.logger.info(f"开始获取地址 '{address}' 的天气信息")
|
||||
|
||||
# 首先解析地址
|
||||
province, city, county = self.parse_city(address)
|
||||
|
||||
if not province and not city:
|
||||
self.logger.error("无法解析地址")
|
||||
return self.get_fallback_weather()
|
||||
|
||||
# 获取天气信息
|
||||
weather_data = None
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
# self.logger.info(f"尝试获取天气信息 (第{attempt + 1}次)")
|
||||
weather_data = self.get_weather_by_qq_api(province, city, county)
|
||||
if weather_data:
|
||||
break
|
||||
time.sleep(1) # 短暂延迟后重试
|
||||
except Exception as e:
|
||||
self.logger.warning(f"第{attempt + 1}次尝试失败: {str(e)}")
|
||||
time.sleep(1)
|
||||
|
||||
if not weather_data:
|
||||
self.logger.warning("获取天气信息失败,使用备用数据")
|
||||
return self.get_fallback_weather()
|
||||
|
||||
# 处理天气数据
|
||||
try:
|
||||
# 标准化天气文本
|
||||
normalized_weather = self.normalize_weather_text(weather_data['weather'])
|
||||
|
||||
# 调整气压值
|
||||
adjusted_pressure = self.adjust_pressure(weather_data['pressure'])
|
||||
|
||||
# 处理温度
|
||||
temperature = float(weather_data['temperature'])
|
||||
|
||||
result = {
|
||||
'weather': normalized_weather,
|
||||
'temperature': round(temperature, 1),
|
||||
'pressure': adjusted_pressure
|
||||
}
|
||||
|
||||
self.logger.info(f"成功获取天气信息: {result}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"处理天气数据时出错: {str(e)}")
|
||||
return self.get_fallback_weather()
|
||||
|
||||
def get_fallback_weather(self) -> Dict:
|
||||
"""
|
||||
获取备用天气数据(当所有API都失败时使用)
|
||||
|
||||
返回:
|
||||
默认天气数据
|
||||
"""
|
||||
self.logger.info("使用备用天气数据")
|
||||
return {
|
||||
'weather': '阴',
|
||||
'temperature': round(random.randint(15, 30), 1),
|
||||
'pressure': round(random.randint(700, 750), 1)
|
||||
}
|
||||
|
||||
def get_weather_simple(self, address: str) -> Tuple[str, float, float]:
|
||||
"""
|
||||
简化接口:直接返回天气、温度、气压
|
||||
|
||||
参数:
|
||||
address: 地址字符串
|
||||
|
||||
返回:
|
||||
(weather, temperature, pressure)
|
||||
"""
|
||||
weather_data = self.get_weather_by_address(address)
|
||||
if weather_data:
|
||||
return weather_data['weather'], weather_data['temperature'], weather_data['pressure']
|
||||
else:
|
||||
fallback = self.get_fallback_weather()
|
||||
return fallback['weather'], fallback['temperature'], fallback['pressure']
|
||||
|
||||
# 创建全局实例
|
||||
weather_api = WeatherAPI()
|
||||
|
||||
# 直接可用的函数
|
||||
def get_weather_by_address(address: str) -> Optional[Dict]:
|
||||
"""
|
||||
根据地址获取天气信息(直接调用函数)
|
||||
|
||||
参数:
|
||||
address: 地址字符串
|
||||
|
||||
返回:
|
||||
{
|
||||
'weather': '晴/阴/雨/雪/风/其他',
|
||||
'temperature': 温度值,
|
||||
'pressure': 气压值
|
||||
} or None
|
||||
"""
|
||||
return weather_api.get_weather_by_address(address)
|
||||
|
||||
def get_weather_simple(address: str) -> Tuple[str, float, float]:
|
||||
"""
|
||||
简化接口:直接返回天气、温度、气压
|
||||
|
||||
参数:
|
||||
address: 地址字符串
|
||||
|
||||
返回:
|
||||
(weather, temperature, pressure)
|
||||
"""
|
||||
return weather_api.get_weather_simple(address)
|
||||
17
globals/global_variable.py
Normal file
17
globals/global_variable.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# 全局变量
|
||||
GLOBAL_DEVICE_ID = "" # 设备ID
|
||||
GLOBAL_USERNAME = "czyuzongwen" # 用户名
|
||||
GLOBAL_CURRENT_PROJECT_NAME = "" # 当前测试项目名称
|
||||
GLOBAL_LINE_NUM = "" # 线路编码
|
||||
GLOBAL_BREAKPOINT_STATUS_CODES = [0,3] # 要获取的断点状态码列表
|
||||
GLOBAL_UPLOAD_BREAKPOINT_LIST = []
|
||||
GLOBAL_UPLOAD_BREAKPOINT_DICT = {}
|
||||
GLOBAL_TESTED_BREAKPOINT_LIST = [] # 测量结束的断点列表
|
||||
LINE_TIME_MAPPING_DICT = {} # 存储所有线路编码和对应的时间的全局字典
|
||||
GLOBAL_BREAKPOINT_DICT = {} # 存储测量结束的断点名称和对应的线路编码的全局字典
|
||||
GLOBAL_NAME_TO_ID_MAP = {} # 存储所有数据员姓名和对应的身份证号的全局字典
|
||||
GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST = [] # 上传成功的`断点列表
|
||||
|
||||
|
||||
|
||||
|
||||
59
globals/ids.py
Normal file
59
globals/ids.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# ids.py
|
||||
# 登录界面
|
||||
LOGIN_USERNAME = "com.bjjw.cjgc:id/et_user_name"
|
||||
LOGIN_PASSWORD = "com.bjjw.cjgc:id/et_user_psw"
|
||||
LOGIN_BTN = "com.bjjw.cjgc:id/btn_login"
|
||||
|
||||
# 更新相关
|
||||
UPDATE_WORK_BASE = "com.bjjw.cjgc:id/btn_update_basepoint"
|
||||
UPDATE_LEVEL_LINE = "com.bjjw.cjgc:id/btn_update_line"
|
||||
UPDATE_LEVEL_LINE_CONFIRM = "com.bjjw.cjgc:id/commit"
|
||||
|
||||
# 弹窗 & 加载
|
||||
ALERT_DIALOG = "android:id/content"
|
||||
LOADING_DIALOG = "android:id/custom"
|
||||
|
||||
# 底部导航栏
|
||||
DOWNLOAD_TABBAR_ID = "com.bjjw.cjgc:id/img_1_layout"
|
||||
MEASURE_TABBAR_ID = "com.bjjw.cjgc:id/img_3_layout"
|
||||
|
||||
# 测量相关
|
||||
MEASURE_BTN_ID = "com.bjjw.cjgc:id/select_point_update_tip_tv"
|
||||
MEASURE_LIST_ID = "com.bjjw.cjgc:id/line_list"
|
||||
MEASURE_LISTVIEW_ID = "com.bjjw.cjgc:id/itemContainer"
|
||||
MEASURE_NAME_TEXT_ID = "com.bjjw.cjgc:id/title"
|
||||
MEASURE_NAME_ID = "com.bjjw.cjgc:id/sectName"
|
||||
MEASURE_BACK_ID = "com.bjjw.cjgc:id/btn_back"
|
||||
MEASURE_BACK_ID_2 = "com.bjjw.cjgc:id/stop_measure_btn"
|
||||
|
||||
# 天气、观测类型
|
||||
MEASURE_TITLE_ID = "com.bjjw.cjgc:id/title_bar"
|
||||
MEASURE_WEATHER_ID = "com.bjjw.cjgc:id/point_list_weather_sp"
|
||||
MEASURE_TYPE_ID = "com.bjjw.cjgc:id/point_list_mtype_sp"
|
||||
MEASURE_SELECT_ID = "android:id/select_dialog_listview"
|
||||
SELECT_DIALOG_TEXT1_ID = "android:id/text1"
|
||||
MEASURE_PRESSURE_ID = "com.bjjw.cjgc:id/point_list_barometric_et"
|
||||
MEASURE_TEMPERATURE_ID = "com.bjjw.cjgc:id/point_list_temperature_et"
|
||||
MEASURE_SAVE_ID = "com.bjjw.cjgc:id/select_point_order_save_btn"
|
||||
|
||||
# 日期选择器
|
||||
DATE_START = "com.bjjw.cjgc:id/date"
|
||||
DATE_END = "com.bjjw.cjgc:id/date_end"
|
||||
SCRCOLL_YEAR = "com.bjjw.cjgc:id/wheelView1"
|
||||
SCRCOLL_MONTH = "com.bjjw.cjgc:id/wheelView2"
|
||||
SCRCOLL_DAY = "com.bjjw.cjgc:id/wheelView3"
|
||||
SCRCOLL_CONFIRM = "com.bjjw.cjgc:id/okBtn"
|
||||
SCRCOLL_CANCEL = "com.bjjw.cjgc:id/cancelBtn"
|
||||
|
||||
# 其他
|
||||
CONNECT_LEVEL_METER = "com.bjjw.cjgc:id/point_conn_level_btn"
|
||||
PINGCHAR_PROCESS = "com.bjjw.cjgc:id/point_measure_btn"
|
||||
LEVEL_METER_MANAGER = "com.bjjw.cjgc:id/btn_back"
|
||||
LEVEL_METER_MANAGER_LIST = "com.bjjw.cjgc:id/total_station_expandlist"
|
||||
START_MEASURE = "com.bjjw.cjgc:id/btn_control_begin_or_end"
|
||||
ALTER_MEASURE = "com.bjjw.cjgc:id/order_title"
|
||||
MATCH_VISIABLE = "com.bjjw.cjgc:id/title_paired_devices"
|
||||
BLUETOOTH_PAIR = "com.bjjw.cjgc:id/paired_devices"
|
||||
REPID_MEASURE = "com.bjjw.cjgc:id/measure_remeasure_all_btn"
|
||||
ONE = "com.bjjw.cjgc:id/auto_measure_all_station_text"
|
||||
|
||||
478
main.py
Normal file
478
main.py
Normal file
@@ -0,0 +1,478 @@
|
||||
# actions.py 主自动化脚本
|
||||
import os
|
||||
import logging
|
||||
import time
|
||||
import subprocess
|
||||
from appium import webdriver
|
||||
from appium.options.android import UiAutomator2Options
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
||||
|
||||
import globals.ids as ids
|
||||
import globals.global_variable as global_variable # 导入全局变量模块
|
||||
import permissions # 导入权限处理模块
|
||||
import globals.apis as apis
|
||||
from globals.driver_utils import init_appium_driver, ensure_appium_server_running, safe_quit_driver, is_app_launched, launch_app_manually
|
||||
from page_objects.login_page import LoginPage
|
||||
from page_objects.download_tabbar_page import DownloadTabbarPage
|
||||
from page_objects.screenshot_page import ScreenshotPage
|
||||
from page_objects.upload_config_page import UploadConfigPage
|
||||
from page_objects.more_download_page import MoreDownloadPage
|
||||
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s: %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler("appium_automation.log"),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
|
||||
class DeviceAutomation:
|
||||
@staticmethod
|
||||
def get_device_id() -> str:
|
||||
|
||||
# """
|
||||
# 获取设备ID,优先使用已连接设备,否则使用全局配置
|
||||
# """
|
||||
# try:
|
||||
# # 检查已连接设备
|
||||
# result = subprocess.run(
|
||||
# ["adb", "devices"],
|
||||
# capture_output=True,
|
||||
# text=True,
|
||||
# timeout=10
|
||||
# )
|
||||
|
||||
# # 解析设备列表
|
||||
# for line in result.stdout.strip().split('\n')[1:]:
|
||||
# if line.strip() and "device" in line and "offline" not in line:
|
||||
# device_id = line.split('\t')[0]
|
||||
# logging.info(f"使用已连接设备: {device_id}")
|
||||
# global_variable.GLOBAL_DEVICE_ID = device_id
|
||||
# return device_id
|
||||
|
||||
# except Exception as e:
|
||||
# logging.warning(f"设备检测失败: {e}")
|
||||
|
||||
"""
|
||||
获取设备ID,优先使用无线连接设备,否则尝试开启无线调试,最后使用全局配置
|
||||
"""
|
||||
try:
|
||||
# 检查已连接设备
|
||||
result = subprocess.run(
|
||||
["adb", "devices"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 解析设备列表
|
||||
wireless_device_id = None
|
||||
usb_device_id = None
|
||||
|
||||
# 先查找无线连接的设备(IP:端口格式)
|
||||
for line in result.stdout.strip().split('\n')[1:]:
|
||||
if line.strip() and "device" in line and "offline" not in line:
|
||||
current_device = line.split('\t')[0]
|
||||
# 检查是否为IP:端口格式的无线连接
|
||||
if ":" in current_device and any(char.isdigit() for char in current_device):
|
||||
wireless_device_id = current_device
|
||||
logging.info(f"使用无线连接设备: {wireless_device_id}")
|
||||
global_variable.GLOBAL_DEVICE_ID = wireless_device_id
|
||||
return wireless_device_id
|
||||
else:
|
||||
# 记录第一个USB连接的设备
|
||||
if not usb_device_id:
|
||||
usb_device_id = current_device
|
||||
|
||||
# 如果没有找到无线连接的设备,尝试使用USB设备开启无线调试
|
||||
if not wireless_device_id and usb_device_id:
|
||||
logging.info(f"未找到无线连接设备,尝试使用USB设备 {usb_device_id} 开启无线调试")
|
||||
|
||||
# 尝试获取设备IP地址
|
||||
try:
|
||||
import re
|
||||
import time
|
||||
|
||||
ip_result = subprocess.run(
|
||||
["adb", "-s", usb_device_id, "shell", "ip", "-f", "inet", "addr", "show", "wlan0"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
# 解析IP地址
|
||||
ip_output = ip_result.stdout
|
||||
if "inet " in ip_output:
|
||||
# 提取IP地址
|
||||
ip_match = re.search(r'inet\s+(\d+\.\d+\.\d+\.\d+)', ip_output)
|
||||
if ip_match:
|
||||
device_ip = ip_match.group(1)
|
||||
logging.info(f"获取到设备IP地址: {device_ip}")
|
||||
|
||||
# 开启无线调试
|
||||
tcpip_result = subprocess.run(
|
||||
["adb", "-s", usb_device_id, "tcpip", "5555"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if "restarting in TCP mode port: 5555" in tcpip_result.stdout:
|
||||
logging.info("无线调试已开启,端口: 5555")
|
||||
|
||||
# 等待几秒钟让设备准备好
|
||||
time.sleep(3)
|
||||
|
||||
# 连接到无线设备
|
||||
connect_result = subprocess.run(
|
||||
["adb", "connect", f"{device_ip}:5555"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if "connected to" in connect_result.stdout:
|
||||
logging.info(f"成功连接到无线设备: {device_ip}:5555")
|
||||
global_variable.GLOBAL_DEVICE_ID = f"{device_ip}:5555"
|
||||
return f"{device_ip}:5555"
|
||||
else:
|
||||
logging.warning(f"连接无线设备失败: {connect_result.stderr}")
|
||||
logging.info(f"使用USB设备: {usb_device_id}")
|
||||
global_variable.GLOBAL_DEVICE_ID = usb_device_id
|
||||
return usb_device_id
|
||||
else:
|
||||
logging.warning(f"开启无线调试失败: {tcpip_result.stderr}")
|
||||
logging.info(f"使用USB设备: {usb_device_id}")
|
||||
global_variable.GLOBAL_DEVICE_ID = usb_device_id
|
||||
return usb_device_id
|
||||
else:
|
||||
logging.warning("未找到设备IP地址")
|
||||
logging.info(f"使用USB设备: {usb_device_id}")
|
||||
global_variable.GLOBAL_DEVICE_ID = usb_device_id
|
||||
return usb_device_id
|
||||
else:
|
||||
logging.warning("无法获取设备IP地址,可能设备未连接到WiFi")
|
||||
logging.info(f"使用USB设备: {usb_device_id}")
|
||||
global_variable.GLOBAL_DEVICE_ID = usb_device_id
|
||||
return usb_device_id
|
||||
except Exception as e:
|
||||
logging.warning(f"开启无线调试时出错: {str(e)}")
|
||||
logging.info(f"使用USB设备: {usb_device_id}")
|
||||
global_variable.GLOBAL_DEVICE_ID = usb_device_id
|
||||
return usb_device_id
|
||||
|
||||
except Exception as e:
|
||||
logging.warning(f"设备检测失败: {e}")
|
||||
|
||||
|
||||
# 使用全局配置
|
||||
device_id = global_variable.GLOBAL_DEVICE_ID
|
||||
logging.info(f"使用全局配置设备: {device_id}")
|
||||
return device_id
|
||||
|
||||
def __init__(self, device_id=None):
|
||||
# 如果没有提供设备ID,则自动获取
|
||||
if device_id is None:
|
||||
self.device_id = self.get_device_id()
|
||||
else:
|
||||
self.device_id = device_id
|
||||
|
||||
# 初始化权限
|
||||
if permissions.grant_appium_permissions(self.device_id):
|
||||
logging.info(f"设备 {self.device_id} 权限授予成功")
|
||||
else:
|
||||
logging.warning(f"设备 {self.device_id} 权限授予失败")
|
||||
|
||||
# 确保Appium服务器正在运行
|
||||
ensure_appium_server_running(4723)
|
||||
|
||||
# 初始化驱动
|
||||
self.init_driver()
|
||||
# 先拼接,后创建测试结果目录
|
||||
self.results_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'test_results')
|
||||
os.makedirs(self.results_dir, exist_ok=True)
|
||||
|
||||
|
||||
def init_driver(self):
|
||||
"""初始化Appium驱动"""
|
||||
try:
|
||||
# 使用全局函数初始化驱动
|
||||
self.driver, self.wait = init_appium_driver(self.device_id)
|
||||
# 初始化页面对象
|
||||
logging.info(f"设备 {self.device_id} 开始初始化页面对象")
|
||||
self.login_page = LoginPage(self.driver, self.wait)
|
||||
self.download_tabbar_page = DownloadTabbarPage(self.driver, self.wait, self.device_id)
|
||||
self.screenshot_page = ScreenshotPage(self.driver, self.wait, self.device_id)
|
||||
self.upload_config_page = UploadConfigPage(self.driver, self.wait, self.device_id)
|
||||
self.more_download_page = MoreDownloadPage(self.driver, self.wait,self.device_id)
|
||||
logging.info(f"设备 {self.device_id} 所有页面对象初始化完成")
|
||||
|
||||
# 检查应用是否成功启动
|
||||
if is_app_launched(self.driver):
|
||||
logging.info(f"设备 {self.device_id} 沉降观测App已成功启动")
|
||||
else:
|
||||
logging.warning(f"设备 {self.device_id} 应用可能未正确启动")
|
||||
# 手动启动应用
|
||||
launch_app_manually(self.driver, self.device_id)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {self.device_id} 初始化驱动失败: {str(e)}")
|
||||
raise
|
||||
|
||||
def is_app_launched(self):
|
||||
"""检查应用是否已启动"""
|
||||
try:
|
||||
return is_app_launched(self.driver)
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {self.device_id} 检查应用启动状态时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def is_element_present(self, by, value):
|
||||
"""检查元素是否存在"""
|
||||
try:
|
||||
self.driver.find_element(by, value)
|
||||
return True
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
|
||||
def handle_app_state(self):
|
||||
"""根据当前应用状态处理相应的操作"""
|
||||
try:
|
||||
login_btn_exists = self.login_page.is_login_page()
|
||||
if not login_btn_exists:
|
||||
logging.error(f"设备 {self.device_id} 未知应用状态,无法确定当前页面,跳转到登录页面")
|
||||
if self.navigate_to_login_page(self.driver, self.device_id):
|
||||
logging.info(f"设备 {self.device_id} 成功跳转到登录页面")
|
||||
return self.handle_app_state() # 递归调用处理登录后的状态
|
||||
else:
|
||||
logging.error(f"设备 {self.device_id} 跳转到登录页面失败")
|
||||
return False
|
||||
|
||||
# 处理登录页面状态
|
||||
logging.info(f"设备 {self.device_id} 检测到登录页面,执行登录操作")
|
||||
max_retries = 1
|
||||
login_success = False
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
if self.login_page.login():
|
||||
login_success = True
|
||||
break
|
||||
else:
|
||||
if attempt < max_retries:
|
||||
logging.warning(f"设备 {self.device_id} 登录失败,准备重试 ({attempt + 1}/{max_retries})")
|
||||
time.sleep(2) # 等待2秒后重试
|
||||
else:
|
||||
logging.error(f"设备 {self.device_id} 登录失败,已达到最大重试次数")
|
||||
|
||||
if not login_success:
|
||||
return False
|
||||
|
||||
logging.info(f"设备 {self.device_id} 登录成功,继续执行更新操作")
|
||||
time.sleep(1)
|
||||
|
||||
# 执行更新操作
|
||||
if not self.download_tabbar_page.download_tabbar_page_manager():
|
||||
logging.error(f"设备 {self.device_id} 更新操作执行失败")
|
||||
return False
|
||||
|
||||
# 获取状态为3的线路。
|
||||
apis.get_line_info_and_save_global(user_name=global_variable.GLOBAL_USERNAME);
|
||||
# # 虚拟数据替代
|
||||
# global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT = {'CDWZQ-2标-龙骨湾右线大桥-0-7号墩-平原': 'L156372', 'CDWZQ-2标-蓝家湾特大 桥-31-31-平原': 'L159206'}
|
||||
# global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST = list(global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.keys())
|
||||
|
||||
# 点击测量导航栏按钮
|
||||
measure_page_btn = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_3_layout"))
|
||||
)
|
||||
measure_page_btn.click()
|
||||
|
||||
|
||||
# 处理平差
|
||||
self.screenshot_page.screenshot_page_manager(self.device_id)
|
||||
|
||||
|
||||
# 检查是否有需要上传的断点
|
||||
if not global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST:
|
||||
logging.info(f"设备 {self.device_id} 断点列表为空,无需执行上传操作")
|
||||
return False
|
||||
|
||||
# 点击上传导航栏按钮
|
||||
upload_page_btn = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_2_layout"))
|
||||
)
|
||||
upload_page_btn.click()
|
||||
|
||||
|
||||
# 遍历断点列表,逐个执行上传操作
|
||||
upload_success_count = 0
|
||||
for breakpoint_name in global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST:
|
||||
try:
|
||||
logging.info(f"设备 {self.device_id} 开始处理断点 '{breakpoint_name}' 的上传")
|
||||
|
||||
# 安全地获取断点信息
|
||||
line_num = global_variable.GLOBAL_UPLOAD_BREAKPOINT_DICT.get(breakpoint_name)
|
||||
if line_num is None:
|
||||
logging.warning(f"设备 {self.device_id} 断点 '{breakpoint_name}' 在字典中未找到,跳过上传")
|
||||
continue
|
||||
if not line_num:
|
||||
logging.warning(f"设备 {self.device_id} 断点 '{breakpoint_name}' 未获取到line_num,跳过上传")
|
||||
continue
|
||||
|
||||
# 执行上传配置管理,传入当前断点名称
|
||||
if self.upload_config_page.upload_config_page_manager(self.results_dir, breakpoint_name, line_num):
|
||||
logging.info(f"设备 {self.device_id} 断点 '{breakpoint_name}' 上传成功")
|
||||
upload_success_count += 1
|
||||
else:
|
||||
logging.error(f"设备 {self.device_id} 断点 '{breakpoint_name}' 上传失败")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {self.device_id} 处理断点 '{breakpoint_name}' 时发生异常: {str(e)}")
|
||||
|
||||
logging.info(f"设备 {self.device_id} 上传配置管理执行完成,成功上传 {upload_success_count}/{len(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)} 个断点")
|
||||
|
||||
# 如果所有断点都上传成功,返回True;否则返回False
|
||||
all_upload_success = upload_success_count == len(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)
|
||||
if all_upload_success:
|
||||
logging.info(f"设备 {self.device_id} 所有断点上传成功")
|
||||
# 把上传成功的断点写入日志文件"上传成功的断点.txt"
|
||||
with open(os.path.join(self.results_dir, "上传成功的断点.txt"), "w", encoding='utf-8') as f:
|
||||
for bp in global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST:
|
||||
f.write(f"{bp}\n")
|
||||
else:
|
||||
logging.warning(f"设备 {self.device_id} 部分断点上传失败")
|
||||
# 把上传成功的断点写入日志文件"上传成功的断点.txt"
|
||||
with open(os.path.join(self.results_dir, "上传成功的断点.txt"), "w", encoding='utf-8') as f:
|
||||
for bp in global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST:
|
||||
f.write(f"{bp}\n")
|
||||
# 把上传失败的断点写入日志文件"上传失败的断点.txt"
|
||||
with open(os.path.join(self.results_dir, "上传失败的断点.txt"), "w", encoding='utf-8') as f:
|
||||
for bp in set(global_variable.GLOBAL_UPLOAD_BREAKPOINT_LIST)-set(global_variable.GLOBAL_UPLOAD_SUCCESS_BREAKPOINT_LIST):
|
||||
f.write(f"{bp}\n")
|
||||
|
||||
return all_upload_success
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {self.device_id} 处理应用状态时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def check_and_click_confirm_popup_appium(self):
|
||||
"""
|
||||
适用于Appium的弹窗检测函数
|
||||
|
||||
Returns:
|
||||
bool: 是否成功处理弹窗
|
||||
"""
|
||||
try:
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
|
||||
# 使用self.driver而不是参数
|
||||
if not hasattr(self, 'driver') or self.driver is None:
|
||||
logging.warning("driver未初始化,无法检测弹窗")
|
||||
return False
|
||||
|
||||
# 检查弹窗消息
|
||||
message_elements = self.driver.find_elements(AppiumBy.XPATH, '//android.widget.TextView[@text="是否退出测量界面?"]')
|
||||
|
||||
if message_elements:
|
||||
logging.info("检测到退出测量界面弹窗")
|
||||
|
||||
# 点击"是"按钮
|
||||
confirm_buttons = self.driver.find_elements(AppiumBy.XPATH, '//android.widget.Button[@text="是" and @resource-id="android:id/button1"]')
|
||||
if confirm_buttons:
|
||||
confirm_buttons[0].click()
|
||||
logging.info("已点击'是'按钮")
|
||||
time.sleep(1)
|
||||
return True
|
||||
else:
|
||||
logging.warning("未找到'是'按钮")
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Appium检测弹窗时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
def navigate_to_login_page(self, driver, device_id):
|
||||
"""
|
||||
补充的跳转页面函数:当设备处于未知状态时,尝试跳转到登录页面
|
||||
|
||||
参数:
|
||||
driver: 已初始化的Appium WebDriver对象
|
||||
device_id: 设备ID,用于日志记录
|
||||
"""
|
||||
try:
|
||||
target_package = 'com.bjjw.cjgc'
|
||||
target_activity = '.activity.LoginActivity'
|
||||
# 使用ADB命令启动Activity
|
||||
try:
|
||||
logging.info(f"尝试使用ADB命令启动LoginActivity: {target_package}/{target_activity}")
|
||||
adb_command = f"adb -s {device_id} shell am start -n {target_package}/{target_activity}"
|
||||
result = subprocess.run(adb_command, shell=True, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
logging.info(f"使用ADB命令启动LoginActivity成功")
|
||||
time.sleep(2) # 等待Activity启动
|
||||
return True
|
||||
else:
|
||||
logging.warning(f"ADB命令执行失败: {result.stderr}")
|
||||
except Exception as adb_error:
|
||||
logging.warning(f"执行ADB命令时出错: {adb_error}")
|
||||
except Exception as e:
|
||||
logging.error(f"跳转到登录页面过程中发生未预期错误: {e}")
|
||||
|
||||
# 所有尝试都失败
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
def run_automation(self):
|
||||
"""运行自动化流程"""
|
||||
try:
|
||||
success = self.handle_app_state()
|
||||
# success = self.test_handle_app_state()
|
||||
if success:
|
||||
logging.info(f"设备 {self.device_id} 自动化流程执行成功")
|
||||
else:
|
||||
logging.error(f"设备 {self.device_id} 自动化流程执行失败")
|
||||
return success
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {self.device_id} 自动化执行过程中发生错误: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
self.quit()
|
||||
|
||||
def quit(self):
|
||||
"""关闭驱动"""
|
||||
safe_quit_driver(getattr(self, 'driver', None), self.device_id)
|
||||
|
||||
# 主执行逻辑
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 单个设备配置 - 现在DeviceAutomation会自动获取设备ID
|
||||
|
||||
try:
|
||||
automation = DeviceAutomation()
|
||||
success = automation.run_automation()
|
||||
|
||||
if success:
|
||||
logging.info(f"设备自动化流程执行成功")
|
||||
else:
|
||||
logging.error(f"设备自动化流程执行失败")
|
||||
except Exception as e:
|
||||
logging.error(f"设备执行出错: {str(e)}")
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
music/901135.wav
Normal file
BIN
music/901135.wav
Normal file
Binary file not shown.
0
page_objects/__init__.py
Normal file
0
page_objects/__init__.py
Normal file
BIN
page_objects/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/download_tabbar_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/download_tabbar_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/login_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/login_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/measure_tabbar_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/more_download_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/more_download_page.cpython-312.pyc
Normal file
Binary file not shown.
BIN
page_objects/__pycache__/screenshot_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/screenshot_page.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
page_objects/__pycache__/upload_config_page.cpython-312.pyc
Normal file
BIN
page_objects/__pycache__/upload_config_page.cpython-312.pyc
Normal file
Binary file not shown.
46
page_objects/call_xie.py
Normal file
46
page_objects/call_xie.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import socket
|
||||
|
||||
def send_tcp_command(command: str, host: str = '127.0.0.1', port: int = 8888, encoding: str = 'utf-8') -> bool:
|
||||
"""
|
||||
向指定TCP端口发送指令
|
||||
|
||||
参数:
|
||||
command: 要发送的指令字符串
|
||||
host: 目标主机地址(默认127.0.0.1)
|
||||
port: 目标端口(默认8888)
|
||||
encoding: 字符串编码格式(默认utf-8)
|
||||
|
||||
返回:
|
||||
发送成功返回True,失败返回False
|
||||
"""
|
||||
# 创建TCP socket并自动关闭(with语句确保资源释放)
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||
try:
|
||||
# 连接服务器(超时时间5秒,避免无限阻塞)
|
||||
sock.settimeout(5.0)
|
||||
sock.connect((host, port))
|
||||
|
||||
# 发送指令(转换为字节流)
|
||||
sock.sendall(command.encode(encoding))
|
||||
print(f"指令 '{command}' 发送成功")
|
||||
return True
|
||||
|
||||
except ConnectionRefusedError:
|
||||
print(f"连接失败:{host}:{port} 未监听或不可达")
|
||||
except socket.timeout:
|
||||
print(f"连接超时:超过5秒未连接到 {host}:{port}")
|
||||
except UnicodeEncodeError:
|
||||
print(f"编码失败:指令包含{encoding}无法编码的字符")
|
||||
except Exception as e:
|
||||
print(f"发送失败:{str(e)}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 发送StartConnect指令
|
||||
send_tcp_command("StartConnect")
|
||||
|
||||
# 也可以发送其他指令,例如:
|
||||
# send_tcp_command("StopConnect")
|
||||
319
page_objects/download_tabbar_page.py
Normal file
319
page_objects/download_tabbar_page.py
Normal file
@@ -0,0 +1,319 @@
|
||||
# 更新基站页面操作
|
||||
# page_objects/download_tabbar_page.py
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException, NoSuchElementException, StaleElementReferenceException
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import globals.ids as ids # 导入元素ID
|
||||
import globals.global_variable as global_variable # 导入全局变量
|
||||
from globals.driver_utils import check_session_valid, reconnect_driver
|
||||
|
||||
class DownloadTabbarPage:
|
||||
def __init__(self, driver, wait, device_id):
|
||||
self.driver = driver
|
||||
self.wait = wait
|
||||
self.device_id = device_id
|
||||
self.logger = logging.getLogger(__name__)
|
||||
# 添加默认的目标日期值
|
||||
self.target_year = 2022
|
||||
self.target_month = 9
|
||||
self.target_day = 22
|
||||
|
||||
def is_download_tabbar_visible(self):
|
||||
"""检查下载标签栏是否可见"""
|
||||
try:
|
||||
return self.driver.find_element(AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID).is_displayed()
|
||||
except NoSuchElementException:
|
||||
self.logger.warning("下载标签栏元素未找到")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查下载标签栏可见性时发生意外错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def click_download_tabbar(self):
|
||||
"""点击下载标签栏"""
|
||||
try:
|
||||
download_tab = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID))
|
||||
)
|
||||
download_tab.click()
|
||||
self.logger.info("已点击下载标签栏")
|
||||
|
||||
# 使用显式等待替代固定等待
|
||||
self.wait.until(
|
||||
lambda driver: self.is_download_tabbar_visible()
|
||||
)
|
||||
return True
|
||||
except TimeoutException:
|
||||
self.logger.error("等待下载标签栏可点击超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"点击下载标签栏时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def update_work_base(self):
|
||||
"""更新工作基点"""
|
||||
try:
|
||||
# 点击更新工作基点
|
||||
update_work_base = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_WORK_BASE))
|
||||
)
|
||||
update_work_base.click()
|
||||
self.logger.info("已点击更新工作基点")
|
||||
|
||||
# 等待更新完成 - 可以添加更具体的等待条件
|
||||
# 例如等待某个进度条消失或成功提示出现
|
||||
time.sleep(2) # 暂时保留,但建议替换为显式等待
|
||||
return True
|
||||
except TimeoutException:
|
||||
self.logger.error("等待更新工作基点按钮可点击超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"更新工作基点时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def _get_current_date(self):
|
||||
"""获取当前开始日期控件的日期值,支持多种格式解析"""
|
||||
try:
|
||||
date_element = self.wait.until(
|
||||
EC.visibility_of_element_located((AppiumBy.ID, ids.DATE_START))
|
||||
)
|
||||
date_text = date_element.text.strip()
|
||||
self.logger.info(f"获取到当前开始日期: {date_text}")
|
||||
|
||||
# 尝试多种日期格式解析
|
||||
date_formats = [
|
||||
"%Y-%m-%d", # 匹配 '2025-08-12' 格式
|
||||
"%Y年%m月%d日", # 匹配 '2025年08月12日' 格式
|
||||
"%Y/%m/%d" # 可选:添加其他可能的格式
|
||||
]
|
||||
|
||||
for fmt in date_formats:
|
||||
try:
|
||||
return datetime.strptime(date_text, fmt)
|
||||
except ValueError:
|
||||
continue # 尝试下一种格式
|
||||
|
||||
# 如果所有格式都匹配失败
|
||||
self.logger.error(f"日期格式解析错误: 无法识别的格式,日期文本: {date_text}")
|
||||
return None
|
||||
|
||||
except TimeoutException:
|
||||
self.logger.error("获取当前日期超时")
|
||||
return None
|
||||
except Exception as e:
|
||||
self.logger.error(f"获取当前日期失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def update_level_line(self):
|
||||
"""更新水准线路,修改为设置2022年9月22日"""
|
||||
try:
|
||||
# 点击更新水准线路
|
||||
update_level_line = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE))
|
||||
)
|
||||
update_level_line.click()
|
||||
self.logger.info("已点击更新水准线路")
|
||||
|
||||
# 获取原始开始日期
|
||||
original_date = self._get_current_date()
|
||||
if not original_date:
|
||||
self.logger.error("无法获取原始开始日期,更新水准线路失败")
|
||||
return False
|
||||
|
||||
# 点击开始日期
|
||||
date_start = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.DATE_START))
|
||||
)
|
||||
date_start.click()
|
||||
self.logger.info("已点击开始日期控件")
|
||||
|
||||
# 处理时间选择器,设置为2022年9月22日
|
||||
if not self.handle_time_selector(2022, 9, 22, original_date):
|
||||
self.logger.error("处理时间选择失败")
|
||||
return False
|
||||
|
||||
return True
|
||||
except TimeoutException:
|
||||
self.logger.error("等待更新水准线路按钮可点击超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"更新水准线路时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def _scroll_to_value(self, picker_id, target_value, original_value, max_attempts=20):
|
||||
"""滚动选择器到目标值,基于原始值计算滚动次数"""
|
||||
try:
|
||||
# 计算需要滚动的次数(绝对值)
|
||||
scroll_count = abs(int(target_value) - int(original_value))
|
||||
self.logger.info(f"需要滚动{scroll_count}次将{picker_id}从{original_value}调整到{target_value}")
|
||||
|
||||
# 确定滚动方向
|
||||
direction = "down" if int(target_value) > int(original_value) else "up"
|
||||
|
||||
# 获取选择器元素
|
||||
picker = self.wait.until(
|
||||
EC.visibility_of_element_located((AppiumBy.ID, picker_id))
|
||||
)
|
||||
|
||||
# 计算滚动坐标
|
||||
x = picker.location['x'] + picker.size['width'] // 2
|
||||
self.logger.info(f"水平位置x为{x}")
|
||||
y_center = picker.location['y'] + picker.size['height'] // 2
|
||||
self.logger.info(f"垂直位置中点y_center为{y_center}")
|
||||
# start_y = y_center if direction == "down" else picker.location['y']
|
||||
# end_y = picker.location['y'] if direction == "down" else y_center
|
||||
# 关键修改:计算选择器高度的五分之一(滑动距离)
|
||||
height_fifth = picker.size['height'] // 5 # 1/5高度
|
||||
|
||||
# 根据方向计算起点和终点,确保滑动距离为 height_fifth
|
||||
if direction == "down":
|
||||
# 向下滚动:从中心点向上滑动1/5高度
|
||||
start_y = y_center
|
||||
end_y = y_center - height_fifth # 终点 = 中心点 - 1/5高度
|
||||
self.logger.info(f"down垂直开始位置start_y为{y_center},垂直结束位置end_y为{end_y}")
|
||||
|
||||
else:
|
||||
# 向上滚动:从中心点向下滑动1/5高度
|
||||
start_y = y_center
|
||||
end_y = y_center + height_fifth # 终点 = 中心点 + 1/5高度
|
||||
self.logger.info(f"up垂直开始位置start_y为{y_center},垂直结束位置end_y为{end_y}")
|
||||
# 执行滚动操作
|
||||
for _ in range(scroll_count):
|
||||
self.driver.swipe(x, start_y, x, end_y, 500)
|
||||
time.sleep(0.5) # 等待滚动稳定
|
||||
return True # 循环scroll_count次后直接返回
|
||||
# # 验证当前值
|
||||
# current_value = picker.text
|
||||
# if current_value == str(target_value):
|
||||
# self.logger.info(f"{picker_id}已达到目标值: {target_value}")
|
||||
# return True
|
||||
|
||||
# 最终验证
|
||||
# final_value = picker.text
|
||||
# if final_value == str(target_value):
|
||||
# self.logger.info(f"{picker_id}已达到目标值: {target_value}")
|
||||
# return True
|
||||
# else:
|
||||
# self.logger.error(f"{picker_id}滚动{scroll_count}次后未达到目标值,当前值: {final_value}")
|
||||
# return False
|
||||
|
||||
except StaleElementReferenceException:
|
||||
self.logger.warning("元素状态已过期,重新获取")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"滚动选择器出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def handle_time_selector(self, target_year, target_month, target_day, original_date=None):
|
||||
"""处理时间选择器,选择起始时间并确认"""
|
||||
self.logger.info(f"传入handle_time_selector的初始日期: {original_date}")
|
||||
try:
|
||||
# 等待时间选择器出现
|
||||
self.wait.until(
|
||||
EC.visibility_of_element_located((AppiumBy.ID, ids.ALERT_DIALOG))
|
||||
)
|
||||
self.logger.info("时间选择对话框已出现")
|
||||
|
||||
# 如果没有提供原始日期,使用当前日期控件的值
|
||||
if not original_date:
|
||||
original_date = self._get_current_date()
|
||||
if not original_date:
|
||||
self.logger.error("无法获取原始日期,处理时间选择失败")
|
||||
return False
|
||||
|
||||
# 滚动选择年份
|
||||
if not self._scroll_to_value(ids.SCRCOLL_YEAR, target_year, original_date.year):
|
||||
self.logger.error("滚动选择年份失败")
|
||||
return False
|
||||
|
||||
# 滚动选择月份
|
||||
if not self._scroll_to_value(ids.SCRCOLL_MONTH, target_month, original_date.month):
|
||||
self.logger.error("滚动选择月份失败")
|
||||
return False
|
||||
|
||||
# 滚动选择日期
|
||||
if not self._scroll_to_value(ids.SCRCOLL_DAY, target_day, original_date.day):
|
||||
self.logger.error("滚动选择日期失败")
|
||||
return False
|
||||
|
||||
# 点击确认按钮
|
||||
confirm_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.SCRCOLL_CONFIRM)) # 日期选择器确认按钮
|
||||
)
|
||||
confirm_btn.click()
|
||||
self.logger.info("已确认时间选择")
|
||||
|
||||
# 点击对话框确认按钮(UPDATE_LEVEL_LINE_CONFIRM)
|
||||
confirm_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.UPDATE_LEVEL_LINE_CONFIRM)) # 选择日期对话框确认按钮
|
||||
)
|
||||
confirm_btn.click()
|
||||
self.logger.info("已点击对话框确认按钮commit")
|
||||
|
||||
# 新增:等待加载对话框出现
|
||||
custom_dialog = self.wait.until(
|
||||
EC.visibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG))
|
||||
)
|
||||
self.logger.info("检测到loading对话框出现")
|
||||
|
||||
if not check_session_valid(self.driver, self.device_id):
|
||||
self.logger.warning(f"设备 {self.device_id} 会话无效,尝试重新连接驱动...")
|
||||
if not reconnect_driver(self.device_id, self.driver):
|
||||
self.logger.error(f"设备 {self.device_id} 驱动重连失败")
|
||||
|
||||
# 新增:等待加载对话框消失(表示更新完成)
|
||||
WebDriverWait(self.driver, 300).until(
|
||||
EC.invisibility_of_element_located((AppiumBy.ID, ids.LOADING_DIALOG))
|
||||
)
|
||||
self.logger.info("loading对话框已消失,更新完成")
|
||||
|
||||
'''点击commit确认按钮后,loading弹窗会出现,等待其加载完成后关闭
|
||||
检测导航栏中的测量tabbar是否出现来确定是否返回True
|
||||
'''
|
||||
# measure_tabbar_btn = self.wait.until(
|
||||
# EC.visibility_of_element_located((AppiumBy.ID, ids.MEASURE_TABBAR_ID))
|
||||
# )
|
||||
# self.logger.info("检测测量tabbar按钮出现")
|
||||
|
||||
return True
|
||||
|
||||
except TimeoutException as e:
|
||||
# 明确超时发生在哪个环节
|
||||
self.logger.error("处理时间选择时超时:可能是等待对话框出现、日期选择器元素或确认按钮超时", exc_info=True)
|
||||
return False
|
||||
except Exception as e:
|
||||
# 细分不同环节的异常
|
||||
self.logger.error(f"处理时间选择对话框时出错:可能是日期滚动、选择器确认或对话框确认环节失败 - {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
def download_tabbar_page_manager(self):
|
||||
"""执行基础更新操作"""
|
||||
try:
|
||||
# 执行基础更新流程
|
||||
self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 开始执行更新流程")
|
||||
|
||||
# 点击下载标签栏
|
||||
if not self.click_download_tabbar():
|
||||
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 点击下载标签栏失败")
|
||||
return False
|
||||
|
||||
# 更新工作基点
|
||||
if not self.update_work_base():
|
||||
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新工作基点失败")
|
||||
return False
|
||||
|
||||
# 更新水准线路
|
||||
if not self.update_level_line():
|
||||
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新水准线路失败")
|
||||
return False
|
||||
|
||||
self.logger.info(f"设备 {global_variable.GLOBAL_DEVICE_ID} 更新操作执行成功")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"设备 {global_variable.GLOBAL_DEVICE_ID} 执行更新操作时出错: {str(e)}")
|
||||
return False
|
||||
78
page_objects/login_page.py
Normal file
78
page_objects/login_page.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# 登录页面操作
|
||||
# page_objects/login_page.py
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
||||
import logging
|
||||
import time
|
||||
|
||||
import globals.ids as ids
|
||||
import globals.global_variable as global_variable # 导入全局变量模块
|
||||
|
||||
class LoginPage:
|
||||
def __init__(self, driver, wait):
|
||||
self.driver = driver
|
||||
self.wait = wait
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def is_login_page(self):
|
||||
"""检查当前是否为登录页面"""
|
||||
try:
|
||||
return self.driver.find_element(AppiumBy.ID, ids.LOGIN_BTN).is_displayed()
|
||||
except NoSuchElementException:
|
||||
return False
|
||||
|
||||
def login(self):
|
||||
"""执行登录操作"""
|
||||
try:
|
||||
self.logger.info("正在执行登录操作...")
|
||||
|
||||
# 获取文本框中已有的用户名
|
||||
username_field = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_USERNAME))
|
||||
)
|
||||
|
||||
# 读取文本框内已有的用户名(.text属性获取元素显示的文本内容)
|
||||
existing_username = username_field.text
|
||||
# 3. 将获取到的用户名写入全局变量中
|
||||
global_variable.GLOBAL_USERNAME = existing_username # 关键:给全局变量赋值
|
||||
|
||||
# 日志记录获取到的已有用户名(若为空,也需明确记录,避免后续误解)
|
||||
if existing_username.strip(): # 去除空格后判断是否有有效内容
|
||||
self.logger.info(f"已获取文本框中的已有用户名: {existing_username}")
|
||||
else:
|
||||
self.logger.info("文本框中未检测到已有用户名(内容为空)")
|
||||
|
||||
# 点击登录按钮
|
||||
login_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, ids.LOGIN_BTN))
|
||||
)
|
||||
login_btn.click()
|
||||
self.logger.info("已点击登录按钮")
|
||||
|
||||
# 等待登录完成
|
||||
time.sleep(3)
|
||||
|
||||
# 检查是否登录成功
|
||||
if self.is_login_successful():
|
||||
self.logger.info("登录成功")
|
||||
return True
|
||||
else:
|
||||
self.logger.warning("登录后未检测到主页面元素")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"登录过程中出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def is_login_successful(self):
|
||||
"""检查登录是否成功"""
|
||||
try:
|
||||
# 等待主页面元素出现
|
||||
self.wait.until(
|
||||
EC.presence_of_element_located((AppiumBy.ID, ids.DOWNLOAD_TABBAR_ID))
|
||||
)
|
||||
return True
|
||||
except TimeoutException:
|
||||
return False
|
||||
506
page_objects/more_download_page.py
Normal file
506
page_objects/more_download_page.py
Normal file
@@ -0,0 +1,506 @@
|
||||
# test_more_download_page.py
|
||||
from appium.webdriver.common.appiumby import AppiumBy
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.common.exceptions import TimeoutException, NoSuchElementException
|
||||
import logging
|
||||
import time
|
||||
from globals.driver_utils import launch_app_manually
|
||||
from page_objects.login_page import LoginPage
|
||||
|
||||
class MoreDownloadPage:
|
||||
def __init__(self, driver, wait,device_id):
|
||||
self.driver = driver
|
||||
self.wait = wait
|
||||
self.device_id = device_id
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def is_on_more_download_page(self):
|
||||
"""通过下载历史数据按钮来判断是否在更多下载页面"""
|
||||
try:
|
||||
# 使用下载历史数据按钮的resource-id来检查
|
||||
download_history_locator = (AppiumBy.ID, "com.bjjw.cjgc:id/download_history")
|
||||
self.wait.until(EC.presence_of_element_located(download_history_locator))
|
||||
self.logger.info("已确认在更多下载页面")
|
||||
return True
|
||||
except TimeoutException:
|
||||
self.logger.warning("未找到下载历史数据按钮,不在更多下载页面")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查更多下载页面时发生意外错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def click_download_button(self):
|
||||
"""点击下载按钮"""
|
||||
try:
|
||||
# 点击下载历史数据按钮
|
||||
download_button = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_history"))
|
||||
)
|
||||
download_button.click()
|
||||
self.logger.info("已点击下载历史数据按钮")
|
||||
|
||||
# 等待下载操作开始
|
||||
# time.sleep(3)
|
||||
|
||||
return True
|
||||
|
||||
except TimeoutException:
|
||||
self.logger.error("等待下载按钮可点击超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"点击下载按钮时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def click_download_original_data(self):
|
||||
"""点击下载原始数据按钮并处理日期选择"""
|
||||
try:
|
||||
# 点击下载原始数据按钮
|
||||
download_original_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_org"))
|
||||
)
|
||||
download_original_btn.click()
|
||||
self.logger.info("已点击下载原始数据按钮")
|
||||
|
||||
# 等待日期选择弹窗出现
|
||||
# time.sleep(2)
|
||||
|
||||
# 点击选择开始日期
|
||||
start_date_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date"))
|
||||
)
|
||||
start_date_btn.click()
|
||||
self.logger.info("已点击选择开始日期")
|
||||
|
||||
# 等待日期选择器出现
|
||||
# time.sleep(2)
|
||||
|
||||
# 滑动年份选择器 - 向上滑动1/5的距离
|
||||
if not self._swipe_year_wheel():
|
||||
self.logger.error("滑动年份选择器失败")
|
||||
return False
|
||||
|
||||
# 点击日期选择器的确定按钮
|
||||
confirm_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
self.logger.info("已确认日期选择")
|
||||
|
||||
# 等待日期选择器关闭
|
||||
# time.sleep(2)
|
||||
|
||||
# 假设弹窗有确定按钮,点击它开始下载
|
||||
try:
|
||||
# 尝试查找并点击下载弹窗的确定按钮
|
||||
download_confirm_btn = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]"))
|
||||
)
|
||||
download_confirm_btn.click()
|
||||
self.logger.info("已点击下载确认按钮")
|
||||
except TimeoutException:
|
||||
self.logger.warning("未找到下载确认按钮,可能不需要确认")
|
||||
|
||||
# 等待下载开始
|
||||
# time.sleep(3)
|
||||
|
||||
return True
|
||||
|
||||
except TimeoutException:
|
||||
self.logger.error("等待下载原始数据按钮可点击超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"点击下载原始数据时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def click_download_result_data(self):
|
||||
"""点击下载成果数据按钮并处理日期选择"""
|
||||
try:
|
||||
# 点击下载成果数据按钮
|
||||
download_result_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/download_result"))
|
||||
)
|
||||
download_result_btn.click()
|
||||
self.logger.info("已点击下载成果数据按钮")
|
||||
|
||||
# 等待日期选择弹窗出现
|
||||
# time.sleep(2)
|
||||
|
||||
# 点击选择开始日期
|
||||
start_date_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/date"))
|
||||
)
|
||||
start_date_btn.click()
|
||||
self.logger.info("已点击选择开始日期")
|
||||
|
||||
# 等待日期选择器出现
|
||||
# time.sleep(2)
|
||||
|
||||
# 滑动年份选择器 - 向上滑动1/5的距离
|
||||
if not self._swipe_year_wheel():
|
||||
self.logger.error("滑动年份选择器失败")
|
||||
return False
|
||||
|
||||
# 点击日期选择器的确定按钮
|
||||
confirm_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/okBtn"))
|
||||
)
|
||||
confirm_btn.click()
|
||||
self.logger.info("已确认日期选择")
|
||||
|
||||
# 等待日期选择器关闭
|
||||
# time.sleep(2)
|
||||
|
||||
# 假设弹窗有确定按钮,点击它开始下载
|
||||
try:
|
||||
# 尝试查找并点击下载弹窗的确定按钮
|
||||
download_confirm_btn = WebDriverWait(self.driver, 5).until(
|
||||
EC.element_to_be_clickable((AppiumBy.XPATH, "//android.widget.Button[contains(@text, '确定') or contains(@text, '下载')]"))
|
||||
)
|
||||
download_confirm_btn.click()
|
||||
self.logger.info("已点击下载确认按钮")
|
||||
except TimeoutException:
|
||||
self.logger.warning("未找到下载确认按钮,可能不需要确认")
|
||||
|
||||
# 等待下载开始
|
||||
# time.sleep(3)
|
||||
|
||||
return True
|
||||
|
||||
except TimeoutException:
|
||||
self.logger.error("等待下载成果数据按钮可点击超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"点击下载成果数据时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def _swipe_year_wheel(self):
|
||||
"""滑动年份选择器的滚轮"""
|
||||
try:
|
||||
# 获取年份选择器滚轮元素
|
||||
year_wheel = self.driver.find_element(AppiumBy.ID, "com.bjjw.cjgc:id/wheelView2")
|
||||
|
||||
# 获取滚轮的位置和尺寸
|
||||
location = year_wheel.location
|
||||
size = year_wheel.size
|
||||
|
||||
# 计算滚轮中心点坐标
|
||||
center_x = location['x'] + size['width'] // 2
|
||||
center_y = location['y'] + size['height'] // 2
|
||||
|
||||
# 计算滑动距离 - 滚轮高度的1/5
|
||||
swipe_distance = size['height'] // 5
|
||||
|
||||
for i in range(3):
|
||||
# 执行滑动操作 - 从中心向上滑动1/5高度
|
||||
self.driver.swipe(center_x, center_y - swipe_distance, center_x, center_y, 500)
|
||||
|
||||
self.logger.info(f"已{i}次滑动月份选择器")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"滑动年份选择器时出错: {str(e)}")
|
||||
return False
|
||||
|
||||
# def wait_for_loading_dialog(self, timeout=900, download_type="unknown"):
|
||||
# """
|
||||
# 检查特定结构的加载弹窗的出现和消失
|
||||
|
||||
# 参数:
|
||||
# timeout: 最大等待时间,默认10分钟(600秒)
|
||||
|
||||
# 返回:
|
||||
# bool: 如果加载弹窗出现并消失返回True,否则返回False
|
||||
# """
|
||||
# try:
|
||||
# self.logger.info(f"开始检查{download_type}加载弹窗...")
|
||||
|
||||
# # 首先检查加载弹窗是否出现
|
||||
# start_time = time.time()
|
||||
# loading_appeared = False
|
||||
|
||||
# # 等待加载弹窗出现(最多等待30秒)
|
||||
# while time.time() - start_time < 30:
|
||||
# try:
|
||||
# # 根据提供的结构查找加载弹窗
|
||||
# # 查找包含ProgressBar和"loading..."文本的弹窗
|
||||
# loading_indicators = [
|
||||
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"),
|
||||
# (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"),
|
||||
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"),
|
||||
# (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]")
|
||||
# ]
|
||||
|
||||
# for by, value in loading_indicators:
|
||||
# try:
|
||||
# element = self.driver.find_element(by, value)
|
||||
# if element.is_displayed():
|
||||
# loading_appeared = True
|
||||
# self.logger.info("数据下载已开始")
|
||||
# self.logger.info("检测到加载弹窗出现")
|
||||
# break
|
||||
# except:
|
||||
# continue
|
||||
|
||||
# if loading_appeared:
|
||||
# break
|
||||
|
||||
# except Exception as e:
|
||||
# pass
|
||||
|
||||
# time.sleep(1)
|
||||
|
||||
# # 如果加载弹窗没有出现,直接返回True
|
||||
# if not loading_appeared:
|
||||
# self.logger.info("未检测到加载弹窗,继续执行")
|
||||
# return True
|
||||
|
||||
# # 等待加载弹窗消失
|
||||
# self.logger.info("等待加载弹窗消失...")
|
||||
# disappearance_start_time = time.time()
|
||||
|
||||
# while time.time() - disappearance_start_time < timeout:
|
||||
# try:
|
||||
# # 检查加载弹窗是否还存在
|
||||
# loading_still_exists = False
|
||||
|
||||
# for by, value in loading_indicators:
|
||||
# try:
|
||||
# element = self.driver.find_element(by, value)
|
||||
# if element.is_displayed():
|
||||
# loading_still_exists = True
|
||||
# break
|
||||
# except:
|
||||
# continue
|
||||
|
||||
# if not loading_still_exists:
|
||||
# self.logger.info("加载弹窗已消失")
|
||||
# return True
|
||||
|
||||
# # 每1分钟记录一次状态
|
||||
# if int(time.time() - disappearance_start_time) % 60 == 0:
|
||||
# elapsed_time = int(time.time() - disappearance_start_time)
|
||||
# self.logger.info(f"加载弹窗仍在显示,已等待{elapsed_time//60}分钟")
|
||||
|
||||
# except Exception as e:
|
||||
# # 如果出现异常,可能弹窗已经消失
|
||||
# self.logger.info("加载弹窗可能已消失")
|
||||
# return True
|
||||
|
||||
# time.sleep(1)
|
||||
|
||||
# # 如果超时,记录错误并返回False
|
||||
# self.logger.error(f"加载弹窗在{timeout}秒后仍未消失")
|
||||
# # return False
|
||||
# # 检查是否有loading加载窗口
|
||||
# try:
|
||||
# self.logger.info(f"检查{download_type}下载是否有loading加载窗口...")
|
||||
# loading_indicators = [
|
||||
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']/android.widget.LinearLayout[@resource-id='android:id/parentPanel']//android.widget.ProgressBar"),
|
||||
# (AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"),
|
||||
# (AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"),
|
||||
# (AppiumBy.XPATH, "//*[contains(@text, 'loading...')]")
|
||||
# ]
|
||||
|
||||
# loading_exists = False
|
||||
# for by, value in loading_indicators:
|
||||
# try:
|
||||
# element = self.driver.find_element(by, value)
|
||||
# if element.is_displayed():
|
||||
# loading_exists = True
|
||||
# break
|
||||
# except:
|
||||
# continue
|
||||
|
||||
# if loading_exists:
|
||||
# self.logger.info(f"检测到{download_type}下载的loading加载窗口,执行重新打开应用操作")
|
||||
|
||||
|
||||
# # 手动启动应用
|
||||
# launch_app_manually(self.driver, self.device_id, "com.bjjw.cjgc", ".activity.LoginActivity")
|
||||
# self.logger.info("已重新启动沉降观测应用")
|
||||
|
||||
# # 点击登录
|
||||
# login_page = LoginPage(self.driver, self.wait)
|
||||
# if login_page.is_login_page():
|
||||
# if login_page.login():
|
||||
# self.logger.info("登录成功")
|
||||
# else:
|
||||
# self.logger.error("登录失败")
|
||||
# return False
|
||||
# else:
|
||||
# self.logger.info("应用已登录,无需重复登录")
|
||||
|
||||
# self.logger.info(f"{download_type}下载超时处理完成,应用已重启")
|
||||
# # 点击img_5_layout(更多下载按钮)
|
||||
# more_download_btn = self.wait.until(
|
||||
# EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout"))
|
||||
# )
|
||||
# more_download_btn.click()
|
||||
# self.logger.info("已点击更多下载按钮")
|
||||
# self.more_download_page_manager_2(download_type)
|
||||
# # 等待页面加载
|
||||
# time.sleep(1)
|
||||
# else:
|
||||
# self.logger.info(f"未检测到{download_type}下载的loading加载窗口")
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"检查{download_type}下载的loading加载窗口时出错: {str(e)}")
|
||||
# # 出错时继续执行,不影响主流程
|
||||
|
||||
# return False
|
||||
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"检查加载弹窗时出错: {str(e)}")
|
||||
# return False
|
||||
|
||||
def _is_loading_present(self):
|
||||
"""私有辅助方法:检测当前页面是否存在加载弹窗"""
|
||||
loading_indicators = [
|
||||
(AppiumBy.XPATH, "//android.widget.FrameLayout[@resource-id='android:id/content']//android.widget.ProgressBar"),
|
||||
(AppiumBy.XPATH, "//android.widget.TextView[@resource-id='android:id/message' and @text='loading...']"),
|
||||
(AppiumBy.XPATH, "//*[contains(@text, 'loading...')]")
|
||||
]
|
||||
for by, value in loading_indicators:
|
||||
try:
|
||||
element = self.driver.find_element(by, value)
|
||||
if element.is_displayed():
|
||||
return True
|
||||
except:
|
||||
continue
|
||||
return False
|
||||
|
||||
def wait_for_loading_dialog(self, timeout=900, download_type="unknown", retry_count=0):
|
||||
"""
|
||||
检查加载弹窗的出现和消失,支持最多1次重试(总共执行2次)
|
||||
"""
|
||||
try:
|
||||
self.logger.info(f"开始检查 {download_type} 加载弹窗 (尝试次数: {retry_count + 1})...")
|
||||
|
||||
# 1. 等待加载弹窗出现(最多30秒)
|
||||
start_time = time.time()
|
||||
loading_appeared = False
|
||||
while time.time() - start_time < 30:
|
||||
if self._is_loading_present():
|
||||
loading_appeared = True
|
||||
self.logger.info(f"检测到 {download_type} 加载弹窗出现")
|
||||
break
|
||||
time.sleep(1)
|
||||
|
||||
if not loading_appeared:
|
||||
self.logger.info(f"未检测到 {download_type} 加载弹窗,视为直接通过")
|
||||
return True
|
||||
|
||||
# 2. 等待加载弹窗消失
|
||||
disappearance_start_time = time.time()
|
||||
while time.time() - disappearance_start_time < timeout:
|
||||
if not self._is_loading_present():
|
||||
self.logger.info(f"{download_type} 加载弹窗已消失")
|
||||
return True
|
||||
|
||||
# 每60秒打印一次日志
|
||||
if int(time.time() - disappearance_start_time) % 60 == 0:
|
||||
self.logger.info(f"等待中...已耗时 {int(time.time() - disappearance_start_time)//60} 分钟")
|
||||
time.sleep(2)
|
||||
|
||||
# 3. 超时处理逻辑
|
||||
self.logger.error(f"{download_type} 加载弹窗在 {timeout} 秒后仍未消失")
|
||||
|
||||
# 检查是否还可以重试(retry_count=0 时执行重试,即第二次执行)
|
||||
if retry_count < 1:
|
||||
self.logger.warning(f"检测到超时,准备进行第 {retry_count + 2} 次尝试(重启应用)...")
|
||||
|
||||
# 执行重启逻辑
|
||||
from globals.driver_utils import launch_app_manually
|
||||
from page_objects.login_page import LoginPage
|
||||
|
||||
launch_app_manually(self.driver, self.device_id, "com.bjjw.cjgc", ".activity.LoginActivity")
|
||||
login_page = LoginPage(self.driver, self.wait)
|
||||
if login_page.is_login_page():
|
||||
login_page.login()
|
||||
|
||||
# 重新导航到下载页面
|
||||
more_download_btn = self.wait.until(
|
||||
EC.element_to_be_clickable((AppiumBy.ID, "com.bjjw.cjgc:id/img_5_layout"))
|
||||
)
|
||||
more_download_btn.click()
|
||||
|
||||
# 递归调用:增加 retry_count
|
||||
return self.more_download_page_manager_2(download_type, retry_count = retry_count + 1)
|
||||
else:
|
||||
self.logger.error(f"{download_type} 已达到最大重试次数,操作失败")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"检查加载弹窗时发生异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def more_download_page_manager_2(self, download_type, retry_count=0):
|
||||
"""
|
||||
修改后的 manager_2,透传 retry_count 参数
|
||||
"""
|
||||
try:
|
||||
if not self.is_on_more_download_page():
|
||||
return False
|
||||
|
||||
self.click_download_button()
|
||||
|
||||
if download_type in ["原始数据", "历史数据"]:
|
||||
self.click_download_original_data()
|
||||
return self.wait_for_loading_dialog(download_type="原始数据", retry_count=retry_count)
|
||||
|
||||
elif download_type == "成果数据":
|
||||
self.click_download_result_data()
|
||||
return self.wait_for_loading_dialog(download_type="成果数据", retry_count=retry_count)
|
||||
|
||||
return False
|
||||
except Exception as e:
|
||||
self.logger.error(f"manager_2 执行出错: {str(e)}")
|
||||
return False
|
||||
|
||||
def more_download_page_manager(self):
|
||||
"""执行更多下载页面管理操作"""
|
||||
try:
|
||||
self.logger.info("开始执行更多下载页面操作")
|
||||
|
||||
# 检查是否在更多下载页面
|
||||
if not self.is_on_more_download_page():
|
||||
self.logger.error("不在更多下载页面")
|
||||
return False
|
||||
|
||||
# 点击下载历史数据按钮
|
||||
if not self.click_download_button():
|
||||
self.logger.error("点击下载历史数据按钮失败")
|
||||
return False
|
||||
|
||||
# 等待下载历史数据页面加载完成
|
||||
# time.sleep(3)
|
||||
|
||||
# 点击下载原始数据按钮
|
||||
if not self.click_download_original_data():
|
||||
self.logger.error("点击下载原始数据按钮失败")
|
||||
return False
|
||||
|
||||
# 等待下载操作完成
|
||||
time.sleep(1)
|
||||
|
||||
# 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失
|
||||
if not self.wait_for_loading_dialog(download_type="原始数据"):
|
||||
self.logger.warning("下载过程中的加载弹窗未在预期时间内消失,但操作已完成")
|
||||
|
||||
# 等待一段时间,确保原始数据下载完成
|
||||
time.sleep(1)
|
||||
|
||||
# 点击下载成果数据按钮
|
||||
if not self.click_download_result_data():
|
||||
self.logger.error("点击下载成果数据按钮失败")
|
||||
return False
|
||||
|
||||
# 使用wait_for_loading_dialog函数等待下载过程中的加载弹窗消失
|
||||
if not self.wait_for_loading_dialog(download_type="成果数据"):
|
||||
self.logger.warning("成果数据下载过程中的加载弹窗未在预期时间内消失,但操作已完成")
|
||||
|
||||
self.logger.info("更多下载页面操作执行完成")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"执行更多下载页面操作时出错: {str(e)}")
|
||||
return False
|
||||
1471
page_objects/screenshot_page.py
Normal file
1471
page_objects/screenshot_page.py
Normal file
File diff suppressed because it is too large
Load Diff
1875
page_objects/upload_config_page.py
Normal file
1875
page_objects/upload_config_page.py
Normal file
File diff suppressed because it is too large
Load Diff
251
permissions.py
Normal file
251
permissions.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# 权限处理
|
||||
import subprocess
|
||||
import logging
|
||||
import time
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s: %(message)s")
|
||||
|
||||
def check_device_connection(device_id: str) -> bool:
|
||||
"""检查设备连接状态"""
|
||||
try:
|
||||
check_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.product.model"]
|
||||
result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
|
||||
if result.returncode == 0:
|
||||
logging.info(f"设备 {device_id} 连接正常,型号: {result.stdout.strip()}")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"设备 {device_id} 连接失败: {result.stderr.strip()}")
|
||||
return False
|
||||
except subprocess.TimeoutExpired:
|
||||
logging.error(f"设备 {device_id} 连接超时")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"检查设备 {device_id} 时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def is_package_installed(device_id: str, package_name: str) -> bool:
|
||||
"""检查包是否已安装"""
|
||||
try:
|
||||
check_cmd = ["adb", "-s", device_id, "shell", "pm", "list", "packages", package_name]
|
||||
result = subprocess.run(check_cmd, capture_output=True, text=True, timeout=10)
|
||||
return result.returncode == 0 and package_name in result.stdout
|
||||
except Exception as e:
|
||||
logging.error(f"检查包 {package_name} 时发生错误: {str(e)}")
|
||||
return False
|
||||
|
||||
def grant_single_permission(device_id: str, package: str, permission: str) -> bool:
|
||||
"""
|
||||
为单个包授予单个权限
|
||||
:return: 是否成功授予
|
||||
"""
|
||||
try:
|
||||
grant_cmd = [
|
||||
"adb", "-s", device_id,
|
||||
"shell", "pm", "grant", package,
|
||||
permission
|
||||
]
|
||||
result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15)
|
||||
|
||||
if result.returncode == 0:
|
||||
logging.info(f"设备 {device_id}:已成功授予 {package} 权限: {permission}")
|
||||
return True
|
||||
else:
|
||||
error_msg = result.stderr.strip()
|
||||
logging.warning(f"设备 {device_id}:授予 {package} 权限 {permission} 失败: {error_msg}")
|
||||
|
||||
# 尝试使用root权限
|
||||
if "security" in error_msg.lower() or "permission" in error_msg.lower():
|
||||
logging.info(f"设备 {device_id}:尝试使用root权限授予 {package} 权限")
|
||||
|
||||
# 重启adb为root模式
|
||||
root_cmd = ["adb", "-s", device_id, "root"]
|
||||
subprocess.run(root_cmd, capture_output=True, text=True, timeout=10)
|
||||
time.sleep(2) # 等待root权限生效
|
||||
|
||||
# 再次尝试授予权限
|
||||
result = subprocess.run(grant_cmd, capture_output=True, text=True, timeout=15)
|
||||
if result.returncode == 0:
|
||||
logging.info(f"设备 {device_id}:使用root权限成功授予 {package} 权限: {permission}")
|
||||
return True
|
||||
else:
|
||||
logging.error(f"设备 {device_id}:即使使用root权限也无法授予 {package} 权限 {permission}: {result.stderr.strip()}")
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"设备 {device_id}:ADB 命令执行失败,返回码 {e.returncode}")
|
||||
logging.error(f"标准输出:{e.stdout.strip()}")
|
||||
logging.error(f"错误输出:{e.stderr.strip()}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logging.error(f"设备 {device_id}:处理 {package} 时发生未知错误:{str(e)}")
|
||||
return False
|
||||
|
||||
def grant_appium_permissions(device_id: str, require_all: bool = False) -> bool:
|
||||
"""
|
||||
为 Appium UiAutomator2 服务授予权限
|
||||
:param device_id: 设备 ID
|
||||
:param require_all: 是否要求所有权限都成功授予
|
||||
:return: 权限授予是否成功(根据require_all参数判断)
|
||||
"""
|
||||
# 首先检查设备连接
|
||||
if not check_device_connection(device_id):
|
||||
return False
|
||||
|
||||
packages_to_grant = [
|
||||
"io.appium.settings",
|
||||
"io.appium.uiautomator2.server",
|
||||
"io.appium.uiautomator2.server.test"
|
||||
]
|
||||
|
||||
# 权限列表(按优先级排序)
|
||||
permissions_to_grant = [
|
||||
"android.permission.WRITE_SECURE_SETTINGS",
|
||||
"android.permission.CHANGE_CONFIGURATION",
|
||||
"android.permission.DUMP",
|
||||
]
|
||||
|
||||
success_count = 0
|
||||
total_attempted = 0
|
||||
package_results = {}
|
||||
|
||||
# 检查并授予权限
|
||||
for package in packages_to_grant:
|
||||
package_results[package] = {"installed": False, "permissions": {}}
|
||||
|
||||
if not is_package_installed(device_id, package):
|
||||
logging.warning(f"设备 {device_id}:包 {package} 未安装,跳过权限授予")
|
||||
package_results[package]["installed"] = False
|
||||
continue
|
||||
|
||||
package_results[package]["installed"] = True
|
||||
package_success = 0
|
||||
package_attempted = 0
|
||||
|
||||
for permission in permissions_to_grant:
|
||||
total_attempted += 1
|
||||
package_attempted += 1
|
||||
|
||||
result = grant_single_permission(device_id, package, permission)
|
||||
package_results[package]["permissions"][permission] = result
|
||||
|
||||
if result:
|
||||
success_count += 1
|
||||
package_success += 1
|
||||
|
||||
# 记录每个包的授权结果
|
||||
logging.info(f"设备 {device_id}:包 {package} 权限授予结果: {package_success}/{package_attempted}")
|
||||
|
||||
# 统计和报告
|
||||
logging.info(f"设备 {device_id}:权限授予完成")
|
||||
logging.info(f"总计: 尝试 {total_attempted} 次,成功 {success_count} 次")
|
||||
|
||||
# 检查每个包的关键权限
|
||||
critical_permission = "android.permission.WRITE_SECURE_SETTINGS"
|
||||
critical_failures = []
|
||||
|
||||
for package, info in package_results.items():
|
||||
if info["installed"] and critical_permission in info["permissions"]:
|
||||
if not info["permissions"][critical_permission]:
|
||||
critical_failures.append(package)
|
||||
|
||||
if critical_failures:
|
||||
logging.warning(f"设备 {device_id}:以下包的关键权限 {critical_permission} 授予失败: {', '.join(critical_failures)}")
|
||||
logging.warning("这可能会影响某些自动化功能,但基础测试通常不受影响")
|
||||
|
||||
# 根据require_all参数返回结果
|
||||
if require_all:
|
||||
# 要求所有权限都成功
|
||||
if critical_failures:
|
||||
logging.error("关键权限授予失败,无法继续(require_all=True)")
|
||||
return False
|
||||
return success_count == total_attempted
|
||||
else:
|
||||
# 不要求所有权限,只要设备连接正常就返回True
|
||||
logging.info(f"设备 {device_id}:权限授予过程完成,建议重启设备或Appium服务使更改生效")
|
||||
return True
|
||||
|
||||
def check_appium_compatibility(device_id: str) -> dict:
|
||||
"""
|
||||
检查Appium兼容性
|
||||
:return: 兼容性报告字典
|
||||
"""
|
||||
try:
|
||||
# 获取Android版本
|
||||
version_cmd = ["adb", "-s", device_id, "shell", "getprop", "ro.build.version.release"]
|
||||
result = subprocess.run(version_cmd, capture_output=True, text=True, timeout=10)
|
||||
android_version = result.stdout.strip() if result.returncode == 0 else "未知"
|
||||
|
||||
report = {
|
||||
"device_id": device_id,
|
||||
"android_version": android_version,
|
||||
"compatibility": "unknown",
|
||||
"notes": [],
|
||||
"suggestions": []
|
||||
}
|
||||
|
||||
try:
|
||||
version_num = float(android_version.split('.')[0])
|
||||
|
||||
if version_num >= 11:
|
||||
report["compatibility"] = "limited"
|
||||
report["notes"].append("Android 11+ 对WRITE_SECURE_SETTINGS权限限制非常严格")
|
||||
report["suggestions"].append("使用--no-reset参数启动Appium")
|
||||
report["suggestions"].append("设置autoGrantPermissions=false")
|
||||
|
||||
elif version_num >= 10:
|
||||
report["compatibility"] = "moderate"
|
||||
report["notes"].append("Android 10 限制了WRITE_SECURE_SETTINGS权限")
|
||||
report["suggestions"].append("可尝试使用root权限的设备")
|
||||
|
||||
elif version_num >= 9:
|
||||
report["compatibility"] = "good"
|
||||
report["notes"].append("Android 9 兼容性较好")
|
||||
|
||||
else:
|
||||
report["compatibility"] = "excellent"
|
||||
report["notes"].append("Android 8或以下版本完全兼容")
|
||||
|
||||
except (ValueError, IndexError):
|
||||
report["notes"].append("无法解析Android版本")
|
||||
|
||||
return report
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"检查兼容性时出错: {str(e)}")
|
||||
return {"device_id": device_id, "error": str(e)}
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 获取设备ID(示例)
|
||||
devices_cmd = ["adb", "devices"]
|
||||
result = subprocess.run(devices_cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split('\n')[1:] # 跳过第一行标题
|
||||
for line in lines:
|
||||
if line.strip() and "device" in line:
|
||||
device_id = line.split('\t')[0]
|
||||
logging.info(f"找到设备: {device_id}")
|
||||
|
||||
# 1. 检查兼容性
|
||||
report = check_appium_compatibility(device_id)
|
||||
logging.info(f"兼容性报告: Android {report.get('android_version', '未知')} - {report.get('compatibility', '未知')}")
|
||||
|
||||
# 2. 授予权限(不要求全部成功)
|
||||
success = grant_appium_permissions(device_id, require_all=False)
|
||||
|
||||
if success:
|
||||
logging.info(f"设备 {device_id} 设置完成,可以开始测试")
|
||||
else:
|
||||
logging.warning(f"设备 {device_id} 权限授予有失败,但可能仍可进行基础测试")
|
||||
|
||||
# 3. 提供建议
|
||||
if "suggestions" in report:
|
||||
logging.info("建议:")
|
||||
for suggestion in report["suggestions"]:
|
||||
logging.info(f" - {suggestion}")
|
||||
else:
|
||||
logging.error("无法获取设备列表,请确保ADB已正确安装且设备已连接")
|
||||
5
test_results/上传失败的断点.txt
Normal file
5
test_results/上传失败的断点.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
CDWZQ-3标-雷庙村大桥-8-号桥台-平原
|
||||
CDWZQ-3标-雷庙村特大桥-5-6-号墩-平原
|
||||
CDWZQ-3标-金马村特大桥-14-号墩-平原
|
||||
CDWZQ-3标-雷庙村特大桥-4#-7-14号墩-平原
|
||||
CDWZQ-3标-老屋坡特大桥-14-21-平原
|
||||
0
test_results/上传成功的断点.txt
Normal file
0
test_results/上传成功的断点.txt
Normal file
BIN
上传人员信息.xlsx
Normal file
BIN
上传人员信息.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user