問題
在 Android 專案接覆蓋率守門時,為了「一次涵蓋兩種 module 型態」「一次相容兩個工具版本」, 連續寫了三個容錯式的設定。三個全部出事,而且出事的方式分成相反的兩種:
- 兩個目標都沒命中 → 靜默通過,產出空的報告
- 兩個目標都命中 → 重複計算,數字變好看,或工具直接拋錯
三次都是同一個念頭:「不確定是哪一種,那就兩種都寫,命中哪個算哪個。」
三個實例
| # | 寫了什麼 | 想涵蓋什麼 | 實際發生 |
|---|---|---|---|
| 1 | add("devDebug", optional = true) + add("debug", optional = true) | app module 有 productFlavors(變體叫 devDebug),library 沒有(叫 debug) | 兩個都沒命中。optional 的語意是找不到就跳過,於是建置成功、報告產出、內容是空的(分母 0) |
| 2 | built_in_kotlinc/*/*/classes/**/*.class | 不確定變體目錄怎麼排,用 * 帶過 | 所有變體都命中。同一個 class 出現在 debug/release/devDebug/prodDebug,分母 31 行算成 53 行 |
| 3 | 同時列 built_in_kotlinc/(AGP 9)與 tmp/kotlin-classes/(AGP 8 以前) | 相容 AGP 降版 | 兩個目錄在 AGP 9 上都存在。9 個 class 變 18 個,JaCoCo 對重複的 class 直接拋 Error while creating report |
第 1 個的代價:診斷被擋住三輪
Kover 產出空報告這件事試了三種設定都沒定位到,因為 optional = true 把
「找不到變體」變成了建置成功——沒有任何一行錯誤訊息。
把 optional 拿掉之後,Gradle 一句話就說出根因:
Could not find the provided variant 'debug' to create a custom variant 'total'.
Specify an existing 'jvm' variant or Android build variant name.真正的根因是當時用的 Kover 0.9.2 認不得 AGP 9 的 build variant。那句訊息一直都在,只是被容錯設定吞掉了。 後來的做法是把 Kover 換成 JaCoCo(上表第 3 個實例就是換過去之後踩的)。
當時我判斷 Kover 沒有更新版可升。事後(2026-10-11)查 Kover 的 GitHub releases,這個判斷不成立: 0.9.4(2025-12-09)的 release note 寫「Added support for Android Gradle Plugin 9.0.0」, 0.9.5(2026-01-30)寫「Fixed support of build variants in Android Gradle Plugin 9.0.0」, 截至查詢日最新版是 0.9.11(2026-09-29)。 (未查證:升級到這些版本是否就能消掉上面那個 variant 錯誤,我沒有實測。)
第 2 個的代價:錯誤以「好消息」的形式出現
分母從 31 行變成 53 行,覆蓋率因此算出 71.70%,比真實的 51.61% 好看。 基線就照那個數字鎖了進去。
重複計算不會讓數字變難看,它讓數字變好看——所以它不會觸發任何懷疑。
第 3 個的代價:在乾淨的環境測不出來
第 3 個錯誤在剛開的 git worktree 裡完全正常,因為那裡的 build 目錄只建過少數幾個變體,
tmp/kotlin-classes/ 根本還沒產生。
merge 進 dev、到建過完整流程的主 checkout 才炸:同一個 commit,主 checkout 數到 18 個 class,worktree 只有 9 個。
修正後兩邊都是 9 個 class、16/31 行、51.61%。
為什麼會一直寫出這種設定
因為它在當下看起來是負責任的:不確定環境長什麼樣,所以兩種都照顧到。
問題是這種寫法把「哪一種成立」這個資訊從程式碼裡拿掉了, 於是執行結果不再能反推設定是否正確:
- 綠燈可能代表「命中了正確的那個」,也可能代表「兩個都沒命中」
- 數字可能代表「量到了」,也可能代表「量了兩次」
而這兩種情況需要完全相反的處置。
怎麼預防再次發生
-
寫明一個,不要寫「或」。 不確定是哪一個,就先去查清楚(
find一下就知道), 查清楚的成本遠低於後面除錯的成本 -
配一個「找不到就大聲失敗」的檢查,取代容錯。這次的做法是在聚合任務加
doFirst:val classFiles = classDirectories.asFileTree.files if (classFiles.isEmpty()) { throw GradleException("找不到任何 .class 檔。class 目錄的路徑可能隨 AGP 版本變了。") }有了這個,寫死單一路徑才是安全的——版本變動時它會擋下來,而不是安靜地算出 0/0
-
診斷靜默失敗的第一步,是把讓它靜默的那個設定拿掉。
optional、ignoreFailures、|| true、continue-on-error、try/except: pass在正常運作時很方便,在出問題時正好擋住那句會告訴你答案的訊息 -
分母突然變大要先懷疑重複計算,不要當成好消息。同理: 掃描到的檔案數、測試數、依賴數突然變多時,先確認不是同一個東西被數了兩次
-
排查重複時,先列出同一個東西出現在哪幾個位置,不要逐條檢查 glob。 這次
find . -name "BuildFlags.class"一行就讓六個位置全部現形 (built_in_kotlinc 的 debug/release、tmp/kotlin-classes、runtime_library_classes_dir、 transformClassesWithAsm 的 debug/release)。 那一步先做的話,兩個重複來源會同時現形,不必分兩次修 -
動到建置產物路徑的改動,要在有完整歷史產物的目錄驗一次才算數。 剛開的 worktree 是乾淨的,乾淨環境測不出「兩個目錄同時存在」這類問題
相關主題
- 工具報的錯不一定是它說的那個問題——形態三「外掛的預設值把你最需要的功能關掉」跟這篇同屬「設定讓你看不到答案」
- 同一輪還撞到 detekt 在 JDK 25 上拋
IllegalArgumentException: 25.0.2,訊息只有一個版本號,完全不指向真正的原因 - 掃描到空集合時,壞掉的形狀和正確的形狀看起來一樣;守門有沒有在擋,只能證明、不能推定