對蘋果開發者而言,因爲平臺審覈週期較長,客戶端代碼致使的線上問題影響時間每每比較久。若是在開發、測試階段可以提早暴露問題,就有助於避免線上事故的發生。代碼覆蓋率檢測正是幫助開發、測試同窗提早發現問題,保證代碼質量的好幫手。php
對於開發者而言,代碼覆蓋率能夠反饋兩方面信息:html
儘管代碼覆蓋率對代碼質量有着上述好處,但在 iOS 開發中卻使用的很少。咱們調研了市場上經常使用的 iOS 覆蓋率檢測工具,這些工具主要存在如下四個問題:node
爲了解決上述問題,咱們深刻調研了覆蓋率報告的生成邏輯,並結合團隊的開發流程,開發了一套嵌入在代碼提交流程中、基於單次代碼提交(git commit)生成報告、對開發者透明的增量代碼測試覆蓋率工具。開發者只須要正常開發,經過模擬器測試開發代碼,commit 本次代碼(commit 和測試順序可交換),推送(git push)到遠端,就能夠在本地看到此次提交代碼的詳細覆蓋率報告了。c++
本文分爲兩部分,先從介紹通用覆蓋率檢測的原理出發,讓讀者對覆蓋率的收集、解析有直觀的認識。以後介紹咱們增量代碼測試覆蓋率工具的實現。git
生成覆蓋率報告,首先須要在 Xcode 中配置編譯選項,編譯後會爲每一個可執行文件生成對應的 .gcno 文件;以後在代碼中調用覆蓋率分發函數,會生成對應的 .gcda 文件。github
其中,.gcno 包含了代碼計數器和源碼的映射關係, .gcda 記錄了每段代碼具體的執行次數。覆蓋率解析工具須要結合這兩個文件給出最後的檢測報表。接下來先看看 .gcno 的生成邏輯。算法
利用 Clang 分別生成源文件的 AST 和 IR 文件,對比發現,AST 中不存在計數指令,而 IR 中存在用來記錄執行次數的代碼。搜索 LLVM 源碼能夠找到覆蓋率映射關係生成源碼。覆蓋率映射關係生成源碼是 LLVM 的一個 Pass,(下文簡稱 GCOVPass)用來向 IR 中插入計數代碼並生成 .gcno 文件(關聯計數指令和源文件)。後端
下面分別介紹IR插樁邏輯和 .gcno 文件結構。數組
代碼行是否執行到,須要在運行中統計,這就須要對代碼自己作一些修改,LLVM 經過修改 IR 插入了計數代碼,所以咱們不須要改動任何源文件,僅需在編譯階段增長編譯器選項,就能實現覆蓋率檢測了。緩存
從編譯器角度看,基本塊(Basic Block,下文簡稱 BB)是代碼執行的基本單元,LLVM 基於 BB 進行覆蓋率計數指令的插入,BB 的特色是:
覆蓋率計數指令的插入會進行兩次循環,外層循環遍歷編譯單元中的函數,內層循環遍歷函數的基本塊。函數遍歷僅用來向 .gcno 中寫入函數位置信息,這裏再也不贅述。
一個函數中基本塊的插樁方法以下:
舉個例子,下面是一段猜數字的遊戲代碼,當玩家猜中了咱們預設的數字10的時候會輸出Bingo
,不然輸出You guessed wrong!
。這段代碼的控制流程圖如圖1所示。
- (void)guessNumberGame:(NSInteger)guessNumber
{
NSLog(@"Welcome to the game");
if (guessNumber == 10) {
NSLog(@"Bingo!");
} else {
NSLog(@"You guess is wrong!");
}
}
複製代碼
例1 猜數字遊戲
這段代碼若是開啓了覆蓋率檢測,會生成一個長度爲 6 的 64 位數組,對照插樁位置,方括號中標記了樁點序號,圖 1 中代碼前數字爲所在行數。
.gcno 是用來保存計數插樁位置和源文件之間關係的文件。GCOVPass 在經過兩層循環插入計數指令的同時,會將文件及 BB 的信息寫入 .gcno 文件。寫入步驟以下:
從上面的寫入步驟能夠看出,.gcno 文件結構由四部分組成:
經過這四部分結構能夠徹底還原插樁代碼和源碼的關聯,咱們以 BB 結構 / BB 行結構爲例,給出結構圖 2 (a) BB 結構,(b) BB 行信息結構,在本章末尾覆蓋率解析部分,咱們利用這個結構圖還原代碼執行次數(每行等高格表明 64bit):
關於 .gcda 的生成邏輯,可參考覆蓋率數據分發源碼。這個文件中包含了 __gcov_flush()
函數,這個函數正是分發邏輯的入口。接下來看看 __gcov_flush()
如何生成 .gcda 文件。
經過閱讀代碼和調試,咱們發如今二進制代碼加載時,調用了llvm_gcov_init(writeout_fn wfn, flush_fn ffn)
函數,傳入了_llvm_gcov_writeout
(寫 gcov 文件),_llvm_gcov_flush
(gcov 節點分發)兩個函數,而且根據調用順序,分別創建了以文件爲節點的鏈表結構。(flush_fn_node * ,writeout_fn_node *
)
__gcov_flush()
代碼以下所示,當咱們手動調用__gcov_flush()
進行覆蓋率分發時,會遍歷flush_fn_node *
這個鏈表(即遍歷全部文件節點),並調用分發函數_llvm_gcov_flush
(curr->fn 正是__llvm_gcov_flush
函數類型)。
void __gcov_flush() {
struct flush_fn_node *curr = flush_fn_head;
while (curr) {
curr->fn();
curr = curr->next;
}
}
複製代碼
觀察__llvm_gcov_flush
的 IR 代碼,能夠看到:
__llvm_gcov_flush
先調用了__llvm_gcov_writeout
,來向 .gcda 寫入覆蓋率信息。__llvm_gcov_ctr.xx
。而__llvm_gcov_writeout
邏輯爲:
生成對應源文件的 .gcda 文件,寫入 Magic number。
循環執行 llvm_gcda_emit_function
: 向 .gcda 文件寫入函數信息。
llvm_gcda_emit_arcs
: 向 .gcda 文件寫入BB執行信息,若是已經存在 .gcda 文件,會和以前的執行次數進行合併。
調用llvm_gcda_summary_info
,寫入校驗信息。
調用llvm_gcda_end_file
,寫結束符。
感興趣的同窗能夠本身生成 IR 文件查看更多細節,這裏再也不贅述。
.gcda 的文件/函數結構和 .gcno 基本一致,這裏再也不贅述,統計插樁信息結構如圖 4 所示。定製化的輸出也能夠經過修改上述函數完成。咱們的增量代碼測試覆蓋率工具解決代碼 BB 結構變更後合併到已有 .gcda 文件不兼容的問題,也是修改上述函數實現的。
在瞭解瞭如上所述 .gcno ,.gcda 生成邏輯與文件結構以後,咱們以例 1 中的代碼爲例,來闡述解析算法的實現。
例 1 中基本塊 B0,B1 對應的 .gcno 文件結構以下圖所示,從圖中能夠看出,BB 的主結構徹底記錄了基本塊之間的跳轉關係。
B0,B1 的行信息在 .gcno 中表示以下圖所示,B0 塊由於是入口塊,只有一行,對應行號能夠從 B1 結構中獲取,而 B1 有兩行代碼,會依次把行號寫入 .gcno 文件。
在輸入數字 100 的狀況下,生成的 .gcda 文件以下:
經過控制流程圖中節點出邊的執行次數能夠計算出 BB 的執行次數,核心算法爲計算這個 BB 的全部出邊的執行次數,不存在出邊的狀況下計算全部入邊的執行次數(具體實現能夠參考 gcov 工具源碼),對於 B0 來講,即看 index=0 的執行次數。而 B1 的執行次數即 index=1,2 的執行次數的和,對照上圖中 .gcda 文件能夠推斷出,B0 的執行次數爲 ctr[0]=1,B1 的執行次數是 ctr[1]+ctr[2]=1, B2 的執行次數是 ctr[3]=0,B4 的執行次數爲 ctr[4]=1,B5 的執行次數爲 ctr[5]=1。
通過上述解析,最終生成的 HTML 以下圖所示(利用 lcov):
以上是 Clang 生成覆蓋率信息和解析的過程,下面介紹美團到店餐飲 iOS 團隊基於以上原理作的增量代碼測試覆蓋率工具。
因爲 gcov 工具(和前面的 .gcov 文件區分,gcov 是覆蓋率報告生成工具)生成的覆蓋率檢測報告可讀性不佳,如圖 9 所示。咱們作的增量代碼測試覆蓋率工具是基於 lcov 的擴展,報告展現如上節末尾圖 8 所示。
比 gcov 直接生成報告多了一步,lcov 的處理流程是將 .gcno 和 .gcda 文件解析成一個以 .info 結尾的中間文件(這個文件已經包含所有覆蓋率信息了),以後經過覆蓋率報告生成工具生成可讀性比較好的 HTML 報告。
結合前兩章內容和覆蓋率報告生成步驟,覆蓋率生成流程以下圖所示。考慮到增量代碼覆蓋率檢測中代碼增量部分須要經過 Git 獲取,比較天然的想法是用 git diff 的信息去過濾覆蓋率的內容。根據過濾點的不一樣,存在如下兩套方案:
分析這兩個方案,第一個方案須要自定義 LLVM 的 Pass,進而會引入如下兩個問題:
而第二個方案相對更加輕量,只須要過濾中間格式文件,不只能夠解決咱們在文章開頭提到的問題,也能夠避免上述問題:
所以咱們實際開發選定的過濾點是在 .info 。在選定了方案 2 以後,咱們對中間文件 .info 進行了一系列調研,肯定了文件基本格式(函數/代碼行覆蓋率對應的文件的表示),這裏再也不贅述,具體能夠參考 .info 生成文檔。
前一節是實現增量代碼覆蓋率檢測的基本方案選擇,爲了更好地接入現有開發流程,咱們作了如下幾方面的優化。
在接入方面,接入增量代碼測試覆蓋率工具只需一次接入配置,同步到代碼倉庫後,團隊中成員無需配置便可使用,下降了接入成本。
在使用方面,考慮到插樁在編譯時進行,對所有代碼進行插樁會很大程度下降編譯速度,咱們經過解析 Podfile(iOS 開發中較爲經常使用的包管理工具 CocoaPods 的依賴描述文件),只對 Podfile 中使用本地代碼的倉庫進行插樁(可配置指定倉庫),下降了團隊的開發成本。
接入增量代碼測試覆蓋率工具後,開發者無需特殊操做,也不須要對工程作任何其餘修改,正常的 git commit 代碼,git push 到遠端就會自動生成並上傳此次 commit 的覆蓋率信息了。
爲了作到這一點,咱們在接入 Pod 的過程當中,自動部署了 Git 的 pre-push 腳本。熟悉 Git 的同窗知道,Git 的 hooks 是開發者的本地腳本,不會被歸入版本控制,如何經過一次配置就讓這個倉庫的全部使用成員都能開啓,是作好這件事的一個難點。
咱們考慮到 Pod 自己會被歸入版本控制,所以利用了 CocoaPods 的一個屬性 script_phase,增長了 Pod 編譯後腳本,來幫助咱們把 pre-push 插入到本地倉庫。利用 script_phase 插入還帶來了另一個好處,咱們能夠直接獲取到工程的緩存文件,也避免了 .gcno / .gcda 文件獲取的不肯定性。整個流程以下:
在實現了覆蓋率的過濾後,咱們在實際開發中遇到了另一個問題:修改分支/循環結構後生成的 .gcda 文件沒法和以前的合併。 在這種狀況下,__gcov_flush
會直接返回,再也不寫入 .gcda 文件了致使覆蓋率檢測失敗,這也是市面上已有工具的通用問題。
而這個問題在開發過程當中很常見,好比咱們給例 1 中的遊戲增長一些提示,當輸入比預設數字大時,咱們就提示出來,反之亦然。
- (void)guessNumberGame:(NSInteger)guessNumber
{
NSInteger targetNumber = 10;
NSLog(@"Welcome to the game");
if (guessNumber == targetNumber) {
NSLog(@"Bingo!");
} else if (guessNumber > targetNumber) {
NSLog(@"Input number is larger than the given target!");
} else {
NSLog(@"Input number is smaller than the given target!");
}
}
複製代碼
這個問題困擾了咱們好久,也推進了對覆蓋率檢測原理的調研。結合前面覆蓋率檢測的原理能夠知道,不能合併的緣由是生成的控制流程圖比原來多了兩條邊( .gcno 和舊的 .gcda 也不能匹配了),反映在 .gcda 上就是數組多了兩個數據。考慮到代碼變更後,原有的覆蓋率信息已經沒有意義了,當發生邊數不一致的時候,咱們會刪除掉舊的 .gcda 文件,只保留最新 .gcda 文件(有變更狀況下 .gcno 會從新生成)。以下圖所示:
結合上述流程,咱們的增量代碼測試覆蓋率工具的總體流程如圖 13 所示。
開發者只需進行接入配置,再次運行時,工程中那些做爲本地倉庫進行開發的代碼庫會被自動插樁,並在 .git 目錄插入 hooks 信息;當開發者使用模擬器進行需求自測時,插樁統計結果會被自動分發出去;在代碼被推到遠端前,會根據插樁統計結果,生成僅包含本次代碼修改的詳細增量代碼測試覆蓋率報告,以及向遠端推送覆蓋率信息;同時若是測試覆蓋率小於 80% 會強制拒絕提交(可配置關閉,百分比可自定義),保證只有通過充分自測的代碼才能提交到遠端。
以上是咱們在代碼開發質量方面作的一些積累和探索。經過對覆蓋率生成、解析邏輯的探究,咱們揭開了覆蓋率檢測的神祕面紗。開發階段的增量代碼覆蓋率檢測,能夠幫助開發者聚焦變更代碼的邏輯缺陷,從而更好地避免線上問題。
丁京,iOS 高級開發工程師。2015 年 2 月校招加入美團到店餐飲事業羣,目前負責大衆點評 App 美食頻道的開發維護。
王穎,iOS 開發工程師。2017 年 3 月校招加入美團到店餐飲事業羣,目前參與大衆點評 App 美食頻道的開發維護。
到店餐飲技術部交易與信息技術中心,負責點評美食用戶端業務,服務於數以億計用戶,經過更好的榜單、真實的評價和完善的信息爲用戶提供更好的決策支持,致力於提高用戶體驗;同時承載全部餐飲商戶端線上流量,爲餐飲商戶提供多種營銷工具,提高餐飲商戶營銷效率,最終達到讓用戶「Eat Better、Live Better」的美好願景!咱們的團隊包含且不限於 Android、iOS、FE、Java、PHP 等技術方向,已完備覆蓋先後端技術棧。只要你來,就能點亮全棧開發技能樹。誠摯歡迎投遞簡歷至 wangkang@meituan.com。