Bootstrap

基于Centos7的roboframework容器化部署步骤

1. 安装Docker、Docker-compose

安装Docker和Docker Compose在CentOS 7上需要按照以下步骤进行操作:

  1. 首先,安装Docker。可以使用以下命令来安装Docker:

sudo yum install docker

  1. 安装完成后,启动Docker并设置开机自启动:

sudo systemctl start docker
sudo systemctl enable docker

  1. 接下来,安装Docker Compose。可以使用以下命令来下载最新版本的Docker Compose:

sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

  1. 接着,赋予Docker Compose执行权限并创建软链接:

sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

  1. 最后,验证Docker Compose是否安装成功:

docker-compose --version


2. 安装Jenkins

2.1. 更新软件包列表:

在终端中执行以下命令,以确保你的系统已更新到最新的软件包列表:

sudo yum update

2.2. 安装 OpenJDK 11:

执行以下命令来安装 OpenJDK 11:

sudo yum install java-11-openjdk-devel

2.3. 验证安装:

安装完成后,可以使用以下命令来验证 Java 是否成功安装,并查看其版本信息:

java -version

2.4. 设置 JAVA_HOME 环境变量:

可以选择设置 JAVA_HOME 环境变量,以便系统和其他应用程序能够找到你的 Java 安装路径。编辑 /etc/profile 或者当前用户的 .bashrc 文件,添加如下行:

export JAVA_HOME=/usr/lib/jvm/java-11-openjdk
export PATH=$JAVA_HOME/bin:$PATH

然后,运行以下命令以使更改生效:

source /etc/profile

2.5. 系统设置设置

系统时间如果和真实时间不一致时执行如下命令:

timedatectl set-timezone Asia/Shanghai

2.6. 更新系统

在开始之前,确保你的系统已经更新到最新版本。你可以使用以下命令更新系统:

sudo yum update

2.7. 添加Jenkins 存储库

下载并安装Jenkins 的最新版本,首先需要添加 Jenkins 的官方存储库。创建一个名为 jenkins.repo 的文件并编辑:

sudo vi /etc/yum.repos.d/jenkins.repo

在文件中添加以下内容:

[jenkins]
name=Jenkins
baseurl=https://pkg.jenkins.io/redhat-stable
gpgcheck=1

2.8. 导入Jenkins 的 GPG 密钥,以验证下载的软件包

sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key

2.9. 安装Jenkins

使用以下命令安装Jenkins:

sudo yum install jenkins

2.10. 启动Jenkins 服务

安装完成后,启动Jenkins 服务并设置它在系统启动时自动启动:

sudo systemctl start jenkins

sudo systemctl enable jenkins

2.11. 配置防火墙

如果系统中启用了防火墙(如Firewalld),则需要允许 Jenkins 使用的端口(默认为 8080)。使用以下命令打开端口:

sudo firewall-cmd --zone=public --add-port=8080/tcp --permanent

sudo firewall-cmd --reload

2.12. 访问Jenkins

现在,你可以通过浏览器访问Jenkins。在浏览器中输入以下地址:

http://your_server_ip_or_domain:8080

2.13. 设置Jenkins

访问Jenkins 网页后,将会要求你解锁 Jenkins。在第一次启动时,Jenkins 会要求你输入初始管理员密码。你可以在服务器上找到该密码并输入。然后,按照屏幕上的指示完成 Jenkins 的设置向导。

2.14. 安装Jenkins插件

完成设置向导后,Jenkins 将会提供一些推荐的插件,你可以选择安装这些插件,也可以选择手动安装所需的插件。

2.14.1. 邮件相关插件

2.14.2. Git相关插件

2.14.3. Ssh插件

2.14.4. 运行python的插件

2.14.5. 运行robotframework插件

2.15. 配置任务、配置全局设置:

2.15.1. 邮件配置

邮件配置截图

2.15.2. 任务配置
  1. 执行shell(ui任务):
#!/bin/bash
docker exec newrf bash -c "rm -rf /home/scripts/zm_ui/report/eageye_chrome && cd /home/scripts/zm_ui/lib1 && python2.7 MainExecute.py /home/scripts/zm_ui/鹰眼V9/ 2 chrome yifang eageye_chrome"

  1. 执行shell(接口任务):
#!/bin/bash
docker exec newrf bash -c "rm -rf /home/scripts/zm_ui/report/eageyeEarlyReport_jk && cd /home/scripts/zm_ui/lib1 && robot -d /home/scripts/zm_ui/report/eageyeEarlyReport_jk /home/scripts/zm_ui/鹰眼早报告-JK"

任务配置截图

2.16. 配置邮件模板:

把下面的模板文件(robot_result.groovy、robot_result_jk.groovy)放入/var/lib/jenkins/email-templates文件夹中(email-templates文件夹需要自己创建)

<%  
 import java.text.DateFormat  
 import java.text.SimpleDateFormat  
 %>  
    <!-- Robot Framework Results -->  
