[lua][openresty]代碼覆蓋率檢測的解決方式

来源:https://www.cnblogs.com/geyunfei/archive/2018/12/20/10147219.html
-Advertisement-
Play Games

廢話在前 什麼是代碼覆蓋率 來自 百度百科 代碼覆蓋(Code coverage)是軟體測試中的一種度量,描述程式中源代碼被測試的比例和程度,所得比例稱為代碼覆蓋率。 開發人員為何關註? 在我們的開發過程中,經常要用各種方式進行自測,或是各種 xUnit 系列,或是 postman,或是直接curl ...


廢話在前

什麼是代碼覆蓋率

來自百度百科

代碼覆蓋(Code coverage)是軟體測試中的一種度量,描述程式中源代碼被測試的比例和程度,所得比例稱為代碼覆蓋率。

開發人員為何關註?

在我們的開發過程中,經常要用各種方式進行自測,或是各種 xUnit 系列,或是 postman,或是直接curl,在我們的代碼交給 QA 同學之前,我們有必要知道自己的自測驗過了多少內容,在這種情況,代碼覆蓋率就是一個重要的衡量指標。

openresty 中的代碼覆率解決方案

我們如果想得到每一次執行的代碼覆率,需要搞定兩件事情:

  1. 可以外在的記錄每一行的代碼
  2. 在記錄的同時,可以知道這一行的代碼上下文是什麼
  3. 更重要的是,我們需要儘可能的不動現有業務代碼

對於第一點,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).

其中文翻譯大體如下:

將給定的方法設定為鉤子,參數maskcount決定了什麼時候鉤子方法被調用.參數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,有了這兩個方法,我們的思路就變得很清析了:

  1. 在生命周期開始時註冊鉤子函數.
  2. 將每一次調用情況記錄彙總.

這裡有一個新的問題,就是,我們的彙總是按調用累加還是只針對每一次調用計算,本著實用的立場,我們是需要進行累加的,那麼,需要使用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,兩行改動,而且是增加!!就達到我們的需求,檢查代碼的覆蓋率.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 楔子 最近一個項目,需要繪製雙線的效果,雙線效果表示的是軌道(類似鐵軌之類的),如下圖所示: 負責這塊功能開發的小伙,姑且稱之為L吧,最開始是通過數學計算的方式來實現這種雙線,也就是在原來的路徑的基礎上,計算出兩條路徑。但是這個過程的計算算挺複雜,而是最終實現的效果很耗性能,性能損耗估計主要在於路徑 ...
  • 一、概念 模板方法模式:在一個方法中定義一個演算法的骨架,而將一些步驟延遲到子類中。模板方法使得子類可以在不改變演算法結構的情況下,重新定義演算法中的某些步驟。 解析:模板方法模式用來創建一個演算法的模板。什麼是模板?模板就是一個方法。更具體地說,這個方法將演算法定義成一組步驟,其中的任何步驟都可以是抽象的, ...
  • 在一次公開課上,聽別人講過全局分散式uuid的設計,聽過twitter的snowflake的設計。也聽過,如果使用單獨的計數器服務,不可能每次都保存當前計數器到文本,自己想到應該可以每隔一些數,例如1萬次,10萬次,反正64位的空間比較大,然後保存起來,那麼就沒有每次保存,對硬碟的寫入壓力。當出現故 ...
  • 建造者模式 一:建造者概述 我們大家可能都會開小汽車,但是當你得到一輛小汽車的時候,你可以用它馳騁馬路,但是你知道它組件複雜的構造過程嗎,並不知道。而我們今天要講的建造者模式其實就是 ,`客戶端無需知道複雜對象的內部組成和裝配方式,只需要知道建造者的類型即可 一步步的創建獨立的複雜對象,不同的具體構 ...
  • PHP註冊樹模式主要用於創建對象的時候將我們的對象與相應的變數進行綁定,從這個角度上說,Yii2的Service Locator和DI Container都用到註冊樹模式。這二者都在內部維護一個數組(key => value),value為對象或者對象定義,在獲取時通過唯一的key來獲取,如果是定義... ...
  • 一、面向過程 我們是怎麼思考和解決上面的問題的呢? 答案是:我們自己的思維一直按照步驟來處理這個問題,這是我們常規思維,這就是所謂的面向過程POP編程 二、面向過程POP為什麼轉換為面向對象OOP 面向過程想的思想步驟越多,變化越多,是無法掌控的,所以有時候非常複雜,就比如我們拿起來手機玩游戲如果按 ...
  • 作者按:《每天一個設計模式》旨在初步領會設計模式的精髓,目前採用 和`python`兩種語言實現。誠然,每種設計模式都有多種實現方式,但此小冊只記錄最直截了當的實現方式 :) 個人技術博客 "godbmw.com" 歡迎來玩! 每周至少 1 篇原創技術分享,還有開源教程(webpack、設計模式)、 ...
  • 觀察者模式 Observer,又被稱為發佈訂閱模式或者源監聽,是一種行為型設計模式,也是也是很多系統中常常用到的一種消息處理機制,Java內置了對觀察者模式的支持,藉助於觀察者模式可以很好地完成消息的發佈與訂閱,本文對觀察者模式進行了簡介,並且給出了意圖和結構的解析,並且給出了Java代碼示例。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...