Go1.20 新版覆蓋率方案解讀

2022-11-29 15:01:44

玩過Go覆蓋率的同學當有所瞭解,Go的覆蓋率方案最初的設計目標僅是針對單測場景,導致其侷限性很大。而為了適配更多的場景,行業內各種部落格、外掛、黑科技介紹也層出不窮。當然,過去我們也開源過Go系統測試覆蓋率收集利器 - goc,算其中比較完善,比較系統的了。且從使用者使用角度來看,goc也確實解決了行業內很多同學的痛點。

而現在,Go官方終於開始正式這個問題了。作者Than McIntosh 於今年3月份提出了新的覆蓋率提案,且看當前實現進度,最快Go1.20我們就能體驗到這個能力,非常贊。

基於作者的Proposal,我們先來看看這個提案細節。

新姿勢: go build -cover

需要明確的是,本次提案不會改變原來的使用姿勢go test -cover,而是新增go build -cover使用入口。從這一變化我們不難看出,新提案主要瞄準的是 "針對程式級的覆蓋率收集" ,而舊版的實際是 "僅針對包級別的覆蓋率收集" ,二者設計目標有明顯的差別。

在新姿勢下,使用流程大體是:

$ go build -o myapp.exe -cover ...
$ mkdir /tmp/mycovdata
$ export GOCOVERDIR=/tmp/mycovdata
$ <run test suite, resulting in multiple invocations of myapp.exe>
$ go tool covdata [command]

整體邏輯也比較清晰:

  1. 先編譯出一個經過插樁的被測程式
  2. 設定好覆蓋率輸出的路徑,然後執行被測程式。到這一步程式本身就會自動的輸出覆蓋率結果到上述路徑了
  3. 通過 go tool covdata 來處理覆蓋率結果

這裡的子命令 covdata 是新引入的工具。而之所需要新工具,主要還是在新提案下,輸出的覆蓋率檔案格式與原來的已有較大的差別。

新版覆蓋率格式

先來看舊版的覆蓋率結果:

  mode: set
  cov-example/p/p.go:5.26,8.12 2 1
  cov-example/p/p.go:11.2,11.27 1 1
  cov-example/p/p.go:8.12,10.3 1 1
  cov-example/p/p.go:14.27,20.2 5 1

大家當比較熟悉,其是文字格式,簡單易懂。

每一行的基本語意為 "檔案:起始行.起始列,結束行.結束列 該基本塊中的語句數量 該基本塊被執行到的次數"

但缺點也明顯,就是 "浪費空間". 比如檔案路徑 cov-example/p/p.go, 相比後面的counter資料,重複了多次,且在通常的profile檔案,這塊佔比很大。

新提案在這個方向上做了不少文章,實現細節上稍顯複雜,但方向較為清晰。

通過分析舊版的每一行能看出,本質上每一行會記錄兩類資訊,一是定位每個基本塊的具體物理位置,二是記錄這個基本塊的語句數量和被執行的次數。雖然執行的次數會變化,但是其他的資訊是不變的,所以全域性上其實只要記錄一份這樣的資訊就好,而這就能大大的優化空間,

所以,新版覆蓋率它實際會實際輸出兩份檔案,一份就是meta-data資訊,用於定位這個被測程式所有包、方法等元資訊,另一份才是counters,類似下面:

➜  tmp git:(master) ✗ ls -l
total 1280
-rw-r--r--  1 jicarl  staff   14144 Nov 28 17:02 covcounters.4d1584597702552623f460d5e2fdff27.8120.1669626144328186000
-rw-r--r--  1 jicarl  staff  635326 Nov 28 17:02 covmeta.4d1584597702552623f460d5e2fdff27

這兩份檔案都是二進位制格式,並不能直觀的讀取。但是藉助covdata工具,可以輕鬆轉化為舊版格式,比較優雅。類似:

go tool covdata textfmt -i=tmp -o=covdata.txt

ps: tmp 是覆蓋率檔案所在目錄。

真 • 全量覆蓋率

一個標準的go程式,基本上由三種型別的程式碼包組成:

  • 自身程式碼
  • 第三方包,通過mod或者vendor機制參照
  • go標準庫

在過去,幾乎所有的工具都只關注業務自身程式碼的插樁,鮮少關注第三方包,更別說go官方標準庫了。這在大部分場景下是沒問題的,但有時有些場景也有例外,比如SDK相關的專案。因為這時候SDK會作為Dependency引入,要想對其插樁就需要額外的開發量。還比如一些CLI程式,執行完命令之後,立馬就結束了,也是非常不利於覆蓋率收集的。

這些問題都是很現實的,且我們在goc專案中也收到過真實的使用者反饋:

不過,現在好了,新版覆蓋率方案也有實際考慮到這些需求,它實際會做到 支援全量插樁+程式退出時主動輸出覆蓋率結果 的原生方式,非常值得期待。

更多覆蓋率使用場景支援: 合併(merge)、刪減(subtract)、交集(intersect)

在實際處理覆蓋率結果時,有很多實用的場景,在新提案中也有提及,比如支援:

  • 合併多次覆蓋率結果 go tool covdata merge -i=<directories> -o=<dir>
  • 刪減已經覆蓋的部分 go tool covdata subtract -i=dir1,dir2 -o=<dir>
  • 得到兩份結果的交集 go tool covdata intersect -i=dir1,dir2 -o=<dir>

在過去,這些場景都需要依賴第三方工具才行,而在新方案中已經無限接近開箱即用了。

不過更復雜的場景,類似遠端獲得覆蓋率結果等(類似goc支援的場景),看起來新方案並沒有原生支援。這個問題,筆者也在issue 討論中提出,看看作者是否後續有解答。

展望與不足

值得注意的是新提案的實現是通過 原始碼插樁+編譯器支援 的方式來混合實現的,與原來go test -cover 純原始碼改寫的方式有了較大的變化。

另外作者提到的 test "origin" queries 功能還是非常讓我興奮的,因為有了它,若想建立 測試用例到原始碼的對映 會變得簡單很多,甚至更進一步的 精準測試,也變的更有想象空間。不過這個功能不會在Go1.20裡出現,只能期待以後了。

作者還提到了一些其他的限制和將來可能的改進,比如 Intra-line coverage, Function-level coverage, Taking into account panic paths 等,感興趣的同學可以自行去Proposal檔案檢視。