<%  
 def robotResults = false  
 def actions = build.actions // List<hudson.model.Action>  
 actions.each() { action ->  
  if( action.class.simpleName.equals("RobotBuildAction") ) { // hudson.plugins.robot.RobotBuildAction  
   robotResults = true %>
                   <div style="width:100%;float:left"> 
                    <table cellspacing="0" cellpadding="4" border="1" align="left">  
                        <thead>
                            <tr bgcolor="#F3F3F3">
                                <td style="text-align:center" colspan="4"><b>自动化测试汇总报告</b></td>    
                            </tr>
							<tr>
								<th>项目名称:</th>
								<td>${project.name}</td>
							</tr>
							<tr>
								<th>构建轮次:</th>
								<td><a href="${rooturl}${build.url}">${build.displayName}(点击查看此轮构建信息)</a></td>
							</tr>
							<tr>
								<th>构建时间:</th>
								<td>${it.timestampString}</td>
							</tr>
							<tr>
								<th>构建时长:</th>
								<td>${build.durationString}</td>
							</tr>
							<tr>
								<th>构建缘由:</th>
								<td><% build.causes.each() { cause -> %> ${cause.shortDescription} <% } %></td>
							</tr>
                            <tr>
                                <td bgcolor="#F3F3F3" style="width:80px"><b>详细报告:</b></td>
                                <td colspan="4"><a href="${rooturl}${build.url}robot/report/report-total.html">点击查看报告详情</a></td>
                            </tr>
							<tr>
                                <td bgcolor="#F3F3F3" style="width:80px"><b>详细日志:</b></td>
                                <td colspan="4"><a href="${rooturl}${build.url}robot/report/log-total.html">点击查看日志详情</a></td>
                            </tr>
                            
                            <tr bgcolor="#F3F3F3">
                                <td><b>用例总数</b></td>
                                <td><b>通过</b></td>
                                <td style="width:60px"><b>不通过</b></td>
                                <td style="width:100px"><b>通过率</b></td>
                            </tr>
                            <tr>
                                <td><%= action.result.overallTotal %></td>
                                <td><b><span style="color:#66CC00"><%= action.result.overallPassed %></span></b></td>
                                <td><b><span style="color:#FF3333"><%= action.result.overallFailed %></span></b></td>
                                <td><%= action.overallPassPercentage %>%</td>
                            </tr>
                            <tr bgcolor="#F3F3F3">  
                                <td colspan="2"><b>Test Name</b></td>  
                                <td><b>Status</b></td>  
                                <td><b>Elapsed Time</b></td>  
                            </tr>  
                        </thead>  
                        <tbody>  

<% def suites = action.result.allSuites  
   suites.each() { suite ->   
    def currSuite = suite  
    def suiteName = currSuite.displayName  
    // ignore top 2 elements in the structure as they are placeholders  
    while (currSuite.parent != null && currSuite.parent.parent != null) {  
     currSuite = currSuite.parent  
     suiteName = currSuite.displayName + "." + suiteName  
    } %>  

                            <tr>
                                <td colspan="4"><b><%= suiteName %></b></td>
                            </tr>  

<%  DateFormat format = new SimpleDateFormat("yyyyMMdd HH:mm:ss")
    def execDateTcPairs = []
    suite.caseResults.each() { tc ->  
      Date execDate = format.parse(tc.starttime)
      execDateTcPairs << [execDate, tc]
    }
    // primary sort execDate, secondary displayName
    execDateTcPairs = execDateTcPairs.sort{ a,b -> a[1].displayName <=> b[1].displayName }
    execDateTcPairs = execDateTcPairs.sort{ a,b -> a[0] <=> b[0] }
    execDateTcPairs.each() {
      def execDate = it[0]
      def tc = it[1]  %>

                            <tr>  
                                <td colspan="2"><%= tc.displayName %></td>  
                                <td><b><span style="color:<%= tc.isPassed() ? "#66CC00" : "#FF3333" %>"><%= tc.isPassed() ? "PASS" : "FAIL" %></span></b></td>  
                                <td><%= tc.getDuration().intdiv(60000)+"分"+(tc.getDuration()-tc.getDuration().intdiv(60000)*60000).intdiv(1000)+"秒" %></td>  
                            </tr>  
                            <% if(tc.errorMsg != null) {%>
                            <tr>
                                <td ><b><span style="font-size:10px;color:#FF3333">错误描述:</span></b></td>
                                <td colspan="3"><span style="font-size:10px"><%= tc.errorMsg%></span></td>

                            </tr>
                            <%
                             }%>
<%  } // tests  
   } // suites %>  
                        </tbody>
                    </table>

                     <p style="color:#AE0000;clear:both">*这个是通过Jenkins自动构建得出的报告,仅供参考。</p>
                    </div>
                    <%  
  } // robot results  
 }  
 if (!robotResults) { %>  
        <p>No Robot Framework test results found.</p>  
<%  
 } %>
<%  
 import java.text.DateFormat  
 import java.text.SimpleDateFormat  
 %>  
    <!-- Robot Framework Results -->  
