提交 bd845030 authored 作者: 陈泽健's avatar 陈泽健

feat(app): 初始化Appium驱动及中控屏设备控制功能

- 添加Appium驱动初始化函数,支持Android设备控制
- 实现滑动操作封装,用于界面导航
- 添加图片亮度对比、特征匹配和直方图相似度计算功能
- 实现RTSP流帧捕获和保存功能
- 封装中控屏灯光、窗帘、空调、信息发布、音乐及大屏控制函数
- 添加ADB连接初始化和断开连接管理函数
- 提供设备控制相关的元素定位和点击重试机制
- 集成截图和日志记录功能用于测试验证
上级 b305fef3
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
预定系统标准版
\ No newline at end of file
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.10 (预定系统标准版)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (预定系统标准版)" project-jdk-type="Python SDK" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/预定系统.iml" filepath="$PROJECT_DIR$/.idea/预定系统.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
import json
import logging
import csv
import os
import re
import threading
import time
from time import sleep
from datetime import datetime
import paho.mqtt.client as mqtt
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
class Mqtt:
def __init__(self, broker_address, port, username=None, password=None, client_id=None):
"""
初始化 MQTT 客户端
:param broker_address: MQTT 代理地址
:param port: MQTT 代理端口
:param username: MQTT 登录用户名(可选)
:param password: MQTT 登录密码(可选)
:param client_id: 客户端ID(可选)
"""
self.broker_address = broker_address
self.port = port
self.username = username
self.password = password
self.client_id = client_id or f"python_client_{os.getpid()}_{time.time()}"
self.client = None
self._received_message_lock = threading.Lock()
self.received_message = None
self.message_type = None # 初始化message_type属性
def connect(self):
"""连接到 MQTT 服务器"""
try:
# 创建客户端时指定client_id
self.client = mqtt.Client(client_id=self.client_id)
if self.username and self.password:
self.client.username_pw_set(self.username, self.password)
self.client.on_connect = self.on_connect
self.client.on_message = self.on_message
# 设置will消息
self.client.will_set(topic="/client/disconnect",
payload=json.dumps({"client_id": self.client_id}),
qos=1,
retain=False)
self.client.connect(self.broker_address, self.port, keepalive=60)
self.client.loop_start()
logging.info(f"MQTT连接成功,Client ID: {self.client_id}")
except Exception as e:
logging.error(f"连接失败: {str(e)}")
raise
def print_current_config(config):
"""
打印当前MQTT消息配置信息
"""
print("当前配置信息:")
for key, value in config.items():
print(f"{key}: {value}")
def disconnect(self):
"""
断开与 MQTT 服务器的连接
"""
if self.client:
try:
self.client.loop_stop() # 停止网络循环
self.client.disconnect() # 断开连接
logging.info("已断开与MQTT 服务器的连接")
except Exception as e:
logging.error(f"断开与MQTT 服务器的连接时发生错误: {e}")
finally:
self.client = None # 确保资源被完全释放
else:
logging.warning("尝试断开连接时,客户端已不存在")
def on_connect(self, client, userdata, flags, rc):
"""
连接成功或失败的回调函数
:param client: 客户端实例
:param userdata: 用户数据
:param flags: 连接标志
:param rc: 返回码
"""
try:
if rc == 0:
logging.info("连接成功")
else:
logging.error(f"连接失败,返回码: {rc}")
# 根据不同的返回码采取不同的措施
if rc == 1:
logging.error("错误:错误的协议版本")
elif rc == 2:
logging.error("错误:无效的客户端标识符")
elif rc == 3:
logging.error("错误:服务器不可用")
elif rc == 4:
logging.error("错误:错误的用户名或密码")
elif rc == 5:
logging.error("错误:未授权")
else:
logging.error("未知错误")
except Exception as e:
logging.exception(f"在处理连接结果时发生异常: {e}")
def on_message(self, client, userdata, msg):
"""
接收到消息的回调函数
:param client: 客户端实例
:param userdata: 用户数据
:param msg: 消息对象
"""
try:
payload = msg.payload.decode('utf-8', errors='replace') # 处理解码错误
logging.info(f"收到消息: {msg.topic} {payload[:50]}...") # 脱敏日志记录
except UnicodeDecodeError as e:
logging.error(f"解码错误: {e}")
payload = "无法解码的消息"
with self._received_message_lock:
self.received_message = payload # 线程安全地存储接收到的消息
def subscribe(self, topic):
"""
订阅指定的主题
:param topic: 主题名称
"""
if self.client is None:
logging.error("客户端未初始化,无法订阅主题")
raise ValueError("客户端未初始化,无法订阅主题")
# 输入验证
if not isinstance(topic, str) or not topic.strip():
logging.error("无效的主题名称")
raise ValueError("无效的主题名称")
try:
self.client.subscribe(topic)
logging.info(f"已订阅主题: {topic}")
except ConnectionError as ce:
logging.error(f"连接错误,无法订阅主题: {topic}, 错误信息: {str(ce)}")
raise
except TimeoutError as te:
logging.error(f"超时错误,无法订阅主题: {topic}, 错误信息: {str(te)}")
raise
except ValueError as ve:
logging.error(f"值错误,无法订阅主题: {topic}, 错误信息: {str(ve)}")
raise
except Exception as e:
logging.error(f"未知错误,无法订阅主题: {topic}, 错误信息: {str(e)}")
raise
def set_message_type(self, message_type):
"""
设置消息类型
此方法用于设置或更改消息类型属性,允许对象根据需要处理不同类型的消息
参数:
message_type: 要设置的消息类型,可以是任何数据类型,但通常应该是字符串、整数或枚举类型
返回:
"""
self.message_type = message_type
def publish(self, topic, message):
"""
发布消息到指定的主题
:param topic: 主题名称
:param message: 消息内容
"""
if self.client:
try:
# 将消息转换为JSON字符串
if self.message_type == dict and isinstance(message, dict):
message = json.dumps(message)
elif message is None:
message = ""
else:
message = str(message)
except (TypeError, ValueError) as e:
logging.error(f"{datetime.now()} - 消息转换失败: {e} - 调用者: {self.__class__.__name__}.publish")
raise
try:
self.client.publish(topic, message)
logging.info(f"{datetime.now()} - 已发布消息到主题: {topic} - 调用者: {self.__class__.__name__}.publish")
logging.info(f"{datetime.now()} - 发送的消息为: {message} - 调用者: {self.__class__.__name__}.publish")
except Exception as e:
logging.error(f"{datetime.now()} - 消息发布失败: {e} - 失败的主题: {topic}, 消息: {message} - 调用者: {self.__class__.__name__}.publish")
raise
def wait_for_message(self, topic, timeout=5):
"""
等待指定主题的消息
:param topic: 主题名称
:param timeout: 超时时间(秒)
:return: 接收到的消息或 None
"""
if self.client is None:
logging.warning("Client is not initialized")
return None
if timeout < 0:
logging.warning("Timeout cannot be negative")
return None
start_time = time.monotonic()
while (time.monotonic() - start_time) < timeout:
try:
with self.lock:
if self.received_message is not None:
return self.received_message
except Exception as e:
logging.error(f"Error accessing received_message: {e}")
try:
time.sleep(0.1)
except Exception as e:
logging.error(f"Error in sleep: {e}")
return None
@staticmethod
def read_config_from_csv(file_path):
"""
从 CSV 文件读取配置
:param file_path: CSV 文件路径
:param allowed_directory: 允许访问的目录
:return: 配置列表
"""
try:
# 验证文件路径
if not os.path.isfile(file_path):
raise FileNotFoundError(f"文件 {file_path} 不存在")
with open(file_path, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
return [row for row in reader]
except FileNotFoundError as e:
print(f"错误: {e}")
return []
except PermissionError as e:
print(f"错误: {e}")
return []
except Exception as e:
print(f"未知错误: {e}")
return []
@staticmethod
def wait_for_message(self, topic, timeout=5):
"""
等待指定主题的消息
:param topic: 主题名称
:param timeout: 超时时间(秒)
:return: 接收到的消息或 None
"""
if not isinstance(topic, str) or not re.match(r'^[a-zA-Z0-9_\-]+$', topic):
raise ValueError("Invalid topic format")
if timeout < 0:
return None
try:
if self.client:
start_time = time.time()
while (time.time() - start_time) < timeout:
if self.has_received_message():
return self.received_message
sleep(0.1)
return None
except AttributeError:
return None
return None
def has_received_message(self):
return hasattr(self, 'received_message')
@staticmethod
def read_config_from_csv(file_path):
"""
从 CSV 文件读取配置
:param file_path: CSV 文件路径
:return: 配置列表
"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"文件 {file_path} 不存在")
try:
with open(file_path, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
config_list = [row for row in reader]
if not config_list:
raise ValueError("CSV 文件内容为空或格式不正确")
logging.info(f"成功读取文件 {file_path}")
return config_list
except Exception as e:
logging.error(f"读取文件 {file_path} 时发生错误: {e}")
return []
@staticmethod
def build_message(config, current_time,topic):
"""
构建消息内容
:param config: 配置字典
:param current_time: 当前时间
:return: 消息字典
"""
#安卓信息设备上报
if topic == "rebootResponseTopic":
return {
"method": "/system/readSystemInfo",
"clientId": config['clientId'],
"result": json.dumps({
"result": {
"buildInfo": {
"appToken": config['appToken'],
"companyNumber": config['companyNumber'],
"cnum": config['cnum'],
"conferenceName": "测试会议室",
"conferenceId": int(config['conferenceId']),
"defaultQrCodeUrl": "http://192.168.5.218:8888/group1/M00/00/21/wKgFyGNBWZmADtnNAAAwrxR0X8s016.png",
"aliasName": "zt",
"serverBaseUrl": "https://192.168.5.218/exapi",
"localBindTime": current_time,
"generalField": "{\"conferencePhone\":\"\",\"chooseTimeType\":1,\"meetingTopicSwitch\":\"1\",\"meetingContentSwitch\":\"1\",\"meetingReverseTypeSwitch\":\"1\",\"seatArrangeSwitch\":\"1\",\"meetingVoteSwitch\":\"1\",\"floorPlanPath\":\"\",\"jumpToPaperless\":2,\"approvalList\":[],\"isLeaderJoin\":false,\"meetingPublishScreenSwitch\":\"1\"}"
},
"faceVersion": "4.2.12021020201.1",
"wgtVersion": "0.0.81",
"deviceModel": "yx_rk3288",
"abiList": ["armeabi-v7a", "armeabi"],
"androidId": "48134e6047a19aaf",
"appName": "UBAINS",
"appPackageName": "com.ubains.local.gviewer",
"appVersion": 78,
"appVersionName": "1.1.78",
"battery": 0,
"bluetoothEnabled": False,
"camerasCount": 1,
"charging": True,
"density": 1,
"freeAndTotalMemory": "1176M/1997M",
"internalAvailableSize": 4306395136,
"internalTotalSize": 4877451264,
"ipAddress": "192.168.5.111",
"macAddress": config['macAddress'],
"networkType": "NETWORK_ETHERNET",
"processCpuUsage": "0.82%",
"resolution": "1280x800",
"romName": "rockchip",
"rooted": True,
"sdkVersionCode": 25,
"sdkVersionName": "7.1.2",
"sysDate": "Tue Oct 22 18:24:52 GMT+08:00 2024",
"sysDatestr": current_time,
"sysElapsedRealtime": "342:26:11",
"sysLanguage": "zh",
"sysSupportedSensors": ["Accelerometer sensor", "Gyroscope sensor",
"Game Rotation Vector Sensor",
"Gravity Sensor"],
"authCode": config['authCode'],
"conferenceName": "测试会议室"
}
})
}
#安卓信息心跳上报
elif topic == "/uams/android/broadcast":
return json.dumps({
"type":"heartbeat",
"clientId" : config['clientId'],
"appId":"com.ubains.uniplatform",
"deviceId": config['deviceId']
})
#毫米波雷达数据上报
elif "/properties/upload" in topic or "/event/upload" in topic:
return json.dumps({
"properties":{
"client_id" : config['client_id'],
"presence_state" : config['presence_state'],
"kaiguan" : config['kaiguan'],
"julishezhi" : config['julishezhi'],
"lingmindushezhi" : config['lingmindushezhi'],
"led":1,
"wifi_mac" : config['wifi_mac'],
"ble_mac" : config['ble_mac'],
"last_connection_time": current_time,
"current_time":current_time,
"device_model" : "c1_100_wifi_u",
"fw_version":"0.0.6",
"sn" : config['sn'],
"ip" : config['ip']
}
})
# 北京富创项目的消息体与主题
elif topic == "/meeting/message/sync":
return json.dumps({
"action": config['action'],
"thirdMessageDTO": [{
"thirdPartyMeetingId": config['thirdPartyMeetingId'],
"messageCompere": "admin@fc",
"thirdPartyUserId": "admin@fc",
"conferenceName": config['conferenceName'],
"thirdPartyRoomId": config['thirdPartyRoomId'],
"messageName": config['messageName'],
"startTime": config['startTime'],
"endTime": config['endTime'],
"companyNumber": config['companyNumber'],
"participantList": ["admin@fc" , "test40", "test41", "test42", "test43", "test44", "test45", "test46", "test47", "test48", "test49",
"test50", "test51", "test52", "test53", "test54", "test55", "test56", "test57", "test58", "test59",
"test60", "test61", "test62", "test63", "test64", "test65", "test66", "test67", "test68", "test69",
"test70", "test71", "test72", "test73", "test74", "test75", "test76", "test77", "test78", "test79",
"test80", "test81", "test82", "test83", "test84", "test85", "test86", "test87", "test88", "test89",
"test90", "test91", "test92", "test93", "test94", "test95", "test96", "test97", "test98", "test99",
"test100", "test101", "test102", "test103", "test104", "test105", "test106", "test107", "test108", "test109",
"test110", "test111", "test112", "test113", "test114", "test115", "test116", "test117", "test118", "test119",
"test120", "test121", "test122", "test123", "test124", "test125", "test126", "test127", "test128", "test129",
"test130", "test131", "test132", "test133", "test134", "test135", "test136", "test137", "test138", "test139",
"test140"],
"signInBeforeMinutes": config['signInBeforeMinutes'],
"signEndTime": config['signEndTime'],
"enableSignOut": config['enableSignOut'],
"signOutMinutesAfterSignIn": config['signOutMinutesAfterSignIn'],
"signOutAfterEndMinutes": config['signOutAfterEndMinutes']
}]
})
# 北京富创项目的门口屏底图更新
elif "/iot/v1/device/service/request/doorscreenBackgroundImageUpdate" in topic:
message = {
"action": "doorscreenBackgroundImageUpdate",
"companyNumber": config['companyNumber'],
"conferenceNumber": config['conferenceNumber'],
"data": {}
}
# 根据 updateMethod 设置不同的背景图片字段
if config.get('updateMethod') == 'restoreDefault':
message['data']['updateMethod'] = 'restoreDefault'
elif config.get('updateMethod') == 'single':
message['data']['updateMethod'] = 'single'
# 判断背景图的类型是Base64还是URL
if "backgroundImageType" == "backgroundImageBase64":
message['data']['backgroundImageBase64'] = config['backgroundImageValue']
elif 'backgroundImageType' == "backgroundImageURL":
message['data']['backgroundImageURL'] = config['backgroundImageValue']
return json.dumps(message)
def send_and_receive_messages(self, topic: str, message: str, num_times: int = 1, timeout: int = 5,
interval: float = 0.2):
"""
发送并接收消息
:param topic: 主题名称
:param message: 消息内容
:param num_times: 发送次数,默认为1
:param timeout: 超时时间(秒),默认为5秒
:param interval: 每次发送之间的间隔时间(秒),默认为0.2秒
"""
if not isinstance(topic, str) or not isinstance(message, str):
raise ValueError("主题和消息必须是字符串类型")
for i in range(num_times):
try:
self.publish(topic, message)
received_message = self.wait_for_message(topic, timeout=timeout)
if received_message:
logging.info("消息接收成功!")
else:
logging.warning("超时时间内未接收到消息。")
sleep(interval)
except (ConnectionError, TimeoutError) as e:
logging.error(f"网络连接或超时错误: {e}")
except ValueError as e:
logging.error(f"值错误: {e}")
except Exception as e:
logging.error(f"未知错误: {e}")
\ No newline at end of file
from appium.webdriver.common.appiumby import AppiumBy
from time import sleep
from appium.options.android import UiAutomator2Options
from django.db.models.fields import return_None
from hytest import *
from pywinauto.mouse import click
from selenium import webdriver
# 创建一个函数,用于初始化Appium驱动程序
def app_setup_driver(platformName, platformVersion, deviceName, appPackage, appActivity, udid):
"""
根据提供的参数设置 Appium 驱动程序。
参数:
- platformName: 操作系统名称
- platformVersion: 操作系统版本
- deviceName: 设备名称
- appPackage: 应用程序包名
- appActivity: 应用程序活动名
- udid: 设备唯一标识符
返回:
- driver: Appium 驱动程序对象
"""
# 定义设备和应用的相关参数,以便 Appium 能够识别和控制设备
desired_caps = {
'platformName': platformName, # 被测手机是安卓
'platformVersion': platformVersion, # 手机安卓版本,如果是鸿蒙系统,依次尝试 12、11、10 这些版本号
'deviceName': deviceName, # 设备名,安卓手机可以随意填写
'appPackage': appPackage, # 启动APP Package名称
'appActivity': appActivity, # 启动Activity名称
'unicodeKeyboard': True, # 自动化需要输入中文时填True
'resetKeyboard': True, # 执行完程序恢复原来输入法
'noReset': True, # 不要重置App
'newCommandTimeout': 6000,
'automationName': 'UiAutomator2',
'skipUnlock': True,
'autoGrantPermissions': True,
'udid':udid
}
# 记录 desired_caps 参数信息
logging.info(f"desired_caps参数:{desired_caps}")
try:
# 记录初始化 Appium 驱动程序的过程
logging.info("正在初始化 Appium 驱动程序...")
# 创建 Appium 驱动程序对象
driver = webdriver.Remote('http://localhost:4723/wd/hub',
options=UiAutomator2Options().load_capabilities(desired_caps))
# 记录 Appium 驱动程序初始化成功
logging.info("Appium 驱动程序初始化成功。")
# 返回 Appium 驱动程序对象
return driver
except Exception as e:
# 记录初始化驱动程序失败的错误信息
logging.error(f"初始化驱动程序失败: {e}")
# 重新抛出异常
raise
# 封装滑动操作
def swipe_up(app_driver):
"""
在应用程序中执行上滑操作。
参数:
- app_driver: 应用程序的驱动对象,用于与设备交互。
返回值:
"""
# 获取屏幕尺寸
size = app_driver.get_window_size()
# 计算滑动的起始和结束坐标
start_x = size['width'] // 2
start_y = int(size['height'] * 0.2) # 起始y坐标,屏幕高度的20%
end_x = start_x
end_y = int(size['height'] * 0.8) # 结束y坐标,屏幕高度的80%
# 执行滑动操作
app_driver.swipe(start_x, start_y, end_x, end_y, duration=500)
# 图片亮度对比函数
# 请使用“pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple”安装PIL库
from PIL import Image
import numpy as np
import os
import logging
def compare_brightness(light_down_path, light_on_path, threshold=1):
"""
对比两张图片的亮度,返回亮度是否增加的布尔值。
light_on_path:传入暗色的图片
light_down_path:传入亮色的图片
threshold:亮度变化的阈值,默认为1
"""
try:
# 打开图片并转换为灰度图像,以便后续处理
image1 = Image.open(light_down_path).convert('L') # 转换为灰度图像
image2 = Image.open(light_on_path).convert('L') # 转换为灰度图像
# 将图像转换为numpy数组,便于计算
array1 = np.array(image1)
array2 = np.array(image2)
# 计算两张图片的平均亮度
avg_brightness1 = np.mean(array1)
avg_brightness2 = np.mean(array2)
# 记录日志,输出两张图片的平均亮度
logging.info(f"关闭灯光时的平均亮度: {avg_brightness1}")
logging.info(f"打开灯光时的平均亮度: {avg_brightness2}")
# 计算亮度变化量
brightness_increase = avg_brightness2 - avg_brightness1
# 记录日志,输出亮度变化量
logging.info(f"亮度变化量: {brightness_increase}")
# 判断亮度变化量是否超过阈值
return brightness_increase > threshold
except Exception as e:
# 异常处理,记录错误日志
logging.error(f"对比亮度时发生错误: {e}", exc_info=True)
return False
# 调用示例
# if __name__ == '__main__':
# logging.info("开始对比亮度")
#
# image1_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\Base\captured_frame2.jpg'
# image2_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\Base\captured_frame.jpg'
#
# # 检查图片路径是否存在
# if not os.path.exists(image1_path):
# logging.error(f"图片 {image1_path} 不存在")
# exit(1)
# if not os.path.exists(image2_path):
# logging.error(f"图片 {image2_path} 不存在")
# exit(1)
#
# # 对比两张截图的亮度
# result = compare_brightness(image1_path, image2_path)
# logging.info(f"亮度比较结果: {result}")
#
# if result:
# logging.info("灯光已成功打开")
# else:
# logging.error("灯光未成功打开")
# 提取特征点并比较图片函数
# 请使用“pip install opencv-python”安装cv2库
import cv2
import logging
from PIL import Image
import numpy as np
import os
def compare_images_feature_matching(image1_path, image2_path):
"""
比较两张图片是否相同
使用特征匹配的方法比较两张图片是否相同。首先验证图片路径和格式,然后根据图片尺寸进行调整,
最后转换为numpy数组进行比较。
参数:
image1_path (str): 第一张图片的路径
image2_path (str): 第二张图片的路径
返回:
dict: 包含比较结果和可能的错误信息
"""
try:
# 验证图片路径是否存在且为有效图片文件
if not os.path.isfile(image1_path) or not os.path.isfile(image2_path):
logging.error("图片路径无效")
return {"result": False, "error": "图片路径无效"}
# 打开两张图片
img1 = Image.open(image1_path)
img2 = Image.open(image2_path)
# 验证图片是否为有效格式
if img1.format not in ['JPEG', 'PNG'] or img2.format not in ['JPEG', 'PNG']:
logging.error("图片格式无效")
return {"result": False, "error": "图片格式无效"}
# 如果尺寸不同,先调整大小
if img1.size != img2.size:
logging.info("图片尺寸不同,调整为相同尺寸进行比较")
img2 = img2.resize(img1.size, Image.ANTIALIAS) # 保持纵横比
# 将图片转换为相同的模式
img1 = img1.convert("RGB")
img2 = img2.convert("RGB")
# 转换为 numpy 数组进行比较
img1_array = np.array(img1)
img2_array = np.array(img2)
# 输出numpy数组信息
logging.info(f"图片1数组: {img1_array},图片2数组: {img2_array}")
# 比较两个数组是否相同
return {"result": np.array_equal(img1_array, img2_array), "error": None}
except FileNotFoundError as e:
logging.error(f"文件未找到: {e}")
return {"result": False, "error": f"文件未找到: {e}"}
except OSError as e:
logging.error(f"图片处理错误: {e}")
return {"result": False, "error": f"图片处理错误: {e}"}
except MemoryError as e:
logging.error(f"内存不足: {e}")
return {"result": False, "error": f"内存不足: {e}"}
except Exception as e:
logging.error(f"未知错误: {e}")
return {"result": False, "error": f"未知错误: {e}"}
# 示例调用
# if __name__ == '__main__':
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# logging.info("开始对比图片")
#
# image1_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\DeviceA-ShareScreen.png'
# image2_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\同屏前-无纸化设备B界面截屏.png'
#
# if not os.path.exists(image1_path):
# logging.error(f"图片 {image1_path} 不存在")
# exit(1)
# if not os.path.exists(image2_path):
# logging.error(f"图片 {image2_path} 不存在")
# exit(1)
#
# # 对比两张截图的相似度
# if compare_images_feature_matching(image1_path, image2_path):
# logging.info("图片相同")
# else:
# logging.error("图片不同")
# 计算直方图相似度函数
import cv2
import numpy
from PIL import Image
import logging
import os
def calculate(image1, image2):
"""
计算两张图片的直方图重合度
通过将图片转换为BGR格式,并计算每张图片的蓝色通道直方图,然后比较这两个直方图的重合度来评估图片的相似度
参数:
image1: 第一张图片,应为RGB格式
image2: 第二张图片,应为RGB格式
返回值:
返回两张图片直方图的重合度,范围在0到1之间,1表示完全重合,即图片高度相似
"""
image1 = cv2.cvtColor(numpy.asarray(image1), cv2.COLOR_RGB2BGR)
image2 = cv2.cvtColor(numpy.asarray(image2), cv2.COLOR_RGB2BGR)
hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
# 计算直方图的重合度
degree = 0
for i in range(len(hist1)):
if hist1[i] != hist2[i]:
degree = degree + (1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
else:
degree = degree + 1
degree = degree / len(hist1)
return degree
# 图片相似性对比函数
def classify_hist_with_split(image1, image2, size=(256, 256)):
"""
根据两张图片的RGB直方图比较它们的相似性。
参数:
image1: 第一张图片的路径。
image2: 第二张图片的路径。
size: 将图片调整到的统一尺寸,默认为(256, 256)。
返回:
两张图片的相似度,值越小表示两张图片越相似。
"""
# 打开图片文件
image1 = Image.open(image1)
image2 = Image.open(image2)
# 将PIL图像转换为OpenCV格式(BGR)
image1 = cv2.cvtColor(numpy.asarray(image1), cv2.COLOR_RGB2BGR)
image2 = cv2.cvtColor(numpy.asarray(image2), cv2.COLOR_RGB2BGR)
# 调整图片尺寸,以确保比较是在相同尺寸下进行
image1 = cv2.resize(image1, size)
image2 = cv2.resize(image2, size)
# 分离图片的RGB通道
sub_image1 = cv2.split(image1)
sub_image2 = cv2.split(image2)
sub_data = 0
# 遍历每个通道,计算并累加相似度
for im1, im2 in zip(sub_image1, sub_image2):
sub_data += calculate(im1, im2)
# 计算平均相似度
sub_data = sub_data / 3
# 返回最终的相似度结果
return sub_data
# 示例调用
# if __name__ == '__main__':
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# logging.info("开始对比图片")
#
# image1_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\同屏后-无纸化设备A界面截屏.png'
# image2_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\同屏后-无纸化设备B界面截屏.png'
#
# if not os.path.exists(image1_path):
# logging.error(f"图片 {image1_path} 不存在")
# exit(1)
# if not os.path.exists(image2_path):
# logging.error(f"图片 {image2_path} 不存在")
# exit(1)
#
# # 对比两张截图的相似度
# result1 = classify_hist_with_split(image1_path, image2_path)
#
# # 确保 result1 是一个标量值
# if isinstance(result1, numpy.ndarray):
# result1 = result1.item()
#
# print("相似度为:" + "%.2f%%" % (result1 * 100))
# 检查输出路径是否有效函数
import cv2
import logging
import os
import shutil # 导入 shutil 模块以检查磁盘空间
def check_output_path(output_path):
"""
检查输出路径是否有效
如果输出目录不存在,则尝试创建它,并检查是否有写权限
参数:
output_path (str): 输出文件的路径
返回:
bool: 如果输出路径有效且可写,则返回True,否则返回False
"""
# 获取输出文件的目录部分
output_dir = os.path.dirname(output_path)
# 检查输出目录是否存在
if not os.path.exists(output_dir):
try:
# 尝试创建输出目录
os.makedirs(output_dir)
logging.info(f"创建目录: {output_dir}")
except Exception as e:
# 如果创建目录失败,记录错误信息并返回False
logging.error(f"无法创建目录 {output_dir}: {e}")
return False
# 检查文件权限
if not os.access(output_dir, os.W_OK):
# 如果没有写权限,记录错误信息并返回False
logging.error(f"没有写权限: {output_dir}")
return False
# 如果一切正常,返回True
return True
# 捕获RTSP流并保存为图像文件函数
def capture_frame_from_rtsp(rtsp_url, file_name, output_path=None):
"""
从RTSP流中捕获一帧并保存为图像文件。
参数:
- rtsp_url: RTSP流的URL。
- file_name: 保存图像文件的名称。
- output_path: 保存图像文件的路径,默认为None,如果未提供则使用默认路径。
返回:
- 成功捕获并保存帧时返回True,否则返回False。
"""
try:
# 验证输入参数
if not rtsp_url:
logging.error("RTSP URL 为空")
return False
# 获取当前脚本所在的根目录
script_dir = os.path.dirname(os.path.abspath(__file__))
root_dir = os.path.dirname(script_dir)
# 构建默认输出路径
if output_path is None:
output_path = os.path.join(root_dir, "reports", "imgs", "Exhibit_Inspect", "Control_Manage", file_name)
# 检查并创建输出目录
if not check_output_path(output_path):
return False
# 打开RTSP流
cap = cv2.VideoCapture(rtsp_url)
if not cap.isOpened():
logging.error("无法打开RTSP流")
return False
# 尝试多次读取帧以确保获取有效帧
for _ in range(5): # 尝试读取5次
ret, frame = cap.read()
if ret and frame is not None:
break
else:
logging.error("无法从RTSP流中读取有效帧")
cap.release()
return False
# 确认帧不为空
if frame is None or frame.size == 0:
logging.error("捕获到的帧为空")
cap.release()
return False
# 检查帧的形状和类型
logging.info(f"捕获到的帧尺寸: {frame.shape}, 数据类型: {frame.dtype}")
# 尝试保存帧为图像文件
success = False
try:
# 使用 cv2.imencode 保存图像到内存中,再写入文件
_, img_encoded = cv2.imencode('.png', frame)
with open(output_path, 'wb') as f:
f.write(img_encoded.tobytes())
success = True
except Exception as e:
logging.error(f"无法保存帧到 {output_path}: {e}")
logging.error(f"检查路径是否存在: {os.path.exists(os.path.dirname(output_path))}")
logging.error(f"检查路径是否可写: {os.access(os.path.dirname(output_path), os.W_OK)}")
# 使用 shutil.disk_usage 检查磁盘空间
try:
total, used, free = shutil.disk_usage(os.path.dirname(output_path))
logging.error(f"检查磁盘空间: {free // (2 ** 20)} MB available")
except Exception as e:
logging.error(f"无法检查磁盘空间: {e}")
if success:
logging.info(f"帧已保存到 {output_path}")
else:
logging.error(f"帧保存失败")
# 释放资源
cap.release()
return success
except Exception as e:
logging.error(f"捕获帧时发生错误: {e}", exc_info=True)
return False
# 中控屏灯光控制函数
def light_control(app_drive):
"""
控制灯光的函数。
该函数通过Appium驱动定位并点击应用中的灯光控制按钮,以开启不同区域的灯光。
参数:
- app_drive: Appium驱动实例,用于与移动应用交互。
"""
# 开启所有区域灯光
# 定位【接待区】灯光
light_reception_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ImageView[4]")
sleep(2)
logging.info("尝试定位【接待区】按钮元素,并点击按钮")
click_with_retry(light_reception_button)
sleep(2)
# 定位【指挥中心】灯光
light_command_center_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ImageView[7]")
sleep(2)
logging.info("尝试定位【指挥中心】按钮元素,并点击按钮")
click_with_retry(light_command_center_button)
sleep(2)
# 定位【影音室】灯光
light_audio_room_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ImageView[6]")
sleep(2)
logging.info("尝试定位【影音室】按钮元素,并点击按钮")
click_with_retry(light_audio_room_button)
sleep(2)
# 定位【会议室】灯光
light_meeting_room_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ImageView[12]")
sleep(2)
logging.info("尝试定位【会议室】按钮元素,并点击按钮")
click_with_retry(light_meeting_room_button)
sleep(2)
# 定位【会商区】灯光
light_meeting_area_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ImageView[10]")
sleep(2)
logging.info("尝试定位【会商区】按钮元素,并点击按钮")
click_with_retry(light_meeting_area_button)
sleep(2)
# 定位【培训室】灯光
light_training_room_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.ImageView[14]")
sleep(2)
logging.info("尝试定位【培训室】按钮元素,并点击按钮")
click_with_retry(light_training_room_button)
sleep(2)
# 中控屏窗帘控制函数
def curtain_control(app_drive, wd):
"""
控制窗帘的上升和下降,并捕获相应状态的截图。
参数:
app_drive: Appium驱动对象,用于操作App。
wd: WebDriver对象,用于捕获屏幕截图。
此函数无返回值。
"""
# 所有窗帘全部上升
logging.info("尝试定位所有【窗帘上升】按钮元素,并点击按钮")
# 上升按钮的定位
curtain_up_locator = ['11', '13', '16', '17', '24']
for i in curtain_up_locator:
curtain_up_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
f"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[2]/android.widget.Button[{i}]")
click_with_retry(curtain_up_button)
sleep(2)
INFO("请检查窗帘上升状态是否正常")
# 截图获取当前中控屏软件窗帘上升的界面
get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "curtain_up")
sleep(30)
# # 测试报告中补充窗帘上升的截图
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "curtain_rtsp_up")
# 通过rtsp流获取当前窗帘的上升效果图
curtain_rtsp_url = "rtsp://admin:huawei@123@192.168.4.18/LiveMedia/ch1/Media2"
logging.info("开始捕获RTSP流中的帧")
if capture_frame_from_rtsp(curtain_rtsp_url, "curtain_rtsp_up.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 所有窗帘全部下降
logging.info("尝试定位所有【窗帘下降】按钮元素,并点击按钮")
# 下降按钮的定位
curtain_down_locator = ['14', '21', '22', '23', '27']
for i in curtain_down_locator:
curtain_down_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
f"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[2]/android.widget.Button[{i}]")
click_with_retry(curtain_down_button)
sleep(2)
sleep(30)
INFO("请检查窗帘下降状态是否正常")
get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "curtain_down")
# 截图获取当前中控屏软件窗帘上升的界面
# 测试报告中补充窗帘下降的截图
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "curtain_rtsp_down")
logging.info("开始捕获RTSP流中的帧")
if capture_frame_from_rtsp(curtain_rtsp_url, "curtain_rtsp_down.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 中控屏空调控制函数
def air_condition_control(app_drive, wd):
"""
控制空调的打开与关闭,并检查其状态显示。
参数:
- app_drive: Appium驱动对象,用于操作移动端应用。
- wd: WebDriver对象,用于捕获屏幕截图。
此函数不返回任何值。
"""
# 点击【打开空调】按钮
logging.info("尝试定位【打开空调】按钮元素,并点击按钮")
open_air_conditioner_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[3]/android.widget.Button[27]")
click_with_retry(open_air_conditioner_button)
sleep(20)
# 这是空调开启的状态显示
INFO("请检查空调开启的状态是否正常")
get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "air_condition_on")
sleep(2)
# 点击【关闭空调】按钮
logging.info("尝试定位【关闭空调】按钮元素,并点击按钮")
close_air_conditioner_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[3]/android.widget.Button[27]")
click_with_retry(close_air_conditioner_button)
sleep(20)
# 这是空调关闭的状态显示
INFO("请检查空调关闭的状态是否正常")
get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "air_condition_off")
sleep(2)
# 中控屏信息发布控制函数
def information_control(app_drive, wd):
"""
控制信息展示和捕获RTSP流中的帧。
参数:
- app_drive: Appium驱动实例,用于操作移动应用。
- wd: WebDriver实例,用于操作网页。
此函数依次选择不同的内容进行播放,捕获RTSP流中的帧,并记录屏幕状态。
"""
# 选择生日快乐内容播放
brithday_information_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[6]/android.widget.Button[12]")
click_with_retry(brithday_information_button)
logging.info("选择生日快乐内容播放")
sleep(5)
# 这是生日快乐主题内容发布
INFO("请检查中控屏软件信息发布界面是否正常")
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_brithday_on")
information_rtsp_url = "rtsp://admin:huawei@123@192.168.4.19/LiveMedia/ch1/Media2" # 替换为你的RTSP流地址
logging.info("开始捕获RTSP流中的帧")
if capture_frame_from_rtsp(information_rtsp_url, "information_brithday_on.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 选择展厅空间结构内容播放
information_space_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[6]/android.widget.Button[13]")
click_with_retry(information_space_button)
logging.info("选择展厅空间结构内容播放")
sleep(5)
# 这是展厅空间结构内容发布
INFO("请检查中控屏软件信息发布界面是否正常显示")
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_space_on")
if capture_frame_from_rtsp(information_rtsp_url, "information_space_on.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 选择欢迎领导发布内容播放
meeting_information_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[6]/android.widget.Button[15]")
click_with_retry(meeting_information_button)
logging.info("选择会议发布内容播放")
sleep(5)
# 这是会议欢迎主题内容发布
INFO("请检查中控屏软件信息发布界面是否正常")
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_meeting_on")
logging.info("开始捕获RTSP流中的帧")
if capture_frame_from_rtsp(information_rtsp_url, "information_meeting_on.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 点击信息发布关闭按钮
INFO("点击信息发布关闭按钮")
information_close_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[6]/android.widget.Button[19]")
click_with_retry(information_close_button)
# 这是信发屏已关闭的界面
INFO("请检查中控屏软件信息发布界面是否正常显示为关闭")
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_off")
if capture_frame_from_rtsp(information_rtsp_url, "information_off.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 中控屏音乐控制函数
def music_control(app_drive, wd):
"""
控制音乐播放的函数,包括播放和停止音乐。
:param app_drive: Appium驱动对象,用于操作App。
:param wd: WebDriver对象,用于浏览器自动化操作。
"""
# 点击【播放音乐】
logging.info("尝试定位【播放音乐】按钮元素,并点击按钮")
play_music_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[5]/android.widget.Button[24]")
click_with_retry(play_music_button)
sleep(2)
sleep(5)
# 这是音乐开启播放后的界面显示
INFO("请检查中控屏软件打开音乐播放后的界面状态显示")
get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "music_on")
# 点击【关闭播放音乐】
logging.info("尝试定位【关闭播放音乐】按钮元素,并点击按钮")
close_play_music_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[5]/android.widget.Button[24]")
click_with_retry(close_play_music_button)
sleep(5)
# 这是音乐关闭播放后的界面显示
INFO("请检查中控屏软件关闭音乐播放后的界面状态显示")
get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "music_off")
# 中控屏控制函数
def command_centre_control(rtsp_url, app_drive, wd):
"""
控制指挥中心大屏的开启和关闭,并 capture RTSP 流的一帧作为日志。
参数:
- rtsp_url: RTSP 流的 URL。
- app_drive: Appium 驱动对象,用于操作移动应用。
- wd: WebDriver 对象,用于执行 Selenium 相关操作。
此函数会尝试打开指挥中心大屏,capture 并记录大屏开启和关闭时的监控视频帧。
"""
# 打开指挥中心大屏
open_center_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[4]/android.widget.Button[13]")
click_with_retry(open_center_button)
sleep(10)
# 这是指挥大屏开启的监控视频显示
INFO("请检查指挥大屏开启的监控视频状态是否正常")
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "command_screen_on")
# 从rtsp流中截取一帧保存为图片
logging.info("开始捕获RTSP流中的帧")
if capture_frame_from_rtsp(rtsp_url, "command_screen_on.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# 这是指挥大屏关闭的监控视频显示
INFO("请检查指挥大屏关闭的监控视频状态是否正常")
SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "command_screen_down")
# 关闭指挥中心大屏幕
logging.info("尝试定位【关闭指挥中心控制】按钮元素,并点击按钮")
close_center_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout[4]/android.widget.Button[13]")
click_with_retry(close_center_button)
sleep(10)
# 从rtsp流中截取一帧保存为图片
logging.info("开始捕获RTSP流中的帧")
if capture_frame_from_rtsp(rtsp_url,
"command_screen_down.png"):
logging.info("帧捕获成功")
else:
logging.error("帧捕获失败")
# app设备初始化adb连接函数
import subprocess
from venv import logger
def app_init(device_ip, port=5555, adb_path=r"C:\Program Files\androidsdk\platform-tools\adb.exe"):
"""
初始化 ADB 连接并检查设备状态
参数:
- device_ip: 设备 IP 地址
- port: ADB 端口号,默认 5555
- adb_path: ADB 工具的可执行文件完整路径(可选)
例如: r"C:\platform-tools\adb.exe"
如果不提供,则默认使用系统PATH中的adb
返回:
- True: 连接成功且设备可用
- False: 连接失败或设备不可用
- adb_path: r"C:\Program Files\androidsdk\androidsdk\platform-tools\adb.exe"(公司电脑adb路径)
- adb_path:r"C:\Program Files\androidsdk\platform-tools\adb.exe"(自动化虚拟机adb路径)
- adb_path:r"E:\androidsdk\androidsdk\platform-tools"(云桌面adb路径)
"""
# 标记初始化过程
INFO("---------- ADB 连接初始化开始 ----------")
INFO(f"目标设备: {device_ip}:{port}")
INFO(f"使用ADB路径: {adb_path if adb_path else '系统PATH'}")
try:
# 构建设备地址
device_address = f"{device_ip}:{port}"
# 确定adb命令
adb_cmd = [adb_path] if adb_path else ['adb']
# 1. 验证ADB可用性
try:
version_result = subprocess.run(
[*adb_cmd, '--version'],
capture_output=True,
text=True,
check=True
)
logging.debug(f"ADB版本信息:\n{version_result.stdout}")
except Exception as e:
logging.error(f"ADB验证失败,请检查路径是否正确: {e}")
if not adb_path:
logging.error("建议:请通过adb_path参数指定完整的adb.exe路径")
return False
# 2. 尝试连接设备
INFO(f"尝试 ADB 连接到设备: {device_address}")
connect_result = subprocess.run(
[*adb_cmd, 'connect', device_address],
capture_output=True,
text=True,
check=False # 改为不抛出异常,手动处理
)
INFO(f"ADB连接命令返回码: {connect_result.returncode}")
INFO(f"ADB连接命令输出: {connect_result.stdout.strip()}")
if connect_result.stderr.strip():
logging.warning(f"ADB连接错误输出: {connect_result.stderr.strip()}")
# 3. 检查设备状态
INFO("获取设备列表验证连接状态...")
devices_result = subprocess.run(
[*adb_cmd, 'devices'],
capture_output=True,
text=True,
check=True
)
logging.debug(f"完整 adb devices 输出:\n{devices_result.stdout}")
# 解析设备列表
devices = devices_result.stdout.strip().split('\n')[1:] # 去掉标题行
INFO(f"找到 {len(devices)} 个设备")
for device in devices:
if not device.strip():
continue
try:
parts = device.split()
if len(parts) >= 2: # 处理可能存在的额外列
ip, status = parts[0], parts[1]
else:
logging.warning(f"设备信息格式异常: {device}")
continue
INFO(f"检测到设备: IP={ip}, 状态={status}")
if ip == device_address:
if status == 'device':
INFO(f"设备 {device_address} 已连接并可用")
return True
elif status == 'offline':
logging.warning(f"设备 {device_address} 处于离线状态,请检查:")
logging.warning("1. 设备网络连接")
logging.warning("2. 设备是否开启ADB调试")
logging.warning("3. 防火墙是否阻止了5555端口")
return False
elif status == 'unauthorized':
logging.error(f"设备 {device_address} 未授权调试:")
logging.error("请在设备上检查并允许USB调试授权提示")
return False
else:
logging.warning(f"设备 {device_address} 处于未知状态: {status}")
return False
except Exception as e:
logging.warning(f"设备信息解析失败: {device}, 错误: {e}")
# 未找到目标设备
logging.error(f"设备 {device_address} 未在设备列表中找到,可能原因:")
logging.error("1. 设备IP地址不正确")
logging.error("2. 设备未开启网络ADB调试")
logging.error("3. 设备未与测试机在同一网络")
logging.error("4. 防火墙阻止了连接")
return False
except subprocess.CalledProcessError as e:
logging.error(f"ADB命令执行失败:")
logging.error(f"命令: {' '.join(e.cmd)}")
logging.error(f"返回码: {e.returncode}")
logging.error(f"错误输出: {e.stderr.strip()}")
return False
except Exception as e:
logging.error(f"发生未知错误: {str(e)}", exc_info=True)
return False
# if __name__ == '__main__':
# # 使用示例:
# device_ip = '192.168.5.156'
#
# # 方式1:使用系统PATH中的adb
# # app_init(device_ip)
#
# # 方式2:指定adb路径
# app_init(
# device_ip,
# )
# app设备退出adb连接函数
def app_quit(device_ip, port=5555, adb_path=r"C:\Program Files\androidsdk\androidsdk\platform-tools\adb.exe"):
"""
断开 ADB 连接并释放资源
参数:
- device_ip: 设备 IP 地址
- port: ADB 端口号,默认 5555
- adb_path: ADB 工具的可执行文件完整路径(可选)
例如: r"C:\platform-tools\adb.exe"
如果不提供,则默认使用系统PATH中的adb
"""
try:
# 标记断开过程
INFO("---------- ADB 连接断开开始 ----------")
INFO(f"目标设备: {device_ip}:{port}")
INFO(f"使用ADB路径: {adb_path if adb_path else '系统PATH'}")
# 构建设备地址
device_address = f"{device_ip}:{port}"
# 确定adb命令
adb_cmd = [adb_path] if adb_path else ['adb']
# 尝试断开连接
INFO(f"尝试断开 ADB 连接: {device_address}")
disconnect_result = subprocess.run(
[*adb_cmd, 'disconnect', device_address],
capture_output=True,
text=True,
check=False # 避免因断开失败而抛出异常
)
INFO(f"ADB断开命令返回码: {disconnect_result.returncode}")
INFO(f"ADB断开命令输出: {disconnect_result.stdout.strip()}")
if disconnect_result.stderr.strip():
logging.warning(f"ADB断开错误输出: {disconnect_result.stderr.strip()}")
# 验证是否断开成功
if "disconnected" in disconnect_result.stdout:
INFO(f"ADB 连接已成功断开: {device_address}")
return True
else:
logging.warning(f"ADB 连接断开失败: {device_address}")
return False
except Exception as e:
logging.error(f"断开ADB连接时发生错误: {str(e)}", exc_info=True)
return False
# app截屏函数
def get_screenshot_with_retry(wd,app_drive, module_name, function_name, step_name, max_retries=3, retry_delay=5):
"""
使用重试机制获取并保存截图。
参数:
app_drive: 实现了get_screenshot_as_file方法的对象,用于获取截图。
module_name: 用于构造保存截图的目录名称。
function_name: 用于构造截图的目录名称。
setp_name:用于构造截图文件的名称
max_retries: 最大重试次数,默认为3次。
retry_delay: 重试间隔时间,默认为5秒。
返回值:
无。如果多次尝试截图失败,则抛出异常。
"""
# 获取当前文件的绝对路径
current_file_path = os.path.abspath(__file__)
# 获取当前文件的父级目录
parent_dir = os.path.dirname(current_file_path)
# 构造目标目录路径
target_dir = os.path.join(parent_dir, '..', 'reports', 'imgs', module_name, function_name)
# 确保目标目录存在,如果不存在则创建
os.makedirs(target_dir, exist_ok=True)
# 构造文件路径
file_path = os.path.join(target_dir, f"{step_name}.png")
#截屏
SELENIUM_LOG_SCREEN(wd, "75%", module_name, function_name, f"{step_name}")
# 使用循环实现重试机制
for _ in range(max_retries):
try:
# 尝试保存截图
app_drive.get_screenshot_as_file(file_path)
# 如果成功,记录日志并退出函数
logging.info(f"截图保存成功: {file_path}")
return
except Exception as e:
# 如果失败,记录日志并等待重试
logging.warning(f"截图失败,重试中... ({e})")
sleep(retry_delay)
# 如果多次尝试均失败,则抛出异常
raise Exception(f"多次尝试截图失败: {file_path}")
# app查找元素函数
def find_element_with_retry(app_driver, by, value, max_retries=3, retry_delay=5):
"""
使用重试机制查找元素。
在WebDriver(driver)中通过给定的查找方式(by)和值(value)来查找页面元素。
如果在指定的最大重试次数(max_retries)内仍然找不到元素,则抛出异常。
每次重试之间会有指定的延迟时间(retry_delay)。
参数:
- driver: WebDriver实例,用于执行查找操作。
- by: 查找元素的方式,如XPath、ID等。
- value: 元素的值,根据'by'参数指定的查找方式对应的具体值。
- max_retries: 最大重试次数,默认为3次。
- retry_delay: 每次重试之间的延迟时间,默认为5秒。
返回:
- 返回找到的元素。
异常:
- 如果超过最大重试次数仍未找到元素,则抛出异常。
"""
for _ in range(max_retries):
try:
# 尝试查找元素,如果成功则立即返回元素
return app_driver.find_element(by, value)
except Exception as e:
# 如果查找元素失败,记录日志并等待一段时间后重试
logging.warning(f"查找元素失败,重试中... ({e})")
sleep(retry_delay)
# 如果达到最大重试次数仍未找到元素,则抛出异常
raise Exception(f"多次尝试查找元素失败: {by}={value}")
# app点击事件函数
def click_with_retry(element, max_retries=3, retry_delay=5):
"""
点击元素的函数,带有重试机制。
参数:
element (obj): 要点击的元素对象。
max_retries (int): 最大重试次数,默认为3次。
retry_delay (int): 每次重试之间的延迟时间,默认为5秒。
异常:
如果超过最大重试次数仍未成功点击元素,则抛出异常。
"""
# 尝试点击元素,直到达到最大重试次数
for _ in range(max_retries):
try:
# 尝试点击元素
element.click()
# 如果点击成功,记录日志并退出函数
logging.info(f"点击元素成功: {element}")
return
except Exception as e:
# 如果点击失败,记录日志并等待下一次重试
logging.warning(f"点击元素失败,重试中... ({e})")
sleep(retry_delay)
# 如果所有重试都失败,抛出异常
raise Exception(f"多次尝试点击元素失败: {element}")
\ No newline at end of file
import csv
import glob
import re
import urllib
from selenium.webdriver.chrome.service import Service
import json
import hmac
import hashlib
import base64
import psutil
import time
import subprocess
import win32api
import win32con
import win32gui
import logging
from hytest import *
from selenium import webdriver
from selenium.common import ElementNotInteractableException
from selenium.webdriver.common.keys import Keys
from urllib.parse import urlencode
from datetime import datetime
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
# import datetime
# 获取当前脚本的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取当前脚本的父目录
parent_dir = os.path.dirname(current_dir)
logging.info(parent_dir)
# 添加路径
sys.path.append(current_dir)
# 配置日志记录器,仅输出到控制台
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler()
]
)
# 浏览器初始化函数
def browser_init(login_type):
"""
初始化浏览器设置和实例。
此函数旨在创建并配置一个Chrome浏览器实例,包括设置Chrome选项以排除不必要的日志,
并尝试打开特定的登录页面。任何初始化过程中出现的错误都会被捕获并记录。
参数:
login_type (str): 指定登录类型,根据不同的登录类型选择不同的URL。
返回:
"""
# 标记初始化过程的开始
INFO("'----------' 正在初始化浏览器 '----------'")
# 创建Chrome选项实例,用于配置浏览器行为
options = webdriver.ChromeOptions()
# 添加实验性选项,排除某些命令行开关以减少输出日志
options.add_experimental_option('excludeSwitches', ['enable-Logging'])
# 忽略证书错误,允许在本地主机上运行时不安全
options.add_argument('--ignore-certificate-errors')
# 禁用自动化控制特征检测,避免被网站识别为自动化流量
options.add_argument('--disable-blink-features=AutomationControlled')
# 允许不安全的本地主机运行,通常用于开发和测试环境
options.add_argument('--allow-insecure-localhost')
# 使用webdriver_manager自动下载并管理chromedriver
# service = ChromeService(ChromeDriverManager().install())
# 使用备用的ChromeDriver下载源
# service = Service(ChromeDriverManager().install())
# 手动指定ChromeDriver的路径
# chromedriver下载地址:https://googlechromelabs.github.io/chrome-for-testing/
# 自动化运行服务器的chromedriver路径:
# 拯救者电脑
# service = Service(r'C:\Users\29194\AppData\Local\Programs\Python\Python310\Scripts\chromedriver.exe')
# EDY电脑
# service = Service(r'C:\Users\EDY\AppData\Local\Programs\Python\Python310\Scripts\chromedriver.exe')
# 云电脑
service = Service(r'E:\Python\Scripts\chromedriver.exe')
# 自动化虚拟机
# service = Service(r'C:\Program Files\Python310\Scripts\chromedriver.exe')
# 尝试创建WebDriver实例并执行初始化操作
try:
# 创建WebDriver实例
wd = webdriver.Chrome(service=service, options=options)
# 设置隐式等待时间为10秒,以允许元素加载
wd.implicitly_wait(60)
# 获取登录URL
login_url = get_login_url_from_config(login_type)
# 打开对应类型的登录页面
wd.get(login_url)
# 最大化浏览器窗口
wd.maximize_window()
# 将WebDriver实例存储在全局存储器中,以便后续使用
GSTORE['wd'] = wd
# 标记初始化过程完成
INFO("'----------' 浏览器初始化完成 '----------'")
except Exception as e:
# 捕获并记录初始化过程中的任何异常
logging.error(f"浏览器初始化失败:{e}")
# 定义调整屏幕分辨率,仅虚拟机电脑环境跑定时任务需要使用。
def change_resolution(width, height):
"""
调整屏幕分辨率
Args:
width (int): 目标分辨率宽度
height (int): 目标分辨率高度
Returns:
bool: 分辨率是否成功调整
"""
try:
# 获取当前显示设备信息
device = win32api.EnumDisplayDevices(None, 0)
current_device = device.DeviceName
logging.info(f"当前显示设备: {current_device}")
dm = win32api.EnumDisplaySettings(current_device, win32con.ENUM_CURRENT_SETTINGS)
current_res = f"{dm.PelsWidth}x{dm.PelsHeight}"
target_res = f"{width}x{height}"
logging.info(f"当前分辨率: {current_res}, 目标分辨率: {target_res}")
if dm.PelsWidth == width and dm.PelsHeight == height:
logging.warning("目标分辨率与当前分辨率相同,无需调整")
return True
# 设置新分辨率
dm.PelsWidth = width
dm.PelsHeight = height
logging.info(f"尝试设置分辨率为: {target_res}")
# 测试模式验证
test_result = win32api.ChangeDisplaySettings(dm, win32con.CDS_TEST)
if test_result != win32con.DISP_CHANGE_SUCCESSFUL:
error_msg = {
win32con.DISP_CHANGE_BADDUALVIEW: "不支持的双视图模式",
win32con.DISP_CHANGE_BADFLAGS: "无效的标志",
win32con.DISP_CHANGE_BADMODE: "不支持的图形模式",
win32con.DISP_CHANGE_BADPARAM: "无效的参数",
win32con.DISP_CHANGE_FAILED: "显示驱动失败",
}.get(test_result, f"未知错误代码: {test_result}")
logging.error(f"分辨率测试失败: {error_msg}")
return False
# 实际应用更改
apply_result = win32api.ChangeDisplaySettings(dm, 0)
if apply_result != win32con.DISP_CHANGE_SUCCESSFUL:
logging.error("分辨率应用失败")
return False
logging.info("分辨率修改成功")
return True
except Exception as e:
logging.exception(f"分辨率调整过程中发生异常: {str(e)}")
return False
# 从配置项config中获取登录URL
def get_login_url_from_config(login_type):
"""
从配置文件中读取登录URL。
参数:
login_type (str): 指定登录类型,根据不同的登录类型选择不同的URL。
返回:
str: 对应的登录URL。
"""
# 检查 login_type 是否为空或 None
if not login_type:
raise ValueError("login_type 不能为空")
# 获取当前脚本的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
# 构建配置文件的绝对路径,指向与 Base 同级的 config.json
config_path = os.path.join(os.path.dirname(current_dir), 'config.json')
# 规范化路径,防止路径遍历攻击
config_path = os.path.normpath(config_path)
# 记录配置文件路径以便调试,对路径进行脱敏处理
logging.info(f"配置文件路径: {os.path.basename(config_path)}")
# 检查文件是否存在
if not os.path.exists(config_path):
# 如果配置文件不存在,则抛出异常
raise FileNotFoundError(f"配置文件 {config_path} 不存在")
try:
# 读取配置文件
with open(config_path, 'r', encoding='utf-8') as config_file:
# 将配置文件内容解析为 JSON 格式
config = json.load(config_file)
# 根据 login_type 获取对应的登录 URL
login_url = config.get(login_type)
# 记录正在打开的登录页面类型和 URL
logging.info(f"正在打开 {login_type} 的登录页面:{login_url}")
except IOError as e:
# 处理文件读取异常
raise IOError(f"读取配置文件失败: {e}")
except json.JSONDecodeError as e:
# 处理 JSON 解析异常
raise json.JSONDecodeError(f"解析配置文件失败: {e}")
# 检查是否成功获取到 URL
if not login_url:
# 如果未找到对应的 URL,则抛出异常
raise ValueError(f"未找到对应的 URL 配置项: {login_type}")
print("正在打开登录页面:" + login_url)
# 返回登录 URL
return login_url
# 管理员登录函数
def user_login(username, password):
"""
管理员登录函数。
该函数通过模拟用户输入用户名、密码和验证码,并点击登录按钮,以实现管理员登录。
"""
# 获取webdriver实例
wd = GSTORE['wd']
# 打印用户名输入信息
INFO(f"输入用户名:{username}")
# 向用户名输入框发送用户名
safe_send_keys((By.XPATH, "//input[@placeholder='请输入账号或手机号或邮箱号']"), f'{username}', wd)
# 打印密码输入信息
INFO(f"输入密码:{password}")
# 向密码输入框发送密码
safe_send_keys((By.XPATH, "//input[@placeholder='请输入密码']"), f"{password}", wd)
# 打印验证码输入信息
INFO("输入验证码:csba")
# 向验证码输入框发送验证码
safe_send_keys((By.XPATH, "//input[@placeholder='请输入图形验证码']"), "csba", wd)
# 隐式等待5秒,以确保验证码被正确处理
wd.implicitly_wait(5)
# 打印登录按钮点击信息
INFO("点击登录按钮")
# 点击登录按钮
safe_click((By.XPATH, "//input[@value='登 录']"), wd)
# 进入预定后台函数
def enter_the_backend():
"""
进入后台系统界面。
该函数通过模拟点击操作,找到并点击后台系统入口,以进入后台界面。
"""
# 记录进入后台系统的操作信息
INFO("进入后台")
# 获取webdriver对象,用于后续的页面元素操作
wd = GSTORE['wd']
# 执行点击操作,通过XPath定位到后台系统入口图标并点击
safe_click((By.XPATH, "//img[@title='后台系统']"), wd)
# 输入框输入值函数
def safe_send_keys(element_locator, value, wd):
"""
安全地向网页元素发送键值。
该函数尝试在指定时间内找到指定的网页元素,如果找到并且元素可见,将先清除元素内容,然后发送指定的键值。
如果在指定时间内未能找到元素或元素不可点击,则捕获相应的异常并打印错误信息。
参数:
element_locator (tuple): 用于定位网页元素的策略和定位器的元组,例如(By.ID, 'element_id')。
value (str): 要发送到元素的键值字符串。
wd: WebDriver实例,用于与浏览器交互。
异常处理:
- TimeoutException: 如果元素在指定时间内未被找到或不可点击。
- NoSuchElementException: 如果元素不存在。
- ElementNotInteractableException: 如果元素存在但不可交互。
"""
try:
# 等待元素在指定时间内可见
element = WebDriverWait(wd, 20).until(EC.visibility_of_element_located(element_locator))
element.clear() # 清除元素的当前值
element.send_keys(value) # 向元素发送指定的键值
except TimeoutException:
# 如果元素在指定时间内未被找到或不可点击,打印超时异常信息
INFO(f"TimeoutException: Element {element_locator} not found or not clickable within 20 seconds.")
except NoSuchElementException:
# 如果元素不存在,打印相应异常信息
INFO(f"NoSuchElementException: Element {element_locator} not found.")
except ElementNotInteractableException:
# 如果元素不可交互,打印相应异常信息
INFO(f"ElementNotInteractableException: Element {element_locator} is not interactable.")
# 点击按钮函数
def safe_click(element_locator, wd):
"""
对其定位器指定的元素执行安全单击。
此函数尝试以处理潜在异常的方式单击元素。
它等待元素可见并可单击,然后再尝试单击它。
如果该元素在20秒内无法点击,或者它不存在,
或者不可交互,它会捕获相应的异常并记录一条信息性消息。
参数:
-element_locator:要单击的元素的定位器,指定为元组。
-wd:用于查找元素并与之交互的WebDriver实例。
"""
try:
# Wait up to 20 seconds for the element to be visible
element = WebDriverWait(wd, 20).until(EC.visibility_of_element_located(element_locator))
# Attempt to click the element
element.click()
except TimeoutException:
# Log a message if the element is not found or not clickable within 20 seconds
INFO(f"TimeoutException: Element {element_locator} not found or not clickable within 20 seconds.")
except NoSuchElementException:
# Log a message if the element is not found
INFO(f"NoSuchElementException: Element {element_locator} not found.")
except ElementNotInteractableException:
# Log a message if the element is not interactable
INFO(f"ElementNotInteractableException: Element {element_locator} is not interactable.")
from time import sleep
from selenium.webdriver.common.by import By
# 议题输入和上传议题文件函数
def issue_send_and_upload(wd, issue_num, issue_name):
"""
输入议题名称以及上传议题文件。
参数:
wd: WebDriver实例,用于操作浏览器
issue_num: 需要上传的议题文件数量
issue_name: 会议议题的名称
异常:
FileNotFoundError: 当议题文件不存在时抛出
Exception: 其他上传过程中可能出现的异常
"""
try:
# 获取当前文件的绝对路径
current_file_path = os.path.abspath(__file__)
# 获取当前文件的父级目录(即Base目录)
parent_dir = os.path.dirname(current_file_path)
# 获取项目根目录(Base目录的上一级)
project_root = os.path.dirname(parent_dir)
# 构建议题文件相对路径(相对于项目根目录)
issue_file_path = [
os.path.join(project_root, "reports", "issue_file", "5.164Scan 安全报告.pdf"),
os.path.join(project_root, "reports", "issue_file", "议题图片.png")
]
# 打印议题文件路径
INFO(f"项目根目录: {project_root}")
INFO(f"议题目录: {os.path.join(project_root, 'reports', 'issue_file')}")
# 打印并输入议题名称
INFO(f"输入议题名称:{issue_name}")
safe_send_keys((By.XPATH, f"(//input[@placeholder='请输入会议议题'])[1]"), f"{issue_name}", wd)
# 点击【上传文件】按钮以开始上传议题文件
INFO("点击【上传文件】按钮")
safe_click((By.XPATH, f"(//div[@class='topicsHandleButton uploadFile'][contains(text(),'上传文件(0)')])[1]"),
wd)
sleep(2)
# 遍历每个议题文件进行上传
uploaded_files = 0
for i in range(issue_num):
try:
# 检查文件是否存在
if not os.path.exists(issue_file_path[i]):
raise FileNotFoundError(f"议题文件不存在: {issue_file_path[i]}")
# 定位【选择文件】按钮
upload_button = WebDriverWait(wd, 10).until(
EC.presence_of_element_located((By.XPATH, '//*[@id="global-uploader-btn"]/input'))
)
# 选择议题文件上传
upload_button.send_keys(issue_file_path[i])
INFO(f"正在上传文件: {os.path.basename(issue_file_path[i])}")
uploaded_files += 1
# 等待文件上传完成
sleep(15)
except FileNotFoundError as e:
logging.error(f"文件上传失败: {str(e)}")
continue
except Exception as e:
logging.error(f"上传文件时发生异常: {str(e)}")
continue
if uploaded_files == 0:
raise Exception("未成功上传任何议题文件")
# 截取上传完成后的屏幕日志
SELENIUM_LOG_SCREEN(wd, "75%", "Exhibit_Inspect", "Meeting_Message", "添加议题文件")
sleep(5)
# 点击【确定】按钮完成上传
INFO("点击【确定】按钮完成上传")
safe_click(
(By.XPATH,
"//div[@aria-label='会议文件上传']//div[@class='el-dialog__footer']//div//span[contains(text(),'确定')]"),
wd
)
sleep(2)
except Exception as e:
logging.error(f"议题上传过程中出现错误: {str(e)}")
raise # 重新抛出异常让调用者处理
# 清除输入框函数
def input_clear(element_locator, wd):
"""
清空输入框中的文本。
该函数通过元素定位器找到指定的输入框,并尝试清空其内容。它使用显式等待来确保元素在尝试清除之前是可见的。
参数:
- element_locator: 用于定位输入框的元素定位器,通常是一个元组,包含定位方法和定位表达式。
- wd: WebDriver实例,用于与浏览器交互。
异常处理:
- TimeoutException: 如果在指定时间内元素不可见,则捕获此异常并打印超时异常消息。
- NoSuchElementException: 如果找不到指定的元素,则捕获此异常并打印未找到元素的消息。
- ElementNotInteractableException: 如果元素不可操作(例如,元素不可见或不可点击),则捕获此异常并打印相应消息。
"""
try:
# 等待元素可见,并在可见后清空输入框。
input_element = WebDriverWait(wd, 20).until(EC.visibility_of_element_located(element_locator))
input_element.clear()
except TimeoutException:
# 如果元素在20秒内不可见,打印超时异常消息。
print(f"TimeoutException: Element {element_locator} not found or not clickable within 20 seconds.")
except NoSuchElementException:
# 如果找不到元素,打印未找到元素的消息。
print(f"NoSuchElementException: Element {element_locator} not found.")
except ElementNotInteractableException:
# 如果元素不可操作,打印元素不可操作的消息。
print(f"ElementNotInteractableException: Element {element_locator} is not interactable.")
# 键盘输入函数,例如【回车】键等操作
def send_keyboard(element_locator, wd):
"""
向指定元素发送键盘事件。
该函数尝试找到页面上的一个元素,并向其发送RETURN键点击事件。它使用显式等待来确保元素在尝试交互之前是可见的。
如果在指定时间内元素不可见、不存在或不可交互,则捕获相应的异常并打印错误消息。
参数:
- element_locator: 用于定位元素的元组,格式为(by, value)。例如,(By.ID, 'element_id')。
- wd: WebDriver实例,用于与浏览器进行交互。
异常处理:
- TimeoutException: 如果元素在20秒内不可见,则捕获此异常并打印超时错误消息。
- NoSuchElementException: 如果找不到指定的元素,则捕获此异常并打印未找到元素的错误消息。
- ElementNotInteractableException: 如果元素不可交互(例如,被遮挡或不可点击),则捕获此异常并打印相应错误消息。
"""
try:
# 等待元素可见,并在可见后向其发送RETURN键点击事件。
element = WebDriverWait(wd, 20).until(EC.visibility_of_element_located(element_locator))
element.send_keys(Keys.RETURN)
except TimeoutException:
# 如果元素在指定时间内不可见,打印超时错误消息。
print(f"TimeoutException: Element {element_locator} not found or not clickable within 20 seconds.")
except NoSuchElementException:
# 如果找不到指定的元素,打印未找到元素的错误消息。
print(f"NoSuchElementException: Element {element_locator} not found.")
except ElementNotInteractableException:
# 如果元素不可交互,打印不可交互错误消息。
print(f"ElementNotInteractableException: Element {element_locator} is not interactable.")
# 获取常规提示文本函数,会同步进行截屏操作
def get_notify_text(wd,element_locator,module_name = None,function_name = None,file_name = None):
"""
获取通知文本信息。
该函数通过WebDriver等待特定的元素出现,并截取其文本内容作为通知信息。此外,它还负责在获取通知文本后进行屏幕截图,
以便于后续的调试或记录需要。
参数:
wd (WebDriver): 由上层传入的WebDriver对象,用于操作浏览器。
element_locator: 用于定位元素的定位器。
module_name: 模块名称,用于日志记录。
function_name: 函数名称,用于日志记录。
file_name: 屏幕截图的名称,用于日志记录。
返回:
str: 提取的通知文本信息。如果未能提取到信息或发生异常,则返回None。
"""
try:
# 使用WebDriverWait等待元素出现,并获取其文本内容
notify_text = WebDriverWait(wd, 20).until(
EC.presence_of_element_located(element_locator)
).text
if module_name or function_name or file_name:
# 如果 module_name, function_name, 或 file_name 有值,则使用这些参数进行屏幕截图
SELENIUM_LOG_SCREEN(wd, "50%", module_name, function_name, file_name)
else:
# 如果 module_name, function_name, 和 file_name 都没有值,则仅使用 wd 和 "50%" 进行屏幕截图
SELENIUM_LOG_SCREEN(wd, "50%")
return notify_text
except Exception as e:
# 当发生异常时,记录异常信息
INFO(f"Exception occurred: {e}")
# 获取列表的查询结果文本函数
def elment_get_text(element_locator, wd):
"""
获取页面元素的文本。
该函数通过显式等待的方式,确保页面元素在20秒内可见并获取其文本。
如果在规定时间内元素不可见、不存在或不可交互,则会捕获相应的异常并打印错误信息。
参数:
- element_locator: 用于定位页面元素的元组,通常包含定位方式和定位值。
- wd: WebDriver对象,用于与浏览器进行交互。
返回:
- element_text: 页面元素的文本。如果发生异常,则返回None。
"""
try:
# 使用WebDriverWait等待页面元素在20秒内可见,并获取其文本。
element_text = WebDriverWait(wd, 20).until(EC.visibility_of_element_located(element_locator)).text
return element_text
except TimeoutException:
# 如果超过20秒元素仍未可见,则捕获TimeoutException异常并打印错误信息。
print(f"TimeoutException: Element {element_locator} not found or not clickable within 20 seconds.")
except NoSuchElementException:
# 如果页面上不存在该元素,则捕获NoSuchElementException异常并打印错误信息。
print(f"NoSuchElementException: Element {element_locator} not found.")
except ElementNotInteractableException:
# 如果元素存在但不可交互(例如被遮挡),则捕获ElementNotInteractableException异常并打印错误信息。
print(f"ElementNotInteractableException: Element {element_locator} is not interactable.")
# 读取csv文件进行数据驱动函数
def read_csv_data(csv_file_path):
"""
读取CSV文件中的数据,并将其转换为一个包含字典的列表,每个字典代表一行测试用例数据。
参数:
csv_file_path (str): CSV文件的路径。
返回:
list: 包含字典的列表,每个字典包含测试用例的名称和参数。
"""
# 打开CSV文件,使用只读模式,确保兼容性并处理编码
with open(csv_file_path, mode='r', newline='', encoding='utf-8') as file:
# 创建CSV阅读器
reader = csv.reader(file)
# 读取表头,为后续数据解析做准备
headers = next(reader)
ddt_cases = []
# 遍历CSV文件中的每一行数据
for row in reader:
# 将每一行数据转换为字典,其中包含测试用例的名称和参数
case = {
'name': row[0],
'para': row[1:]
}
# 将转换后的测试用例数据添加到列表中
ddt_cases.append(case)
# 日志记录:CSV文件已读取
INFO("CSV文件已读取")
# 返回包含所有测试用例数据的列表
return ddt_cases
# 读取测试用例xlsx文件中的JSON数据进行数据驱动函数1
import json
from hytest import INFO
def read_xlsx_data(xlsx_file_path, sheet_name=None, case_type=None):
"""
从指定的Excel文件中读取测试用例数据,支持按工作表名称和用例类型筛选。
参数:
xlsx_file_path (str): Excel文件的路径。
sheet_name (str, optional): 指定要读取的工作表名称。若为None,则读取所有工作表。
case_type (str, optional): 指定要筛选的用例类型(对应“功能类别”列)。若为None,则不进行筛选。
返回:
list: 解析后的JSON格式测试用例列表。
异常:
FileNotFoundError: 当指定的文件不存在时抛出。
Exception: 其他未预期的错误将被记录并重新抛出。
"""
try:
INFO(f"尝试打开文件路径: {xlsx_file_path}")
if not os.path.exists(xlsx_file_path):
raise FileNotFoundError(f"文件未找到: {xlsx_file_path}")
workbook = openpyxl.load_workbook(xlsx_file_path, read_only=True)
INFO("XLSX文件成功打开")
# 确定需要处理的工作表列表
sheets_to_process = [workbook[sheet_name]] if sheet_name else workbook.worksheets
ddt_cases = []
for sheet in sheets_to_process:
INFO(f"正在处理工作表: {sheet.title}")
headers = [cell.value for cell in sheet[3]]
INFO(f"表头列名: {headers}")
# 查找必需的列索引
try:
json_index = headers.index('JSON')
category_index = headers.index('功能类别')
except ValueError as e:
INFO(f"工作表 {sheet.title} 缺少必要列,跳过: {e}")
continue
# 遍历数据行并解析JSON
for row_num, row in enumerate(sheet.iter_rows(min_row=4, values_only=True), start=4):
json_data = row[json_index]
if not json_data or not json_data.strip():
INFO(f"行 {row_num} 的JSON数据为空,跳过")
continue
try:
# 打印原始JSON数据用于调试
INFO(f"行 {row_num} 原始JSON数据: {json_data}")
parsed_json = json.loads(json_data)
category = row[category_index]
# 根据case_type筛选用例
if case_type and category != case_type:
continue
ddt_cases.append(parsed_json)
INFO(f"行 {row_num} JSON解析成功: {parsed_json}")
except json.JSONDecodeError as e:
logging.error(f"行 {row_num} 的JSON数据解析失败: {e}\n数据内容: {json_data}")
except Exception as e:
logging.error(f"行 {row_num} 处理时发生未知错误: {e}")
INFO(f"XLSX文件处理完成,共找到 {len(ddt_cases)} 条用例")
return ddt_cases
except Exception as e:
logging.error(f"处理文件时出错: {e}")
raise
finally:
if workbook:
workbook.close()
import openpyxl
def clear_columns_in_xlsx(xlsx_file_path, sheet_name=None, columns_to_clear=None):
"""
将XLSX文件中指定列的单元格值设置为空。
参数:
xlsx_file_path (str): XLSX文件的路径。
sheet_name (str, optional): 工作表的名称。如果未指定,则使用活动工作表。
columns_to_clear (list, optional): 需要清空的列名列表。如果未指定,则不执行任何操作。
返回:
"""
if not columns_to_clear:
logging.warning("未指定需要清空的列名列表,函数将不执行任何操作。")
return
try:
# 打开XLSX文件
workbook = openpyxl.load_workbook(xlsx_file_path)
except FileNotFoundError:
raise FileNotFoundError(f"文件未找到: {xlsx_file_path}")
except Exception as e:
raise Exception(f"无法打开文件: {e}")
# 选择工作表
if sheet_name:
try:
sheet = workbook[sheet_name]
except KeyError:
raise KeyError(f"工作表未找到: {sheet_name}")
else:
sheet = workbook.active
# 读取表头,从第三行开始
headers = [cell.value for cell in sheet[3]]
# 打印表头列名
logging.info(f"表头列名: {headers}")
# 找到需要清空的列的索引
column_indices_to_clear = [headers.index(column) for column in columns_to_clear if column in headers]
if not column_indices_to_clear:
logging.warning("指定的列名在表头中未找到,函数将不执行任何操作。")
return
# 遍历XLSX文件中的每一行数据,从第四行开始
for row in sheet.iter_rows(min_row=4):
for col_index in column_indices_to_clear:
row[col_index].value = None # 将单元格值设置为空
# 保存修改后的文件
try:
workbook.save(xlsx_file_path)
logging.info(f"文件 {xlsx_file_path} 已保存,指定列的单元格值已清空。")
except Exception as e:
logging.error(f"保存文件时出错: {e}")
# if __name__ == "__main__":
# # 示例调用
# clear_columns_in_xlsx(r'D:\GithubData\自动化\ubains-module-test\预定系统\测试数据\长安大学测试用例.xlsx',
# sheet_name='会议审批', columns_to_clear=['测试结果', '测试频次', '日志截图'])
# 获取当前进程的 CPU 占用率函数
def get_cpu_usage(interval=1):
"""
获取当前进程的 CPU 占用率。
:param interval: 计算 CPU 使用率的时间间隔(秒)
:return: 当前进程的 CPU 占用率(百分比)
"""
try:
process = psutil.Process(os.getpid())
cpu_usage = process.cpu_percent(interval=interval)
if isinstance(cpu_usage, (int, float)):
return cpu_usage
else:
logging.error("CPU 使用率数据类型不正确")
return None
except psutil.NoSuchProcess:
logging.error("进程不存在")
return None
except psutil.AccessDenied:
logging.error("权限不足")
return None
except Exception as e:
logging.error(f"未知错误: {e}")
return None
# 删除目录下的图片文件函数
def delete_images_in_directory(directory):
"""
删除指定目录下的所有图片文件。
该函数会遍历指定的目录,寻找并删除所有扩展名在image_extensions列表中的图片文件。
参数:
directory (str): 图片文件所在的目录路径。
返回:
无返回值。
"""
# 指定要删除的图片文件扩展名
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff']
# 遍历目录中的所有文件
for filename in os.listdir(directory):
# 获取文件的完整路径
file_path = os.path.join(directory, filename)
# 检查文件是否为图片文件
if any(filename.lower().endswith(ext) for ext in image_extensions):
try:
# 删除文件
os.remove(file_path)
print(f"已删除文件: {file_path}")
except Exception as e:
print(f"删除文件 {file_path} 时出错: {e}")
# 判断非法字符函数
def is_valid_password(password):
"""
验证密码的有效性。
有效密码需满足以下条件:
1. 必须是一个字符串。
2. 长度至少为11个字符。
3. 必须包含至少一个小写字母、一个大写字母和一个数字。
4. 不得包含连续3位相同的字符。
5. 不得包含连续3位连续的字符(如123, abc)。
参数:
password (str): 待验证的密码。
返回:
bool: 如果密码有效则返回True,否则返回False。
"""
try:
# 基本类型检查
if not isinstance(password, str):
raise ValueError("Password must be a string")
# 检查长度,密码至少需要11个字符
if len(password) < 11:
return False
# 使用正则表达式检查密码是否包含大小写字母和数字
if not re.match(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{11,}$', password):
return False
# 检查连续3位及以上不重复且不连续组合
for i in range(len(password) - 2):
# 检查是否有连续3位相同
if password[i] == password[i + 1] == password[i + 2]:
return False
# 检查是否有连续3位是连续字符(如123, abc)
if abs(ord(password[i + 1]) - ord(password[i])) == 1 and abs(ord(password[i + 2]) - ord(password[i + 1])) == 1:
return False
return True
except Exception as e:
logging.error(f"An error occurred: {e}")
return False
# 退出浏览器并释放资源函数
def browser_quit():
"""
退出浏览器并释放资源。
该函数从全局存储中获取浏览器驱动实例,并调用其quit方法来关闭浏览器并释放相关资源。
"""
# 清除浏览器
INFO("清除浏览器")
# 从全局存储中获取浏览器驱动实例
wd = GSTORE['wd']
# 调用浏览器驱动实例的quit方法,关闭浏览器并释放资源
wd.quit()
import os
import glob
import logging
# from datetime import datetime
# 获取最新的HTML报告文件,并拼接网页访问连接函数
def get_latest_report_file(report_dir, base_url):
"""
获取指定目录下最新的HTML报告文件,并返回带有基础URL的完整路径。
:param report_dir: 报告文件所在的目录
:param base_url: 基础URL
:return: 最新的HTML报告文件的完整URL,如果没有找到则返回None
"""
# 记录调用此函数的日志
logging.info("开始调用get_latest_report_file函数获取报告文件")
# 确保报告目录存在
if not os.path.exists(report_dir):
logging.error(f"报告目录 {report_dir} 不存在。")
return None
# 获取指定目录下所有符合模式的HTML报告文件
report_files = glob.glob(os.path.join(report_dir, 'report_*.html'))
# 打印找到的文件列表
logging.debug(f"找到的报告文件: {report_files}")
# 如果没有找到报告文件,记录警告信息并返回None
if not report_files:
logging.warning("在指定目录中没有找到报告文件。")
return None
# 找到最新修改的报告文件
latest_file = max(report_files, key=os.path.getmtime)
# 获取最新报告文件的最后修改时间
last_modified_time = datetime.fromtimestamp(os.path.getmtime(latest_file)).strftime('%Y-%m-%d %H:%M:%S')
# 记录最新报告文件的信息
logging.info(f"最新报告文件: {latest_file}, 最后修改时间: {last_modified_time}")
# 将文件路径转换为相对于基础URL的相对路径
relative_path = os.path.relpath(latest_file, report_dir)
# 生成完整的URL
full_url = f"{base_url}/{relative_path}".replace("\\", "/")
# 返回完整的URL
return full_url
# 钉钉群机器人消息发送函数
def dingding_send_message(latest_report, title, mobile, ding_type):
"""
发送钉钉机器人消息
参考接口文档:https://open.dingtalk.com/document/orgapp/custom-robots-send-group-messages#title-7fs-kgs-36x
:param latest_report: 测试报告链接
:param title: 消息标题
:param mobile: 需要@的手机号列表
:param ding_type: 钉钉机器人类型,用于选择不同的 Webhook URL 和密钥
"""
# 记录调用此函数的日志
logging.info("开始构建并发送钉钉机器人消息")
# 钉钉机器人的 Webhook URL 和密钥(正式环境)
# webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=b0eea0bbf097ce3badb4c832d2cd0267a50486f395ec8beca6e2042102bb295b'
# secret = 'SEC928b11659c5fd6476cfa2042edbf56da876abf759289f7e4d3c671fb9a81bf43'
# 钉钉机器人的 Webhook URL 和密钥(测试环境)
if ding_type == '标准版巡检':
webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=7fbf40798cad98b1b5db55ff844ba376b1816e80c5777e6f47ae1d9165dacbb4'
secret = 'SEC610498ed6261ae2df1d071d0880aaa70abf5e67efe47f75a809c1f2314e0dbd6'
elif ding_type == '展厅巡检':
webhook_url = 'https://oapi.dingtalk.com/robot/send?access_token=061b6e9b1ae436f356cfda7fe19b6e58e46b62670046a78bd3a4d869118c612d'
secret = 'SEC93212bd880aad638cc0df2b28a72ef4fdf6651cacb8a6a4bc71dcf09705d458d'
# 生成时间戳
timestamp = str(round(time.time() * 1000))
# 生成签名
secret_enc = secret.encode('utf-8')
string_to_sign = f'{timestamp}\n{secret}'
string_to_sign_enc = string_to_sign.encode('utf-8')
hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
sign = urllib.parse.quote_plus(base64.b64encode(hmac_code))
# 构建最终的 Webhook URL
params = {
'access_token': webhook_url.split('=')[1],
'timestamp': timestamp,
'sign': sign
}
encoded_params = urllib.parse.urlencode(params)
final_webhook_url = f'https://oapi.dingtalk.com/robot/send?{encoded_params}'
# 记录最终的 Webhook URL
logging.info(f"钉钉机器人Webhook URL: {final_webhook_url}")
# 调用测试结果获取函数
browser_init("兰州中石化项目测试环境")
wd = GSTORE['wd']
# print(latest_report)
test_result = get_test_result(latest_report, wd)
browser_quit()
# 构建消息体
headers = {'Content-Type': 'application/json'}
message = {
'msgtype': 'link',
'link': {
'title': title,
'messageUrl': latest_report,
'text': f"通过:{test_result['pass_percent']}" + f"失败:{test_result['fail_percent']}" + f"异常:{test_result['exception_percent']}",
},
"at": {
"atMobiles": [mobile],
"isAtAll": True
}
}
try:
# 发送 POST 请求
response = requests.post(final_webhook_url, data=json.dumps(message), headers=headers)
# 检查响应状态码
if response.status_code == 200:
logging.info('消息发送成功!')
logging.info(f'响应内容: {response.text}')
else:
logging.error(f'消息发送失败,状态码: {response.status_code}')
logging.error(f'响应内容: {response.text}')
except requests.exceptions.RequestException as e:
logging.error(f'请求异常: {e}')
# 运行自动化测试函数,并调用获取测试报告链接和钉钉机器人消息发送函数
def run_automation_test(report_title, report_url_prefix, test_case , ding_type):
"""
运行自动化测试并生成报告。
参数:
- report_title: 报告的标题
- report_url_prefix: 报告URL的前缀
- test_case: 测试用例脚本执行的标签名
- ding_type: 钉钉通知的类型,备用参数,当前代码中未使用
"""
# 记录测试开始的日志
logging.info("开始自动化测试...")
# 构建运行测试命令
command = [
'hytest',
'--report_title', report_title,
'--report_url_prefix', report_url_prefix,
'--tag', test_case
]
# 记录将要执行的命令日志
logging.info(f"执行命令: {' '.join(command)}")
try:
# 执行测试命令并获取结果
result = subprocess.run(command, capture_output=True, text=True, check=True)
# 记录命令的标准输出和错误输出
logging.debug(f"命令标准输出: {result.stdout}")
logging.debug(f"命令错误输出: {result.stderr}")
except subprocess.CalledProcessError as e:
# 处理子进程调用失败的异常
logging.error(f"命令执行失败,返回码 {e.returncode}: {e.output}")
except OSError as e:
# 处理操作系统相关的异常
logging.error(f"发生操作系统错误: {e}")
finally:
# 无论测试是否成功,都记录测试结束的日志
logging.info("自动化测试完成。")
# 调用回调函数处理后续操作
get_reportfile_send_dingding(f"{report_title}", report_url_prefix, ding_type)
# 定义一个函数,用于获取最新的报告文件,并返回其URL,并调用钉钉消息发送函数
def get_reportfile_send_dingding(report_title, report_url_prefix, ding_type):
"""
获取最新的报告文件并通过钉钉发送报告链接。
参数:
report_title (str): 报告的标题。
report_url_prefix (str): 报告URL的前缀。
ding_type (str): 钉钉消息的类型。
返回:
"""
# print(GSTORE['case_pass'])
try:
# 获取报告文件所在的目录
report_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..','reports')
# 获取基础URLs
base_url = report_url_prefix
# 获取最新的报告文件
latest_report = get_latest_report_file(report_dir, base_url)
# 如果找到了最新的报告文件,则发送报告链接到钉钉
if latest_report:
logging.info(f"最新报告文件URL: {latest_report}")
try:
# 记录调用钉钉消息通知函数的日志
logging.info("开始调用钉钉消息通知函数")
# 调用钉钉发送消息接口进行推送测试报告链接
dingding_send_message(latest_report, report_title, "13724387318", ding_type)
# 记录钉钉消息通知函数调用成功的日志
logging.info("钉钉消息通知函数调用成功")
except Exception as e:
# 记录钉钉消息通知函数调用失败的日志
logging.error(f"钉钉消息通知函数调用失败: {e}")
else:
# 记录没有找到报告文件的日志
logging.warning("没有找到报告文件以发送。")
except subprocess.CalledProcessError as e:
# 处理子进程调用失败的异常
logging.error(f"命令执行失败,返回码 {e.returncode}: {e.output}")
except OSError as e:
# 处理操作系统相关的异常
logging.error(f"发生操作系统错误: {e}")
finally:
# 无论是否成功,都记录测试结束的日志
logging.info("自动化测试完成。")
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException, NoSuchElementException
# 点击并拖拽函数
def single_click_and_drag(source_element_locator, target_element_locator, wd):
"""
实现元素从source_element单击后拖拽到target_element的功能
:param wd: WebDriver实例
:param source_element_locator: 拖拽起始元素的定位器
:param target_element_locator: 拖拽目标元素的定位器
"""
try:
# 等待页面完全加载
sleep(3)
# 找到源元素和目标元素
source_element = WebDriverWait(wd, 120).until(EC.element_to_be_clickable(source_element_locator))
target_element = WebDriverWait(wd, 120).until(EC.element_to_be_clickable(target_element_locator))
# 使用ActionChains执行单击并拖拽操作
actions = ActionChains(wd)
actions.click_and_hold(source_element) # 单击并按住源元素
actions.move_to_element(target_element) # 移动到目标元素
actions.release() # 释放鼠标
actions.perform()
logging.info("单击并拖拽操作成功")
except TimeoutException as e:
logging.error(f"元素查找超时: {e}")
except NoSuchElementException as e:
logging.error(f"元素未找到: {e}")
except Exception as e:
logging.error(f"发生未知错误: {e}")
# 获取check.txt文件并解析指定信息函数
import requests
import os
import chardet
from urllib3.exceptions import InsecureRequestWarning
# 禁用 InsecureRequestWarning 警告
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
def fetch_and_parse_check_txt(url, save_path, extract_info):
"""
获取check.txt文件并解析指定信息
:param url: check.txt文件的URL
:param save_path: 文件保存路径
:param extract_info: 需要提取的信息列表,例如 ['[m]ysql', '[r]edis', '[f]dfs_storaged', '[f]dfs_tracker', '[e]mqx', 'ubains-meeting-api-1.0-SNAPSHOT.jar', 'ubains-meeting-inner-api-1.0-SNAPSHOT.jar', 'uwsgi']
:return: 提取的信息字典
"""
try:
# 发送HTTPS请求获取文件内容
response = requests.get(url, verify=False) # verify=False 忽略SSL证书验证,生产环境不推荐
response.raise_for_status() # 如果响应状态码不是200,抛出异常
# 检测文件编码
detected_encoding = chardet.detect(response.content)['encoding']
logging.info(f"检测到的编码: {detected_encoding}")
# 如果检测到的编码为空或不准确,可以手动指定编码
if not detected_encoding or detected_encoding == 'ascii':
detected_encoding = 'utf-8' # 假设文件编码为 utf-8
# 将响应内容解码为字符串
content = response.content.decode(detected_encoding)
# 将文件内容保存到指定目录
with open(save_path, 'w', encoding='utf-8') as file:
file.write(content)
# 解析文件内容
parsed_info = {}
for line in content.split('\n'):
for info in extract_info:
if info in line and info not in parsed_info:
service_name = info
service_status = line.split(info, 1)[1].strip()
parsed_info[service_name] = service_status
break # 找到后跳出内层循环,继续处理下一行
return parsed_info
except requests.exceptions.RequestException as e:
logging.exception(f"请求错误: {e}")
return None
# 检查服务状态函数
import telnetlib
def check_service_status(host, port):
"""
检查服务状态。
通过尝试连接到指定的主机和端口来检查服务是否可用。
参数:
- host: 服务所在的主机地址。
- port: 服务监听的端口。
返回值:
"""
try:
# 创建Telnet对象并连接到服务器
tn = telnetlib.Telnet(host, port)
INFO(f"成功连接到 {host}:{port}")
# 可以在这里发送命令或读取响应
# 例如,读取服务器的欢迎信息
response = tn.read_until(b"\n", timeout=5)
INFO(f"服务器响应: {response.decode('ascii')}")
# 关闭连接
tn.close()
except Exception as e:
INFO(f"连接失败: {e}")
# 设置腾讯会议会议号为全局变量函数
def set_tx_meeting_id(element_locator, wd):
"""
设置腾讯会议ID到全局存储。
该函数通过元素定位器获取腾讯会议ID,并将其存储在全局变量GSTORE中,
以便在其他地方访问和使用该ID。
参数:
- element_locator: 用于定位腾讯会议ID元素的定位器。
- wd: WebDriver实例,用于与网页交互。
返回值:
无返回值。
"""
# 获取腾讯会议ID文本
tx_meeting_id = elment_get_text(element_locator, wd)
# 将腾讯会议ID存储在全局存储中
GSTORE['tx_meeting_id'] = tx_meeting_id
# 获取腾讯会议会议号函数
def get_tx_meeting_id():
"""
获取腾讯会议ID。
从全局存储器GSTORE中获取预先存储的腾讯会议ID。这个函数没有输入参数,直接返回存储的会议ID。
Returns:
str: 腾讯会议ID。
"""
return GSTORE.get('tx_meeting_id')
# 云喇叭设备注册函数
def voice_device_register(app_id, app_secret, device_sn):
"""
注册语音设备。
向指定的API发送POST请求,以注册一个语音设备。需要提供应用的ID和密钥,以及设备的序列号。
参数:
app_id (str): 应用的唯一标识符。
app_secret (str): 应用的秘密密钥。
device_sn (str): 设备的序列号。
返回:
无直接返回值,但会记录请求结果到日志。
"""
# 构建请求体,包含注册所需的必要信息
body = {
"app_id": app_id,
"app_secret": app_secret,
"device_sn": device_sn
}
# 设置请求头,指定内容类型为JSON
headers = {
"Content-Type": "application/json"
}
try:
# 发送POST请求到注册URL,请求体序列化为JSON格式
response = requests.post("https://wdev.wmj.com.cn/deviceApi/register", headers=headers, data=json.dumps(body))
# 检查HTTP响应状态码,确保请求成功
response.raise_for_status()
# 解析响应的JSON内容
response_json = response.json()
# 记录成功的请求日志
logging.info("请求成功: %s", response_json)
except requests.exceptions.RequestException as e:
# 处理请求异常,记录错误日志
logging.error("请求失败: %s", e)
except ValueError as e:
# 处理解析响应异常,记录错误日志
logging.error("解析响应失败: %s", e)
# 云喇叭设备设置函数
import requests
def cloud_voice_setting(app_id, app_secret, device_sn):
"""
设置云语音功能。
:param app_id: 应用ID
:param app_secret: 应用密钥
:param device_sn: 设备序列号
:return: 服务器响应结果
"""
url = "https://wdev.wmj.com.cn/deviceApi/send"
# 写死的data参数
data = {
"cmd_type": "setting",
"info": {
"volume": 10, # 0-9,音量由小到大,默认为中间值
"speed": 2, # 0-9,语速由慢到快,默认为中间值正常语速
"tone": 4, # 0-9,语调由低到高,默认为中间值正常语调
"speaker": 0 # 0为女生,支持中英文
}
}
# 构建请求体
payload = {
"app_id": app_id,
"app_secret": app_secret,
"device_sn": device_sn,
"data": data
}
# 发送POST请求
try:
response = requests.post(url, json=payload)
response.raise_for_status() # 如果响应状态码不是200,抛出异常
logging.info(response.json()) # 打印响应的JSON数据
except requests.exceptions.RequestException as e:
logging.error({"status": "error", "message": str(e)})
# 示例调用
# if __name__ == "__main__":
# app_id = os.getenv("APP_ID", "a98a124c6c3252f6612fc544a0d0fa79")
# app_secret = os.getenv("APP_SECRET", "88bc1ec4eba624f47b2200a4ce8c3852")
# device_sn = os.getenv("DEVICE_SN", "W703BB44444")
# cloud_voice_setting(app_id, app_secret, device_sn)
# 云喇叭设备播放函数
def play_cloud_voice(app_id, app_secret, device_sn):
"""
播放云语音功能。
本函数通过发送HTTP POST请求,触发远程语音设备播放指定的语音内容。
参数:
- app_id: 应用ID,用于标识应用。
- app_secret: 应用密钥,用于验证应用的身份。
- device_sn: 设备序列号,用于标识具体的语音设备。
"""
# 注册设备
voice_device_register(app_id, app_secret, device_sn)
sleep(5) # 可以考虑使用异步编程或非阻塞的方式替代
# 设置云喇叭的音量以及语速参数
cloud_voice_setting(app_id, app_secret, device_sn)
sleep(10)
# 定义请求URL
url = os.getenv("CLOUD_VOICE_API_URL", "https://wdev.wmj.com.cn/deviceApi/send")
# 构建请求体,包括应用ID、应用密钥、设备序列号和语音播放指令
body = {
"app_id": app_id,
"app_secret": app_secret,
"device_sn": device_sn,
"data": {
"cmd_type": "play",
"info": {
"tts": "一、二、三、四、五、六、七、八、九、十 一、二、三、四、五、六、七、八、九、十 一、二、三、四、五、六、七、八、九、十、一、二、三、四、五、六、七、八、九、十",
"inner": 10, # wifi版特有
"volume": 5 # 4G版本1-7,wifi版1-10
}
}
}
# 设置请求头,指定内容类型为JSON
headers = {
"Content-Type": "application/json"
}
try:
# 发送POST请求
response = requests.post(url, headers=headers, data=json.dumps(body))
# 根据响应状态码判断请求是否成功
if response.status_code == 200:
logging.info(f"请求成功: {response.json()}")
else:
logging.error(f"请求失败: 状态码 {response.status_code}, 响应内容 {response.text}")
except requests.exceptions.RequestException as e:
logging.error(f"请求过程中发生异常: {e}")
except json.JSONDecodeError as e:
logging.error(f"JSON解析失败: {e}")
except Exception as e:
logging.error(f"发生未知异常: {e}")
# # 示例调用
# if __name__ == "__main__":
# app_id = os.getenv("APP_ID", "a98a124c6c3252f6612fc544a0d0fa79")
# app_secret = os.getenv("APP_SECRET", "88bc1ec4eba624f47b2200a4ce8c3852")
# device_sn = os.getenv("DEVICE_SN", "W703BB44444")
# play_cloud_voice(app_id, app_secret, device_sn)
# 获取测试报告通过率等参数的函数
import logging
import re
from selenium.webdriver.common.by import By
def get_test_result(latest_report, wd):
"""
获取测试结果页面的通过率、失败率和异常率
:param latest_report: 测试结果页面的URL
:param wd: WebDriver实例,用于访问和操作网页
:return: 包含通过率、失败率和异常率的字典
"""
# 初始化测试结果字典
test_result = {
"pass_percent": "",
"fail_percent": "",
"exception_percent": "",
"blocking_rate": ""
}
# 访问测试结果页面
wd.get(latest_report)
sleep(5)
# 点击简略显示
safe_click((By.XPATH,"//div[@id='display_mode']"), wd)
sleep(5)
# 定义一个函数来获取和解析百分比
def get_percentage(selector, wd):
text = elment_get_text(selector, wd)
logging.info(f"获取的文本:{text}")
match = re.search(r'(\d+(\.\d+)?)%', text)
if match:
return match.group(0)
else:
logging.error(f"未找到百分比匹配项,文本内容: {text}")
return "0"
# 获取通过率
pass_percent = get_percentage((By.CSS_SELECTOR, "div[class='result_barchart'] div:nth-child(1) span:nth-child(1)"), wd)
test_result["pass_percent"] = pass_percent
# 获取失败率
fail_percent = get_percentage(
(By.CSS_SELECTOR, "body > div.main_section > div.result > div > div:nth-child(2) > span"), wd)
test_result["fail_percent"] = fail_percent
# 获取异常率
exception_percent = get_percentage(
(By.CSS_SELECTOR, "body > div.main_section > div.result > div > div:nth-child(3) > span"), wd)
test_result["exception_percent"] = exception_percent
# 获取阻塞率
blocking_rate = get_percentage((By.CSS_SELECTOR, "div[class='result'] div:nth-child(4) span:nth-child(1)"), wd)
test_result["blocking_rate"] = blocking_rate
# 输出test_result
logging.info(test_result)
print(test_result)
sleep(5)
# 返回测试结果字典
return test_result
# if __name__ == "__main__":
# browser_init("展厅预定巡检")
# wd = GSTORE['wd']
# test_result = get_test_result("http://nat.ubainsyun.com:31134/report_20250217_094401.html",wd)
# print(test_result)
# 获取本机IP地址函数
import yaml
import logging
import socket
import subprocess
def get_local_ip():
"""
获取本机的局域网IP地址。
此函数通过尝试与外部网络通信来确定本机的局域网IP地址。它利用了UDP协议,
并连接到一个知名的公共IP地址和端口,以此来获取本机的IP地址信息。
Returns:
str: 本机的局域网IP地址。
"""
try:
# 创建一个UDP套接字
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 连接到一个公共的IP地址和端口
sock.connect(("8.8.8.8", 80))
# 获取本地IP地址
local_ip = sock.getsockname()[0]
finally:
# 确保在所有情况下关闭套接字
sock.close()
return local_ip
# 更新ngrok.cfg文件中的IP地址 函数
def update_ngrok_config(config_path, new_ip):
"""
更新ngrok配置文件中的IP地址
本函数尝试打开并解析ngrok配置文件,更新其中的IP地址,然后将更改保存回配置文件中
参数:
config_path (str): ngrok配置文件的路径
new_ip (str): 需要更新到配置文件中的新IP地址
返回:
"""
try:
# 打开并安全地加载ngrok配置文件
with open(config_path, 'r', encoding='utf-8') as file:
config = yaml.safe_load(file)
# 更新IP地址
config['tunnels']['nat1']['proto']['tcp'] = f"{new_ip}:80"
# 将更新后的配置安全地写回文件
with open(config_path, 'w', encoding='utf-8') as file:
yaml.safe_dump(config, file, default_flow_style=False, allow_unicode=True)
# 记录成功更新的日志信息
logging.info(f"ngrok.cfg 更新成功,新的IP地址为: {new_ip}")
except Exception as e:
# 记录更新过程中出现的错误
logging.error(f"更新ngrok.cfg文件时出错: {e}")
# 启动ngrok函数
def start_ngrok(ngrok_path, config_path):
"""
启动ngrok工具。
在尝试启动ngrok之前,此函数会先终止已运行的ngrok进程(如果有的话)。
然后使用指定的配置文件路径启动ngrok,并记录启动结果。
参数:
ngrok_path (str): ngrok可执行文件的路径。
config_path (str): ngrok配置文件的路径。
返回:
无返回值。
"""
try:
# 终止已运行的ngrok进程
kill_ngrok()
# 构建启动ngrok的命令
command = [ngrok_path, '-config', config_path, 'start', 'nat1']
# 使用构建的命令启动ngrok
subprocess.Popen(command, shell=True)
# 记录ngrok启动成功的信息
logging.info(f"ngrok 启动成功")
except Exception as e:
# 记录启动ngrok时发生的错误
logging.error(f"启动ngrok时出错: {e}")
# 停止ngrok进程函数
def kill_ngrok():
"""
尝试终止所有正在运行的ngrok进程。
"""
try:
# 使用 taskkill 命令终止所有 ngrok 进程
subprocess.run(['taskkill', '/F', '/IM', 'ngrok.exe'], check=True)
logging.info("终止所有 ngrok 进程成功")
except subprocess.CalledProcessError as e:
# 如果没有找到 ngrok 进程,记录相关信息
logging.info("没有找到 ngrok 进程")
except Exception as e:
# 如果终止 ngrok 进程时发生其他错误,记录错误信息
logging.error(f"终止 ngrok 进程时出错: {e}")
# if __name__ == '__main__':
# logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
#
# # 获取本机IP地址
# local_ip = get_local_ip()
# logging.info(f"本机IP地址: {local_ip}")
#
# # 更新ngrok.cfg文件
# ngrok_config_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\ngrok\ngrok-调试主机\ngrok.cfg'
# update_ngrok_config(ngrok_config_path, local_ip)
#
# # 启动ngrok
# ngrok_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\ngrok\ngrok-调试主机\ngrok.exe'
# start_ngrok(ngrok_path, ngrok_config_path)
# 字符串转换枚举类型函数
def get_by_enum(type_str):
"""
将字符串类型的定位器类型转换为 selenium.webdriver.common.by.By 枚举类型。
参数:
type_str (str): 定位器类型字符串,例如 'XPATH'。
返回:
selenium.webdriver.common.by.By: 对应的 By 枚举类型。
"""
# 将输入的定位器类型字符串转换为大写,以匹配 By 枚举类型的命名
type_str = type_str.upper()
# 根据输入的字符串类型返回对应的 By 枚举类型
if type_str == 'XPATH':
return By.XPATH
elif type_str == 'ID':
return By.ID
elif type_str == 'NAME':
return By.NAME
elif type_str == 'CLASS_NAME':
return By.CLASS_NAME
elif type_str == 'CSS_SELECTOR':
return By.CSS_SELECTOR
elif type_str == 'TAG_NAME':
return By.TAG_NAME
elif type_str == 'LINK_TEXT':
return By.LINK_TEXT
elif type_str == 'PARTIAL_LINK_TEXT':
return By.PARTIAL_LINK_TEXT
else:
# 如果输入的定位器类型字符串不匹配任何已知的 By 枚举类型,抛出 ValueError 异常
raise ValueError(f"未知的定位器类型: {type_str}")
# 获取当前时间并格式化为 'HH:MM' 格式的函数,用于会议预定使用
# import datetime
def get_current_time_formatted():
"""
获取当前时间并格式化为 'HH:MM' 格式,并选择最近的未来时间点(如 00:00, 00:15, 00:30, 00:45 等)。
返回:
str: 最近的未来时间点字符串,例如 '17:00'。
"""
# 获取当前时间
current_time = datetime.now()
current_time_formatted = current_time.strftime("%H:%M")
# 定义时间点列表
time_points = [
"00:00", "00:15", "00:30", "00:45",
"01:00", "01:15", "01:30", "01:45",
"02:00", "02:15", "02:30", "02:45",
"03:00", "03:15", "03:30", "03:45",
"04:00", "04:15", "04:30", "04:45",
"05:00", "05:15", "05:30", "05:45",
"06:00", "06:15", "06:30", "06:45",
"07:00", "07:15", "07:30", "07:45",
"08:00", "08:15", "08:30", "08:45",
"09:00", "09:15", "09:30", "09:45",
"10:00", "10:15", "10:30", "10:45",
"11:00", "11:15", "11:30", "11:45",
"12:00", "12:15", "12:30", "12:45",
"13:00", "13:15", "13:30", "13:45",
"14:00", "14:15", "14:30", "14:45",
"15:00", "15:15", "15:30", "15:45",
"16:00", "16:15", "16:30", "16:45",
"17:00", "17:15", "17:30", "17:45",
"18:00", "18:15", "18:30", "18:45",
"19:00", "19:15", "19:30", "19:45",
"20:00", "20:15", "20:30", "20:45",
"21:00", "21:15", "21:30", "21:45",
"22:00", "22:15", "22:30", "22:45",
"23:00", "23:15", "23:30", "23:45"
]
# 将当前时间转换为 datetime 对象
current_time_dt = datetime.strptime(current_time_formatted, "%H:%M")
# 初始化最近时间点和最小时间差
closest_time_point = None
min_time_diff = float('inf')
# 遍历时间点列表,找到最近的未来时间点
for time_point in time_points:
time_point_dt = datetime.strptime(time_point, "%H:%M")
# 如果时间点在当前时间之后
if time_point_dt > current_time_dt:
time_diff = (time_point_dt - current_time_dt).total_seconds()
if time_diff < min_time_diff:
min_time_diff = time_diff
closest_time_point = time_point
# 如果没有找到未来的时间点(即当前时间已经是最后一个时间点),则选择下一个天的最早时间点
if closest_time_point is None:
closest_time_point = time_points[0]
return closest_time_point
# 会议创建函数
def meeting_message(meeting_room_name, message_type, message_name, project_type, wd):
"""
会议室会议预定功能的实现。
该函数通过模拟用户交互来预定会议室会议。它首先搜索指定的会议室,然后填写会议信息,
包括会议名称和类型,最后选择会议时间并完成预定。
参数:
- meeting_room_name (str): 会议室名称,用于搜索指定的会议室。
- message_type (str): 会议类型,用于填写会议信息。
- message_name (str): 会议名称,用于填写会议信息。
- project_type (str): 项目类型,用于判断是否需要输入特定信息,如固定电话。
- wd: WebDriver实例,用于操作浏览器。
返回:
无返回值。
"""
print("到这了")
# 切换至会议室列表界面,开始新一轮的审批会议创建
safe_click((By.XPATH, "//li[@class='meeting_list_li']//span[contains(text(),'会议室列表')]"), wd)
# 先搜索会议室
safe_click((By.XPATH, "//i[@class='el-collapse-item__arrow el-icon-arrow-right']"), wd)
sleep(1)
safe_send_keys((By.XPATH, "//input[@placeholder='请输入会议室名称']"), meeting_room_name, wd)
INFO(f"搜索结果为:{meeting_room_name}")
# 点击【查询】按钮
safe_click((By.XPATH, "//span[contains(text(),'查询')]"), wd)
sleep(2)
# 点击【会议预定】按钮
safe_click((By.XPATH, "//span[@class='MeetingCityList_t_btn']"), wd)
sleep(2)
# 输入会议名称并勾选MessageType类型
safe_send_keys((By.XPATH, "//input[@placeholder='请输入会议名称']"), message_name, wd)
safe_click(
(By.XPATH, f"//div[@class='reserve_input']//span[@class='el-checkbox__label'][contains(text(),'{message_type}')]"), wd)
sleep(1)
# 判断是否为项目定制的预约界面
if project_type == "长安大学":
# 输入固定电话
safe_send_keys((By.XPATH,"//input[@placeholder='固定电话']"), "13724387311", wd)
# 选择会议时间,点击【快速预定】按钮
current_time = get_current_time_formatted()
print(f"获取当前的时间{current_time}")
safe_click((By.XPATH, f"//div[normalize-space()='{current_time}']"), wd)
sleep(1)
safe_click((By.XPATH, "//div[@class='header_Quick']"), wd)
sleep(2)
# 点击【确定】按钮
safe_click((By.XPATH, "//button[@type='button']//span[contains(text(),'预定')]"), wd)
sleep(2)
# 会议状态设置函数
def message_satus_control(message_name, message_type, control_type, wd):
"""
结束会议流程。
参数:
- message_name: 会议名称,用于搜索特定的会议。
- message_type: 会议类型,用于判断是否需要进行额外的确认操作。
- control_type: 控制类型,用于选择相应的会议控制操作。
- wd: WebDriver实例,用于操作浏览器。
该函数通过模拟用户交互来结束会议,包括搜索会议、点击相关按钮等操作。
"""
# 输入会议名称并搜索
safe_send_keys((By.XPATH, "//input[@placeholder='输入关键字搜索']"), message_name, wd)
send_keyboard((By.XPATH, "//input[@placeholder='输入关键字搜索']"), wd)
sleep(2)
# 点击【更多操作】结束会议数据
# 提前开始会议
safe_click((By.XPATH, "//span[contains(text(),'更多操作')]"), wd)
sleep(1)
safe_click((By.XPATH, "//li[contains(text(),'会议状态')]"), wd)
sleep(1)
# 选择提前结束
safe_click((By.XPATH, f"//span[contains(text(),'{control_type}')]"), wd)
sleep(1)
safe_click((By.XPATH, "//div[@slot='footer']//span[contains(text(),'确定')]"), wd)
sleep(2)
# 针对特定会议平台的额外确认操作
if message_type == "会控":
safe_click((By.XPATH,
"//button[@class='el-button el-button--default el-button--small el-button--primary ']//span[contains(text(),'确定')]"),
wd)
sleep(1)
# 进入会议预约界面函数
def enter_meeting_booking_page(meeting_room_name, wd):
"""
进入会议室预订页面并填写会议信息。
:param meeting_room_name: 会议室名称,用于搜索特定的会议室。
:param wd: WebDriver实例,用于操作浏览器。
"""
# 先搜索会议室
safe_click((By.XPATH, "//i[@class='el-collapse-item__arrow el-icon-arrow-right']"), wd)
sleep(1)
safe_send_keys((By.XPATH, "//input[@placeholder='请输入会议室名称']"), meeting_room_name, wd)
INFO(f"搜索结果为:{meeting_room_name}")
# 点击【查询】按钮
safe_click((By.XPATH, "//span[contains(text(),'查询')]"), wd)
sleep(2)
# 点击【会议预定】按钮
safe_click((By.XPATH, "//span[@class='MeetingCityList_t_btn']"), wd)
sleep(2)
# 输入会议名称并勾选MessageType类型
safe_send_keys((By.XPATH, "//input[@placeholder='请输入会议名称']"), "测试", wd)
sleep(1)
# 选择会议时间
current_time = get_current_time_formatted()
print(f"获取当前的时间{current_time}")
safe_click((By.XPATH, f"//div[normalize-space()='{current_time}']"), wd)
sleep(1)
# 删除会议函数
def del_message(message_name, wd):
"""
删除会议消息。
根据会议名称搜索并删除会议。
参数:
- message_name: 会议名称,用于搜索特定的会议。
- wd: WebDriver实例,用于执行网页自动化操作。
此函数无返回值。
"""
# 输入会议名称并搜索
safe_send_keys((By.XPATH, "//input[@placeholder='输入关键字搜索']"), message_name, wd)
send_keyboard((By.XPATH, "//input[@placeholder='输入关键字搜索']"), wd)
sleep(2)
# 点击【删除会议】按钮
safe_click((By.XPATH, "//span[contains(text(),'删除会议')]"), wd)
sleep(2)
# 点击【确定】按钮
safe_click((By.XPATH, "//button[contains(@class,'el-button el-button--default el-button--small el-button--primary')]//span[contains(text(),'确定')]"), wd)
sleep(1)
# 进入【会议室列表】界面
safe_click((By.XPATH, "//span[contains(text(),'会议室列表')]"), wd)
sleep(1)
# 设置会议审批状态函数
def approval_status_control(message_name, approval_type, wd):
"""
设置会议审批状态。
参数:
- message_name: 会议名称,用于搜索特定的会议。
- approval_type: 审批类型,用于选择审批操作(如驳回)。
- wd: WebDriver实例,用于与浏览器交互。
"""
# 进入会议审批模块界面
safe_click((By.XPATH, "//span[contains(text(),'会议审批')]"), wd)
sleep(1)
# 进入待我审批界面
safe_click((By.XPATH, "//div[@id='tab-second']"), wd)
sleep(1)
# 搜索会议
safe_send_keys((By.XPATH, "//input[@placeholder='请输入会议名称搜索']"), message_name, wd)
send_keyboard((By.XPATH, "//input[@placeholder='请输入会议名称搜索']"), wd)
sleep(1)
# 点击【审批】按钮
safe_click((By.XPATH, "//button[@type='button']//span[contains(text(),'审批')]"), wd)
sleep(1)
# 输入驳回意见
safe_send_keys((By.XPATH, "//textarea[@placeholder='请输入审批意见']"), "审批意见", wd)
# 点击【驳回】按钮
safe_click((By.XPATH, f"//span[contains(text(),'{approval_type}')]"), wd)
sleep(1)
# xlsx文件写入函数
import os
import openpyxl
from openpyxl import load_workbook
from openpyxl.drawing.image import Image
from openpyxl.utils import get_column_letter
import logging
def write_xlsx_data(xlsx_file_path, sheet_name, function_number, test_result, log_screenshot):
"""
在XLSX文件的指定行中填充测试结果和日志截图(图片嵌入单元格)。
参数:
xlsx_file_path (str): XLSX文件的路径。
sheet_name (str): 工作表的名称。
function_number (str): 功能编号,用于匹配行。
test_result (str): 测试结果。
log_screenshot (str): 日志截图路径。
"""
try:
workbook = load_workbook(xlsx_file_path)
except FileNotFoundError:
raise FileNotFoundError(f"文件未找到: {xlsx_file_path}")
except Exception as e:
raise Exception(f"无法打开文件: {e}")
# 选择工作表
try:
sheet = workbook[sheet_name] if sheet_name else workbook.active
except KeyError:
raise KeyError(f"工作表未找到: {sheet_name}")
# 读取表头
headers = [cell.value for cell in sheet[3]]
required_columns = ["功能编号", "测试结果", "日志截图"]
indices = {}
for col in required_columns:
try:
indices[col] = headers.index(col)
except ValueError:
raise ValueError(f"表头中缺少必要列: {col}")
# 查找目标行
target_row = None
for row in sheet.iter_rows(min_row=4):
if row[indices["功能编号"]].value == function_number:
target_row = row
break
else:
raise ValueError(f"未找到功能编号为 {function_number} 的行")
# 填充测试结果
test_result_cell = target_row[indices["测试结果"]]
test_result_cell.value = test_result
# 处理图片
if log_screenshot:
img = Image(log_screenshot)
img_cell = target_row[indices["日志截图"]]
# 计算单元格尺寸(单位转换)
col_letter = get_column_letter(img_cell.column)
col_width = sheet.column_dimensions[col_letter].width or 8.43 # 默认列宽
row_height = sheet.row_dimensions[img_cell.row].height or 15.0 # 默认行高
# 将Excel单位转换为像素(经验公式)
cell_width_px = col_width * 7 # 1字符 ≈ 7像素
cell_height_px = row_height * 1.333 # 1磅 ≈ 1.333像素
# 按比例缩放图片
img_ratio = img.width / img.height
if cell_width_px / cell_height_px > img_ratio:
new_height = cell_height_px
new_width = cell_height_px * img_ratio
else:
new_width = cell_width_px
new_height = cell_width_px / img_ratio
img.width = new_width
img.height = new_height
# 将图片锚定到单元格
sheet.add_image(img, f"{col_letter}{img_cell.row}")
# 保存文件
try:
workbook.save(xlsx_file_path)
logging.info(f"文件已保存,功能编号 {function_number} 的图片已嵌入")
except Exception as e:
logging.error(f"保存失败: {e}")
raise
# import xlwings as xw
# import os
# import logging
# def write_xlsx_data2(xlsx_file_path, sheet_name, function_number, test_result, log_screenshot):
# """
# 在XLSX文件的指定行中填充测试结果和嵌入日志截图(通过xlwings实现)。
#
# 参数:
# xlsx_file_path (str): XLSX文件的路径。
# sheet_name (str): 工作表的名称。
# function_number (str): 功能编号,用于匹配行。
# test_result (str): 测试结果。
# log_screenshot (str): 日志截图路径。
# """
# if not os.path.exists(xlsx_file_path):
# raise FileNotFoundError(f"文件未找到: {xlsx_file_path}")
#
# try:
# # 打开Excel文件
# wb = xw.Book(xlsx_file_path)
# except Exception as e:
# raise Exception(f"无法打开文件: {e}")
#
# try:
# # 选择工作表
# if sheet_name:
# sheet = wb.sheets[sheet_name]
# else:
# sheet = wb.sheets.active
# except KeyError:
# wb.close()
# raise KeyError(f"工作表 '{sheet_name}' 不存在")
#
# # 读取表头(第三行)
# try:
# headers = sheet.range("3:3").value # 获取第三行所有值
# except Exception as e:
# wb.close()
# raise Exception(f"读取表头失败: {e}")
#
# # 确定列索引(转换为1-based列号)
# try:
# function_number_col = headers.index("功能编号") + 1
# test_result_col = headers.index("测试结果") + 1
# log_screenshot_col = headers.index("日志截图") + 1
# except ValueError as e:
# wb.close()
# raise ValueError(f"表头列缺失: {e}")
#
# # 遍历数据行(从第四行开始)
# last_row = sheet.used_range.last_cell.row
# found = False
# for row_number in range(4, last_row + 1):
# current_value = sheet.range((row_number, function_number_col)).value
# if str(current_value) == str(function_number):
# # 更新测试结果
# sheet.range((row_number, test_result_col)).value = test_result
#
# # 插入图片(如果存在)
# if log_screenshot and os.path.exists(log_screenshot):
# try:
# # 获取目标单元格
# cell = sheet.range((row_number, log_screenshot_col))
#
# # 插入图片并绑定到单元格
# pic = sheet.pictures.add(log_screenshot,
# left=cell.left,
# top=cell.top,
# width=cell.width,
# height=cell.height)
# pic.api.Placement = 3 # 设置为xlMoveAndSize模式
# except Exception as e:
# logging.error(f"插入图片失败: {e}")
# wb.close()
# raise
#
# found = True
# break
#
# if not found:
# wb.close()
# raise ValueError(f"未找到功能编号为 {function_number} 的行")
#
# # 保存并关闭
# try:
# wb.save()
# logging.info(f"文件 {xlsx_file_path} 已保存,数据已写入功能编号为 {function_number} 的行。")
# except Exception as e:
# logging.error(f"保存文件时出错: {e}")
# raise
# finally:
# wb.close()
import paramiko
import time
import paramiko
import paramiko
def get_remote_log_with_paramiko(host, username, private_key_path, passphrase, log_path, num_lines=1000, timeout=30, filter_word=None):
"""
使用 Paramiko 获取远程服务器的日志文件内容,并通过过滤词过滤日志内容.
Args:
host (str): 服务器 IP 地址或域名.
username (str): 用户名.
private_key_path (str): SSH 私钥文件路径.
passphrase (str): 私钥文件的 passphrase.
log_path (str): 日志文件路径.
num_lines (int): 要获取的日志行数 (默认 100).
timeout (int): SSH 命令执行的超时时间(秒).
filter_word (str): 过滤词,只有包含该词的日志行才会被返回 (默认 None).
Returns:
str: 获取的日志内容,如果出错返回 None.
"""
try:
print(f"Loading private key from {private_key_path}...")
if passphrase:
print(f"passphrase为:{passphrase}")
print(f"private_key_path:{private_key_path}")
private_key = paramiko.RSAKey.from_private_key_file(private_key_path, password=passphrase)
else:
private_key = paramiko.RSAKey.from_private_key_file(private_key_path)
print(f"Private key loaded successfully from {private_key_path}")
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
print(f"Connecting to {host} as {username}...")
client.connect(host, username=username, pkey=private_key, timeout=timeout)
command = f"tail -n {num_lines} {log_path}"
print(f"Executing command: {command}")
stdin, stdout, stderr = client.exec_command(command, timeout=timeout)
error = stderr.read().decode('utf-8')
if error:
print(f"Error: {error}")
return None
output = stdout.read().decode('utf-8')
print("Successfully retrieved log content.")
print("Full log content:")
print(output) # 打印完整的日志内容
if filter_word:
filtered_output = "\n".join([line for line in output.split('\n') if filter_word in line])
if not filtered_output:
print(f"No lines found containing the filter word: {filter_word}")
return filtered_output
return output
except paramiko.ssh_exception.PasswordRequiredException:
print("Error: The private key file is encrypted but no passphrase was provided.")
return None
except paramiko.ssh_exception.SSHException as e:
print(f"SSH Error: {e}")
return None
except Exception as e:
print(f"An error occurred: {e}")
return None
finally:
if 'client' in locals():
client.close()
if __name__ == "__main__":
host = "192.168.5.218" # 替换为你的服务器 IP 或域名
username = "root" # 替换为你的用户名
# private_key_path = "C:\\Users\\29194\\.ssh\\id_rsa" # 替换为你的私钥文件路径
private_key_path = "C:/Users/29194/.ssh/id_rsa"
passphrase = "Ubains@123" # 确保这是你的私钥文件的正确 passphrase
log_path = "/var/www/java/api-java-meeting2.0/logs/ubains-INFO-AND-ERROR.log" # 替换为你要读取的日志文件路径
filter_word = "" # 替换为你想要过滤的关键字
log_content = get_remote_log_with_paramiko(host, username, private_key_path, passphrase, log_path)
if log_content:
print(log_content)
else:
print("Failed to retrieve log content.")
import paramiko
import threading
import time
class LogCollector:
def __init__(self, host, username, private_key_path, passphrase, log_path, timeout=30, keepalive_interval=60):
self.host = host
self.username = username
self.private_key_path = private_key_path
self.passphrase = passphrase
self.log_path = log_path
self.timeout = timeout
self.keepalive_interval = keepalive_interval # 保持连接活跃的时间间隔(秒)
self.client = None
self.channel = None
self.log_content = []
self.collecting = False
self.lock = threading.Lock()
self.thread = None
def connect(self):
try:
private_key = paramiko.RSAKey.from_private_key_file(self.private_key_path, password=self.passphrase)
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.client.connect(self.host, username=self.username, pkey=private_key, timeout=self.timeout)
print(f"Connected to {self.host} as {self.username}")
except Exception as e:
print(f"Failed to connect: {e}")
self.client = None
def start_collection(self):
if self.collecting:
print("Already collecting logs.")
return
self.connect()
if self.client is None:
print("Failed to start collection due to connection failure.")
return
self.channel = self.client.invoke_shell()
command = f"tail -f {self.log_path}\n"
self.channel.send(command)
self.collecting = True
self.thread = threading.Thread(target=self.collect_logs)
self.thread.start()
print("Log collection started.")
def stop_collection(self, output_file):
if not self.collecting:
print("Not collecting logs.")
return
self.collecting = False
if self.channel:
self.channel.close()
if self.client:
self.client.close()
print("Log collection stopped.")
with open(output_file, 'w', encoding='utf-8') as f:
f.write('\n'.join(self.log_content))
print(f"Log content saved to {output_file}")
def collect_logs(self):
try:
while self.collecting:
if self.channel.recv_ready():
output = self.channel.recv(1024).decode('utf-8')
with self.lock:
self.log_content.append(output.strip())
print(output.strip())
else:
time.sleep(0.1) # 避免CPU占用过高
# 发送keepalive包以保持连接活跃
self.client.get_transport().send_ignore()
time.sleep(self.keepalive_interval)
except Exception as e:
print(f"Error in collect_logs: {e}")
self.collecting = False
finally:
if self.channel:
self.channel.close()
if self.client:
self.client.close()
print("Log collection thread terminated.")
# 使用示例
if __name__ == "__main__":
host = "192.168.5.218"
username = "root"
private_key_path = "C:\\Users\\29194\\.ssh\\id_rsa" # 替换为你的私钥文件路径
passphrase = "Ubains@123" # 替换为你的 passphrase
log_path = "/var/www/java/api-java-meeting2.0/logs/ubains-INFO-AND-ERROR.log"
output_file = "collected_logs.txt"
collector = LogCollector(host, username, private_key_path, passphrase, log_path)
# 开始收集日志
collector.start_collection()
# 假设一段时间后停止收集日志
try:
while True:
time.sleep(1) # 保持主程序运行
except KeyboardInterrupt:
print("Stopping log collection due to user interrupt.")
collector.stop_collection(output_file)
print("日志收集完成!")
\ No newline at end of file
## 更新记录
1. 2024-10-21
- 修改了自动化测试脚本架构,使用PO模式,将共有方法封装在base目录下,任何模块都可以进行调用。
- 将测试数据csv为文件统一放在测试数据目录下,并标注所属模块功能。
- 将驱动加载方式改为更加灵活的自动下载方式,避免其他人员使用时手动下载驱动。
- 补充安卓信息模块的Mqtt主题上报以及接收脚本,但暂时还未与实际mqtt主题进行调试,需要先整理出所有的mqtt主题,再进行代码调试。
2. 2024-10-22
- 补充安卓信息模块的Mqtt主题上报以及接收脚本,但目前安卓信息上报后系统界面仍显示为离线。需进一步了解业务流后进行调试。后续完整多个安卓信息上报实现大量设备同时在线的功能验证。
- 将安卓信息上报的MQTT相关函数封装到base目录下,方便后续调用以及维护管理。
- 将MQTT上报消息通过csv进行读取,方便后续维护。
3. 2024-10-23
- 优化安卓信息模块代码,MQTT一次性上报csv文件中的每行数据,并重复执行上报。
- 了解清楚安卓信息的状态判断,后续将调试redis连接以及将键值对存入redis。
4. 2024-10-24
- 优化MQTT底层方法,通过判断topic的值来发送不同的消息体。
- 优化安卓信息上报.py脚本通过读配置文件中的topic,调用build_message时传入topic进行主题判断。后续方便进行MQTT主题与消息体的管理。
- 补充毫米波雷达数据上报数据,毫米波设备的device_id需要与实际设备匹配才行。
- 优化安卓信息上报.py脚本通过读配置文件中的topic,调用build_message时传入topic进行主题判断。后续方便进行MQTT主题与消息体的管理。
- 修改提交git时代码中出现了很多非法字符的问题。
5. 2024-10-28
- 优化了Mqtt_Send中封装的函数,补充异常处理以及日志优化。
6. 2024-11-02
- 将原有pytest框架代码改为hytest框架使用,优化了脚本执行效率,并对应增加异常处理。
- MQTT相关模块目前调整了Mqtt_Send.py文件中封装的方法优化,增加异常处理。处理了登录成功断言失败的问题。
- 补充用户管理模块的新增用户部分代码,当前存在新增界面弹窗后定位失败问题。
7. 2024-11-04
- 将用户管理模块的代码进行重构,将新增用户、删除用户、修改用户等公用的操作封装到base目录下,方便后续管理。
- 使用元素定位插件进行定位,解决定位失败的问题。
- 测试报告中补充用例截图。
- 完成毫米波雷达的模拟数据上报,系统界面根据上报信息来回显,目前只造了100个模拟数据。
8. 2024-11-05
- 完成用户管理模块的新增用户、删除用户、用户查询。补充脚本注释信息。
9. 2024-11-06
- 完成用户管理模块的主流程代码。优化ddt_cases的读取方式,从csv中读取测试数据。并且将csv写入ddt_cases的代码封装成函数,后续方便管理。调试安卓信息MQTT上报没有成功的问题。问题已解决。
- 优化ddt_cases的读取方式,从csv中读取测试数据。并且将csv写入ddt_cases的代码封装成函数,后续方便管理。
- 调试安卓信息MQTT上报没有成功的问题。问题已解决。
10. 2024-11-07
- 补充MQTT模块的README文档。
- 补充脚本执行的命令注释。
11. 2024-11-08
- 优化封装好的SELENIUM_LOG_SCREEN函数,增加三个参数用来指定截屏文件存放路径以及截屏文件名称。
12. 2024-11-11
- 补充部门管理模块脚本,目前输出了部门新增、部门删除、部门编辑、部门查询。部门编辑还存在问题,待解决。
- 测试调用钉钉机器人发送消息接口,调用失败,根据接口文档排查是由于webhook地址缺少拼接的密钥和时间戳导致,后续进一步验证。
- 优化登录模块的脚本,使用hytest框架形式。
13. 2024-11-13
- 已将部门管理、用户管理模块完全输出完成,部门编辑存在定位失败问题,暂时没排查出来。
- 输出会议室管理相关模块的脚本,当前已输出功能管理模块的新增与编辑功能代码。
- 封装正则表达式函数用来判断密码的字符格式。优化代码的可维护性。
14. 2024-11-14
- 输出功能管理模块中的功能查询、功能删除相关代码。
- 输出区域管理模块中的区域新增、区域删除相关代码。
- 调整base类中封装的get_notify_text函数,调整传参类型,优化代码的可维护性,减少代码冗余。
- 输出会议室管理模块中的会议室新增代码,待后续完善。
15. 2024-11-18
- 输出会议室管理模块下的会议室新增、会议室删除、会议室查询的代码处理。剩余会议室编辑未输出。
16. 2024-11-19
- 输出会议室管理模块下的会议室编辑代码处理,增加注释。
- 补充完善当前已完成模块的脚本执行指令。
- 输出会议预定代码,预约主流程已完成。
17. 2024-11-20
- 排查驱动加载失败问题,问题由浏览器驱动与当前浏览器版本不同导致,已优化代码为自动根据浏览器版本下载对应驱动。
- 补充关于定制化项目的脚本路径,补充工商银行查询停车缴费系统数据的代码。
- 优化会议预约的脚本,增加预约类型、消息提醒、通知方式以及是否创建模板的代码判断,根据对应需求创建对应类型的会议。
- 输出会议修改代码,增加预约类型的判断,处理对于周期会议的修改特殊处理。会议议题还需要补充完善。
18. 2024-11-21
- 实现钉钉群消息提醒接口调用,后续切换到其他测试公司进行定时推送验证。
19. 2024-11-25
- 实现定时执行任务并调用钉钉群消息发送函数,但调试过程中存在一些没有调用的问题,待排查。
- 钉钉群消息提醒的param参数改为link链接形式,通过链接打开测试报告进行查看。
20. 2024-11-26
- 调试定时任务的问题,还需再进一步调试。
- 补充运维集控的MQTT底层方法与消息体构建。
- 调试定时任务执行后钉钉消息没有发送的问题。将定时任务与测试报告文件获取封装成函数进行调用,方便后续维护管理。目前调试发现定时任务执行别的脚本时会出现异常问题,需要进一步排查。
21. 2024-11-27
- 调试定时任务执行完成后没有发送钉钉群消息的问题,并优化SSL处理方式。
22. 2024-11-28
- 封装get_reportfile_send_dingding函数用来获取测试报告文件并拼接IP地址,并发送钉钉群消息。优化定时任务run_automation_test函数的调用处理,并增加对于的日志输出。
- 补充定时任务执行的注解。
- 调整元素定位的显示等待时间,避免因服务器的网络波动导致元素获取异常。
- 调整钉钉发送消息调用函数,去除callback回调。补充部门管理模块的定时执行任务。调整get_reportfile_send_dingding函数的参数。
23. 2024-11-29
- 调整预定系统后台管理模块的执行顺序,将所有预定后台管理模块整合到一个执行指令中,优化各个模块之间的逻辑处理。调整各个模块的__st__.py的浏览器初始化调用。
- 调整截屏函数的图片存放路径,并且将服务80端口映射出来,实现公网访问测试报告文件。增加ngrok程序,后续调整自动执行ngrok映射。
24. 2024-11-30
- 调试定时任务执行后测试报告中的用例执行异常,问题是由一个调用方法写错导致,已调整解决。
25. 2024-12-09
- 调整admin_login函数为传参形式。
- 补充展厅巡检流程,预定SMC视讯会议,并进行安卓信息截屏。后续补充会控操作功能流程进行调试。
26. 2024-12-10
- 调整browser_init函数改为传参方式传入登录页面URL,调整相关脚本的函数调用。
- 根据展厅预定系统的巡检流程输出脚本程序。进一步调试会控界面部分元素定位失败的问题以及议题文件上传失败的问题。
- 调试appium移动端脚本,控制手机进入腾讯会议查看会控界面是否正常。补充appium运行日志,调试定位失败问题。
27. 2024-12-11
- 调试appium移动端脚本,控制手机进入腾讯会议。调试定位失败问题。更换定位工具以及定位方法调用。
- 调试排查测试报告网址打不开问题,处理会控界面会场拖拽问题。
- 处理issue_send_and_upload函数议题文件上传失败的问题。
28. 2024-12-12
- 处理模拟器无纸化无法读取PAK问题,实现通过模拟器进行app自动化巡检流程。
- 处理appium自动化截屏。
- 输出富创项目的MQTT会议推送相关代码处理。
- 输出富创项目的MQTT人脸签到订阅测试脚本,输出富创项目两个MQTT相关服务的README文档。
29. 2024-12-13
- 补充MQTT的消息监听,监听到消息后,会通过日志输出消息体内容,可用于所有预定项目测试使用。
- 补充富创项目的MQTT消息监听,监听到消息后,会通过日志输出消息体内容,可用于富创项目测试替代第三方工具使用。
- 处理富创项目的MQTT预约、修改和删除会议消息体,增加代码可维护性。补充标准版安卓信息的监听代码。
30. 2024-12-14
- 更新展厅巡检定时任务。通过覆盖的方式将appium的截图替换掉hytest的截图,从而实现appium的测试报告生成。
31. 2024-12-16
- 补充展厅预定巡检的截图信息,补充开发调试使用的毫米波雷达模拟.
- 修改主README文档,补充目录说明,执行指令,以及报告查看内容.
- 调整展厅巡检脚本的操作流程,增加会场静音等操作与截图。输出会议修改相关代码。
32. 2024-12-17
- 处理展厅无纸化1.0的巡检流程,保留原无纸化2.0的验证流程。增加相关的信息截图。
- 处理展厅巡检流程。
33. 2024-12-19
- 处理富创项目的mqtt消息体参数值。
- 调整钉钉发送消息的密钥和接口获取方式,通过最外层传参来进行控制需要发送的钉钉群。
- 优化appium相关代码的注释。
34. 2024-12-20
- 调整dingding调用函数的传参问题,调整定时任务执行时间为工作日。
- 补充讯飞、运维以及统一平台的巡检流程,并整合展厅巡检目录的执行初始化文件。
- 补充统一平台的软终端入会流程,完善统一平台的会控巡检流程。
25. 2024-12-23
- 调整富创项目的MQTT消息体,补充参会人数据推送。处理展厅巡检的时间格式问题。
- 补充对于服务器状态的监测,并体现在报告中做判断。
- 补充历史会议模块中会议筛选功能的自动化验证处理。
26. 2024-12-24
- 根据实际使用时巡检人员的反馈进行调整,定位会控按钮时增加判断,如定位不到即会控创建失败。
- 调整巡检的执行时间。
- 拆分出MQTT通用工具目录,用于后续开发测试人员进行模拟设备调试以及MQTT相关的程序验证。
- 补充历史会议再次预定、会议模板查询以及再次预定功能的代码处理。
27. 2024-12-25
- 根据展厅巡检使用时遇到的问题进行调整,在议题文件上传前做文件是否存在的校验,若不存在则跳出函数。补充讯飞流程的切换窗口句柄代码。
- 补充会议审批预约以及取消审批功能的自动化处理。调整会议模板以及历史会议的自动化处理顺序,无需重复初始化浏览器。
- 处理各模块间的执行流程优化。
- 补充会议审批相关功能之间的流程自动化处理,会议审批模块已补充完成。给所有功能模块打上标签,后续通过运行标签的形式调试预定系统整体功能测试流程。
- 处理优化展厅巡检相关流程。
28. 2024-12-26
- 调整服务状态监测读取文本文件函数fetch_and_parse_check_txt的重复赋值问题。
- 补充展厅巡检关于桌牌系统是否可用的简单检测。
- 调整富创数据,调整标准版定时任务的启动标签为预定系统功能,执行预定系统全部功能自动化验证。
29. 2024-12-27
- 补充展厅中控屏的灯光控制以及窗帘控制。
30. 2024-12-30
- 封装亮度判断的compare_brightness函数处理,后续通过亮度判断灯光是否正确打开。
31. 2024-12-31
- 补充对于rtsp流抓取一帧保存为图片,然后再进行对应的亮度判断。处理优化调用。
- 补充展厅中开屏的关于灯光控制、窗帘控制、指挥大屏控制以及配合监控视频来判断灯光与指挥大屏是否正确打开。
- 优化capture_frame_from_rtsp函数的异常判断。
- 封装中控屏对应控制模块函数,减少主函数的代码冗杂,增加可维护性。
32. 2025-01-02
- 调整优化内部参会人。
- 处理展厅会议室内中控屏的摄像头跟踪功能,并打开无纸化升降屏。调整优化。
33. 2025-01-03
- 调试展厅巡检的异常,补充对视讯资源异常情况的判断处理。
- 切换标准版预订系统功能检测环境,定时运行程序,并进行调试,修复出现的异常情况。
34. 2025-01-06
- 补充展厅巡检后的会议创建流程。
- 补充展厅中控屏巡检流程的rtsp视频流截取监控画面,优化主题函数的代码,将每个设备控制封装为函数调用,增加可维护性。
35. 2025-01-07
- 增加窗帘下降后的等待时间再进行监控rtsp流截屏。
36. 2025-01-09
- 将手动执行adb连接的操作合并到程序中,并在每个adb连接设备模块执行完后断开对应设备的adb连接。处理可能因网络连接自动断开adb后无法进行远程app自动化操作的问题。
- 封装app自动化的初始化函数与清除函数,补充自动化adb连接设备以及判断。
37. 2025-01-10
- 处理因浏览器驱动自动更新造成自动化下载导致的驱动问题。目前改为手动指定chromedriver目录。
38. 2025-01-11
- 调整等待时间,更换无纸化adb连接ip。
- 调整会议预约的判断流程,适配网络不稳定场景。
39. 2025-01-13
- 调整展厅巡检窗帘控制的等待时间。调整标准版功能巡检至测试发布192.168.5.235环境。去除移动端自动化测试完成后的退出操作。
40. 2025-01-14
- 调整日志记录器。
41. 2025-01-15
- 调整截图文件的缩放比,实现测试报告点击图片弹窗放大功能。
42. 2025-01-16
- 优化browser_init初始化函数的传参方式以及系统环境判断处理。优化ngrok以及定时任务注解。
- 调整优化。处理截屏函数的兼容性,优化URL的判断。
- 系统地址获取方式改为读取config.json进行获取对应的环境信息。
- 封装配置项读取函数增加异常处理判断,调整涉及的相关代码部分,调整模拟器adb连接初始化方式。
43. 2025-01-17
- 调整优化。
44. 2025-01-20
- 根据测试报告打印的异常信息,调整会议主流程相关代码的定位方式。
45. 2025-01-22
- 根据测试报告处理统一平台没有截图的问题。调整议题上传函数等待时间。
- 封装云喇叭的注册与播放接口调用,后续对接至展厅巡检流程中。
46. 2025-01-23
- 增加麦克风与摄像头控制按钮控制,以及云喇叭调用播放音频流程.调整控制顺序.
47. 2025-02-05
- 补充麦克风控制开启、播放云喇叭音频以及查看讯飞转写结果的巡检流程。补充摄像头掉线后运维系统控制界面的巡检流程。
48. 2025-02-06
- 优化处理展厅统一平台视讯会议巡检腾讯终端入会流程。
- 根据测试报告处理历史会议模块异常问题。
49. 2025-02-07
- 根据标准版功能巡检测试报告处理会议审批模块的异常。
50. 2025-02-08
- 删除未使用的代码片段,优化代码注释。
- 根据标准版功能巡检测试报告处理会议主流程功能模块中的异常。
- 优化展厅巡检代码流程,增加展厅会议室中控屏系统关的操作,将设备断电。
- 根据标准版功能巡检测试报告处理审批会议、会议模板、用户管理和部门管理模块的异常操作。
51. 2025-02-10
- 处理展厅巡检测试报告中的参数获取异常。
- 展厅巡检补充离开模式、系统开的操作,将展厅内设备进行断电。
52. 2025-02-12
- 排查展厅巡检中的语音转录与摄像头关闭操作失效问题,原因是缺少一个返回操作,并且麦克风和摄像头的元素定位发生变化导致操作失败,缩短等待时间。
- 历史会议模块与会议审批模块补充调用清除浏览器驱动函数。
- 补充审批会议模块的日志输出。调整审批会议创建的执行步骤。
53. 2025-02-13
- 增加get_test_result函数通过正则匹配获取通过率、失败率以及异常率,再调用钉钉消息通知输出通过率等值。优化处理。
54. 2025-02-14
- 补充讯飞语音转录功能流程,调试通过。
- 补充实现无纸化同屏巡检流程,但是还存在图片匹配失败问题。处理优化。
55. 2025-02-17
- 处理无纸化同屏操作流程。调整正则表达式获取测试报告的百分比参数。
56. 2025-02-18
- 讯飞语音转录补充云喇叭调用,更新ngrok映射前ip地址,将无纸化同屏打上展厅巡检标签。
- 补充了自动获取本机IP地址以及自动运行ngrok开启映射,避免后续IP改变后无法打开报告问题。
- 补充开启ngrok进程之前先关闭已运行的进程,然后再启动ngrok映射.规避因多个ngrok共存出现的问题.
57. 2025-02-19
- 根据展厅巡检报告处理展厅中控屏步骤缺失问题,是由元素定位值发生变化导致无法定位成功。但是多运行几次发现有时候adb连接会自动断开或者程序会自动闪退。
- 补充图片对比函数,还需调整一下判断的图像点阈值。
- 讯飞系统的定位元素改为CSS,因国际化导致原先的XPATH可能会由变化。
58. 2025-02-22
- 排查展厅巡检报告中无纸化会议操作的截图显示错误问题,补充上一无纸化同屏流程的结束同屏操作步骤。在初始化函数补充adb连接是否可用的判断,如连接失败,则不进行后续操作。
- 处理展厅巡检中控屏的信息发布屏流程步骤缺失问题,增加异常重试机制。
- 封装三个移动端的函数,分别用于设备控制时的异常重试机制以及设备截屏操作,增加相应的异常处理。
59. 2025-02-26
- 优化MQTT模块增加账号密码输入,输出通过读取测试用例的JSON数据格式进行自动化测试的demo程序。
60. 2025-02-28
- 处理展厅中控屏图片过大问题,优化处理。初步调试测试用例JSON数据读取。
61. 2025-03-03
- 调试测试用例JSON数据读取方式实现系统登录模块功能。
62. 2025-03-05
- 排查展厅巡检报告中的异常,处理会场名称变动,导致会场搜索失败问题,改为模糊查询与CSS定位,处理腾讯软终端元素变动问题。
- 排查展厅巡检报告中讯飞语音转录为空的问题。
- 调整讯飞语音转录的流程,登录讯飞语音转录系统后先判断客户端盒子是否在线,如果在线就进行后续的转录文字提取判断。config.json配置文件增加展厅讯飞系统配置项。
63. 2025-03-06
- 补充展厅无纸化中控2.0的同屏巡检代码。补充展厅无纸化2.0的主流程验证代码。
- 处理展厅统一平台因会议室变动导致的异常,会议室搜索改为模糊查询,会控界面的终端拖拽元素调整更新。
- 处理测试用例JSON读取登录功能测试脚本。
- Base函数库中的函数缩略补充函数使用说明,增加相关注释,删除无用函数。
64. 2025-03-07
- 删除中控屏操作的多余截屏函数调用。处理get_screenshot_with_retry函数的目录构建传参问题。
65. 2025-03-10
- 处理read_xlsx_data函数增加sheet传参,实现测试用例中会议室列表的部分功能。
- 封装获取当前会议时间转换格式函数,用于所有的会议预定功能测试使用。实现测试用例中会控-SMC的部分功能。
66. 2025-03-11
- 实现测试用例中会控-腾讯会议的部分功能。
- 封装会议创建、结束会议与会议状态操作的公用方法,通过会议室名称与会议类型来做判断,减少代码冗杂。实现测试用例中会议创建的部分功能。
- 实现测试用例中的会议修改和会议历史记录的部分功能测试。
- 实现测试用例中的会议模板的部分功能测试。
67. 2025-03-12
- 实现读取测试用例JSON数据全局配置、账号管理以及信息统计的部分功能测试。优化get_notify_text传参。
- 补充读取测试用例JSON数据会议审批和会议室管理的部分功能测试。优化message_meeting函数传参。
- 补充读取测试用例JSON数据授权码管理的部分功能测试。
- 补充读取测试用例JSON数据安卓信息、系统管理和信息发布模块的部分功能测试。处理优化富创项目的新需求topic主题与MQTT消息体。
- 处理异常情况,补充截图操作。
68. 2025-03-17
- 补充实现展会的AI创会的简单代码处理,调整config.json配置项增加长安大学的配置信息。
- 优化user_login函数,增加approval_status_control会议审批的公用函数。实现长安大学取消审批短信通知的JSON测试。
69. 2025-03-19
- 处理datetime类重复导入的问题,调整get_current_time_formatted函数相关的代码。
70. 2025-03-20
- 补充clear_columns_in_xlsx函数实现在读取测试用例前,先将测试结果和日志截图清空。
71. 2025-03-21
- 实现通过JSON数据中对应的name值来区分测试用例数量。
- 补充write_xlsx_data函数用以将测试结果与日志截图写入到xlsx测试用例文件中。调试write_xlsx_data函数与统一平台的窗口切换关闭。
72. 2025-03-24
- 优化write_xlsx_data函数实现表格自动填充测试结果和日志截图功能。
- 处理会议测试用例所有模块的JSON数据格式以及代码处理,规范化类名。
73. 2025-03-25
- 处理展厅巡检会议文件空问题,排查展厅巡检的讯飞系统转录,调试没有问题,正常转录成功。
74. 2025-04-01
- 补充工商银行项目的JSON数据自动化测试。
75. 2025-04-02
- 将ngrok.cfg配置文件本机IP改为127.0.0.1,避免本机IP变化后无法打开测试报告。
- 处理工商银行JSON数据格式错误问题,处理优化。
76. 2025-04-08
- 缩短元素等待时间,调整展厅巡检视讯会议的会控按钮判断。增加议题文件上传未完成时点击【确定】弹出的确认框补充点击确认操作。
77. 2025-04-09
- 优化read_xlsx_data用例文件读取函数,增加功能类别判断,实现标准版和各项目的区分执行。补充工商银行项目的车牌功能验证。config.json文件增加工商银行生产环境。
78. 2025-04-11
- 补充get_remote_log_with_paramiko函数来获取服务器的日志文件。补充config.json的统一平台测试环境。
79. 2025-04-14
- 登录模块增加excel写入函数调用。调试将图片嵌入单元格的功能,验证不可行。
80. 2025-04-16
- 修复展厅巡检预约系统因为元素定位失败导致的创建会议失败问题,优化issue_send_and_upload议题上传函数。
- 修复因xlsx文件打开读取后没有关闭导致的文件损坏问题,优化read_xlsx_data函数,增加关闭文件操作。
81. 2025-04-17
- 修复因xlsx文件读取失败导致的报错。
82. 2025-04-18
- 修复展厅巡检预定系统因页面bug导致的异常定位报错。处理优化。
- 优化get_remote_log_with_paramiko函数,增加filter_word过滤词打印日志。
83. 2025-04-27
- 增加LogCollector类实现从服务器获取日志,在测试步骤开始前进行获取,到步骤测试结束后获取结束,输出为日志文本文件。
84. 2025-05-21
- 展厅预定系统更新了前后台分离,代码处理切换窗口操作。
85. 2025-05-22
- 解决展厅桌牌系统定位错误问题,config.json文件增加展厅预定系统后台登录链接,优化桌牌的系统登录跳转。
- 排查展厅门口的中控屏没有巡检截图的问题,排查后发现中控屏的软件包名与类名因为更新程序后不一样了,调整展厅中控屏初始化新的软件包名与类名。1
86. 2025-05-25:
- 增加兰州项目定制自动化处理,调试兰州自动化的JSON数据运行。
87. 2025-05-26:
- 使用crate_csv_test脚本生成安卓信息上报与心跳上报的csv文件,并使用MQTT脚本模拟两千台设备上报消息至预定系统。
- 处理MQTT创建客户端实例时补充账号密码的配置。
88. 2025-05-27:
- 安卓信息消息监听脚本也增加MQTT账号密码的配置处理。
- 调试兰州中石化项目会议申报模块的自动化JSON数据。
89. 2025-06-03:
- 兰州中石化项目输出会议申报模块的JSON数据,调试自动化运行,补充会议申报模块的JSON数据。
90. 2025-06-05:
- 兰州中石化项目输出角色权限组的部分JSON数据,调试自动化运行。排查展厅自动化失败问题,处理chrome版本升级后,chromedriver版本没对上问题。
91. 2025-06-10:
- 兰州中石化项目输出议题申报流程的JSON数据,调试自动化运行。补充代办事宜模块流程的JSON数据自动化测试。
92. 2025-06-11:
- 处理展厅巡检预定系统创建会议因议题文件上传的路径错误导致的异常问题,优化app_init和app_quit的代码,补充adb路径配置,增加对应异常的日志打印。
- 兰州中石化项目,补充决策会议、会议管理、议题列表以及角色权限管理模块的自动化JSON数据测试,调试运行。代码已提交至git。
93. 2025-06-12:
- 测试报告发送前增加blocking_rate用例阻塞率的获取以及发送。
94. 2025-06-13:
- 兰州中石化项目模块增加tag标签。
95. 2025-06-16:
- 标准版注释clear_columns_in_xlsx和write_xlsx_data方法调用。定时任务补充兰州。
96. 2025-06-18:
- base增加read_xlsx_data_auto函数,支持sheet_name为None时,默认遍历整个测试用例所有模块从第一个sheet到最后的sheet执行,若sheet_name传参时,则为单独执行某个模块测试。
- 删除原函数,避免混淆。
- read_xlsx_data函数优化,调试兰州中石化项目自动化测试。
97. 2025-07-01:
- feat(预定系统): 更新脚本支持多环境配置,新增 JSON 配置文件,支持不同环境的配置,重构代码,实现环境配置的动态加载- 添加命令行参数,允许选择更新环境,优化日志输出,提高错误处理能力。
98. 2025-08-20:
- 更新展厅巡检结尾步骤,增加腾讯会议、讯飞转录以及SMC选项。
99. 2025-11-10:
- 补充新统一平台的测试用例及脚本运行。
\ No newline at end of file
import sys
import os
# 获取 Base 目录的绝对路径,并加入 sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from 预定系统.Base.base import *
def suite_teardown():
browser_quit()
\ No newline at end of file
import sys
import os
from time import sleep
# 获取 Base 目录的绝对路径,并加入 sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from 新统一平台.Base.base import *
def suite_setup():
STEP(1, "初始化浏览器")
browser_init("新统一平台")
def suite_teardown():
browser_quit()
\ No newline at end of file
import sys
import os
# 获取当前脚本的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
# 构建统一平台的绝对路径
platform_path = os.path.abspath(os.path.join(current_dir, '..','..','..'))
# 添加路径到系统路径中
sys.path.append(platform_path)
# 导入模块
from 统一平台.base.bases import *
# 构建XLSX文件的绝对路径
xlsx_file_path = os.path.join(current_dir, '..', '..', '测试数据', '新统一平台测试用例.xlsx')
class NewUnifiedPlatform:
#执行指令:
# cd ./新统一平台/
# hytest --tag 新统一平台
tags = ['新统一平台']
ddt_cases = read_xlsx_data(xlsx_file_path, case_type="标准版")
def teststeps(self):
"""
执行测试步骤函数,主要用于执行读取的测试用例并进行信息统计模块功能测试操作
"""
# 从全局存储中获取webdriver对象
wd = GSTORE['wd']
name = self.name
# 刷新页面
# wd.refresh()
wd.refresh()
sleep(5)
for step in self.para:
# 赋值页面类型page
page_type = step.get('page')
# 赋值元素定位类型,并将字符串转为Enum类型
locator_type = get_by_enum(step.get('locator_type'))
# 赋值元素值
locator_value = step.get('locator_value')
# 赋值元素类型,例如:click点击、input输入框等
element_type = step.get('element_type')
# 赋值元素值,例如输入框的输入值
element_value = step.get('element_value')
# 赋值预期结果
expected_result = step.get('expected_result')
# 赋值等待时间
# sleep_time = step.get('sleep_time')
# 赋值操作步骤
step_name = step.get('step')
INFO(
f"步骤名称: {step_name}\n"
)
if element_type == "click":
safe_click((locator_type, locator_value), wd)
sleep(5)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "input":
safe_send_keys((locator_type, locator_value), element_value, wd)
sleep(2)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "SwitchWindow":
# 将字符转换为int类型
element_value = int(element_value)
wd.switch_to.window(wd.window_handles[element_value])
sleep(2)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "login":
# 退出系统登录
safe_click((By.XPATH, "//div[@class='quit']"), wd)
sleep(5)
INFO(f"开始登录,账号为:{element_value[0]},密码为:{element_value[1]}")
safe_send_keys((By.XPATH, "//input[@placeholder='手机号/用户名/邮箱']"), "admin@XTY", wd)
safe_send_keys((By.XPATH, "//input[@placeholder='密码']"), "Ubains@4321", wd)
safe_send_keys((By.XPATH, "//input[@placeholder='图形验证']"), "csba", wd)
sleep(2)
INFO("对协议进行勾选")
safe_click((By.XPATH, "//div[@aria-label='提示']//span[contains(text(),'确定')]"), wd)
INFO("已经勾选协议了")
sleep(2)
safe_click((By.XPATH, "//div[@id='pane-1']//div//span[contains(text(),'登录')]"), wd)
INFO("已经点击登录了")
sleep(2)
elif element_type == "getTips":
notify_text = get_notify_text(wd, (locator_type, locator_value))
INFO(f"获取到的提示信息为:{notify_text}")
sleep(2)
CHECK_POINT(f"获取到的提示信息为:{notify_text}", expected_result in notify_text)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "getText":
text = elment_get_text((locator_type, locator_value), wd)
INFO(f"获取到的文本信息为:{text}")
CHECK_POINT(f"获取到的文本信息为:{text}", expected_result in text)
SELENIUM_LOG_SCREEN(wd, "75")
sleep(2)
\ No newline at end of file
import sys
import os
from time import sleep
# 获取 Base 目录的绝对路径,并加入 sys.path
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from 新统一平台.Base.base import *
def suite_setup():
STEP(1, "初始化浏览器")
browser_init("新统一平台")
def suite_teardown():
browser_quit()
\ No newline at end of file
import sys
import os
# 获取当前脚本的绝对路径
current_dir = os.path.dirname(os.path.abspath(__file__))
# 构建统一平台的绝对路径
platform_path = os.path.abspath(os.path.join(current_dir, '..','..','..'))
# 添加路径到系统路径中
sys.path.append(platform_path)
# 导入模块
from 统一平台.base.bases import *
# 构建XLSX文件的绝对路径
xlsx_file_path = os.path.join(current_dir, '..', '..', '测试数据', '新统一平台测试用例.xlsx')
class NewUnifiedPlatform:
#执行指令:
# cd ./统一平台/
# hytest --tag 新统一平台
tags = ['新统一平台']
ddt_cases = read_xlsx_data(xlsx_file_path, case_type="标准版")
def teststeps(self):
"""
执行测试步骤函数,主要用于执行读取的测试用例并进行信息统计模块功能测试操作
"""
# 从全局存储中获取webdriver对象
wd = GSTORE['wd']
name = self.name
# 刷新页面
# wd.refresh()
wd.refresh()
sleep(5)
for step in self.para:
# 赋值页面类型page
page_type = step.get('page')
# 赋值元素定位类型,并将字符串转为Enum类型
locator_type = get_by_enum(step.get('locator_type'))
# 赋值元素值
locator_value = step.get('locator_value')
# 赋值元素类型,例如:click点击、input输入框等
element_type = step.get('element_type')
# 赋值元素值,例如输入框的输入值
element_value = step.get('element_value')
# 赋值预期结果
expected_result = step.get('expected_result')
# 赋值等待时间
# sleep_time = step.get('sleep_time')
# 赋值操作步骤
step_name = step.get('step')
INFO(
f"步骤名称: {step_name}\n"
)
if element_type == "click":
safe_click((locator_type, locator_value), wd)
sleep(5)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "input":
safe_send_keys((locator_type, locator_value), element_value, wd)
sleep(2)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "SwitchWindow":
# 将字符转换为int类型
element_value = int(element_value)
wd.switch_to.window(wd.window_handles[element_value])
sleep(2)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "login":
# 退出系统登录
safe_click((By.XPATH, "//div[@class='quit']"), wd)
sleep(5)
INFO(f"开始登录,账号为:{element_value[0]},密码为:{element_value[1]}")
safe_send_keys((By.XPATH, "//input[@placeholder='手机号/用户名/邮箱']"), "admin@xty", wd)
safe_send_keys((By.XPATH, "//input[@placeholder='密码']"), "Ubains@4321", wd)
safe_send_keys((By.XPATH, "//input[@placeholder='图形验证']"), "csba", wd)
sleep(2)
INFO("对协议进行勾选")
safe_click((By.XPATH, "//div[@aria-label='提示']//span[contains(text(),'确定')]"), wd)
INFO("已经勾选协议了")
sleep(2)
safe_click((By.XPATH, "//div[@id='pane-1']//div//span[contains(text(),'登录')]"), wd)
INFO("已经点击登录了")
sleep(2)
elif element_type == "getTips":
notify_text = get_notify_text(wd, (locator_type, locator_value))
INFO(f"获取到的提示信息为:{notify_text}")
sleep(2)
CHECK_POINT(f"获取到的提示信息为:{notify_text}", expected_result in notify_text)
SELENIUM_LOG_SCREEN(wd, "75")
elif element_type == "getText":
text = elment_get_text((locator_type, locator_value), wd)
INFO(f"获取到的文本信息为:{text}")
CHECK_POINT(f"获取到的文本信息为:{text}", expected_result in text)
SELENIUM_LOG_SCREEN(wd, "75")
sleep(2)
\ No newline at end of file
{
"标准版新统一平台": "https://192.168.5.44"
}
\ No newline at end of file
server_addr: ngrok.ubsyun.com:9083
trust_host_root_certs: false
tunnels:
nat1:
proto:
tcp: 127.0.0.1:80
remote_port: 31135
ngrok -config=ngrok.cfg start nat1
\ No newline at end of file
import schedule
import queue
from Base.base import *
import time
import logging
if getattr(sys, 'frozen', False):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(__file__))
cases_dir = os.path.join(base_dir, 'cases')
if not os.path.isdir(cases_dir):
print("cases_dir 路径为:", cases_dir)
raise FileNotFoundError("请将 cases 目录放在程序同级目录下!")
"""
调试主机-执行指令:
1.打开一个终端输入:
- cd .\预定系统\
- python -m http.server 81 --directory reports
2.打开新终端输入:
- cd .\预定系统\ngrok\ngrok-调试主机\
- .\start.bat
3.再打开一个终端输入:
- cd .\预定系统\
- python .\定时执行功能测试.py
自动化运行虚拟机-执行指令:
1.打开一个终端输入:
- cd .\预定系统\
- python -m http.server 81 --directory reports
2.打开新终端输入:
- cd .\预定系统\ngrok\ngrok-自动化运行虚拟机
- .\start.bat
3.再打开一个终端输入:
- cd .\预定系统\
- python .\定时执行功能测试.py
"""
import os
import sys
def get_resource_path(relative_path):
"""
获取打包后的资源文件真实路径(适用于 PyInstaller)
:param relative_path: 相对于打包时指定的路径
:return: 真实路径字符串
"""
if getattr(sys, 'frozen', False): # 是否被打包成 .exe
base_path = sys._MEIPASS
else:
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
# 创建一个任务队列,用于存储待处理的任务
task_queue = queue.Queue()
def run_task(task, *args, **kwargs):
# 将任务及其参数放入任务队列
task_queue.put((task, args, kwargs))
logging.debug(f"任务已加入队列: {task.__name__} with args: {args} and kwargs: {kwargs}")
def worker():
# 工作线程的主循环
while True:
# 从任务队列中获取任务及其参数
task, args, kwargs = task_queue.get()
try:
# 记录任务开始执行的时间
logging.debug(f"开始执行任务: {task.__name__} with args: {args} and kwargs: {kwargs}")
# 执行任务并获取结果
result = task(*args, **kwargs)
# 如果任务有返回结果,记录日志
if result:
logging.info(result)
except Exception as e:
# 捕获任务执行过程中发生的任何异常并记录错误日志
logging.error(f"执行任务时发生错误: {e}", exc_info=True)
finally:
# 无论任务是否成功执行,都标记任务已完成
task_queue.task_done()
# 记录任务完成的时间
logging.debug(f"任务完成: {task.__name__}")
def start_workers(num_workers):
# 启动指定数量的工作线程
for _ in range(num_workers):
# 创建一个新的工作线程,目标函数为 worker,设置为守护线程
threading.Thread(target=worker, daemon=True).start()
# 启动3个工作线程
start_workers(3)
# 定时执行预定系统测试任务
schedule.every().day.at("10:00").do(run_task, run_automation_test, report_title="预定系统测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="JSON测试", ding_type="标准版巡检")
schedule.every().day.at("07:00").do(run_task, run_automation_test, report_title="兰州中石化项目测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="兰州中石化项目", ding_type="标准版巡检")
# 定时执行展厅巡检任务
# schedule.every().day.at("07:45").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31136", test_case="展厅巡检", ding_type="展厅巡检")
schedule.every().monday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="展厅巡检", ding_type="展厅巡检")
schedule.every().thursday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="展厅巡检", ding_type="展厅巡检")
schedule.every().wednesday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="展厅巡检", ding_type="展厅巡检")
schedule.every().tuesday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="展厅巡检", ding_type="展厅巡检")
schedule.every().friday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31133", test_case="展厅巡检", ding_type="展厅巡检")
# schedule.every().saturday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31136", test_case="展厅巡检", ding_type="展厅巡检")
# schedule.every().sunday.at("07:42").do(run_task, run_automation_test, report_title="展厅巡检测试报告", report_url_prefix="http://nat.ubainsyun.com:31136", test_case="展厅巡检", ding_type="展厅巡检")
try:
# 无限循环,持续检查并执行计划任务
while True:
schedule.run_pending() # 检查并执行所有待处理的任务
time.sleep(1) # 每秒检查一次
except KeyboardInterrupt:
# 捕获用户中断信号 (Ctrl+C)
logging.info("Scheduler interrupted by user.")
except Exception as e:
# 捕获其他未预期的异常
logging.error(f"Unexpected error: {e}", exc_info=True)
\ No newline at end of file
import csv
def create_AndroidMessageUp_csv():
# 定义基础数据
base_data = {
"topic": "rebootResponseTopic",
"appToken": "AND-2IK-0021",
"companyNumber": "CN-2IK-UBAINS",
"cnum": "",
"conferenceId": "100", # 初始值为字符串
"macAddress": "20:59:20:00:28:01",
}
# 输出文件路径
output_file = "MQTT模块/MQTT安卓上报_2000条.csv"
# 生成2000条数据
with open(output_file, mode="w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
# 写入表头
writer.writerow([
"topic", "clientId", "appToken", "companyNumber", "cnum",
"conferenceId", "macAddress", "authCode", "clientId", "deviceId"
])
for i in range(1, 2001):
# 格式化编号
index_str = f"{i:04d}"
client_id = f"48134e6047a19a{i:04d}"
device_id = f"aa44e258a4e1e{i:04d}"
app_token = f"AND-2IK-{index_str}"
auth_code = app_token
# conferenceId 从100开始递增
current_conference_id = str(int(base_data["conferenceId"]) + i)
# 写入一行数据
writer.writerow([
base_data["topic"], client_id, app_token, base_data["companyNumber"],
base_data["cnum"], current_conference_id, base_data["macAddress"],
auth_code, client_id, device_id
])
print(f"成功生成 {output_file} 文件,包含2000条数据。")
import csv
def create_Androidbroadcast_csv():
# 基础配置
base_topic = "/uams/android/broadcast"
output_file = "MQTT模块/MQTT心跳上报_2000条.csv"
# 生成2000条数据
with open(output_file, mode="w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
# 写入表头(根据原始CSV结构)
writer.writerow([
"topic", "clientId", "appToken", "companyNumber", "cnum",
"conferenceId", "macAddress", "authCode", "clientId", "deviceId"
])
for i in range(1, 2001):
# 格式化编号为4位数,如0001, 0002...
index_str = f"{i:04d}"
# clientId 和 deviceId 的格式
client_id = f"48134e6047a19a{index_str}"
device_id = f"aa44e258a4e1e{index_str}"
# 按照指定格式写入空字段和动态字段
writer.writerow([
base_topic, "", "", "", "",
"", "", "", client_id, device_id
])
print(f"成功生成 {output_file} 文件,包含2000条数据。")
# if __name__ == "__main__":
# create_AndroidMessageUp_csv()
# create_Androidbroadcast_csv()
\ No newline at end of file
......@@ -2,15 +2,8 @@ import sys
import os
# 获取 Base 目录的绝对路径,并加入 sys.path
current_dir = os.path.dirname(os.path.abspath(__file__))
base_dir = os.path.abspath(os.path.join(current_dir, '..', 'Base'))
if base_dir not in sys.path:
sys.path.insert(0, base_dir)
try:
from base import *
except ModuleNotFoundError as e:
print(f"ModuleNotFoundError: {e}")
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..')))
from 预定系统.Base.base import *
def suite_teardown():
browser_quit()
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论