之前因為強者我同學阿蹦大神的關係,接觸了neovim這個大型專案,光星星數就有9300多顆,是我星星最多的project的9300多倍lol。
雖然說看了幾個issue,大部分都插不上話--討論的層次太高了,偶爾有個好像比較看得懂的,trace下去之後提出解法,沒想到是個不徹底的解法,pull request就被拒絕了TAT,要參加這個超過9000顆星星的project,像我這種花盆果然還是「垃圾請再加油」
雖然說是這樣,但我還是趁這個機會,研究一下如何使用 git bisect
在project裡面找到洞洞。
基本上project無論用了多少test,多少還是跟我的腦袋一樣有一些洞,要如何找到洞洞就是一門學問了,git 提供了git bisect這個指令幫助開發者找到出錯的地方
我們用一個比較小的project: pyquery 來實驗這個功能,這是強者我同學JJL大神參與的專案
我們trace 一下 issue74 Behavior of PyQuery.is_() is different from jquery 因為這個issue 發生在v1.2.4,但到了v1.2.9已經消失了,那我想知道這從哪裡發生的(這種狀況比較少見,一般都是有錯要找哪裡出錯了),就用bisect 來找吧。
首先是bisect 的基本設定,步驟大概是:
- 啟動bisect
- 指定一個good commit
- 指定一個bad commit
bad commit 在歷史上要比good commit 來得晚 - bisect 會從bad commit 一路回溯到good commit 為止。
可以透過checkout tag的方式作大範圍的搜尋,以免bisect檢查太多commit,在這個例子中,我們發現v1.2.8->v1.2.9的過程中這個bug 被修掉了。
因此我們設定:
$ git bisect start
$ git bisect bad 1.2.9
$ git bisect good 1.2.8
Bisecting: 11 revisions left to test after this (roughly 4 steps)
[bc1b16509cec70de7a32354026443fca777f4d7d] created a .gitignore file
(which is almost a copy of .hgignore with some minor changes and comments)
這時候我們已經進入bisect 狀態,用git branch的話會看到現在是(no branch)狀態。
要說明一下這裡的good, bad只是bisect上的一個概念,對應到 bisect 的用途:找到是哪個 checkout 把 good 的程式變成 bad 的程式,它會從 good 開始找到 bad,至於裡面是不是真的 good/bad,這由開發者決定。
這時bisect會checkout 處在good/bad 中間位置的版本,我們執行事先寫好的一個測試檔test.py,它會自動測試這個 issue 的狀態
from pyquery import PyQuery as pq
x = pq("<div></div>")
y = pq("<div><table></table></div>")
print(x.is_("table"))
print(y.is_("table"))
執行發現它還是回傳False/False,因此我們輸入
$ git bisect bad
Bisecting: 5 revisions left to test after this (roughly 3 steps)
[b81a9e8a2b0d48ec0c64d6de14293dd4a680a20b] fixed issue #9
bisect 會以binary search的方式checkout 一個更舊的版本,然後你再測試一次。
經過五次的bad/good的測試結果,bisect回傳:
300cd0822505a4bd308acd1520ff3ef0f20f8635 is the first bad commit
commit 300cd0822505a4bd308acd1520ff3ef0f20f8635
Author: Gael Pasgrimaud <gael@gawel.org>
Date: Fri Jan 3 10:35:30 2014 +0100
fixed issue #19
:040000 040000 1d9cb3b170a8fdb2846e3c0e0fb6d2be9a9538d5 07d3a40ff73dda078d7543be2fab2f9f927b0c1f M pyquery
這樣就抓到這個 fixed issue #19 的commit 就是修好這個issue 的commit 了,最後要用
$ git bisect reset
結束bisect狀態。
上面這個方法好像還是不夠方便,理論上bisect 支援git bisect run這個方法,可以送一個script 給它,它會自動執行,
- 回傳值0表示這個commit 是good
- 回傳值1表示這個commit 是bad
- 回傳值125表示這個commit 要skip掉。
所以我改了上面這個script 為:
import sys
from pyquery import PyQuery as pq
x = pq("<div></div>")
y = pq("<div><table></table></div>")
if x.is_("table") == False and y.is_("table") == False:
sys.exit(1)
else:
sys.exit(0)
可是不知道為啥,bisect run ./test.py的結果,每個commit 都會是bad的輸出…這真的是太奇怪了,我猜有可能會是git bisect的問題也說不定,有空再來詳加研究。
8/28增補:
後來經過阿蹦大神的指出,問題可能是出在*.pyc上面,因為python要是看到現在的pyc跟現在的時間相同,就不會更新pyc而是直接跑pyc。
git bisect run會極速的checkout 舊分枝,跑python script,看結果跑下一步;而pyc的檢定大概是用秒在算的,就變成python並沒有更新pyc檔,反而是用pyc跑出同樣的結果,bisect 自然出錯了。
解決方法有兩個:
第一個是寫一個shell script test.sh,先刪掉所有pyc檔之後,再執行python script:
find . -name "*.pyc" -exec rm {} \;
./test.py
然後執行 git bisect run ./test.sh
第二個是讓python script 跑慢一點,讓python能察覺到python 的版本變化:
import time
time.sleep(1)
第三個應該才是根本的解法:
在python 的shebang上面加上 -B的參數就好了
#!/use/bin/env python -B
執行結果就跟手動的一樣了,去你的pyc。
結論:
bisect很好用?不是,我認為從這個案例中最重要的概念,其實是自動測試的重要性,如果程式能保持一個自動測試的script,在除錯上可以透過script 自動找到錯誤點,不需要人工手動介入。
試想若每個commit都需要手動10步驟的測試,兩個版本間有10個commit ,測試步驟立刻變成100步,但用script只要一個指令就能知道是好是壞,搭配bisect才能事半功倍。