百度单测生成技术如何召回线上服务的异常问题?
導讀:線上系統異常問題一直以來都是使人”聞風喪膽”的,傳統手段在解決這類問題時面臨著相應的技術瓶頸。基于此,探索基于單元測試召回異常問題的方法,實現了一套通用且無人參與的單測生成系統,在百余模塊上落地取得了一定的效果。從近代碼手段的單元測試著手,圍繞基于單測生成技術召回異常問題的應用實踐展開。主要介紹該方案0到1的整體建設思路、并從理解代碼、構造高覆蓋測試用例數據、生成測試用例代碼以及分析失敗用例這四方面展開介紹。
引言
本文提出一種基于白盒手段召回異常問題的通用方法,并以C/C++語言為例,介紹該方法在百度服務端的落地思路。
一 背景
線上穩定性問題一直以來是備受大家關注的。在影響產品收益或用戶體驗的同時,也影響著QA的口碑。
為了避免這類問題發生在線上,測試人員有一系列的異常測試召回手段可以采取,常見的有:基于壓力測試、基于功能測試、基于單元測試或者基于靜態掃描的異常召回手段。然而在足夠完備的召回能力下,還是會有問題漏出到線上,這又是為什么呢?
二 問題分析
帶著上面的疑問,本文對業界內現有異常測試手段在高成本、低召回這兩個問題維度上進行了對比分析,對比結果如下表2.1所示。
表2.1 業界常用異常測試手段缺點對比分析
整體上看,當前的召回手段存在滯后性或成本高的問題,像基于壓力測試的異常召回手段資源消耗較高,基于功能測試的異常召回手段除開發成本較高外,還存在異常場景不易構造等滯后性問題?;趩卧獪y試和靜態檢查來發現代碼問題已是這些手段中缺點相對較少的,接下來更詳細地對比下單元測試和靜態檢查這兩種召回異常的手段。
如今,靜態檢查已是最常見的異常發現手段,其接入成本低,輕量級。以靜態掃描方式來檢查,不需要編譯運行、不占用資源。但其存在以下問題:
- 滯后性:線上出了問題后才能轉化為規則規避同類問題復發。
- 準召低:規則靠人來設計,對某些場景會存在漏掉或者誤報的問題,需要case by case的解決。
- 不可持續:缺少圍繞規則的生態建設,可能出現規則重復開發、缺少規則貢獻者、規則上線后無法有效評估等問題。
基于單元測試來召回異常問題有兩個缺點:開發成本高、依賴人的意識。開發人員針對本次功能中的重要函數,編寫其對應的單元測試代碼來進行測試。選擇哪些函數,驗證哪些異常場景都依賴開發人員的經驗和主動程度。但它也有以下優點:
- 測試最小單元,易于構造數據,驗證正確性
- 便于后續功能回歸
- 資源消耗小
- 能更早發現問題,定位和解決成本低
經過上述分析后,可以得出基于單元測試的優勢遠大于其缺點的結論,于是大膽假設:能否最大化它的優勢,解決依賴『寫』和『經驗』的問題?!醋詣幼珜懏惓5膯卧獪y試代碼,主動發現代碼健壯性問題。
在這一設想下,提出一種可持續的、主被動結合、高ROI的穩定性問題召回漏斗,智能UT作為靜態代碼檢查環節后的主動召回手段,動態分析召回問題。
圖2.1 穩定性召回漏斗圖
三 解決思路
19年初,在對生成用例代碼的強訴求下,調研了C/C++語言業界中比較通用且優秀的單測生成工具:C++ test和Wings,在開發成本、召回能力和是否開源三方面進行了對比,對比結果如下表3.1所示。顯然,無論是C++ test還是Wings,都無法滿足業務線在復雜業務場景下完全自動化對復雜類型函數生成單測代碼的訴求以及擴展,因此需要自建單測代碼生成能力。
表3.1 業界C/C++單測生成工具調研對比
將開發人員對一個函數撰寫單元測試代碼的過程進行拆解,各關鍵步驟如圖3.1所示,將整個過程抽象為確認待測試函數->分析代碼->構造測試數據->生成測試代碼這4個過程。
- 確認待測試函數:本次提交的代碼中,并非所有的變更或新增函數都需要測試,可以結合函數屬性(如構造函數、析構函數等)、修改內容(如測試相關的代碼、日志邏輯等無風險函數)。
- 分析代碼:匯總被測函數的代碼,如參數(輸入參數、內部依賴其它依賴參數)、返回值等信息。
- 構造測試數據:主動構造被測函數所需的用例數據,無需人工參與。
- 生成測試代碼:主動生成測試被測函數的代碼,無需人工參與。
- 解決思路的關鍵是通過代碼分析等白盒技術來實現一鍵異常單元測試代碼的能力,真實模擬開發人員撰寫單元測試代碼。
圖3.1開發人員實現單測代碼編寫的過程
四 實現方案
基于上一節分析,整個技術方案設計如下圖所示,本節重點介紹代碼分析、測試用例生成、代碼生成能力和執行分析的實現思路。
圖4.1 技術方案設計圖
4.1 代碼分析能力
代碼分析的目標是期望能通過靜態代碼掃描的手段,將復雜的函數代碼抽象成結構化的函數特征數據,可以類比編譯符號表?;谶@份結構化數據能直接感知函數調用方式、變量聲明和賦值方式等行為。
4.1.1 代碼特征
C/C++語言中,尤其是C++這類面向對象的語言,函數調用和類的聲明創建方式和普通變量不同,存在更豐富的語法多樣性。首先要明確該語言在代碼分析過程中需要獲得的信息內容,重點考慮的因素如下:
- 函數調用:普通函數調用、類的成員函數調用
在調用類的普通成員函數前,需要先實例化類的對象,而非成員函數可直接調用。 - 變量聲明實現:普通變量、class或struct變量、stl變量等
不同變量聲明賦值方式都不同,需要能夠區分是普通變量還是class、struct、stl變量 - 修飾符:const、static、virtual、inline等
加了修飾符的變量或函數會影響它的調用、實例化、賦值方式 - 文件級別信息:頭文件,命名空間
頭文件和命名空間不全或者缺失會影響測試代碼的編譯 - 其它
一些影響賦值、實例化語法的其它屬性。如類是否禁用了拷貝/賦值構造函數等。
基于上述思路,最初敲定獲取如下代碼特征信息:
表4.1 代碼特征信息
4.1.2 特征存儲
將特征存儲以xml文件格式存儲,存儲為代碼結構數據(CodeStructData,CSD),且保證周邊模塊能基于該份產出獲取函數調用方式、變量聲明和賦值方式。根據不同類型和賦值方式約定schema,如type、baseType1、parmType等屬性,Demo如下圖所示。
type:實際類型
baseType1:該變量實際屬于類別,如內置類型、數組類型、STL類型等。
parmType:聲明類型,生成代碼時可直接取該字段作為變量的聲明類型。
圖4.2 經源碼分析得到的CSD樣例
4.1.3 特征采集
這一環節,期望能在不編譯的條件下,以靜態代碼掃描方式提取代碼信息,且工具要輕量、高效、支持開源,以便于后續需求迭代。
在綜合對比下,最終選擇cppcheck,一個開源的靜態代碼檢查工具,除此之外還可以基于它的符號表來做二次開發。為了采集函數調用鏈信息和其它全局信息,內部對cppcheck進行了改造,后續會單獨介紹,本文不多贅述。采集過程如下圖4.3所示。
圖4.3 基于cppcheck的代碼分析方案示意圖
4.2 用例數據生成能力
4.2.1 解決思路**
用例數據生成能力屬于Fuzzing技術領域中關鍵的一個環節,常見的fuzz數據手段有基于生成的和基于變異的兩種方式。一般會使用覆蓋率來衡量fuzz能力,比如函數覆蓋、行覆蓋或分支覆蓋。
- 基于變異法:根據已知數據樣本通過變異的方法,生成測試用例。比如著名的AFL-fuzz技術,其主要處理過程如下圖4.4所示:
圖4.4 AFL-Fuzz處理過程
- 基于生成法:根據已知協議或接口規范進行建模,生成測試用例。比如libfuzzer可以在不指定初始數據集下,通過被測目標的接口類型,隨機生成字節數據,喂給被測目標。
在生成用例數據時,避免用例爆炸也是生成的條件之一,過多的用例會存在用例無效和運行時效低問題。
本文在傳統的基于生成法構造用例數據的基礎上,除了被測目標接口協議外,充分利用路徑和分支信息來指導fuzz數據,覆蓋更多分支內的邏輯,還引入了其它白盒特征,如變量擴散關聯性等去降低對無效用例的生成,最后以函數覆蓋和分支覆蓋作為fuzz能力的度量指標。
解決思路如下圖4.5所示,數據生成層由CSD處理模塊、路徑選擇模塊、參數選擇模塊和生成&篩選模塊構成。針對不同類型的變量,選取不同的異常候選集,生成初始用例集合,再經過用例篩選策略得到最終的測試用例集。
圖4.5 測試用例生成方案示意圖
4.2.2 路徑選擇
路徑選擇模塊包含表達式約束求解、路徑可達分析以及路徑合并。這一部分的目的是指導數據生成對分支的覆蓋。路徑的提取,主要通過遍歷上一節提取的程序控制流數據來完成,可以采取深度優化遍歷或廣度優先遍歷,不影響結果。
為了避免路徑爆炸,可以先提取出期望測試覆蓋的目標,遍歷時每次選擇一個可以覆蓋待測試目標的路徑。
1)約束求解是指對路徑上的分支表達式進行求解計算,分別計算出表達式為真和假時的符號值。這里需要先對表達式進行替換,例如將函數調用替換成變量,便于計算。替換后的表達式可以使用開源的庫進行求解,如z3。
2)路徑可達分析是指以分支如if、while、for、switch為節點,計算節點內求解出的變量值或變量范圍,對函數內部各節點進行連接后,得到一個圖。結合每個節點變量的范圍,對圖中的路徑進行剔除,刪掉不可達的路徑。
3)路徑合并是指將含有交集的節點合并成一條路徑,減少后續用例生成數量。如下圖4.6中對_index_i和_index_j構造用例時,構造出{_index_i=1, _index_j=2}來滿足同時覆蓋17行和22行兩個分支的數據。在處理時需要分析出分支內部是否存在return、continue、break這類的跳轉或返回關鍵字,避免出現badcase。
圖4.6 程序示例
4.2.3 候選數據源
各類型的候選異常數據可分為靜態數據和動態數據。
- 靜態數據指通過歷史經驗維護的一份類型邊界值和業務邊界值數據庫。
- 動態數據指通過業務數據采集和變異算法,基于模塊日志、流量等數據源通過插樁的方式挖掘出的業務值或經過變異得到的異常邊界值。
4.2.4 用例生成&篩選
基于上述步驟得到各參數候選值集合后,便可對參數之間進行組合,得到用例集合,參數組合的方式直接影響著用例量級,此階段重點考慮如何避免用例爆炸,減量不減質量。
經統計,大約70%以上的軟件問題是由一個或2個參數作用引起的。因此參數因子兩兩組合就成為了軟件測試中一種實施性較強同時又比較有效的方法。如果采用全排列組合方式,在某業務場景下,某類classA類型作為函數形參,假設該classA有1000個成員變量,其成員變量全部為v類型,類型v有4個取值,v=[-1,0,1,-2147483649],那么全排列組合后的用例數據量高達4^1000個。
可見,單純的全排列組合能保證當前兩兩因子組合覆蓋的場景最豐富,但會面臨case爆炸問題,這不符合實際應用背景。
其實,生成一個最小測試用例集是一個NPC問題,因此學術界一般是將找到一個盡可能小的測試用例集去覆蓋所有可能的配對來作為研究目標。本文先后使用兩個步驟來減輕用例的量級。
通過分析自定義類型參數其成員屬性擴散性,只對類/結構體中實際被用到的成員屬性構造數據。
圖4.7 函數內變量和其成員變量樣例
- 剔除冗余用例:采用基于生成的方式,選擇一種參數組合算法,生成合適的測試用例。常見的生成技術大體可分為組合設計法、啟發式算法、元啟發探索法。
- 組合設計法:一般是圍繞正交表或其它代數的思路生成測試用例。
- 啟發式算法:一般是逐條地或逐因素擴展地生成測試用例。
如經典的AETG算法:首先按貪心算法生成一定數量N個測試用例,然后從這N個測試用例中選擇一個能最多覆蓋未覆蓋配對集合中參數對的用例,將這個用例添加進已經形成的測試用例集T中,直至達到覆蓋目標。如IPO算法,通過先水平、再垂直的方式擴充用例。 - 元啟發式算法:如遺傳算法、模擬退火、蟻群算法等,大致過程如下圖4.8所示。
圖4.8 元啟發式用例探索大致過程
啟發式和元啟發式都屬于局部搜索算法,不能保證最優,但可以保證處理時間。還可以將逐條生成法和元啟發方式結合,引入錯誤風險系數、組合約束和參數優先級等信息,進一步優化組合方式。
本文初期利用逐條生成的方式,基于基礎的成對法來減少重復無效的輸入。以一個例子簡單介紹本文使用的2-Wise testing成對法思路(其原理可參考文末提供的資料):
假定有三個輸入變量,X、Y、Z,取值分別為D(X)={x1,x2,x3},D(Y)={y1,y2},D(Z)={z1,z2};
如果用全排列法,得到的測試用例集有3 X 2 X 2 = 12個用例,具體測試用例如下左圖4.9所示,通過2-Wise testing處理后僅獲得6個用例。
圖4.9 全排列用例和成對法算法過程
本文通過上述方法,有效剔除了90%以上的無用測試用例數據。最終將保留下來的測試用例以json格式存儲,作為測試數據集合,方便擴展和供其它場景使用。數據Demo如下圖4.10所示,以函數名、func_data、變量名作為key,以具體的參數值作為value。
圖4.10 測試用例集合demo圖
當前這種生成方式是以參數和參數之間相互獨立為假設前提,思路簡單,而實際業務場景下,參數和參數之間是可能存在關聯的,在生成方式上還有較大提升空間,后期會在當前逐條生成的能力下,引入元啟發探索算法,如在該領域效果比較顯著的遺傳算法或模擬退火算法,在每生成一條測試用例時都調用探索算法,以提升覆蓋率和重要覆蓋元素為目標,生成有效測試用例集,這也是智能UT中的最重要的『智能』場景之一,數據是揭錯之本。
4.3 代碼生成能力
4.3.1 解決思路
代碼生成領域目前主要有兩個重要方向:程序生成和代碼補全。生成測試代碼屬于程序生成方向,采用深度學習算法生成代碼是目前學術界當前比較重要的研究方向,已經基于一些開源的代碼作為語料庫取得了一定的技術突破,但因存在泛化能力弱的問題,還無法在工業界落地。
在實際技術落地中,程序生成的正確性直接影響測試任務的穩定性,考慮到這一約束,本文目前采用基于語法規則和模板的生成方式來生成測試用例代碼。語法規則和代碼結構數據正確即可保證生成代碼語法正確,達到生成即可編譯的目標。
具體實現方案參考如下圖4.11所示,將上述步驟得到的代碼結構數據和測試用例集合數據下發給代碼生成處理模塊,模塊通過控制層選擇不同語言對應的生成器,再根據不同類型選擇對應的生成算子。對于可變內容,深度遍歷代碼結構數據的每一個函數節點、參數節點和全局節點,針對各自節點下的代碼信息,獲取對應的語法適配生成算子來生成目標代碼,從而得到測試用例代碼,再結合模板中的固定源碼,封裝成可編譯運行的單測代碼。這一過程可以類比編譯器結合語法樹生成目標代碼的過程。
像C/C++語言,生成基于Gtest的死亡測試封裝的測試用例代碼,測試被測函數是否非預期死亡。還可以基于當前的生成框架,便捷地擴展其它語法規則來生成不同語言不同形式的用例代碼。
圖4.11 代碼生成方案示意圖
4.3.2 完整demo展示
如下圖所示是一個被測源碼exlore_filter函數經過代碼分析、用例數據生成后得到測試用例代碼的過程樣例。
圖4.12 測試用例集合demo圖
4.4 失敗用例分析
基于上一節介紹的代碼生成能力,可獲得可編譯的測試用例代碼,通過編譯適配模塊生成編譯命令,執行編譯后即可得到可執行的測試程序。如何保證運行測試程序后快速獲取失敗信息,降低人介入的分析成本,是本節重點介紹的內容。
整個分析過程中可能存在的問題如下:
1、可讀性差:測試用例失敗后,其堆棧/crash不完整,或者無用信息太多。像c/c++語言的gtest死亡測試,用例crash后是無堆棧信息打印出來的,常規方式是通過gdb來獲取堆棧內容,當堆棧文件過大超過3G時,讀取速度會很慢。
2、重復的堆棧/crash
- 同一函數同一代碼行問題重復,主要是不同用例之間命中的問題重復。
例如如下場景的find函數,在輸入用例為{arr=nullptr,len=1}和{arr=nullptr,len=2}時都會命中sum+=arr[0]這一行的crash。
圖4.13 被測代碼片段
- 不同代碼行問題重復,主要是代碼語義相近導致的問題重復
例如如下場景的兩個程序片段A和程序片段B,_dest是類Action的成員變量,會在程序運行的其它階段被賦值。add_to_dest和get_from_dest分別crash在write_dest->write和read_dest->read行。其代碼行內容是不同的,但crash的語義是相同的,都是使用了空指針_dest導致程序crash。
圖4.14 被測代碼片段A
圖4.15 被測代碼片段B
3.定位成本高
對新人或不熟悉堆棧文件的人來說,測試用例代碼、CR以及堆棧信息都完備的情況下,依然存在跟進排查無頭緒的問題。
4.修復標準不統一
哪些問題一定要修復、哪些問題可以忽略掉,不同業務線缺少統一的標準。
本文通過堆棧內容存儲、堆棧內容分析、去重、失敗原因預測以及失敗問題分級等手段來解決上述問題,解決思路如下圖所示,每個階段細節較多,本文不重點展開介紹。
圖4.16 堆棧分析過程
4.5 技術架構
面向業務落地需要考慮如何將工具的能力發揮到合適的階段,做到恰到好處,結合研發開發習慣,我們考慮了如下兩方面因素:
- 存量問題需要修復周期:業務模塊直接掃描,會因歷史遺留問題過多而產生較大修復成本,需要一定的時間來消化。
- 迭代時只需要關注變更影響面:在變更流水線上掃描全量代碼,對全量代碼生成用例會造成資源浪費以及執行效率低的問題。
基于上述考慮,我們將落地方式劃分為兩種模式:存量和增量。
- 存量:新接入模塊建議先跑全量,掃描存量問題,讓研發團隊出統一修復負責人,進行統一修復,消除存量隱患。也可以在daily任務或全量回歸任務中跑存量掃描模式。
- 增量:是指只針對變更代碼,通過白盒分析手段,分析出其影響的代碼范圍,如直接影響(改動函數)、間接影響(未改動但邏輯上會有影響),只對影響范圍內的函數進行測試。
在修改代碼提交后,可觸發流水線跑增量模式的任務。
這里還可以引入風險考量,評估出函數修改內容是否需要測試,剔除掉無風險函數。
基于上述思路,將代碼分析、用例數據生成和代碼生成能力集成到如下技術架構中,和百度內部策略中臺、數據中臺、可視化平臺等能力結合,貫徹測試準備、測試執行、測試分析到問題定位這四個維度,完成基于單測生成的異常召回工具的建設和落地。
圖4.17 落地架構圖
部分任務結果如下圖所示,研發人員本地開發提交代碼后自動觸發流水線綁定的智能UT測試任務,通過報告可查看到crash問題詳情,包括失敗原因、失敗堆棧內容等。
圖4.18 任務執行展示樣例圖
五 效果
1. 工程效果
· 實踐:探索出基于單測解決異常問題的通用方案,已在C/C++語言上落地實踐,累計生成千萬余行測試代碼,其它語言進行中
· 高覆蓋:冷啟函數覆蓋50%+,分支覆蓋20%+
· 低資源:機器資源消耗同系統級測試相比可忽略
· 低人耗:自動適配UT及測試代碼編譯能力,無需人工搭建單測框架和維護
2. 業務效果
· 落地:覆蓋140+重點后端模塊、lib庫
· 存量召回:召回存量問題900余例
· 增量召回:增量召回問題200余例
參考資料
2.Fuzzing:https://baike.baidu-com/item/%E6%A8%A1%E7%B3%8A%E6%B5%8B%E8%AF%95/2848962?fr=aladdin
期待你的加入
百度開發者中心已開啟征稿模式,歡迎開發者登錄developer.baidu.com進行投稿,優質文章將獲得豐厚獎勵和推廣資源。
總結
以上是生活随笔為你收集整理的百度单测生成技术如何召回线上服务的异常问题?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: san-hot-loader 应用及原理
- 下一篇: 技术干货 | 基于 Doris 构建的小