系列: 與 AI 一起學寫程式

測試通過率從 54% 到 100%

從 54% 走到 100% 測試通過的三個週末。

從 54% 到 100% 全綠 三個週末,四個根本原因修復,一個誠實的數字 54% 63% 63% 100% 週六上午 週六下午 第 2 週 第 3 週
唯一無法動手腳的數字:56 個測試中 56 個通過。

當夥伴在評估一個空間執行時期時,他們應該要的第一個數字就是整合測試的通過率。以下是我們如何把自己的通過率從勉強能動帶到刀槍不入——以及為什麼軌跡比快照更重要。

對於一個程式碼庫,不會說謊的數字是測試通過率。銷售數字可以動手腳。星星數可以灌水。程式碼行數可以充版面。測試通過率就是測試執行器說的那個數字,而測試執行器不在乎你的感受。

一週前的星期六我打開筆電時,那個數字是 54%。五十二個測試裡的二十八個。不算災難。不是綠的。這種數字意味著程式碼庫大致能動,但你說不清楚它到底哪裡不能動。到那個星期六晚上,數字是 63%(33/52)。到剛結束的這個星期日,是 100%(56/56)。這兩個數字之間的路,就是這篇文章要講的。

一開始數字為什麼低

幾個彼此交疊的原因。

殘根(stub)通過了本該不及格的測試。這是每隔幾個週末就會冒出來的主題。測試套件裡有些測試在拿回傳值和零比較,而殘根實作剛好回傳零,測試就剛好判它們成功。測試框架沒有說謊。是測試在套套邏輯。

有些斷言與實際的錯誤碼不匹配。有個測試斷言 font_set_data 在壞輸入時回傳 -1。真正的實作回傳 -3(對應 RAKU_ERROR_INVALID_PARAMETER,比測試當初編寫時所針對的碼更精確)。兩種行為都是合法的。這個測試寫在錯誤碼統一之前。修法是更新測試、接受任何負的錯誤碼,而不是改動實作。

遙測測試沒有可追蹤的狀態。有一類測試在演練遙測事件管線,斷言「我發出事件 X 之後,遙測系統應該記得它」。當時的遙測子系統是一個什麼都不記得的殘根事件追蹤器。測試失敗不是因為管線壞了,而是因為根本還沒有管線。

眼動追蹤模組的回傳值慣例是反的。眼動追蹤 C API 的某些函式回傳 1 表示成功,因為它們是在某個 Win32 風味的開發日寫成的。引擎的其餘部分以 0 表示成功。針對引擎其餘部分編寫的測試在眼動追蹤模組上失敗,不是因為程式碼爛,而是因為慣例漂移。

MSVC 建置在一個不透明控制代碼轉型上有硬性編譯錯誤。XrInstance 是 OpenXR 的不透明控制代碼型別。為了序列化把它轉型成 uint64_t,需要用 reinterpret_cast 而不是 static_cast。MSVC 大聲地報出 C2440。修法是一行變更。它所遮蔽的測試失敗規模更大。

工作實際長什麼樣

我想把細節寫下來,因為修測試的模式是可以重複的。

星期六上午(29/52 → 31/52):解掉那些讓部分測試連編譯都過不了的建置錯誤。reinterpret_cast 的修復立即解鎖了兩個測試,還讓一個原本藏在編譯失敗背後的第三個測試浮了出來。

星期六中午(31/52 → 33/52):把遙測殘根換成有狀態的事件追蹤實作。殘根是什麼都不做的兩行函式。真正的實作在一個執行緒安全的 vector 裡追蹤事件、開放一個查詢 API,並對測試框架裡的三個遙測測試產生正確的行為。把 telemetry_stubs.cpp 寫成一個像樣的測試替身,貼近正式遙測子系統到讓測試是基於真實理由通過的程度。

星期六下午(33/52 → 33/52,數量沒增加但品質跳了一級):修正 test_edge_cases 裡那個檢查了錯誤錯誤碼的斷言。修法是讓斷言接受任何負的錯誤碼,而不是它當初針對的那個特定 -1。這個測試現在演練的是真實實作的真實錯誤路徑。

