解放劳动力,持续集成的记录

在开始的时候,我也尝试去找一些常用的框架或者机制,但是在实际情况面前,纷纷被我否了。在与8、9位测试工程师应聘者聊过之后,坚定了造个轮子的信念。

# 需求和目的

特殊情况,情况即条件,条件艰苦,对于现成框架来说是灾难。

  • 人员分散,部分开发工程师分隔在两地,开发和测试分隔在两地;
  • 工作负载最重的工作站在本地,而非云上,48核的云上实例实在是有些贵;
  • 各服务之间存在着公司VPN的隔离,工作站在公司内网以外(还是因为异地办公);
  • 输出为Android ROM包,体积较大。

想要达到的目的,从结构上看,目的很轻,现成框架会显得太过臃肿。

  • 可在触发后,自动完成提测版本发布和回归操作;
  • 可在版本输出前后,运行自动化测试脚本,进行集成回归测试;
  • 可方便添加独立的测试或者处理模块。

    # 思路和流程设计

从包的角度的流水

APP源码 -> APKs -> Android SDK源码 -> ROM -> 自动化测试
-> (通过) -> 输出ROM
-> (不通过) -> 定位APK -> 定位app源码

从服务的角度的流水

APP源码仓库监控服务/APK包仓库监控服务/手动命令监控服务
-> 上传/转移发送服务
-> 上传/转移接收服务
-> 自动编译打包服务
-> ROM部署安装服务
-> 自动化测试脚本运行服务
-> 测试结果反馈服务

设计图

命名为MUI-CI的设计流程图

# 解释

实现该想法其实前前后后花了大概一周的时间,从设计图上可以看出来,其实工作量主要还是各个环节的联调。原理比较简单,使用MQTT来联通处在各处的服务。贴一下core的部分代码,其他模块的结构基本一致。

各模块结构示意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
├── core-with-mqtt
│   ├── core_data_mainflow.py
│   ├── core_data_mainflow.pyc
│   ├── core_with_mqtt.py
│   ├── handle_apk_dir.py
│   ├── handle_apk_dir.pyc
│   ├── handle_make_end.py
│   ├── handle_make_end.pyc
│   ├── handle_todo_list.py
│   ├── handle_todo_list.pyc
│   ├── reload_core_with_mqtt.sh
│   ├── stop_core_with_mqtt.sh
│   ├── todo_data_info.py
│   ├── todo_data_info.pyc
│   └── todolist
├── module-auto-install
│   ├── handle_install_rom.py
│   ├── hook-shell
│   │   ├── doAdbInstallROM.sh
│   │   └── doCheckDeviceAlive.sh
│   └── module_auto_install.py
├── module-email-report
│   └── module_email_report.py
├── module-help-ota
│   ├── handle_check_rom.py
│   ├── handle_get_rom.py
│   ├── handle_upload_rom.py
│   ├── hook-shell
│   │   ├── doCheckRomMd5.sh
│   │   ├── doGetRom.sh
│   │   └── doUploadRom.sh
│   ├── module_help_ota.py
│   └── module_help_ota_cmd_and_info.py
├── module-info-report
│   └── module_info_report.py
├── module-make-sdk
│   ├── data_make_sdk.py
│   ├── handle_make_v310.py
│   ├── handle_make_v620.py
│   ├── hook-shell
│   │   ├── doMakeV310.sh
│   │   └── doMakeV620.sh
│   ├── module_make_sdk.py
│   └── reload_module_make_sdk.sh
├── module-todo-imme
│   └── module_todo_imme.py
├── module-upload-receive
│   ├── local
│   │   ├── myupload.html
│   │   ├── nginx.conf
│   │   └── upload.lua
│   └── proxy
│   └── nginx.conf
├── module-upload-send
│   ├── handle_upload_by_post.py
│   ├── module_upload_send.py
│   └── reload_module_upload_send.sh
├── module-watch-dir
│   ├── module-watch-dir.py
│   └── reload_module_watch_dir.sh

CORE部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
SUBTOPIC = 'muici/#'
TOPIC_COMM_RESP = 'muici/mainflow/resp'
HOST_NAME = "54.223.74.22"
def on_connect(client, userdata, flags, rc):
if rc == 0:
client.subscribe(SUBTOPIC)
def on_message(client, userdata, msg):
mStep = mData.MAINFLOW_STEP
# 处理WATCHDIR UPDATE事务
if mData.FLOW_TOPIC['WATCHDIR_UPDATE'] == msg.topic:
sendResponse(msg.topic)
if mStep == mData.MAINFLOW_STEPS["HANDLE_START"]:
thread.start_new_thread(handleApkDirEvent.process, (msg.payload, ))
# 处理UPLOAD_SEND_IMME_END事务
elif mData.FLOW_TOPIC["UPLOAD_SEND_IMME_END"] == msg.topic:
sendResponse(msg.topic)
if mStep == mData.MAINFLOW_STEPS["HANDLE_UPLOAD"] or mStep == mData.MAINFLOW_STEPS["HANDLE_START"]:
thread.start_new_thread(
handleApkDirEvent.process, (msg.payload, ))
# 处理MAKE SDK END事务
elif mData.FLOW_TOPIC["MAKESDK_END"] == msg.topic:
sendResponse(msg.topic)
if mStep == mData.MAINFLOW_STEPS["HANDLE_SDK"]:
thread.start_new_thread(
handleMakeEndEvent.process, (msg.payload, ))
# 处理TODOIMME事务
elif mData.FLOW_TOPIC["TODO_IMME"] == msg.topic:
sendResponse(msg.topic)
if mStep == mData.MAINFLOW_STEPS["HANDLE_START"]:
thread.start_new_thread(handleTODOList.process, (msg.payload, ))
# 处理PING事务
elif mData.FLOW_TOPIC["STATUS_PING"] == msg.topic:
sendResponse(msg.topic)
message = eval(msg.payload)
if message['event'] == 'ping' and message['module'] == "core":
ping_back()
def sendResponse(message):
message = "resp: " + str(message)
mqttpublish.single(TOPIC_COMM_RESP, str(message), hostname=HOST_NAME)
def ping_back():
if mData.MAINFLOW_STEP == mData.MAINFLOW_STEPS["HANDLE_START"]:
status = 'Free'
else:
status = 'Busy'
message = {
'event': 'pingback',
'step': mData.MAINFLOW_STEP,
'status': status
}
mqttp.single(mData.FLOW_TOPIC["STATUS_PING"],
str(message), hostname=mData.HOST_NAME)
if __name__ == '__main__':
################# 初始化流程状态 ###############
mData.reload_mainflow()
dataTODOInfo.reloadTODOList()
################# 初始化MQTT broker连接 ###############
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.connect("54.223.74.22", 1883, 60)
client.loop_forever()

# 几条经验

  • 调试消息类的业务,请提前准备好测试消息,可以不依赖其他环节,各个击破。
  • python模块的天生单例特性,对维护全局数据很有帮助。
  • 使用统一的结构来开发各模块,对后期维护和开发过程都有很大便利。
  • 善用数据库,可以减轻大量的日常运维工作。

# 版本更新

V1.2

  • 编译服务落地所有增量包UTC值数据库化
  • 编译服务加入锁机制
  • 编译服务加入BC机器人
  • 解决通知机器人不能连续发送多个消息的问题,解决不支持多版本串行编译的问题
  • 丰富通知消息的信息,包括更新的包等有用信息