│ Jenkinsfile // Jenkins 流水线配置
│ main-ci.py // CI 执行入口
│ pytest.ini // pytest 参数配置
│ requirements.txt // 第三方依赖
├─case // 用例目录
│ └─test_<PLATFORM_NAME> // 平台用例目录
│ │ └─test_<PACKAGE_NAME> // 包用例目录
│ │ │ conftest.py // fixture 配置
│ │ │ test_<MODULE_NAME>.py // 模块用例
│ │ ├─common // 模块通用功能
│ │ ├─data // 模块配置
│ │ ├─page // 模块 page 封装
│ │ └─resource // 资源依赖
├─ci // CI 配置目录
│ │ docker-compose.yml // Docker Compose Linux 配置
│ │ docker-compose-win.yml // Docker Compose Windows 配置
│ └─Dockerfile // Docker Python 容器配置
├─config // 全局配置
├─log // 测试过程中生成的 log
├─modules // 自动化组维护的模块
└─results // 测试数据目录
/*
* 需要额外安装Jenkins插件:
* - Extended Choice Parameter
* - Subversion / Git
* - Allure Jenkins
*/
def NODES = params.getOrDefault('NODES', 'master') // 执行节点
def PLATFORM = params.getOrDefault('PLATFORM', '') // 平台
class Globals {
static String MODULES = '' // 测试用例
static LinkedHashMap PORTS = [
'XX_COM': '/dev/null',
'YY_COM': '/dev/null',
'ZZ_COM': '/dev/null',
] // 外部设备端口
}
// 获取用例模块
def getModules() {
cases = sh(
script: "ls ./case/test_${PLATFORM.toLowerCase()}/ |grep test_",
returnStdout: true
)
int start = 0
for (int i=0; i < cases.length(); i++ ) {
if (cases.getAt(i) == '\n') {
Globals.MODULES += "./case/test_${PLATFORM.toLowerCase()}/" + cases.substring(start, i) + ','
start = i + 1
}
}
}
// 获取外部设备的端口
def getPorts() {
// 刷新设备规则
sh 'udevadm control --reload-rules || true'
sh 'udevadm trigger || true'
// 等待10s再获取设备
sleep 10
ports = sh(
script: "ls -l /dev/ |grep -E 'ttyUSB-|ttyACM-' | awk '{ print \$9,\$10,\$11 }'",
returnStdout: true
)
int start = 0
for (int i=0; i<ports.length(); i++) {
if (ports.getAt(i) == '\n' && start < ports.length()) {
def device = '/dev/' + ports.substring(start, i).split('->')[-1].strip()
def link = ports.substring(start, i).split('->')[0].strip()
switch(link) {
case 'ttyUSB-XX':
Globals.PORTS['XX_COM'] = device
break
case 'ttyUSB-YY':
Globals.PORTS['YY_COM'] = device
break
case 'ttyUSB-ZZ':
Globals.PORTS['ZZ_COM'] = device
break
default:
println "unknown device: ${link}"
}
start = i + 1
}
}
}
pipeline {
agent {
node {
label NODES
}
}
stages {
stage('Set Environment Variables') {
steps {
script {
getPorts()
getModules()
modules = input(
message: '请选择测试用例',
parameters: [
extendedChoice(name: 'MODULES', type: 'PT_MULTI_SELECT', value: Globals.MODULES, visibleItemCount: 20, description: '测试用例'),
text(name: 'MODULES_EXTRA', description: '测试用例补充项,填写后和MODULES合并,多项英文逗号分隔')
]
)
// 添加环境变量
def envs = "BUILD_URL=${env.BUILD_URL}\nCI=${env.CI}\n"
for (_ in params) {
envs += "${_.key}=${_.value.toString().split('\n').join('')}\n"
}
for (_ in Globals.PORTS) {
envs += "${_.key}=${_.value.toString().split('\n').join('')}\n"
}
for (_ in modules) {
envs += "${_.key}=${_.value.toString().split('\n').join('')}\n"
}
writeFile(
file: '.env',
text: envs
)
}
}
}
stage('Run Test Cases') {
steps {
script {
// 打印环境变量
sh 'cat ./.env'
// 宿主机adbd开启全网段监听
sh 'adb kill-server && adb -a -P 5037 nodaemon server >/tmp/adb_server_log 2>&1 &'
// CI相关文件拷贝到根目录
sh "cp -rfp ./ci/* ."
// 停止并删除全部容器服务
sh 'docker-compose rm -sf || true'
// 清理测试报告数据
sh 'rm -rf ./allure-results || true'
// 创建桥接网卡
sh 'docker network create --driver=bridge --subnet=172.31.0.0/16 --gateway=172.31.0.1 minicase || true'
// 启动容器服务
sh 'docker-compose up --build -d'
// 输出python-client容器的打印
sh 'docker-compose logs -f python-client'
}
}
}
}
post {
success {
// 生成测试报告
allure(
includeProperties: false,
jdk: '',
results: [[path: 'allure-results']]
)
}
always {
// 停止服务并删除容器
sh 'docker-compose rm -sf || true'
// 删除镜像和缓存
sh 'docker system prune -f || true'
// 杀掉adbd
sh 'adb kill-server'
}
}
}
FROM python:3.9-bullseye@sha256:92c5a135b66384a322edc1dee5f9af8b245304b7618a246b2ca87442e2ee8cce
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
#=====================
# Prepare Requirements
#=====================
WORKDIR /root
COPY requirements.txt .
RUN pip install --upgrade -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple && \
rm requirements.txt
#===============================================
# Install Android Platform Tools For ADB Command
#===============================================
# https://dl.google.com/android/repository/platform-tools_r33.0.1-linux.zip
ENV SDK_VERSION=platform-tools_r33.0.1-linux
ENV ANDROID_HOME=/root
COPY bin/${SDK_VERSION}.zip $ANDROID_HOME/
RUN unzip $ANDROID_HOME/${SDK_VERSION}.zip && \
rm $ANDROID_HOME/${SDK_VERSION}.zip && \
chmod a+x -R $ANDROID_HOME && \
chown -R root:root $ANDROID_HOME
# https://askubuntu.com/questions/885658/android-sdk-repositories-cfg-could-not-be-loaded
RUN mkdir -p ~/.android && \
touch ~/.android/repositories.cfg
ENV PATH=$PATH:$ANDROID_HOME/platform-tools
#===========================
# Install OpenJDK and Allure
#===========================
RUN sed -i -E 's/(deb|security).debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list && \
apt-get -y update && \
apt-get install -y --no-install-recommends \
openjdk-11-jdk-headless \
ffmpeg && \
rm -rf /var/lib/apt/lists/*
# https://github.com/allure-framework/allure2/releases/download/2.17.3/allure-2.17.3.zip
ENV ALLURE_VERSION=allure-2.17.3
ENV ALLURE_HOME=/root
COPY bin/${ALLURE_VERSION}.zip $ALLURE_HOME/
RUN unzip $ALLURE_HOME/${ALLURE_VERSION}.zip && \
rm $ALLURE_HOME/${ALLURE_VERSION}.zip && \
chmod a+x -R $ALLURE_HOME && \
chown -R root:root $ALLURE_HOME
ENV PATH=$PATH:$ALLURE_HOME/$ALLURE_VERSION/bin
#================================
# Install Chrome Driver for Opera
#================================
# https://registry.npmmirror.com/-/binary/chromedriver/2.35/chromedriver_linux64.zip
ENV CHROME_DRIVER_VERSION=chromedriver_2.35_linux64
ENV CHROME_DRIVER_HOME=/root
COPY bin/${CHROME_DRIVER_VERSION}.zip $CHROME_DRIVER_HOME/
RUN unzip ${CHROME_DRIVER_HOME}/${CHROME_DRIVER_VERSION}.zip && \
rm ${CHROME_DRIVER_HOME}/${CHROME_DRIVER_VERSION}.zip && \
chmod a+x -R $CHROME_DRIVER_HOME && \
chown -R root:root $CHROME_DRIVER_HOME
ENV PATH=$PATH:$ALLURE_HOME
#=====================
# Prepare Project Code
#=====================
RUN mkdir -p /MiniCase
WORKDIR /MiniCase
COPY . .
version: '2.3'
services:
python-client:
container_name: python-client
build: .
depends_on:
appium-server:
condition: service_healthy
selenium-server:
condition: service_healthy
command: bash ./script/run.sh
devices:
- "${XX_COM}:/dev/ttyUSB-XX" # 外部设备映射
- "${YY_COM}:/dev/ttyUSB-YY"
- "${ZZ_COM}:/dev/ttyUSB-ZZ"
- "/dev/video0:/dev/video0" # 摄像头
environment:
APPIUM_SERVER: appium-server:4723
SELENIUM_SERVER: selenium-server:4444
ADB_SERVER_SOCKET: tcp:172.31.0.1:5037
ANDROID_ADB_SERVER_HOST: 172.31.0.1
ANDROID_ADB_SERVER_PORT: 5037
env_file: .env
volumes:
- /root/.android:/root/.android
- ${PWD}/allure-results:/MiniCase/allure-results
privileged: true
tty: true
networks:
- minicase
appium-server:
container_name: appium-server
image: appium/appium
environment:
REMOTE_ADB: 'true'
ANDROID_DEVICES: "${ADB_TV},${ADB_MOBILE}"
ADB_SERVER_SOCKET: tcp:172.31.0.1:5037
RELAXED_SECURITY: 'true'
volumes:
- /root/.android:/root/.android
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:4723/wd/hub/status" ]
interval: 20s
timeout: 10s
retries: 10
networks:
- minicase
selenium-server:
container_name: selenium-server
image: selenium/standalone-chrome
shm_size: 2gb
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:4444/wd/hub/status" ]
interval: 20s
timeout: 10s
retries: 10
networks:
- minicase
networks:
minicase:
external: true