实现一个完整的 Jenkins 自动化测试流水线,包含以下功能:
功能 | 描述 |
---|---|
✅ 参数化构建 | 支持 Git 地址、分支、平台、设备 ID、筛选用例、发送通知 等参数 |
✅ 写入配置文件 | 将参数写入配置文件,便于后续py脚本调用,无需再流水线脚本多次传入变量 |
✅ 执行自动化测试脚本 | 使用 Python + Airtest/Pytest 执行测试 |
✅ 归档日志和结果 | 保存 [log/]目录 jenkins或者http服务器 |
✅ 发送通知 | 通过邮件或钉钉发送测试报告 |
[Jenkins Pipeline]
↓
[clone or pull 拉取代码]
↓
[writeJSON → jenkins_config.json 写入配置]
↓
[python jenkins/env_setup.py 更新虚拟环境]
↓
[python jenkins/test_executor.py 执行测试] → [解析allure报告] → [写入测试结果到result.json] → [备份log/allure报告到文件服务]
↓
[python jenkins/notification_sender.py] → [解析result.json.py] → [ 拼接通知内容] → [发送邮件/钉钉通知]
pipeline {
agent any
environment {
// 设置工作空间路径为 ${WORKSPACE}/SuuntoTest
WORKSPACE_DIR = "${env.WORKSPACE}\\SuuntoTest"
// git项目一致的话,多个任务公用一个job,避免重复拉取代码
// 设置工作空间路径为 ${WORKSPACE} 的上级目录 + /AndroidSmoke,多个任务公用一个代码目录
// WORKSPACE_DIR = "${new File(env.WORKSPACE).getParent()}/AndroidSmoke"
}
parameters {
// string(name: 'JOB_NAME', defaultValue: 'SuuntoTest', description: 'Jenkins Job 名称')
string(name: 'GIT_REPO_URL', defaultValue: '', description: 'Git SSH 地址')
string(name: 'BRANCH', defaultValue: 'dengping_dev', description: '分支名称')
string(name: 'PLATFORM', defaultValue: 'android', description: '测试平台 (android/ios/ng/dilu)')
string(name: 'DEVICE_ID', defaultValue: '', description: '设备 ID')
string(name: 'TESTCASE', defaultValue: 'testcase_android/', description: '测试用例目录')
string(name: 'TEST_MARKER', defaultValue: 'SMOKE and not NG', description: '''
根据mark标签选择测试 适配-m 参数:
示例:
SMOKE
SMOKE and not NG
''')
string(name: 'TEST_NAME_EXPR', defaultValue: '', description: '''
根据测试函数/类名匹配选择测试 适配-k 参数:
示例:
test_week_tab
TestCalendar
''')
// string(name: 'NOTIFY_EMAIL', defaultValue: '[email protected]', description: '接收通知的邮箱')
credentials(name: 'GIT_SSH_CRED', credentialType: 'ssh', defaultValue: 'github-ssh', description: 'SSH凭证用于拉取代码-私钥Jenkins配置的凭据ID')
string(name: 'SEND_METHOD', defaultValue: 'dingtalk', description: '通知方式email_163、email_aliyun、dingtalk') }
stages {
stage('Checkout Code') {
steps {
script {
echo "准备拉取代码到目录: ${env.WORKSPACE_DIR}"
if (!fileExists("${env.WORKSPACE_DIR}\\.git")) {
echo "首次拉取,执行 git clone..."
bat """
@echo off
setlocal
set WORKSPACE_DIR=${env.WORKSPACE_DIR}
if not exist \"${env.WORKSPACE_DIR}\" mkdir \"${env.WORKSPACE_DIR}\"
cd /d \"${env.WORKSPACE_DIR}\"
echo 当前路径: %CD%
echo 克隆代码到SuuntoTest
git clone ${GIT_REPO_URL} SuuntoTest
"""
} else {
echo "已存在代码仓库,执行 git pull..."
dir("${env.WORKSPACE_DIR}") {
withCredentials([sshUserPrivateKey(credentialsId: 'GIT_SSH_CRED', keyFileVariable: 'GIT_KEY')]) {
bat """
@echo on
set GIT_SSH_COMMAND=ssh -i \"${env.GIT_KEY}\"
cd /d \"${env.WORKSPACE_DIR}\"
git pull origin ${BRANCH}
"""
}
}
}
}
}
}
stage('Initialize Config') {
steps {
script {
echo "初始化配置并写入 jenkins_config.json"
def JENKINS_CONFIG = [
GIT_REPO_URL: params.GIT_REPO_URL,
BRANCH: params.BRANCH,
WORKSPACE_DIR: env.WORKSPACE_DIR,
PLATFORM: params.PLATFORM,
DEVICE_ID: params.DEVICE_ID,
TESTCASE: params.TESTCASE,
JENKINS_BUILD_URL: env.BUILD_URL,
NOTIFY_EMAIL: params.NOTIFY_EMAIL,
JOB_URL: env.JOB_URL,
SEND_METHOD: env.SEND_METHOD,
BUILD_UR: env.BUILD_UR,
BUILD_NUMBER: env.BUILD_NUMBER,
// NODE_NAME = "node/3"
]
def CONFIG_PATH = "${env.WORKSPACE_DIR}\\jenkins\\jenkins_config.json"
writeJSON file: CONFIG_PATH, json: JENKINS_CONFIG
}
}
}
stage('Setup Virtual Environment') {
steps {
script {
echo "设置虚拟环境并安装依赖..."
dir(env.WORKSPACE_DIR) {
bat """
@echo on
python jenkins\\env_setup.py
"""
}
}
}
}
stage('Run Tests') {
steps {
script {
echo "开始执行测试用例..."
try {
dir(env.WORKSPACE_DIR) {
bat """
@echo on
.venv\\Scripts\\python.exe jenkins\\executor.py
"""
}
} catch (Exception e) {
currentBuild.result = 'FAILURE'
throw e
}
}
}
}
// stage('Archive Artifacts') {
// steps {
// script {
// def latestLogDir = powershell(returnStdout: true, script: '''
// $logPath = "$env:WORKSPACE_DIR\\log"
// if (-Not (Test-Path -Path $logPath)) {
// Write-Output ""
// exit 1
// }
// $latestDir = Get-ChildItem -Path $logPath -Directory | Sort-Object -Property Name -Descending | Select-Object -First 1
// if ($latestDir) {
// Write-Output $latestDir.Name
// } else {
// Write-Output ""
// }
// ''').trim()
// if (latestLogDir == "") {
// echo "没有找到日志目录,跳过归档"
// return
// }
// echo "${env.WORKSPACE_DIR}\\\\log\\\\${latestLogDir}\\\\allure\\\\rawdata"
// echo "${env.WORKSPACE}\\\\allure-report"
// // 生成报告(重新解析项目里allure报告的json文件)
// // bat """
// // @echo on
// // allure generate "${env.WORKSPACE_DIR}\\\\log\\\\${latestLogDir}\\\\allure\\\\rawdata" -o "${env.WORKSPACE_DIR}\\\\allure-report" --clean
// // """
// // 发布报告(必须指向 .json 数据目录)
// // allure includeProperties: false, jdk: '', results: [[path: "${env.WORKSPACE_DIR}\\\\allure-report\\\\data\\\\test-cases"]]
// // allure includeProperties: false, jdk: 'JDK1.8', results: [[path: "log\\\\${latestLogDir}\\\\allure\\\\rawdata"]]
// // allure includeProperties: false, jdk: '', results: [[path: "SuuntoTest\\\\log\\\\${latestLogDir}\\\\allure\\\\rawdata"]]
// // def artifactPath = "SuuntoTest\\\\log\\\\${latestLogDir}\\\\**"
// // archiveArtifacts artifacts: artifactPath, allowEmptyArchive: false
// }
// }
// }
stage('Send Notification') {
steps {
script {
echo "发送测试结果通知..."
dir("${env.WORKSPACE_DIR}") {
bat '''
@echo on
set PYTHONIOENCODING=utf-8
cd /d \"${env.WORKSPACE_DIR}\"
.venv\\Scripts\\python.exe jenkins\\notification_sender.py
'''
}
}
}
}
}
post {
success {
echo "✅ 流水线执行成功"
}
failure {
echo "❌ 流水线执行失败"
}
}
}
注意:确保 Jenkins 工作空间中存在
log/
文件夹用于存放日志。
jenkins/
├── __init__.py
├── config_manager.py
├── env_setup.py
├── jenkins_config.json
├── notification_sender.py
├── result.json
└── test_executor.py
config_manager.py: 配置管理脚本,用于处理Jenkins的配置相关操作。
env_setup.py: 环境设置脚本,可能用于准备测试或部署环境。
jenkins_config.json: Jenkins配置文件,存储Jenkins的相关配置信息。
notification_sender.py: 通知发送脚本,可能用于在任务完成后发送通知。
result.json: 结果文件,通常用于存储测试结果或其他执行输出。
test_executor.py: 测试执行器脚本,负责运行测试用例并生成报告。
jenkins_config.json
。writeJSON
行为。# config_manager.py
import json
import os
import sys
# 获取当前脚本所在目录(即 jenkins/ 目录)
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
CONFIG_FILE = os.path.join(CURRENT_DIR, 'jenkins_config.json')
def update_config(new_values, config_file=CONFIG_FILE):
"""
更新指定的配置字段,保留原有字段
:param new_values: 新传入的配置字典
:param config_file: 配置文件路径
"""
# 如果配置文件存在,加载现有配置
if os.path.exists(config_file):
with open(config_file, 'r', encoding='utf-8') as f:
current_config = json.load(f)
else:
current_config = {}
# 只更新传入的字段,None 值将被忽略
for key, value in new_values.items():
if value is not None:
current_config[key] = value
# 写回配置文件
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(current_config, f, indent=4)
print(f"配置已更新至 {os.path.abspath(config_file)}")
def load_config():
"""加载 jenkins_config.json 配置文件"""
if not os.path.exists(CONFIG_FILE):
raise FileNotFoundError(f"配置文件 {CONFIG_FILE} 不存在")
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
if __name__ == "__main__":
import sys
import json
if len(sys.argv) < 2:
print("Usage: python config_manager.py ''" )
print("或: python config_manager.py '@'" )
print("示例: python config_manager.py '{\"PLATFORM\": \"android\"}'")
print("示例: python config_manager.py '@test_params.json'")
sys.exit(1)
raw_input = sys.argv[1]
# 如果以 @ 开头,则从文件加载 JSON
if raw_input.startswith('@'):
file_path = raw_input[1:]
try:
with open(file_path, 'r', encoding='utf-8') as f:
raw_json = f.read()
except Exception as e:
print(f"❌ 无法读取文件: {e}")
sys.exit(1)
else:
raw_json = raw_input
# 去除首尾可能存在的单引号或双引号
raw_json = raw_json.strip().lstrip("'").lstrip('"').rstrip("'").rstrip('"')
try:
new_config = json.loads(raw_json)
except json.JSONDecodeError as e:
print(f"❌ JSON 解析失败: {e}")
print("请确保输入的是合法的 JSON 格式字符串,例如:")
print("{\"PLATFORM\": \"android\", \"DEVICE_ID\": \"emulator-5554\"}")
sys.exit(1)
update_config(new_config)
print("✅ 更新后的配置:")
print(load_config())
jenkins_config.json
中读取配置;result.json
测试结果文件;.venv
虚拟环境调用。import shutil
import subprocess
import os
import sys
import time
import json
import config_manager
# from jenkins import config_manager
print(f"Python executable: {sys.executable}")
print(f"Current working directory: {os.getcwd()}")
print("sys.path:")
for p in sys.path:
print(p)
def run_tests(workspace_dir, testcase, device_id, platform):
"""
从 jenkins_config.json 中读取参数并执行测试用例
"""
# 切换到项目根目录
os.chdir(workspace_dir)
# 设置 Python 虚拟环境路径
python_executable = os.path.join(workspace_dir, '.venv', 'Scripts', 'python.exe')
# print(python_executable)
# 检查 python.exe 是否存在
if not os.path.exists(python_executable):
raise FileNotFoundError(f"找不到虚拟环境中的 python.exe: {python_executable}")
script_path = os.path.join(workspace_dir, 'run_jenkins_main.py')
start_time = time.time()
print("正在执行测试脚本...")
result = subprocess.run([
python_executable,
script_path,
platform,
device_id,
testcase
], capture_output=True, text=True)
time.sleep(5)
# 显式打印输出
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("STDERR:")
print(result.stderr)
end_time = time.time()
# stdout = result.stdout
# stderr = result.stderr
# exit_code = result.returncode
log_dir = os.path.join(workspace_dir, "log")
allure_results = count_allure_results(log_dir)
duration_seconds = round(end_time - start_time, 2)
hours, remainder = divmod(duration_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
duration = f"{int(hours)}小时{int(minutes)}分钟{seconds:.2f}秒"
result_data = {
"start_time": start_time,
"end_time": end_time,
"duration": duration,
"platform": platform,
"device_id": device_id,
"testcase": testcase,
"passed": allure_results.get("passed", 0),
"failed": allure_results.get("failed", 0),
"broken": allure_results.get("broken", 0),
"skipped": allure_results.get("skipped", 0),
"total": allure_results.get("total", 0),
"pass_rate": allure_results.get("pass_rate", 0),
# "exit_code": exit_code,
# "stdout": stdout,
# "stderr": stderr
}
# 将结果写入本地文件
return result_data
def write_result_to_file(result_data, workspace_dir):
result_file = os.path.join(workspace_dir, 'jenkins/result.json')
with open(result_file, 'w', encoding='utf-8') as f:
json.dump(result_data, f, indent=4)
print(f"测试结果已保存到 {os.path.abspath(result_file)}")
def count_allure_results(log_dir):
"""
统计 allure 报告中的成功和失败用例数
"""
# 获取 log_dir 下的所有子文件夹
sub_dirs = [d for d in os.listdir(log_dir) if os.path.isdir(os.path.join(log_dir, d))]
if not sub_dirs:
print("没有找到子文件夹。")
return
# 找出最新的子文件夹
latest_sub_dir = max(sub_dirs, key=lambda x: os.path.getctime(os.path.join(log_dir, x)))
result_dir = os.path.join(log_dir, latest_sub_dir, "allure", "rawdata")
print(result_dir)
passed = 0
failed = 0
broken = 0
skipped = 0
for file_name in os.listdir(result_dir):
if file_name.endswith(".json"):
file_path = os.path.join(result_dir, file_name)
with open(file_path, "r", encoding="utf-8") as f:
data = json.load(f)
status = data.get("status")
if status == "passed":
passed += 1
elif status == "failed":
failed += 1
elif status == "broken":
broken += 1
elif status == "skipped":
skipped += 1
total = passed + failed + broken + skipped
pass_rate = round(passed / (total - skipped) * 100, 2) if (total - skipped) > 0 else 100
return {
"passed": passed,
"failed": failed,
"broken": broken,
"skipped": skipped,
"total": total,
"pass_rate": pass_rate
}
def copy_log_file_to_http_server(log_dir):
"""
将 log_dir 下的最新子文件夹复制到 D:\auto_test_log\allure-report
"""
# 获取 log_dir 下的所有子文件夹
sub_dirs = [d for d in os.listdir(log_dir) if os.path.isdir(os.path.join(log_dir, d))]
if not sub_dirs:
print("没有找到子文件夹。")
return
# 找出最新的子文件夹
latest_sub_dir = max(sub_dirs, key=lambda x: os.path.getctime(os.path.join(log_dir, x)))
source_path = os.path.join(log_dir, latest_sub_dir)
# 定义目标路径
target_base_path = r"D:\auto_test_log\allure-report"
target_path = os.path.join(target_base_path, latest_sub_dir)
# 如果目标路径已存在,则删除旧目录
if os.path.exists(target_path):
shutil.rmtree(target_path)
# 复制文件夹
try:
shutil.copytree(source_path, target_path)
print(f"成功将 {latest_sub_dir} 复制到 {target_path}")
except Exception as e:
print(f"复制失败: {e}")
if __name__ == "__main__":
config = config_manager.load_config()
workspace_dir = config.get('WORKSPACE_DIR')
device_id = config.get('DEVICE_ID', '')
testcase = config.get('TESTCASE', 'testcase_android/calendar/test_calendar.py::TestCalendar::test_week_tab')
# testcase = "testcase_android/calendar/test_calendar.py::TestCalendar::test_week_tab"
platform = config.get('PLATFORM', 'android')
result_data = run_tests(workspace_dir, testcase, device_id, platform)
print(result_data)
write_result_to_file(result_data, workspace_dir)
log_dir = os.path.join(workspace_dir, 'log') # 确定日志目录
copy_log_file_to_http_server(log_dir) # 复制最新日志文件夹
# print(count_allure_results(log_dir))
jenkins_config.json
和 result.json
加载数据;_generate_report_content()
提供结构化报告。# notification_sender.py
import os
import json
import time
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import requests
# 读取配置文件
JENKINS_DIR = os.path.dirname(os.path.abspath(__file__))
RESULT_JSON_PATH = os.path.join(JENKINS_DIR, 'result.json')
CONFIG_JSON_PATH = os.path.join(JENKINS_DIR, 'jenkins_config.json')
def _load_result_data():
"""从 result.json 加载测试结果数据"""
with open(RESULT_JSON_PATH, 'r', encoding='utf-8') as f:
return json.load(f)
def _load_config_data():
"""从 jenkins_config.json 加载配置数据"""
with open(CONFIG_JSON_PATH, 'r', encoding='utf-8') as f:
return json.load(f)
def get_latest_log_dir(workspace_dir):
# 获取 log 目录下最新的子目录名称(按修改时间排序)
# workspace_dir = _load_config_data().get('WORKSPACE_DIR')
log_dir = os.path.join(workspace_dir, 'log')
log_dirs = [d for d in os.listdir(log_dir) if os.path.isdir(os.path.join(log_dir, d))]
return max(log_dirs, key=lambda x: os.path.getmtime(os.path.join(log_dir, x)))
# def build_log_url():
# # 获取 log 目录的最新子目录
# log_dir = get_latest_log_dir()
# # 在build——url中截取IP地址
# job_url = _load_config_data().get('JOB_URL')
# job_url = job_url.split('/job')[0].replace('8080', '8081')
# # 拼接 log url
# log_url = f"{job_url}/log/{log_dir}"
# return log_url
#
#
# def build_allure_url():
# allure_url = build_log_url() + "/allure/report"
# return allure_url
def build_log_url_and_allure_url(workspace_dir, job_url):
"""
构建日志URL和Allure报告URL。
本函数旨在生成两个重要的URL:
1. 日志URL(log_url),用于访问最新日志目录下的日志信息。
2. Allure报告URL(allure_url),用于展示最新测试结果的Allure报告。
无参数。
返回:
- log_url: 最新日志的访问URL。
- allure_url: Allure报告的访问URL。
"""
# 获取 log 目录的最新子目录
log_dir = get_latest_log_dir(workspace_dir)
# 从配置数据中提取作业URL,并修改端口从8080到8085,以访问日志服务
job_ip = job_url.split('/')[2].replace(':8080', ':8085')
# 构建日志URL
log_url = f"http://{job_ip}/{log_dir}"
# 在日志URL基础上,构建Allure报告URL
allure_url = log_url + "/allure/report"
# 返回日志URL和Allure报告URL
return log_url, allure_url
# def get_jenkins_log_url():
# # 需要在流水线脚本中set 变量
# # set LATEST_LOG_DIR =${latestLogDir}
# # set JENKINS_BUILD_URL =${env.BUILD_URL}
# build_url = os.getenv("JENKINS_BUILD_URL", "未知")
# latest_log_dir = os.getenv("LATEST_LOG_DIR", "")
#
# if build_url == "未知" or not latest_log_dir:
# return "未知"
#
# return f"{build_url.rstrip('/')}/artifact/SuuntoTest/log/{latest_log_dir}/"
def get_jenkins_log_dir():
job_url = _load_config_data().get('JOB_URL') # e.g., http://localhost:8080/job/AndroidSmokePipline/
build_number = _load_config_data().get("BUILD_NUMBER") # Jenkins 构建号,如 "35"
node_name = _load_config_data().get("NODE_NAME", "node/3") # 可以在 Jenkins Pipeline 中设置或默认使用 node/3
if not build_number:
print("BUILD_NUMBER 环境变量未设置,日志路径可能不准确")
build_number = "unknown"
log_dir = get_latest_log_dir()
# 构造完整日志路径
return f"{job_url.rstrip('/')}/{build_number}/execution/{node_name}/ws/SuuntoTest/log/{log_dir}/"
def get_jenkins_log_dir_new():
build_url = _load_config_data().get('JENKINS_BUILD_URL') # Jenkins 提供的 BUILD_URL
log_dir = get_latest_log_dir()
# 使用 artifact 路径访问日志目录
return f"{build_url.rstrip('/')}/artifact/SuuntoTest/log/{log_dir}/"
def _generate_report_content(result_info, get_repo_url, branch, jenkins_build_url, workspace_dir, job_url):
"""
生成统一的报告内容
:param result_info: 测试结果字典
:param config_info: 配置信息字典
:return: (title, content)
"""
title = f"【自动化测试报告】 - {result_info['platform']}"
# # 构造 Jenkins 下的 log 文件夹访问路径
# jenkins_log_url = jenkins_url.rstrip('/') + 'artifact/log/'
content = f"""
【Jenkins 构建地址】
{jenkins_build_url}
【Git 项目信息】
仓库地址:{get_repo_url}
分支:{branch}
【执行时间】
开始时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result_info['start_time']))}
结束时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result_info['end_time']))}
总耗时:{result_info['duration']}
【执行信息】
平台:{result_info['platform']}
设备ID:{result_info['device_id']}
测试用例:{result_info['testcase']}
【日志目录及报告】
Jenkins 日志访问地址:{build_log_url_and_allure_url(workspace_dir, job_url)[0]}
allure 报告访问地址:{build_log_url_and_allure_url(workspace_dir, job_url)[1]}
【执行结果】
passed:{result_info['passed']}
failed:{result_info['failed']}
broken:{result_info['broken']}
skipped:{result_info['skipped']}
总数:{result_info['total']}
通过率:{result_info['pass_rate']}%
"""
return title, content
# def send_email_163():
# """
# 使用 163 邮箱发送测试结果通知。
# """
# result_info = _load_result_data()
# config_info = _load_config_data()
#
# title, content = _generate_report_content(result_info, config_info)
#
# # 从配置中获取邮件信息
# sender = config_info.get("MAIL_USERNAME_163", "[email protected]")
# receiver = config_info.get("NOTIFY_EMAIL_163", "[email protected]")
# smtp_server = "smtp.163.com"
# smtp_port = 465
# username = config_info.get("MAIL_USERNAME_163", "")
# password = config_info.get("MAIL_PASSWORD_163", "")
#
# message = MIMEText(content, 'plain', 'utf-8')
# message['From'] = Header(sender)
# message['To'] = Header(receiver)
# message['Subject'] = Header(title)
# server = None
# try:
# server = smtplib.SMTP_SSL(smtp_server, smtp_port)
# server.login(username, password)
# server.sendmail(sender, [receiver.split(',')], message.as_string())
# print("163 邮件通知已发送")
# except Exception as e:
# print(f"发送 163 邮件失败: {e}")
# finally:
# server.quit()
#
#
def send_email_aliyun(get_repo_url, branch, jenkins_build_url, workspace_dir, job_url):
"""
使用 阿里云邮箱 发送测试结果通知。
"""
result_info = _load_result_data()
config_info = _load_config_data()
title, content = _generate_report_content(result_info, get_repo_url, branch, jenkins_build_url, workspace_dir, job_url)
# 从配置中获取邮件信息
sender = ""
receiver = ""
smtp_server = ""
smtp_port = 465
username = ""
password = ""
message = MIMEText(content, 'plain', 'utf-8')
message['From'] = Header(sender)
message['To'] = Header(receiver)
message['Subject'] = Header(title)
server = None
try:
server = smtplib.SMTP_SSL(smtp_server, smtp_port)
server.login(username, password)
if "," in receiver:
server.sendmail(sender, receiver.split(','), message.as_string())
else:
server.sendmail(sender, receiver, message.as_string())
print("阿里云邮箱通知已发送")
except Exception as e:
print(f"发送阿里云邮件失败: {e}")
finally:
server.quit()
def send_dingtalk(webhook_url, jenkins_build_url, get_repo_url, branch, workspace_dir, job_url):
"""
使用钉钉机器人发送测试结果通知。
"""
result_info = _load_result_data()
if not webhook_url:
print("缺少钉钉 Webhook URL,请在 jenkins_config.json 中配置 DINGTALK_WEBHOOK")
return
title = f"【自动化测试报告】 - {result_info['platform']}"
text = f"""
#### {title}
> **Jenkins 构建地址**
{jenkins_build_url}
account/password:user_1/user123
> **Git 项目信息**
仓库地址:{get_repo_url}
分支:{branch}
> **执行时间**
开始时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result_info['start_time']))}
结束时间:{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(result_info['end_time']))}
总耗时:{result_info['duration']}
> **执行信息**
平台:{result_info['platform']}
设备ID:{result_info['device_id']}
测试用例:{result_info['testcase']}
> **测试报告、日志**
测试报告地址:{build_log_url_and_allure_url(workspace_dir, job_url)[1]}
日志地址:{build_log_url_and_allure_url(workspace_dir, job_url)[0]}
> **执行结果**
passed:{result_info['passed']}
failed:{result_info['failed']}
broken:{result_info['broken']}
skipped:{result_info['skipped']}
总数:{result_info['total']}
通过率:{result_info['pass_rate']}%
"""
data = {
"msgtype": "text",
"text": {
"content": text,
"mentioned_list": [] # 可指定提醒用户 @all 表示全体成员
}
}
print(data)
try:
response = requests.post(webhook_url, json=data)
if response.status_code == 200:
print("钉钉通知已发送")
else:
print(f"钉钉通知发送失败: {response.text}")
except Exception as e:
print(f"发送钉钉通知失败: {e}")
if __name__ == "__main__":
# 自动化测试new群
webhook_url = ''
config_info = _load_config_data()
send_method = config_info.get("SEND_METHOD", 'dingtalk')
# send_method = "email_aliyun"
jenkins_build_url = config_info.get("JENKINS_BUILD_URL")
get_repo_url = config_info.get("GIT_REPO_URL")
branch = config_info.get("BRANCH")
workspace_dir = config_info.get("WORKSPACE_DIR")
job_url = config_info.get("JOB_URL")
if send_method == "dingtalk":
send_dingtalk(webhook_url, jenkins_build_url, get_repo_url, branch, workspace_dir, job_url)
# elif send_method == "email_163":
# send_email_163()
elif send_method == "email_aliyun":
send_email_aliyun(get_repo_url, branch, jenkins_build_url, workspace_dir, job_url)
else:
print("无法识别的推送方式")
# print(get_latest_log_dir())
# print(get_jenkins_log_dir())
# print(get_jenkins_log_dir_new())
# send_dingtalk()
# print(build_allure_url())
# print(build_log_url_and_allure_url()[1])
# print(build_log_url_and_allure_url()[0])
# env_setup.py
import sys
import os
import subprocess
import config_manager
# 获取当前脚本所在目录(即 jenkins/ 目录)
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
# 获取项目根目录(SuuntoTest/)
PROJECT_ROOT = os.path.abspath(os.path.join(CURRENT_DIR, '..'))
def setup_virtual_env(workspace_dir):
"""
设置虚拟环境并安装依赖。
:param workspace_dir: 工作空间目录
"""
os.chdir(workspace_dir)
venv_path = os.path.join(workspace_dir, '.venv')
# Step 1: 创建虚拟环境(如果不存在)
if not os.path.exists(venv_path):
print("Creating virtual environment...")
result = subprocess.run([sys.executable, "-m", "venv", venv_path])
if result.returncode != 0:
print("Failed to create virtual environment.")
exit(1)
# Step 2: 确保虚拟环境中 python.exe 存在
venv_python = os.path.join(venv_path, "Scripts", "python.exe")
if not os.path.exists(venv_python):
print(f"Error: {venv_python} does not exist! Virtualenv creation failed.")
exit(1)
# Step 3: 升级 pip(使用虚拟环境中的 python)
print("Upgrading pip in virtual environment...")
subprocess.run([venv_python, "-m", "pip", "install", "--upgrade", "pip"])
# Step 4: 安装 pipenv(使用虚拟环境中的 pip)
venv_pip = os.path.join(venv_path, "Scripts", "pip.exe")
print("Installing pipenv...")
# 检查虚拟环境中pipenv是否存在
if not os.path.exists(os.path.join(venv_pip, "Scripts", "pipenv")):
subprocess.run([venv_pip, "install", "pipenv"])
# 判断是否存在 Pipfile.lock
has_lock_file = os.path.exists(os.path.join(workspace_dir, 'Pipfile.lock'))
venv_pipenv = os.path.join(venv_path, "Scripts", "pipenv.exe")
try:
if has_lock_file:
print('Using existing Pipfile.lock to install dependencies...')
result = subprocess.run([venv_pipenv, 'install'], check=True)
# 检测安装结果,虚拟环境是否有airtest库
if not os.path.exists(os.path.join(venv_path, 'Lib', 'site-packages', 'airtest')):
print('airtest not found in Pipfile.lock, installing...')
# 删除 Pipfile.lock
if os.path.exists(os.path.join(workspace_dir, 'Pipfile.lock')):
os.remove(os.path.join(workspace_dir, 'Pipfile.lock'))
# 重新 lock 并安装
try:
subprocess.run([venv_pipenv, 'lock', '--pypi-mirror', 'https://pypi.tuna.tsinghua.edu.cn/simple'],
check=True)
subprocess.run([venv_pipenv, 'install'], check=True)
except subprocess.CalledProcessError as retry_e:
print(f'Retry failed with error code {retry_e.returncode}')
print('Tip: Manually check your Pipfile or network connection.')
exit(1)
else:
raise FileNotFoundError("No Pipfile.lock found, forcing lock and install...")
except subprocess.CalledProcessError as e:
print(f'Failed to install dependencies with error code {e.returncode}')
print('Attempting to remove Pipfile.lock and retrying...')
# 删除 Pipfile.lock
if os.path.exists(os.path.join(workspace_dir, 'Pipfile.lock')):
os.remove(os.path.join(workspace_dir, 'Pipfile.lock'))
# 重新 lock 并安装
try:
subprocess.run([venv_pipenv, 'lock', '--pypi-mirror', 'https://pypi.tuna.tsinghua.edu.cn/simple'], check=True)
subprocess.run([venv_pipenv, 'install'], check=True)
except subprocess.CalledProcessError as retry_e:
print(f'Retry failed with error code {retry_e.returncode}')
print('Tip: Manually check your Pipfile or network connection.')
exit(1)
if __name__ == '__main__':
config = config_manager.load_config()
setup_virtual_env(PROJECT_ROOT)
# 接收Jenkins传入的platform,device_id,testcase
import sys
import time
from testcase_android.runner_android import AndroidSmokeTestRunner
from testcase_ios.runner_ios import IosSmokeTestRunner
from jenkins import config_manager
platform_android = "android"
platform_ios = "ios"
platform_ng = "ng"
platform_dilu = "dilu"
concat_char = "+"
if __name__ == '__main__':
print("run_jenkins_main.py is running!")
config = config_manager.load_config()
if len(sys.argv) >= 3:
platform = sys.argv[1]
device_id = sys.argv[2]
testcase = sys.argv[3]
else:
print("No enough command line arguments, using default values.")
config = config_manager.load_config()
platform = config.get('PLATFORM', 'android')
device_id = config.get('DEVICE_ID', '')
testcase = config.get('TESTCASE', 'testcase_android/')
test_mark = config.get('TEST_MARKER', '')
test_name_expr = config.get('TEST_NAME_EXPR', '')
start_time = time.time()
runner = None
if platform == platform_android:
runner = AndroidSmokeTestRunner(phone_udid=device_id, testcase=testcase, open_allure_report=False)
elif platform == platform_ios:
runner = IosSmokeTestRunner(phone_udid=device_id, testcase=testcase)
elif platform == platform_ng:
# create ng platform smoke test runner
pass
elif platform == platform_dilu:
# create ng platform smoke test runner
pass
elif platform == platform_android + concat_char + platform_ng:
# create android+ng platform smoke test runner
pass
elif platform == platform_android + concat_char + platform_dilu:
# create android+dilu platform smoke test runner
pass
elif platform == platform_ios + concat_char + platform_ng:
# create ios+ng platform smoke test runner
pass
elif platform == platform_ios + concat_char + platform_dilu:
# create ios+dilu platform smoke test runner
pass
else:
print("args are error, exit")
exit(1)
# 接收pytest -m -k 参数
if test_mark or test_name_expr:
runner.start_test(test_mark=test_mark, test_name_expr=test_name_expr)
else:
runner.start_test()
在 Jenkins 创建任务时建议添加如下参数:
参数名 | 类型 | 示例值 | 用途 |
---|---|---|---|
GIT_REPO_URL |
String | https://github.com/your-repo.git | Git 仓库地址 |
GIT_SSH_CRED |
String | https://github.com/your-repo.git | SSH凭据ID |
BRANCH |
String | main | 分支名称 |
PLATFORM |
String | android / ios | 平台 |
DEVICE_ID |
String | emulator-5554 / 3bbca138 | 设备 ID |
TESTCASE |
String | testcase_android/test_example.py::TestClass::test_method | 测试用例 支持-m -k参数根据节点 ID 选择测试:testcase_android/calendar/test_calendar.py::TestCalendar::test_week_tab根据mark标签选择测试:-m SMOKE根据测试名称选择测试:-k test_month_tab |
SEND_METHOD |
Choice | email_163 / dingtalk | 通知方式 |
NOTIFY_EMAIL_163 |
String | [email protected] | 163 收件人 |
MAIL_USERNAME_163 |
String | [email protected] | 163 登录用户名 |
MAIL_PASSWORD_163 |
Password | ********* | 163 授权码 |
DINGTALK_WEBHOOK |
String | https://oapi.dingtalk.com/robot/send?access_token=xxx | 钉钉 Webhook |
WORKSPACE_DIR |
String | E:/project/SuuntoTest/ | Jenkins 工作空间路径(可选) |
功能 | 说明 |
---|---|
多设备支持 | 支持多设备并行执行测试,一个job同时运行多次会污染配置文件(缓存变量,测试用例多少配置?未知),多job可解决 |
多节点支持 | 用例数量过多后支持多节点并行执行测试 |
多节点绑定多设备ID | 一个节点绑定多台设备,支持多节点多设备并行(设备ID跟节点做映射) |
自动分配空闲节点 | 定时自动触发,自动分配空闲节点,并发测试(设备ID跟节点做映射) |
读取已有allure报告 | 生成可视化报告 |
每日执行通过率记录看板 | 保留每次定时任务执行的测试记录,通过看板展示在报告通知 |
失败重试机制 | 失败用例,二次运行,只跑失败用例 |
模块化通知 | 通过配置文件,将通知发送到对应模块的测试群,@对应人员(后期,自动化需要分模跑的时候) |