廢話在前 什麼是代碼覆蓋率 來自 百度百科 代碼覆蓋(Code coverage)是軟體測試中的一種度量,描述程式中源代碼被測試的比例和程度,所得比例稱為代碼覆蓋率。 開發人員為何關註? 在我們的開發過程中,經常要用各種方式進行自測,或是各種 xUnit 系列,或是 postman,或是直接curl ...
廢話在前
什麼是代碼覆蓋率
來自百度百科
代碼覆蓋(Code coverage)是軟體測試中的一種度量,描述程式中源代碼被測試的比例和程度,所得比例稱為代碼覆蓋率。
開發人員為何關註?
在我們的開發過程中,經常要用各種方式進行自測,或是各種 xUnit 系列,或是 postman,或是直接curl,在我們的代碼交給 QA 同學之前,我們有必要知道自己的自測驗過了多少內容,在這種情況,代碼覆蓋率就是一個重要的衡量指標。
openresty 中的代碼覆率解決方案
我們如果想得到每一次執行的代碼覆率,需要搞定兩件事情:
- 可以外在的記錄每一行的代碼
- 在記錄的同時,可以知道這一行的代碼上下文是什麼
- 更重要的是,我們需要儘可能的不動現有業務代碼
對於第一點,lua的debug庫中有一個非常神奇的鉤子函數sethook
,其官方文檔如下:
debug.sethook ([thread,] hook, mask [, count])
Sets the given function as a hook. The string mask and the number count describe when the hook will be called. The string mask may have any combination of the following characters, with the given meaning:
- 'c': the hook is called every time Lua calls a function;
- 'r': the hook is called every time Lua returns from a function;
- 'l': the hook is called every time Lua enters a new line of code.Moreover, with a count different from zero, the hook is called also after every count instructions.
When called without arguments, debug.sethook turns off the hook.
When the hook is called, its first argument is a string describing the event that has triggered its call: "call" (or "tail call"), "return", "line", and "count". For line events, the hook also gets the new line number as its second parameter. Inside a hook, you can call getinfo with level 2 to get more information about the running function (level 0 is the getinfo function, and level 1 is the hook function).
其中文翻譯大體如下:
將給定的方法設定為鉤子,參數
mask
和count
決定了什麼時候鉤子方法被調用.參數mask
可以是下列字元的組合:
- 'c' 當lua開始執行一個方法時調用;
- 'r' 當lua執行一個方法在返回時調用;
- 'l' 當lua每執行到一行代碼時調用.即lua從0開始執行一個方法的每一行時,這個鉤子都會被調用.
如果調用時不傳任何參數,則會移除相應的鉤子.當一個鉤子方法被調用時,第一個參數表明瞭調用這個鉤子的事件:
"call"
(或"tail call"
),"return"
,"line"
和"count"
.對於執行代碼行的事件,新代碼的行號會作為第二個參數傳入鉤子方法,可以用debug.getinfo(2)
得到其他上下文信息.
在這個官方的說明裡,lua已經貼心的告訴我們使用方式————配合debug.getinfo,那麼debug.getinfo是什麼?其實我們在之前討論錯誤輸出時已經使用過這個方法,其官方文檔如下:
debug.getinfo ([thread,] f [, what])
Returns a table with information about a function. You can give the function directly or you can give a number as the value of f, which means the function running at level f of the call stack of the given thread: level 0 is the current function (getinfo itself); level 1 is the function that called getinfo (except for tail calls, which do not count on the stack); and so on. If f is a number larger than the number of active functions, then getinfo returns nil.
The returned table can contain all the fields returned by lua_getinfo, with the string what describing which fields to fill in. The default for what is to get all information available, except the table of valid lines. If present, the option 'f' adds a field named func with the function itself. If present, the option 'L' adds a field named activelines with the table of valid lines.
For instance, the expression debug.getinfo(1,"n").name returns a name for the current function, if a reasonable name can be found, and the expression debug.getinfo(print) returns a table with all available information about the print function.
這個API的說明中文含義大體如下:
以table的形式返回一個函數的信息,可以直接調用這個方法或是傳入一個表示調用堆棧深度的參數f,0表示當前方法(即getinfo本身),1表示調用getinfo的方法(除了最頂層的調用,即不在任何方法中),以此類推。如果傳入的值比當前堆棧深度大,則返回nil.
返回的table內欄位包含由lua_info返回的所有欄位。預設調用會除了代碼行數信息的所有信息。當前版本下,傳入
'f'
會增加一個func
欄位表示方法本身,傳入'L'
會增加一個activelines
欄位返回函數所有可用行數。
例如如果當前方法是一個有意義的命名,
debug.getinfo(1,"n").name
可以得到當前的方法名,而debug.getinfo(print)
可以得到print方法的所有信息。
OK,有了這兩個方法,我們的思路就變得很清析了:
- 在生命周期開始時註冊鉤子函數.
- 將每一次調用情況記錄彙總.
這裡有一個新的問題,就是,我們的彙總是按調用累加還是只針對每一次調用計算,本著實用的立場,我們是需要進行累加的,那麼,需要使用ngx.share_dict 來保存彙總信息.
基於以上考慮,封裝一個libs/test/hook.lua文件,內容如下:
local debug = load "debug"
local cjson = load "cjson"
local M = {}
local mt = { __index = M }
local sharekey = 'test_hook'
local cachekey = 'test_hook'
function M:new()
local ins = {}
local share = ngx.shared[sharekey]
local info ,ret = share:get(cachekey)
if info then
info = cjson.decode(info)
else
info = {}
end
ins.info = info
setmetatable(ins,mt)
return ins
end
function M:sethook ()
debug.sethook(function(event,line)
local info = debug.getinfo(2)
local s = info.short_src
local f = info.name
local startline = info.linedefined
local endline = info.lastlinedefined
if string.find(s,"lualib") ~= nil then
return
end
if self.info[s] == nil then
self.info[s]={}
end
if f == nil then
return
end
if self.info[s][f] ==nil then
self.info[s][f]={
start = startline,
endline=endline,
exec = {},
activelines = debug.getinfo(2,'L').activelines
}
end
self.info[s][f].exec[tostring(line)]=true
end,'l')
end
function M:save()
local share = ngx.shared[sharekey]
local ret = share:set(cachekey,cjson.encode(self.info),120000)
end
function M:delete()
local share = ngx.shared[sharekey]
local ret = share:delete(cachekey)
self.info = {}
end
function M:get_report()
local res = {}
for f,v in pairs(self.info) do
item = {
file=f,
funcs={}
}
for m ,i in pairs(v) do
local cover = 0
local index = 0
for c,code in pairs(i.activelines) do
if i.activelines[c] then
index = index + 1
end
if i.exec[tostring(c)] or i.exec[c] then
cover = cover +1
end
end
item.funcs[#item.funcs+1] = { name = m ,coverage= string.format("%.2f",cover / index*100 ) .."%"}
end
res[#res+1]=item
end
return res
end
return M
這樣,我們只需要在content_by_lua的最開始加上:
local hook = load "libs.test.hook"
local test = hook:new()
test:sethook()
--other code ..
在最末加上:
test:save()
即可統計代碼覆蓋率。
是的,沒錯,我們至今只增加了4行業務代碼
但是統計了,應該怎麼進行輸出呢?
加個介面好了:)
因為現在lor用的多,所以,乾脆加個lor的路由文件(libs/test/lorapi.lua):
local hook = require 'libs.test.hook'
local router = lor:Router ()
local M = {}
router:get('/test/coverage/json-report',
function(req,res,next)
local t = hook:new()
res:json(t:get_report())
end)
router:get('/test/coverage/txt-report',
function(req,res,next)
local t = hook:new()
local msg = "Report"
local rpt = t:get_report()
for i ,v in pairs(rpt) do
msg =msg.."\r\n"..v.file
for j,f in pairs(v.funcs) do
msg = msg .."\r\n\t function name:" .. f.name .."\tcoverage:"..f.coverage
end
end
msg =msg .."\r\nEnd"
res:send(msg)
end)
return router
這樣,在我們的lor路由文件裡加個requre,加個use,兩行改動,而且是增加!!就達到我們的需求,檢查代碼的覆蓋率.