一個週末之後(33/52 → 33/52 → 53/52 → 56/56):眼動追蹤的慣例漂移花的時間最多。眼動追蹤的 C API 有著和引擎其餘部分不同的回傳值慣例。對齊慣例需要同時更新實作(成功回傳 0)並更新呼叫端來預期 0。一旦對齊,二十多個測試同時轉綠。連鎖效應是真的。

再下一個週末是 test_memory_leaks 的週末。九個測試因為只在洩漏偵測框架下才浮現的記憶體相關原因而失敗。那些修復是無法濃縮成一行的細緻工作:InputQueue 原本是個空操作殘根,需要真正的 add/get/predict/trim 行為;RollbackSession::initialize 必須在接受回呼之前驗證它們;NetworkQualityEstimator 必須從 send/ack 配對中真正追蹤 RTT 與封包遺失;而 ECS World::clear 必須清空 free-indices 佇列,防止下一個配置週期重用過期索引。

最後那一個(ECS 的 free-indices 佇列)是那種不會造成當機、但日後會產生極其隱微 bug 的 bug。free-indices 佇列是實體元件系統在實體被銷毀後重用控制代碼的機制。如果 clear 在佇列裡留下過期索引,下一個被建立的實體會拿到與先前已刪除實體重疊的控制代碼,而指向已刪除實體的參照會無聲地開始指向新實體。難以診斷。極易引入。記憶體洩漏測試抓到它,是因為洩漏偵測器追蹤了哪些控制代碼被配置過。

到那個週末結束時,測試套件是 56/56。100%。

我學到了什麼

三件事。

慣例漂移在你量測它之前是看不見的。眼動追蹤模組單獨運作一直沒問題。它以 1 表示成功、引擎其餘部分以 0 表示成功這件事,之前還沒咬過任何人,因為沒有人針對它寫過跨模組測試。測試套件一旦大到橫跨多個模組,就一次以二十種不同的方式暴露了這個漂移。測試套件就是慣例接受稽核的方式。

通過測試的殘根,比不通過的殘根更糟。兩者都是殘根。兩者最終都需要真正的實作。不通過測試的殘根對自己是殘根這件事很誠實。剛好通過測試的殘根,是程式碼庫對自己說的謊。能把說謊殘根翻出來的那次稽核,就是對程式碼庫改善最大的稽核。

連鎖效應才是大獎。眼動追蹤慣例的修復一口氣解鎖了二十個測試。ECS World::clear 的記憶體洩漏修復又解鎖了九個。測試通過率的大幅提升不是來自修二十個個別的 bug。而是來自修好四個根本原因問題,每一個都有多個下游測試失敗。

夥伴與開發者該從中帶走什麼

如果你正在為合作評估一個引擎,而測試通過率低於 90%,問問軌跡。一個從 54% 出發、三個週末走到 100% 的團隊,和一個在 80% 停了六個月不動的團隊,是不同的團隊。

如果你在跑代理驅動的工作流程、而測試通過率不在你想要的位置,不要假設是代理需要更聰明。去看測試。那些套套邏輯的測試、慣例漂移、剛好通過的殘根。修法通常在測試框架裡,不在實作裡。

如果你是在打造自主 PR 編程代理的 AI 實驗室,我認為最有預測力的指標是「代理的 PR 落地之後,測試通過率有沒有上升」。很多代理落地的 PR 出了程式碼,也出了針對那些程式碼會通過的測試,但對真實產品的涵蓋率毫無實際改善。能推動整合測試通過率的代理是不一樣的代理。值得為此最佳化。

一週前的星期六,那個數字是 54。今晚是 100。測試執行器不在乎我的感受。這一點測試執行器是對的。

星期日帶著一套全綠的測試合上筆電。下個週末回來繼續蓋。

在一個能證明自身品質的執行時期上打造

RakuAI 是你的助手在真實世界中棲身的 AI 原生空間執行時期。綠色的測試、誠實的訊號、引擎級的紀律——看看你的團隊能在它上面出貨什麼。

← 所有文章