<%  
 def robotResults = false  
 def actions = build.actions // List<hudson.model.Action>  
 actions.each() { action ->  
  if( action.class.simpleName.equals("RobotBuildAction") ) { // hudson.plugins.robot.RobotBuildAction  
   robotResults = true %>
                   <div style="width:100%;float:left"> 
                    <table cellspacing="0" cellpadding="4" border="1" align="left">  
                        <thead>
                            <tr bgcolor="#F3F3F3">
                                <td style="text-align:center" colspan="4"><b>自动化测试汇总报告</b></td>    
                            </tr>
							<tr>
								<th>项目名称:</th>
								<td>${project.name}</td>
							</tr>
							<tr>
								<th>构建轮次:</th>
								<td><a href="${rooturl}${build.url}">${build.displayName}(点击查看此轮构建信息)</a></td>
							</tr>
							<tr>
								<th>构建时间:</th>
								<td>${it.timestampString}</td>
							</tr>
							<tr>
								<th>构建时长:</th>
								<td>${build.durationString}</td>
							</tr>
							<tr>
								<th>构建缘由:</th>
								<td><% build.causes.each() { cause -> %> ${cause.shortDescription} <% } %></td>
							</tr>
                            <tr>
                                <td bgcolor="#F3F3F3" style="width:80px"><b>详细报告:</b></td>
                                <td colspan="4"><a href="${rooturl}${build.url}robot/report/report.html">点击查看报告详情</a></td>
                            </tr>
							<tr>
                                <td bgcolor="#F3F3F3" style="width:80px"><b>详细日志:</b></td>
                                <td colspan="4"><a href="${rooturl}${build.url}robot/report/log.html">点击查看日志详情</a></td>
                            </tr>
                            
                            <tr bgcolor="#F3F3F3">
                                <td><b>用例总数</b></td>
                                <td><b>通过</b></td>
                                <td style="width:60px"><b>不通过</b></td>
                                <td style="width:100px"><b>通过率</b></td>
                            </tr>
                            <tr>
                                <td><%= action.result.overallTotal %></td>
                                <td><b><span style="color:#66CC00"><%= action.result.overallPassed %></span></b></td>
                                <td><b><span style="color:#FF3333"><%= action.result.overallFailed %></span></b></td>
                                <td><%= action.overallPassPercentage %>%</td>
                            </tr>
                            <tr bgcolor="#F3F3F3">  
                                <td colspan="2"><b>Test Name</b></td>  
                                <td><b>Status</b></td>  
                                <td><b>Elapsed Time</b></td>  
                            </tr>  
                        </thead>  
                        <tbody>  

<% def suites = action.result.allSuites  
   suites.each() { suite ->   
    def currSuite = suite  
    def suiteName = currSuite.displayName  
    // ignore top 2 elements in the structure as they are placeholders  
    while (currSuite.parent != null && currSuite.parent.parent != null) {  
     currSuite = currSuite.parent  
     suiteName = currSuite.displayName + "." + suiteName  
    } %>  

                            <tr>
                                <td colspan="4"><b><%= suiteName %></b></td>
                            </tr>  

<%  DateFormat format = new SimpleDateFormat("yyyyMMdd HH:mm:ss")
    def execDateTcPairs = []
    suite.caseResults.each() { tc ->  
      Date execDate = format.parse(tc.starttime)
      execDateTcPairs << [execDate, tc]
    }
    // primary sort execDate, secondary displayName
    execDateTcPairs = execDateTcPairs.sort{ a,b -> a[1].displayName <=> b[1].displayName }
    execDateTcPairs = execDateTcPairs.sort{ a,b -> a[0] <=> b[0] }
    execDateTcPairs.each() {
      def execDate = it[0]
      def tc = it[1]  %>

                            <tr>  
                                <td colspan="2"><%= tc.displayName %></td>  
                                <td><b><span style="color:<%= tc.isPassed() ? "#66CC00" : "#FF3333" %>"><%= tc.isPassed() ? "PASS" : "FAIL" %></span></b></td>  
                                <td><%= tc.getDuration().intdiv(60000)+"分"+(tc.getDuration()-tc.getDuration().intdiv(60000)*60000).intdiv(1000)+"秒" %></td>  
                            </tr>  
                            <% if(tc.errorMsg != null) {%>
                            <tr>
                                <td ><b><span style="font-size:10px;color:#FF3333">错误描述:</span></b></td>
                                <td colspan="3"><span style="font-size:10px"><%= tc.errorMsg%></span></td>

                            </tr>
                            <%
                             }%>
<%  } // tests  
   } // suites %>  
                        </tbody>
                    </table>

                     <p style="color:#AE0000;clear:both">*这个是通过Jenkins自动构建得出的报告,仅供参考。</p>
                    </div>
                    <%  
  } // robot results  
 }  
 if (!robotResults) { %>  
        <p>No Robot Framework test results found.</p>  
<%  
 } %>


3. 安装Git(如果自带则省略)、配置Git把代码拉到本地

3.1. 安装依赖包

yum install -y curl policycoreutils-python openssh-server

执行截图

3.2. 设置 SSH 开机自启动并启动 SSH 服务

执行截图

systemctl enable sshd
systemctl start sshd

3.3. 添加GitLab 软件包仓库

curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | sudo bash

执行截图

3.4. 安装 GitLab

sudo EXTERNAL_URL=“实例公网 IP 地址”
yum install -y gitlab-ce

执行截图

安装的版本:gitlab-ce.x86_64 0:15.3.1-ce.0.el7
安装成功,注意整个安装后,gitLab占了大概有接近3G空间。
完成安装后,你需要设置external_url(如上图说明);

vim /etc/gitlab/gitlab.rb

修改文件中的 external_url=http://192.168.XX.XX”,#你安装软件的服务器 IP ADDR;
然后执行命令:

gitlab-ctl reconfigure

3.5. 修改默认端口号

第一步:修改/etc/gitlab/gitlab.rb文件,如果有如下参数直接修改,如果没有就新增
nginx['listen_port'] = 8088
第二步:修改默认的gitlab nginx的web服务80端 /var/opt/gitlab/nginx/conf/gitlab-http.conf

