随着lua在公司各业务中落地范围的逐步扩大,我们对lua项目的质量要求与标准也在不断提高,为此,我们提供了应用于黑盒测试阶段的lua结构化测试方法,在业务测试的过程中,通过代码覆盖率技术收集代码的执行数据来完成qa测试过程与代码测试质量的量化。但在过去两年多的业务演进中,接入方的工程类型、结构、业务特性呈现出越来越多的多样化发展,导致lua覆盖率工具接入的兼容成为急需解决的问题,因此需要我们做进一步兼容与升级。
lua覆盖率计算的流程分为三个部分:1、lua端进行执行数据采集 2、客户端覆盖率sdk完成采集数据的存储与上报 3、代码质量平台完成覆盖率报告的解析、计算、合并与生成
数据采集
采集端的实现主要由改造后的luaCov完成。luaCov主要通过lua标准中的debug库提供的sethook方法来实现。debug库提供了一系列api可方便开发者自定义自己的lua调试器,比如可以获取活动的函数堆栈,当前执行的代码行号等。覆盖率计算中使用到的已执行行号即可通过debug空中的sethook方法来完成。sethook是lua的钩子函数,即事件回调。lua虚拟机在执行指令码的时候会检查用户是否设置了该回调,如果设置了就调用该钩子函数
"c": 当函数被调用的时候触发,且在函数获得参数之前调用
"r": 当函数返回之前触发hook,且不能在此时获得函数的返回值
"l": 当执行到某一行代码之前触发hook
lua层的sethook方法会调用lua虚拟机c层的lua_sethook方法,lua_sethook方法实际上只是做了一个简单的参数赋值给lua状态机lua_State,lua_State中有以下参数:
struct lua_State {
// 用于跟踪行的调试
const Instruction *oldpc;
// Hook回调函数
volatile lua_Hook hook;
// 函数中执行了多少条指令后触发
int basehookcount;
int hookcount;
// 事件掩码:LUA_MASKCALL,LUA_MASKRET,LUA_MASKLINE,LUA_MASKCOUNT
l_signalT hookmask;
// 是否允许HOOK,默认为允许,在一个HOOK回调里面不允许
lu_byte allowhook;
};
设置了Hook回调之后,Lua会在下面几种事件回调
LUA_MASKCALL 调用函数时回调,在调用一个函数对象之前,都会先调用luaD_precall,在这个函数里触发事件。
LUA_MASKRET 函数返回时回调,完成一个函数时,会先调用luaD_poscall,在这个函数里触发事件。
LUA_MASKLINE 执行一行时代码时回调,vmfetch中每执行一条指令都会调用luaG_traceexec函数,在其中判断新行并触发事件
LUA_MASKCOUNT 每执行count条指令时回调,vmfetch中每执行一条指令都会调用luaG_traceexec函数,在其中判断是否执行了count条件指令并触发事件。
在lua虚拟机中,每执行一条指令都会调用luaG_traceexec,其中会判断是否满足mask掩码的回调条件,触发回调。基于以上,我们即可完成覆盖率数据的采集,lua实现代码如下:
local rawcoroutinecreate = coroutine.create
coroutine.create = function(...)
local co = rawcoroutinecreate(...)
debug.sethook(co, runner.debug_hook, "l")
return co
end
local function safeassert(ok, ...)
if ok then
return ...
else
error(..., 0)
end
end
coroutine.wrap = function(...)
local co = rawcoroutinecreate(...)
debug.sethook(co, runner.debug_hook, "l")
return function(...)
return safeassert(coroutine.resume(co, ...))
end
end
根据公司业务特点,除了执行行号、次数、最大执行次数、脚本文件名,还需要保存bid,commit等信息,并转为指定格式
runner.debug_hook = runner.hook_new()
function runner.hook_new()
return function(_, line_nr, level)
level = level or 2
if not runner.initialized then
return
end
local name = debug.getinfo(level, "S").source
if getExtension(name) == "lua" then
name = getFileName(name)
end
name = replace(name, "%.", "/")
name = name .. '.lua'
if runner.file_included(name) then
return
end
local file_name = tostring(runner.bid) .. "_" .. name
local commit_id = runner.commit_id[file_name]
if commit_id == nil then
return
end
name = tostring(runner.bid) .. "#" .. commit_id .. "#" .. name
local data = runner.data
local file = data[name]
if not file then
file = { max = 0, max_hits = 0 }
data[name] = file
end
if line_nr > file.max then
file.max = line_nr
end
local hits = (file[line_nr] or 0) + 1
file[line_nr] = hits
if hits > file.max_hits then
file.max_hits = hits
end
end
end
在lua端将数据采集到后,覆盖率sdk通过mln实现lua bridge方法完成数据的接收与存储。将采集到的数据以特定格式存储在手机设备上等待触发上报策略。
在qa测试的过程中,通过特定的操作(冷启动,压后台等)触发数据上报,携带所需信息进行上报到服务器端
服务端接收到上报数据后进行数据解析,在qa根据需求创建测试任务后即可生成测试报告。
根据上报的数据进行解析聚合,以commitId为维度进行聚合,方便后续进行报告合并
在测试过程中,伴随bug的解决,代码也在不断更新,不同测试时期对应的代码版本不同,即采集上来的执行数据中,行号也存在偏差,在合并报告的时候需要将不同时期版本的代码进行校准,将不同版本相同代码行数的测试执行数据进行合并。
根据测试任务中的diff范围生成增量代码数据,在源码报告中标记增量代码数据范围,再根据增量代码范围和执行情况统计覆盖率数据
截止到目前为止,累计接入lua工程163个,lua覆盖率任务累计服务980+次。覆盖率工具作为测试辅助手段之一,使qa在做黑盒测试的同时达到了灰盒测试的效果,将qa的测试过程数据化、透明化、可视化。通过对代码执行情况进行分析,可精准锁定高风险代码,为测试质量进行兜底。