<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
這個真的不是標題黨,我寫程式碼20+年,真心認為 go fuzzing 是我見過的最牛逼的程式碼自測方法。我在用 AC自動機 演演算法改進關鍵字過濾效率(提升~50%),改進 mapreduce 對 panic 的處理機制的時候,都通過 go fuzzing 發現了邊緣情況的 bug。所以深深的認為,這是我見過最牛逼的程式碼自測方法,沒有之一!
go fuzzing 至今已經發現了程式碼質量極高的 Go 標準庫超過200個bug,見:github.com/dvyukov/go-…
春節程式設計師之間的祝福經常是,祝你程式碼永無 bug!雖然調侃,但對我們每個程式設計師來說,每天都在寫 bug,這是事實。程式碼沒 bug 這事,只能證偽,不能證明。即將釋出的 Go 1.18 官方提供了一個幫助我們證偽的絕佳工具 - go fuzzing。
Go 1.18 大家最關注的是泛型,然而我真的覺得 go fuzzing 真的是 Go 1.18 最有用的功能,沒有之一!
本文我們就來詳細看看 go fuzzing:
首先,你需要升級到 Go 1.18
Go 1.18 雖然還未正式釋出,但你可以下載 RC 版本,而且即使你生產用 Go 更早版本,你也可以開發環境使用 go fuzzing 尋找 bug
根據 官方檔案 介紹,go fuzzing 是通過持續給一個程式不同的輸入來自動化測試,並通過分析程式碼覆蓋率來智慧的尋找失敗的 case。這種方法可以儘可能的尋找到一些邊緣 case,親測確實發現的都是些平時很難發現的問題。
官方介紹寫 fuzz tests 的一些規則:
fuzzing arguments 只支援以下型別:
string, []byte
int, int8, int16, int32/rune, int64
uint, uint8/byte, uint16, uint32, uint64
float32, float64
bool
fuzz target 不要依賴全域性狀態,會並行跑。
如果我寫了一個 fuzzing test,比如:
// 具體程式碼見 https://github.com/zeromicro/go-zero/blob/master/core/mr/mapreduce_fuzz_test.go func FuzzMapReduce(f *testing.F) { ... }
那麼我們可以這樣執行:
go test -fuzz=MapReduce
我們會得到類似如下結果:
fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 3s, execs: 3338 (1112/sec), new interesting: 56 (total: 57) fuzz: elapsed: 6s, execs: 6770 (1144/sec), new interesting: 62 (total: 63) fuzz: elapsed: 9s, execs: 10157 (1129/sec), new interesting: 69 (total: 70) fuzz: elapsed: 12s, execs: 13586 (1143/sec), new interesting: 72 (total: 73) ^Cfuzz: elapsed: 13s, execs: 14031 (1084/sec), new interesting: 72 (total: 73) PASS ok github.com/zeromicro/go-zero/core/mr 13.169s
其中的 ^C 是我按了 ctrl-C 終止了測試,詳細解釋參考官方檔案。
按照我使用下來的經驗總結,我把最佳實踐初步總結為以下四步:
接下來我們以一個最簡單的陣列求和函數來展示一下上述步驟,go-zero 的實際案例略顯複雜,文末我會給出 go-zero 內部落地案例,供大家參考複雜場景寫法。
這是一個注入了 bug 的求和的程式碼實現:
func Sum(vals []int64) int64 { var total int64 for _, val := range vals { if val%1e5 != 0 { total += val } } return total }
你至少需要給出一個 fuzzing argument,不然 go fuzzing 沒法生成測試程式碼,所以即使我們沒有很好的輸入,我們也需要定義一個對結果產生影響的 fuzzing argument,這裡我們就用 slice 元素個數作為 fuzzing arguments,然後 Go fuzzing 會根據跑出來的 code coverage 自動生成不同的引數來模擬測試。
func FuzzSum(f *testing.F) { f.Add(10) f.Fuzz(func(t *testing.T, n int) { n %= 20 ... }) }
這裡的 n 就是讓 go fuzzing 來模擬 slice 元素個數,為了保證元素個數不會太多,我們限制在20以內(0個也沒問題),並且我們新增了一個值為10的語料(go fuzzing 裡面稱之為 corpus),這個值就是讓 go fuzzing 冷啟動的一個值,具體為多少不重要。
這一步的重點是如何編寫可驗證的 fuzzing target,根據給定的 fuzzing arguments 寫出測試程式碼的同時,還需要生成驗證結果正確性用的資料。
對我們這個 Sum 函數來說,其實還是比較簡單的,就是隨機生成 n 個元素的 slice,然後求和算出期望的結果。如下:
func FuzzSum(f *testing.F) { rand.Seed(time.Now().UnixNano()) f.Add(10) f.Fuzz(func(t *testing.T, n int) { n %= 20 var vals []int64 var expect int64 for i := 0; i < n; i++ { val := rand.Int63() % 1e6 vals = append(vals, val) expect += val } assert.Equal(t, expect, Sum(vals)) }) }
這段程式碼還是很容易理解的,自己求和和 Sum 求和做比較而已,就不詳細解釋了。但複雜場景你就需要仔細想想怎麼寫驗證程式碼了,不過這不會太難,太難的話,可能是對測試函數沒有足夠理解或者簡化。
此時就可以用如下命令跑 fuzzing tests 了,結果類似如下:
$ go test -fuzz=Sum fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 0s, execs: 6672 (33646/sec), new interesting: 7 (total: 6) --- FAIL: FuzzSum (0.21s) --- FAIL: FuzzSum (0.00s) sum_fuzz_test.go:34: Error Trace: sum_fuzz_test.go:34 value.go:556 value.go:339 fuzz.go:334 Error: Not equal: expected: 8736932 actual : 8636932 Test: FuzzSum Failing input written to testdata/fuzz/FuzzSum/739002313aceff0ff5ef993030bbde9115541cabee2554e6c9f3faaf581f2004 To re-run: go test -run=FuzzSum/739002313aceff0ff5ef993030bbde9115541cabee2554e6c9f3faaf581f2004 FAIL exit status 1 FAIL github.com/kevwan/fuzzing 0.614s
那麼問題來了!我們看到了結果不對,但是我們很難去分析為啥不對,你仔細品品,上面這段輸出,你怎麼分析?
對於上面失敗的測試,我們如果能列印出輸入,然後形成一個簡單的測試用例,那我們就可以直接偵錯了。列印出來的輸入最好能夠直接 copy/paste 到新的測試用例裡,如果格式不對,對於那麼多行的輸入,你需要一行一行調格式就太累了,而且這未必就只有一個失敗的 case。
所以我們把程式碼改成了下面這樣:
func FuzzSum(f *testing.F) { rand.Seed(time.Now().UnixNano()) f.Add(10) f.Fuzz(func(t *testing.T, n int) { n %= 20 var vals []int64 var expect int64 var buf strings.Builder buf.WriteString("n") for i := 0; i < n; i++ { val := rand.Int63() % 1e6 vals = append(vals, val) expect += val buf.WriteString(fmt.Sprintf("%d,n", val)) } assert.Equal(t, expect, Sum(vals), buf.String()) }) }
再跑命令,得到如下結果:
$ go test -fuzz=Sum fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 0s, execs: 1402 (10028/sec), new interesting: 10 (total: 8) --- FAIL: FuzzSum (0.16s) --- FAIL: FuzzSum (0.00s) sum_fuzz_test.go:34: Error Trace: sum_fuzz_test.go:34 value.go:556 value.go:339 fuzz.go:334 Error: Not equal: expected: 5823336 actual : 5623336 Test: FuzzSum Messages: 799023, 110387, 811082, 115543, 859422, 997646, 200000, 399008, 7905, 931332, 591988, Failing input written to testdata/fuzz/FuzzSum/26d024acf85aae88f3291bf7e1c6f473eab8b051f2adb1bf05d4491bc49f5767 To re-run: go test -run=FuzzSum/26d024acf85aae88f3291bf7e1c6f473eab8b051f2adb1bf05d4491bc49f5767 FAIL exit status 1 FAIL github.com/kevwan/fuzzing 0.602s
根據上面的失敗 case 的輸出,我們可以 copy/paste 生成如下程式碼,當然框架是自己寫的,輸入引數可以直接拷貝進去。
func TestSumFuzzCase1(t *testing.T) { vals := []int64{ 799023, 110387, 811082, 115543, 859422, 997646, 200000, 399008, 7905, 931332, 591988, } assert.Equal(t, int64(5823336), Sum(vals)) }
這樣我們就可以很方便的偵錯了,並且能夠增加一個有效 unit test,確保這個 bug 再也不會出現了。
我相信,Go 1.18 釋出了,大多數專案線上程式碼不會立馬升級到 1.18 的,那麼 go fuzzing 引入的 testing.F 不能使用怎麼辦?
線上(go.mod)不升級到 Go 1.18,但是我們本機是完全推薦升級的,那麼這時我們只需要把上面的 FuzzSum 放到一個檔名類似 sum_fuzz_test.go 的檔案裡,然後在檔案頭加上如下指令即可:
// go:build go1.18 // +build go1.18
注意:第三行必須是一個空行,否則就會變成 package 的註釋了。
這樣我們線上上不管用哪個版本就不會報錯了,而我們跑 fuzz testing 一般都是本機跑的,不受影響。
上面講的步驟是針對簡單情況的,但有時根據失敗 case 得到的輸入形成新的 unit test 並不能復現問題時(特別是有 goroutine 死鎖問題),問題就變得複雜起來了,如下輸出你感受一下:
go test -fuzz=MapReduce fuzz: elapsed: 0s, gathering baseline coverage: 0/2 completed fuzz: elapsed: 0s, gathering baseline coverage: 2/2 completed, now fuzzing with 10 workers fuzz: elapsed: 3s, execs: 3681 (1227/sec), new interesting: 54 (total: 55) ... fuzz: elapsed: 1m21s, execs: 92705 (1101/sec), new interesting: 85 (total: 86) --- FAIL: FuzzMapReduce (80.96s) fuzzing process hung or terminated unexpectedly: exit status 2 Failing input written to testdata/fuzz/FuzzMapReduce/ee6a61e8c968adad2e629fba11984532cac5d177c4899d3e0b7c2949a0a3d840 To re-run: go test -run=FuzzMapReduce/ee6a61e8c968adad2e629fba11984532cac5d177c4899d3e0b7c2949a0a3d840 FAIL exit status 1 FAIL github.com/zeromicro/go-zero/core/mr 81.471s
這種情況下,只是告訴我們 fuzzing process 卡住了或者不正常結束了,狀態碼是2。這種情況下,一般 re-run 是不會復現的。為什麼只是簡單的返回錯誤碼2呢?我仔細去看了 go fuzzing 的原始碼,每個 fuzzing test 都是一個單獨的程序跑的,然後 go fuzzing 把模糊測試的程序輸出扔掉了,只是顯示了狀態碼。那麼我們如何解決這個問題呢?
我仔細分析了之後,決定自己來寫一個類似 fuzzing test 的常規單元測試程式碼,這樣就可以保證失敗是在同一個程序內,並且會把錯誤資訊列印到標準輸出,程式碼大致如下:
func TestSumFuzzRandom(t *testing.T) { const times = 100000 rand.Seed(time.Now().UnixNano()) for i := 0; i < times; i++ { n := rand.Intn(20) var vals []int64 var expect int64 var buf strings.Builder buf.WriteString("n") for i := 0; i < n; i++ { val := rand.Int63() % 1e6 vals = append(vals, val) expect += val buf.WriteString(fmt.Sprintf("%d,n", val)) } assert.Equal(t, expect, Sum(vals), buf.String()) } }
這樣我們就可以自己來簡單模擬一下 go fuzzing,但是任何錯誤我們可以得到清晰的輸出。這裡或許我沒研究透 go fuzzing,或者還有其它方法可以控制,如果你知道,感謝告訴我一聲。
但這種需要跑很長時間的模擬 case,我們不會希望它在 CI 時每次都被執行,所以我把它放在一個單獨的檔案裡,檔名類似 sum_fuzzcase_test.go,並在檔案頭加上了如下指令:
// go:build fuzz // +build fuzz
這樣我們需要跑這個模擬 case 的時候加上 -tags fuzz 即可,比如:
go test -tags fuzz ./...
上面介紹的是一個範例,還是比較簡單的,如果遇到複雜場景不知道怎麼寫,可以先看看 go-zero 是如何落地 go fuzzing 的,如下所示:
MapReduce - github.com/zeromicro/g…
模糊測試了 死鎖 和 goroutine leak,特別是 chan + goroutine 的複雜場景可以借鑑
stringx - github.com/zeromicro/g…
模糊測試了常規的演演算法實現,對於演演算法類場景可以借鑑
以上就是Go語言開發程式碼自測絕佳go fuzzing用法詳解的詳細內容,更多關於Go開發go fuzzing程式碼自測的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45