問題

我給自己定的刪分支規矩,列了五條刪之前要做的檢查,最後一句是:

五條的共同點:每一條都指定了「拿什麼當基準」。 檢查跑了、有輸出、但比錯基準 = 等於沒檢查。

那條規矩當初只寫給「刪分支」。但在一輪 Android app 的對齊作業裡, 同一個形狀在五個完全不同的領域各出現一次——備份、覆蓋率、建置系統、 模擬器、UI 自動化。後來又在 git 分支上踩到第六次。所以它不是刪分支的特例,是一個通用的失效模式。

六種形態

#檢查在做什麼比錯了什麼基準症狀
1備份存完解開驗一次空輸入被當成有效——tar -tf 讀空輸入回傳 0空備份通過驗證、被提交,取代掉上一個完整的回復點
2覆蓋率算分母範圍太寬——glob 的 * 收進同一個 class 的所有變體分母虛胖(31 行算成 53 行),覆蓋率「變好看」
3契約測試餵樣本建置系統不知道這個輸入,沒宣告成 task input樣本被清空,task 判定 UP-TO-DATE 根本沒重跑,離開碼 0
4boot smoke 判斷 app 有沒有掛時間範圍錯——讀的是累積的 crash buffer讀到先前 instrumentation test 的舊紀錄,誤報「app 掛了」
5boot smoke 讀畫面上的文字管道錯——uiautomator dump /dev/tty 在非互動 shell 下讀不到誤報「畫面壞了」,而 app 完全正常
6判斷「我這個分支動了哪些檔案」基準自己移動了——git diff main..HEAD 的 main 在開 worktree 之後前進了 18 個 commit把別人推進 main 的改動誤判成自己改的,差點停手不 merge。正確基準是 git merge-base main HEAD

第 3 個的機制,Gradle 文件寫得很直接:Gradle 靠比對宣告過的輸入與輸出決定能不能跳過 task,影響輸出的輸入沒宣告,「task 就會被當成 up to date,但其實不是」(Gradle: Incremental build)。

第 6 個的機制:git diff A..B 跟 git diff A B 同義,比的是兩個端點的現況;git diff A...B 才等於 git diff $(git merge-base A B) B,也就是「B 從分岔點以來改了什麼」(git-diff)。git merge-base 找的是兩個 commit 的最佳共同祖先(git-merge-base)。所以「我這個分支動了什麼」要用三個點,或明確先取 merge-base。

為什麼這一族特別難抓

每一個都有輸出,而且輸出看起來是合理的。

  • 第 1 個印「✅ 收了 113 個檔 → 加密存檔」,離開碼 0
  • 第 2 個印一個更高的覆蓋率百分比——錯誤以「好消息」的形式出現
  • 第 3 個印 BUILD SUCCESSFUL
  • 第 4、5 個印的是「失敗」,但失敗的理由是假的

沒有任何一個會拋例外、沒有任何一個會讓建置變紅。 正向跑永遠是綠的(或者紅得理直氣壯)。

第 6 個又多一層:基準在你不看的時候自己動了。 前五種的基準是寫錯的, 寫的當下就錯;第 6 種的基準寫的當下是對的——main..HEAD 在剛開分支時完全正確, 是別人往 main 推了 18 個 commit 之後才變錯的。 凡是拿「分支名」當基準的檢查都有這個性質:分支是會動的指標,不是固定點。

四個診斷問句

每加一道檢查、每查一次問題,問自己這四句:

一、這個檢查量的是什麼?它跟我想知道的是同一件事嗎?

第 1 個實例:我想知道「備份裡有沒有東西」,而 tar -tf 的離開碼回答的是 「這是不是一個合法的 tar」。空 tar 也是合法的 tar。 → 改成數項目數,0 個就不算通過。

二、它的時間範圍是哪一段?是「這一次」還是「累積至今」?

第 4 個實例:crash buffer 是累積的。要看「這次啟動有沒有掛」, 就得在啟動前 logcat -b crash -c 清掉。 同一類的還有「拿舊報告當新結果」(測試失敗時不產生新報告,守門讀上一次的)。

三、它的範圍邊界在哪?會不會收到不該收的?

第 2 個實例:built_in_kotlinc/*/*/classes 的 * 匹配到 debug/release/ devDebug/prodDebug 全部變體。 → 排查重複最快的一步是先列出同一個東西出現在哪幾個位置 (find . -name "SomeClass.class"),而不是逐條檢查 glob。

四、取不到的時候它回什麼?

第 3、5 個實例:讀不到內容與「內容是空的」在輸出上一模一樣。 → 每個取值步驟都要「取不到就大聲失敗」,不要退回預設值、不要當成 0。

怎麼預防再次發生

  • 反向驗證是唯一抓得到的手段。正向跑永遠是綠的——植入一個該被擋的東西, 確認它真的被擋。而且植入之後要先確認行為真的變了。 我踩過:把 header() 改成 headers.toMultimap()[...],以為這樣就變成大小寫敏感了; 但 OkHttp 的 toMultimap() 回的是以 String.CASE_INSENSITIVE_ORDER 排序的 TreeMap (OkHttp 原始碼 Headers.kt), 那個改法根本沒改變行為,測試沒紅
  • 數字突然變好要先懷疑。 覆蓋率分母變大、掃到的套件變多、測試數變多—— 先確認不是同一個東西被數了兩次
  • 在有完整歷史產物的環境驗一次。 剛開的 worktree 是乾淨的, 「兩個目錄同時存在」這類問題在那裡測不出來
  • 把基準寫進檢查的訊息裡。 不要只印「通過」,印「拿什麼跟什麼比、各是多少」。 這一輪的守門腳本都改成這樣(版控記的 sha:X/本機樣本的 sha:Y), 看輸出就能發現基準不對
  • 這條規矩原本只寫在刪分支的那幾條檢查裡,適用範圍其實是所有檢查

相關