server { ## HTTPS server
listen *:8088;

第三步:初始化并重启组件:

gitlab-ctl reconfigure
gitlab-ctl restart

3.6. 设置管理员账号密码

获取管理员帐户默认密码

cat /etc/gitlab/initial_root_password

你可以看到 Password:后面的内容就是管理员用户 root 的密码;
访问gitlab 的服务页面,注意:最新版访问地址,直接就是80端口就可以访问了。

http://IP;

会出现如下登录页面:
用户名用 root,密码用上面命令显示的密码登录;
进入后最好更新密码。

用户名用 root,密码用上面命令显示的密码登录;进入后最好更新密码。

3.7. 更改默认的仓库存储目录(可选)

默认安装时,默认的仓库存储路径在 /var/opt/gitlab/git-data目录下,仓库存储在子目录repositories里面,可以通过修改/etc/gitlab/gitlab.rb文件中git_data_dirs参数来自定义父目录。
vi /etc/gitlab/gitlab.rb

把该注释打开,使用自己的数据盘路径即可。

没有数据下,我们可以这样操作
先关闭应用,然后再重载一下配置,最后启动。

启动:gitlab-ctl start
关闭:gitlab-ctl stop
重启:gitlab-ctl restart
重载配置:gitlab-ctl reconfigure
查看状态:gitlab-ctl status

3.8. 创建Project

3.8.1. 安装Git工具

yum install -y git

3.8.2. 生成密钥文件

使用ssh-keygen生成密钥文件.ssh/id_rsa.pub

执行截图

[root@test local]# ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
SHA256:66kFPQg5L+O5EUBC5dyIUlZzyxD/tn+T5zGwNWCF9fs root@test.novalocal
The key's randomart image is:
+---[RSA 2048]----+
|o.=o=..      oo  |
| =+ oB .    .. . |
|...++.+     o   .|
|.  . + +   . .  .|
|    + + S   . o. |
|   . = o +   + ..|
|    +   +   ..o E|
|     o o o  + .o |
|    . ..o .. +.  |
+----[SHA256]-----+

3.8.3. 获取SSHKEY 公钥

cat .ssh/id_rsa.pub

执行截图

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDnxr79U+gHxvM9Byg/VdZCi7oSk8zEWtJJ8glj+276fj0R4GuMertetQFaqDeuAeSlGi9sUtYCrLLxixJVlc4yAH3yyqCLv+8H3ORP9vaO/cq05NOMOBxwmkMqvuODh0CHdYo18ECcLm8fIP3T8R0daaSISqg3RyPQLbLppfw3F9/JezsjEBa5myRCe/7e8w/0I+LuLJAd1zwRHYe1svpVW5fkuvWB6SkqIB3d90gCVSYx2GXwkQw8Bxuy4Z4zbohmcQVyTv2jPouBJ/VBMGSy3/ryS9DnUhXwzccroOfDAc9ymTS+rLW6ClxW74IGdq3X7Y6o56o6ufo0jbKyYF5J root@test.novalocal

3.8.4. 在GitLab的主页中新建一个Project


在页面上选择 new project 按钮,进入如下页面,可以根据需要创建项目;
我选择了 Create from template 创建项目,这样,创建的项目会默认有项目基础代码。

选择了用Spring template 创建项目,输入项目名称后点击 Create Project;

3.8.5. 添加ssh key


选择新建的项目,导入步骤3.8.3中生成的密钥文件内容:
选择 Add SSH key,在如下画面中输入步骤的密钥内容,完成 Add SSH Key;
ssh key添加完成:

3.8.6. 点击项目,查看ssh地址和http地址


项目地址,该地址在进行clone操作时需要用到:

3.9. 简单配置

3.9.1. 配置使用Git仓库的人员姓名

git config --global user.name “root”
 

3.9.2. 配置使用Git仓库的人员email填写自己的邮箱

git config --global user.email “[email protected]

3.9.3. 克隆项目

在本地生成同名目录,并且目录中会有所有的项目文件

mkdir /root/wkspace & cd /root/wkspace


执行如下命令,获取项目auto001 的代码;

git clone http://IP/gitlab-instance-51b259bf/auto001.git

执行后会有 /root/wkspace/auto001 目录,目录下放了项目的资源及代码;


4. 部署RF容器

以下操作最好在同一目录中,在宿主机上我们需要先安装python(自带)、RF(假定路径为/python/site-packages/robot,如果不同的话需修改docker-compose文件中的映射路径),从4.4章节开始,我们默认要修改的是宿主机上的RF配置文件(容器中因为做了映射,会自动同步)

4.1. 制作pip安装清单文件

requests==2.22.0
robotframework==3.1.2
robotframework-httplibrary==0.4.2
robotframework-jsonlibrary==0.3.1
robotframework-pabot==0.91
robotframework-requests==0.6.2
robotframework-ride==1.5.2.1
robotframework-selenium2library==3.0.0
robotframework-seleniumlibrary==4.1.0
selenium==3.141.0
soupsieve==1.9.5
urllib3==1.24.1
arrow==0.15.5

4.2. 制作镜像(Dockerfile文件)

# 使用官方的 Python 2.7 镜像作为基础镜
FROM python:2.7-slim

# 设置工作目录
WORKDIR /app

# 复制当前目录下的 requirements.txt 文件到工作目录
COPY requirements.txt .

RUN apt update && pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 使用 pip 安装依赖库
RUN python -m pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
RUN apt update
RUN apt install -y vim
RUN apt install unzip
# 将当前目录下的所有文件复制到工作目录中
COPY . .
# 执行应用程序
CMD ["tail", "-f", "/dev/null"]

然后使用命令制作镜像包 docker build -t 容器名:latest .

4.3. 制作容器编排文件,启动容器

version: "3"
services:
  robot:
    image: pythonmy-rb
    container_name: pyrobot2.7
    command: tail -f /dev/null
    ports:
      - "8098:8098"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      - /tmp/.X11-unix:/tmp/.X11-unix
      - /home/centos-git/zm_ui:/home/scripts/zm_ui
      - /centos-python/site-packages/robot:/usr/local/lib/python2.7/site-packages/robot
      - /centos-python/site-packages/SeleniumLibrary/keywords:/usr/local/lib/python2.7/site-packages/SeleniumLibrary/keywords
    privileged: true
    environment:
      - DISPLAY=unix:0
      - GDK_SCALE
      - GDK_DPI_SCALE

其中

  • /home/scripts/zm_ui:/home/scripts/zm_ui

左边是Jenkins拉取用例脚本的目录,右边是rf容器的用例脚本目录

  • /python/site-packages/robot:/usr/local/lib/python2.7/site-packages/robot
  • /python/site-packages/SeleniumLibrary/keywords:/usr/local/lib/python2.7/site-packages/SeleniumLibrary/keywords

此处是重试机制需要修改的文件夹,由于重试需要修改第三方库这里直接再本机修改映射到容器内 就不至于每次重启容器都需要重新更改了

使用命令启动rf容器docker-compose -f docker-compose-rf.yml up -d

4.4. 修改CallRobot

由于linux环境下RF没有pybot.bat(之前在Windows环境上使用pybot执行RF),所以直接使用robot方式运行脚本,修改脚本如下(直接改宿主机上面的),要改动的部分标黄底

# -*- coding: utf-8 -*-
import os
import sys
from robot import run_cli,run
from MultiThreadings import MultiThreadings
from GetNowTime import GetNowTime
import Util
import SendEmail
import shutil

reload(sys)
sys.setdefaultencoding("utf8")

class ExecRobot(object):
    def __init__(self,report_name):
        self.report_name = report_name
        #获取当前工作目录
        self.work_path = os.getcwd()
        #获取项目路径下的report路径
        self.save_path = os.path.abspath(os.path.join(os.path.dirname(self.work_path), "report\\" + report_name))
    def exec_pybot(self,project_path,execute_times,browser_type,project_type='yifang'):
        #定义一个字典,存放登录的用户名和密码
        #user_info = {"systemtest06":"u*CCT2"}
        if project_type == 'yifang':
            #user_info = {"systemtest06":"u*CCT2","systemtest07":"g7SdT*","systemtest09":"gpX7*n","systemtest01":"kDm*74"}
            #user_info = {"systemtest06":"u*CCT2","systemtest07":"g7SdT*"}
            user_info = {"systemtest10": "H3*f8m", "systemtest07": "g7SdT*"}
        elif project_type == 'shiwei':
            #user_info = {'systemtest08':"mmW5z*","systemtest09":"yu*3yH","systemtest06":"qg9A4*","systemtest07":"S9*2ac"}
            user_info = {'systemtest08':"mmW5z*","systemtest09":"yu*3yH"}
        elif project_type == 'shadan':
            #user_info = {"zhuoma_hn":"*#VMoPo1ioab*#","zhuoma_cs":"*#VMoPo1ioab*#","zhuoma_yl":"*#VMoPo1ioab*#","zhuoma_us":"*#VMoPo1ioab*#"}
            # user_info = {"zhuoma_cs":"*#VMoPo1ioab*#"}
            # user_info = {"sds_test_zm1":"SfmIHPwt*#","sds_test_zm2":"*#SfmIHPwt*#"}
            user_info = {"sds_test_zm1": "SfmIHPwt*#", "zhuoma_hn": "*#VMoPo1ioab*#"}
        elif project_type == 'ketao':
            #user_info = {"[email protected]":"111111","[email protected]":"111111","[email protected]":"111111","[email protected]":"111111"}
            user_info = {"[email protected]":"111111","[email protected]":"111111"}
        elif project_type == 'oplate':
            #user_info = {"[email protected]":"111111","[email protected]":"111111","[email protected]":"111111","[email protected]":"111111"}
            user_info = {"[email protected]":"111111","[email protected]":"111111"}
        elif project_type == 'shiweihd':
            #user_info = {"[email protected]":"111111","[email protected]":"111111","[email protected]":"111111","[email protected]":"111111"}
            user_info = {"[email protected]":"123456789"}    
        elif project_type == 'yiling':
            #user_info = {"[email protected]":"111111","[email protected]":"111111","[email protected]":"111111","[email protected]":"111111"}
            user_info = {"systemtest09":"gpX7*n"}
        elif project_type == 'yingyan':
            #user_info = {"[email protected]":"111111","[email protected]":"111111","[email protected]":"111111","[email protected]":"111111"}
            user_info = {"systemtest08":"Rd*6gW","systemtest06":"u*CCT2"}
        # user_info = {"systemtest06":"u*CCT2"}
        elif project_type == "review":
            user_info = {"systemtest05": "Thq8*M"}
        elif project_type == "dspjc":
            user_info = {"systemtest08": "Rd*6gW","systemtest06": "u*CCT2"}

        cmd_list = []
        try:
            cmd = 'taskkill /F /IM geckodriver.exe'
            os.system(cmd)
        except:
            print "No process found"
        # 启动线程前,先将上次运行所有的报告删除
        Util.clear_all_report(self.save_path)
        i = 1
        for username, password in user_info.items():
            # C:\Python27\python.exe MainExecute.py G:\ZM_Release\鹰击V9 2 chrome yifang egatek_chrome
            # pybot -o ../report/egatek_chrome/output-1.xml -r ../report/egatek_chrome/report-1.html -l ../report/egatek_chrome/log-1.html -v username:systemtest10 -v password:H3*f8m -v browsertype:chrome --retry 2 G:\ZM_Release\鹰击V9
            cmd = r"robot -o ../report/{0}/output-{1}.xml -r ../report/{0}/report-{1}.html -l ../report/{0}/log-{1}.html -v username:{3} -v password:{4} -v browsertype:{6} --retry {2} {5} "\
                .format(self.report_name,i, execute_times, username, password, project_path, browser_type)
            cmd_list.append(cmd)
            i = i + 1
        try:
            MultiThreadings().multi(cmd_list)
        except Exception as e:
            print "fail " + str(e)

    def exec_rebot(self):
        # rebot -N Compatibility -d work/zm_ui/report/egatek_chrome/ -o ../egatek_chrome/output-total.xml -l../egatek_chrome/log-total.html -r ../egatek_chrome/report-total.html ../report/egatek_chrome/output-*.xml
        cmd2 = r"rebot -N Compatibility -d {0} -o ../report/{1}/output-total.xml -l../report/{1}/log-total.html -r ../report/{1}/report-total.html ../report/{1}/output-*.xml" \
            .format(self.save_path,self.report_name)
        os.system(cmd2)


if __name__=="__main__":
    report_name = "egatek_chrome"
    rb=ExecRobot(report_name)
    path = 'E:\ZM_AutoTest_RF\鹰击'
    rb.exec_pybot(path.encode("gbk"),1,'firefox')
    rb.exec_rebot()

4.5. 重试机制

通过改写Robot Framework源代码增加--retry选项,实现test级别的失败用例自动再执行,比如用例失败后,会重新运行N次,直至成功or 耗尽重试次数,生成的日志和报告文件中只会体现最后一次执行的结果。

类似如下命令格式:

robot --retry 3 trainning

具体实现:

  1. 修改文件/usr/local/lib/python2.7/site-packages/robot/run.py,在USAGE变量里添加retry参数
 -F --extension value     Parse only files with this extension when executing
                          a directory. Has no effect when running individual
                          files or when using resource files. If more than one
                          extension is needed, separate them with a colon.
                          Examples: `--extension txt`, `--extension robot:txt`
                          New in RF 3.0.1. Starting from RF 3.2 only `*.robot`
                          files are parsed by default.
 -N --name name           Set the name of the top level suite. By default the
                          name is created based on the executed file or
                          directory.
 -H --retry retry     Set the retry times if test failed.
  1. 在run.py文件,RobotFramework类增加make方法,并在开始之前导入库from xml.dom import minidom
def make(self,outxml):
    xmldoc = minidom.parse(outxml)
    suiteElementList = xmldoc.getElementsByTagName('suite')
    mySuite = []
    for suiteElement in suiteElementList:
        if suiteElement.childNodes is not None:
            for element in suiteElement.childNodes:
                if element.nodeName == 'test':
                    mySuite.append(suiteElement)
                    break
    for suite in mySuite:
        testElements = {}
        for element in suite.childNodes:
            if element.nodeName == 'test':
                name = element.getAttribute('name')
                if testElements.get(name) == None:
                    testElements.update({name:[element]})
                else:
                    testElements.get(name).append(element)
        for n,el in testElements.iteritems():
            for i in el[0:-1]:
                textElement = i.nextSibling
                suite.removeChild(i)
                suite.removeChild(textElement)
    savefile = open(outxml,'w')
    root = xmldoc.documentElement
    root.writexml(savefile)
    savefile.close()
  1. 调用make函数,在main函数中添加:

self.make(settings.output)

  1. 打开robot/conf/setting.py文件,修改_cli_opts字典,增加

'Retry': ('retry', 3)

代码截图

  1. 修改itemlist.py文件 (文件路径:python/site-packages/robot/model/itemlist.py)
def visit(self, visitor):
    for item in self:
        if self.__module__ == 'robot.model.testcase' and hasattr(visitor, "_context"):
            testStatus = ''
            for i in range(0, int(visitor._settings._opts['Retry'])):
                if testStatus != 'PASS':
                    if item.name in visitor._executed_tests:
                        visitor._executed_tests.pop(item.name)
                    item.visit(visitor)
                    testStatus = visitor._context.variables['${PREV_TEST_STATUS}']
                else:
                    break
        else:
            item.visit(visitor)

4.6. 恢复过期的关键字

代码路径:

/usr/local/lib/python2.7/site-packages/SeleniumLibrary/keywords/browsermanagement.py

@keyword
def get_browser_name_and_version(self, browser='firefox'):
    """Get the current browser name and version."""
    if browser == 'chrome':
        browserNameAndVersion = self.driver.capabilities['browserName'] + "_" + self.driver.capabilities[
            'browserVersion']
        print
        browserNameAndVersion
    elif browser == 'firefox':
        browserNameAndVersion = self.driver.capabilities['browserName'] + "_" + self.driver.capabilities[
            'browserVersion']
    else:
        pass
    return browserNameAndVersion

4.7. 加入自定义关键词

代码路径:/usr/local/lib/python2.7/site-packages/SeleniumLibrary/keywords/element.py

@keyword
def scroll_to_target_ele(self, locator):
    """ scrolle Element to target Element"""
    element = self.find_element(locator)
    self.driver.execute_script("arguments[0].scrollIntoView(false);", element)


5. 部署Selenium-Grid容器

5.1. 拉取镜像:

docker pull selenium/hub

docker pull selenium/node-chrome:109.0

docker pull selenium/node-chrome:标签版本号

5.2. 安装容器编排工具docker-compose

sudo apt install docker-compose

5.3. 创建docke-compose.yml文件

touch docker-compose.yml

version: "3"
services:
  chrome01:
    image: selenium/node-chrome:109.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    ports:
      - "7901:7900"
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SCREEN_WIDTH=1920
      - SCREEN_HEIGHT=1080
      - SCREEN_DEPTH=24
      - SCREEN_DPI=74
      - SE_NODE_SESSION_TIMEOUT=2000
      - E_SESSION_REQUEST_TIMEOUT=2000
      - SE_SESSION_RETRY_INTERVAL=2
      - SE_NODE_MAX_SESSIONS=2
      - SE_NODE_MAX_INSTANCES=2
      - SE_NODE_REGISTER_CYCLE=5000
      - START_XVFB=false
    volumes:
      - /dev/shm:/dev/shm
    chrome01:
    image: chrome-124.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    ports:
      - "7901:7900"
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SCREEN_WIDTH=1920
      - SCREEN_HEIGHT=1080
      - SCREEN_DEPTH=24
      - SCREEN_DPI=74
      - SE_NODE_SESSION_TIMEOUT=2000
      - E_SESSION_REQUEST_TIMEOUT=2000
      - SE_SESSION_RETRY_INTERVAL=2
      - SE_NODE_MAX_SESSIONS=2
      - SE_NODE_MAX_INSTANCES=2
      - SE_NODE_REGISTER_CYCLE=5000
      - START_XVFB=false
    volumes:
      - /dev/shm:/dev/shm
    privileged: true

  chrome02:
    image: selenium/node-chrome:110.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    ports:
      - "7902:7900"
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SCREEN_WIDTH=1920
      - SCREEN_HEIGHT=1080
      - SCREEN_DEPTH=24
      - SCREEN_DPI=74
      - SE_NODE_SESSION_TIMEOUT=2000
      - E_SESSION_REQUEST_TIMEOUT=2000
      - SE_SESSION_RETRY_INTERVAL=2
      - SE_NODE_MAX_SESSIONS=2
      - SE_NODE_MAX_INSTANCES=2
      - SE_NODE_REGISTER_CYCLE=5000
      - START_XVFB=false
    volumes:
      - /dev/shm:/dev/shm
    chrome01:
    image: chrome-124.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    ports:
      - "7901:7900"
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SCREEN_WIDTH=1920
      - SCREEN_HEIGHT=1080
      - SCREEN_DEPTH=24
      - SCREEN_DPI=74
      - SE_NODE_SESSION_TIMEOUT=2000
      - E_SESSION_REQUEST_TIMEOUT=2000
      - SE_SESSION_RETRY_INTERVAL=2
      - SE_NODE_MAX_SESSIONS=2
      - SE_NODE_MAX_INSTANCES=2
      - SE_NODE_REGISTER_CYCLE=5000
      - START_XVFB=false
    volumes:
      - /dev/shm:/dev/shm
    privileged: true

  selenium-hub:
    image: selenium/hub
    container_name: selenium-hub
    ports:
      - "4442:4442"
      - "4443:4443"
      - "4444:4444"
    environment:
      - GRID_MAX_SESSION=4
      - GRID_THROW_ON_CAPABILITY_NOT_PRESENT=true 
      - GRID_NEW_SESSION_WAIT_TIMEOUT=-1 
      - GRID_BROWSER_TIMEOUT=3000
      - GRID_TIMEOUT=3000
      - GRID_CLEAN_UP_CYCLE=3000
    chrome01:
    image: chrome-124.0
    shm_size: 2gb
    depends_on:
      - selenium-hub
    ports:
      - "7901:7900"
    environment:
      - SE_EVENT_BUS_HOST=selenium-hub
      - SE_EVENT_BUS_PUBLISH_PORT=4442
      - SE_EVENT_BUS_SUBSCRIBE_PORT=4443
      - SCREEN_WIDTH=1920
      - SCREEN_HEIGHT=1080
      - SCREEN_DEPTH=24
      - SCREEN_DPI=74
      - SE_NODE_SESSION_TIMEOUT=2000
      - E_SESSION_REQUEST_TIMEOUT=2000
      - SE_SESSION_RETRY_INTERVAL=2
      - SE_NODE_MAX_SESSIONS=2
      - SE_NODE_MAX_INSTANCES=2
      - SE_NODE_REGISTER_CYCLE=5000
      - START_XVFB=false
    volumes:
      - /dev/shm:/dev/shm
    privileged: true

5.4. 启动容器:

docker-compose -f docker-composen.yml up -d

5.5. Jenkins构建自动化项目(此处自动化脚本为无头模式执行,且分布式到部署的节点中执行)

自动化脚本为无头模式执行,且分布式到部署的节点中执行

自动化脚本无头模式打开浏览器执行

5.6. 然后在selenium-grid查看执行情况:

http://IP:4444/ui#

界面截图

至此步已完成docker+selenium-grid的UI自动化测试环境部署


6. 升级seleniumGrid容器浏览器及驱动

6.1. 什么是dockerfile

Dockerfile是一个包含用于组合映像的命令的文本文档。可以使用在命令行中调用任何命令。 Docker通过读取Dockerfile中的指令自动生成映像。

6.2. dockerfile操作指令

指令

含义

FROM[镜像]

指定新镜像所基于的镜像,第一条指令必须为FROM指令,每创建一个镜像就需要一条FROM指令。例如centos:7。from有两层含义:①开启一个新的镜像②必须写的一行指令

MAINTAINER[名字]

说明新镜像的维护人信息(可写可不写)

RUN命令

每一条RUN后面跟一条命令,在所基于的镜像上执行命令,并提交到新的镜像中,RUN必须大写

COPY[源文件/目录][目标文件/目录]

将本地主机上的文件/目录复制到目标地点,源文件/目录要与dockerfile在相同的目录中,copy只能用于复制,add复制的同时,如果复制的对象是压缩包,ADD还可以解压,copy比add节省资源

6.3. 升级策略(浏览器及驱动下载地址)

采取的更新方式是根据原有镜像升级浏览器及驱动先下载linux版本浏览器和驱动,放入/opt目录下,再根据dockerfile文件,对原有的镜像进行驱动和浏览器的替换。

谷歌浏览器下载地址:

https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb

谷歌浏览器驱动下载地址:

http://npm.taobao.org/mirrors/chromedriver/<指定驱动版本>/chromedriver_linux64.zip

6.4. 执行Dockerfile,根据原有的镜像生成更新浏览器及驱动的新镜像

#FROM selenium/node-chrome:112.0.1
FROM chrome-110.0              #指定新镜像所基于的镜像,基于该镜像进行生成新镜像,根据实际情况修改
COPY chromedriver /bin/         #将本地chrome 移入至指定镜像的/bin/目录中
COPY google-chrome-stable_current_amd64.deb /tmp/    #将本地浏览器安装文件移入至指定镜像的/tmp/目录

RUN sudo dpkg -i /tmp/google-chrome-stable_current_amd64.deb#执行谷歌浏览器安装操作
RUN sudo sed -i '10 i\export LANGUAGE="zh_CN.UTF-8"' /opt/google/chrome/google-chrome #修改浏览器默认语言设置,防止执行自动化时打开的是语言为英文浏览器

执行Doockerfile文件(默认根据当前目录下的dockerfile):

sudo docker build -t 新镜像名 .

执行成功后界面截图

查看生成的镜像

docker images

6.5. 使用shell脚本实现半自动化更新浏览器及对应驱动

(文件放在/opt目录下,如果需要放在其他文件夹需要修改shell脚本中的文件夹)

Shell脚本如下所示:

#!/bin/bash

rm -rf chromedriver
rm -rf chromedriver-linux64.zip
rm -rf google-chrome-stable_current_and64.deb
rm -rf LICENSE.chromedriver
rm -rf chromedriver-linux64

# Specify the browser and driver versions you want to download
BROWSER_VERSION=$1
#if u want call the method u just Incoming parameters
#112.0
DRIVER_VERSION=$2
#112.0.5615.28

# Specify the URL to download the browser and driver
BROWSER_URL=https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
DRIVER_URL=https://storage.googleapis.com/chrome-for-testing-public/$DRIVER_VERSION/linux64/chromedriver-linux64.zip

# Specify the path to the local directories where the browser and driver will be installed
BROWSER_PATH=/home/docker/google-chrome-stable_current_amd64.deb
DRIVER_PATH=/home/docker/chromedriver

# Remove the existing browser and driver
echo "Removing existing browser and driver..."
sudo rm -rf $BROWSER_PATH
sudo rm -rf $DRIVER_PATH

# Download the browser and driver
echo "Downloading browser and driver..."
wget $BROWSER_URL -P /home/docker
wget $DRIVER_URL -P /home/docker


# Install the browser and driver
echo "Installing browser and driver..."
sudo unzip chromedriver-linux64.zip
cp /home/docker/chromedriver-linux64/chromedriver /home/docker

# Build the Docker image
echo "Building Docker image..."

sudo docker build -t chrome-$BROWSER_VERSION .

在/opt目录下执行更新并生成新的镜像命令

bash update_chrome.sh BROWSER_VERSION DRIVER_VERSION

执行完毕后,生成名为chrome-$BROWSER_VERSION的镜像

需要注意的是,浏览器在官网只能找到最新版本,需要自己查看最新版浏览器的版本号,查找出对应的驱动版本,修改对应的BROWSER_VERSIONDRIVER_VERSION即可。

(最好是用root权限执行):bash update_chrome.sh BROWSER_VERSION DRIVER_VERSION即可直接完成新镜像制作。

每次更新浏览器的时候只需要在/opt目录下执行update_chrome.sh文件即可完成更新,需要注意修改脚本版本号即可完成最新浏览器版本的镜像生成。

6.6. 修改节点浏览器默认系统语言

再容器子节点容器内部进入/opt/google/chrome/google-chrom文件中添加如下代码即可完成对浏览器默认语言设置的修改。

export LANGUAGE="zh_CN.UTF-8"

6.7. GitHub拉取项目进行更新(推荐)

GitHub项目地址:GitHub - SeleniumHQ/docker-selenium: Provides a simple way to run Selenium Grid with Chrome, Firefox, and Edge using Docker, making it easier to perform browser automation

  1. 安装git
  2. 克隆项目

git clone GitHub - SeleniumHQ/docker-selenium: Provides a simple way to run Selenium Grid with Chrome, Firefox, and Edge using Docker, making it easier to perform browser automation

  1. 克隆好项目后,需要修改/docker-selenium/NodeChrome/Dockerfile文件的链接地址

https://dl-ssl.google.com/linux/linux_signing_key.pub改为https://dl.google.com/linux/linux_signing_key.pub即可

  1. 随后执行

VERSION=$SELENIUM_VERSION make chrome_upgrade_version

VERSION=4.20.0 make chrome_upgrade_version

指定浏览器

修改此处


7. 所有环境在Jenkins联调时出现的问题汇总:

7.1. Jenkins权限不足导致构建失败

报错如下图所示

问题原因

权限不足导致Jenkins构建脚本失败。

解决办法

  1. 添加jenkins用户到用户组:

-a<追加> 必须与-G选项一起使用,把用户追加到某些组中。

-G<群组> 修改用户所属的附加群组

sudo usermod -a -G docker jenkins

  1. 修改/var/run/docker.sock文件的权限

sudo chmod -R 777 /var/run/docker.sock

  1. 修改仓库读写权限

sudo chmod 777 /home/centos-git

7.2. Jenkins打开robotframework报告失败,下图所示:

打开失败:

解决办法:

在管理jenkins-->脚本命令行输入如下脚本:

System.setProperty("hudson.model.DirectoryBrowserSupport.CSP","sandbox allow-scripts; default-src 'none'; img-src 'self' data: ; style-src 'self' 'unsafe-inline' data: ; script-src 'self' 'unsafe-inline' 'unsafe-eval' ;")

7.3. 生成的结果报告页面图片加载失败,下图所示

解决办法:

进入Manage Jenkins->Script console,输入如下命令并进行执行

System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "")

当输入命令,并运行后,如果为空,则说明配置好了

7.4. Jenkins执行完毕后生成报告出现错误,遇到ascii码报错

解决办法

在/usr/local/lib/python2.7site-packages/robot/utils/application.py中显式指定编码

import sys
reload(sys)
sys.setdefaultencoding('utf-8')

7.5. Docker容器内报错权限不足:ls: cannot open directory ‘.‘: Permission denied

解决办法:在编排文件中添加privileged: true


;