系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
Change Ref: PR #18-feat: wal.reset / wal.current_seq API 與 session.bind 冪等化
Issue: TestPilot 每次開跑都會清除舊紀錄、重新確認 UART 連線;即使 UART 根本沒換,minicom 仍然會斷線。
Root Cause: 原本只需要確認現況的 Setup,被實作成無條件拆掉並重建既有 UART 連線。
Solution: wal reset 只重設本次測試紀錄;session.bind 發現同一條連線仍然有效時,直接回覆 already_bound。
驗證: 新增 8 項 serialwrap 端自動化整合測試;PR 記錄完整 Test Suite 共 186 項通過。
上一篇最後,我只是想讓每次 Test Run 都從一份乾淨的 UART 流水帳開始,流程卻直接把 serialwrap 關掉重開。
結果帳確實清乾淨了。
工程師旁邊正在看的 minicom 也一起葛屁。
清帳歸清帳,不要再拉著全家人一起陪葬。
既然問題已經找到,那我們就開工動手術吧。
在這裡我們先新增:
serialwrap wal reset
serialwrap wal current-seq
wal reset 只把上一輪 UART 紀錄封存,讓下一輪從新的紀錄編號開始。
serialwrap 本身繼續運作,minicom 當然也不用重新連線。
wal current-seq 則讓 TestPilot 直接詢問流水帳目前記到第幾筆,不必自己打開一份還在持續成長的檔案,再猜最後一筆到底寫完了沒有。
我重新打開 minicom,再跑一次 TestPilot。
這次清 WAL 沒斷。
很好。
終於正常了。
接著 TestPilot 進入下一個 Setup 步驟,重新確認 UART 綁定。
minicom 又斷了。
……蛤?活見鬼啦~
這裡先補上前面幾篇沒有完整畫出來的連線關係。
Day 3 之後,serialwrap 已經接管實體 UART。
工程師開 minicom 時,不再直接打開實體串口,而是連到 serialwrap 提供的虛擬終端。
Agent 與 TestPilot 則不走 minicom。它們會透過 serialwrap 的命令介面,把 Command 或 Setup Request 交給同一個背景服務。
Human
minicom ── PTY ─────────────┐
│
Agent ── serialwrap CLI ─────┼── serialwrapd ── TTY ── DUT
│
TestPilot ─ serialwrap CLI ──┘
底下的 TTY,是實際連到 DUT 的串口裝置。
上面的 PTY,則是 serialwrap 提供給 minicom 使用的虛擬終端。
三條路的入口不同,但最後都會進到同一個 serialwrapd。
這也解釋了上一篇為什麼 TestPilot 一重啟 serialwrap,minicom 就會跟著斷線。
TestPilot 為了讓每次 Run 都有一份乾淨的 WAL,第一版 Setup 直接管理整個 serialwrap 生命週期:
Stop daemon
→ Clean WAL
→ Start daemon
→ Bind sessions
→ Run cases
從 TestPilot 看來,它只是把測試環境重新準備一次。
但 daemon 一停,不只 Agent 的 Command 入口消失,所有提供給 minicom 的 PTY 也會一起被銷毀。
所以 wal reset 解掉的第一件事,就是讓 TestPilot 可以只換流水帳,不再拆掉三條入口共同依賴的 serialwrap。
問題是,daemon 留下來以後,Setup 還有下一步。
它仍然要重新確認 UART 綁定,到底還要在UART繞多久啊??Orz
TestPilot 每次開跑前,都會呼叫一次 session.bind,確認目前使用的 UART 仍然是原本指定的設備。
這個確認本身很合理。
測試可能中斷、重跑,USB Serial 可能被重新插拔,甚至 DUT 都可能換成另一組測試項目。
TestPilot 不能因為上一輪接過,就永遠假設這一輪什麼都沒變,這個世界沒有那麼善良。
問題出在 session.bind 的處理順序。
只要 serialwrap 當下已經有一條 UART 連線,它就先把既有連線拆掉,再按照 Request 裡指定的設備重新接一次。
它沒有先判斷:
現在這條連線,是不是本來就已經接在同一台設備上?
所以即使:
底下仍然是同一條 TTY。
上面的 PTY 還在正常工作。
minicom 也正透過這個 PTY 看著 UART。
現行流程仍然會把 PTY 銷毀,再接回同一條 TTY,最後產生一個新的 PTY。
同一條 TTY
+ 還活著的 PTY
+ 正在看的 minicom
↓ session.bind
拆掉既有連線
↓
舊 PTY 消失
↓
接回同一條 TTY
↓
產生新的 PTY
TTY 沒換。
PTY 沒壞。
TestPilot 只是問:
現在還是接這條嗎?
serialwrap 的回答方式卻是:
等一下,我先拔掉重接。好了,確定還是這條。
而且最後還會回:
{
"ok": true
}
從 API 的角度,它成功了。
從我的 minicom 看來,我只是又得重開一次。
線沒掉。
板子沒換。
minicom 也沒做任何事。
它只是剛好站在 Setup 的爆破範圍內。
這就像當年的平偉哥,車騎得好好的,先拆你的座墊,然後宣布:
沒毛病。
把兩個斷線點放在一起看,問題其實是同一個。
TestPilot 的 Setup 不一定只會執行一次。
測試可能 Retry,流程可能從中途 Resume,前一輪也可能沒有正常收尾。外部工具重新送一次相同的準備指令,這很正常,也很必要。
所以 Setup 裡的操作不能建立在:
這個 Request 應該只會送一次。
更不能要求 TestPilot 在呼叫前,先猜 serialwrap 現在有沒有連線、是不是同一台設備,以及這次確認會不會順便把 minicom 踢掉。
TestPilot 只需要表達:
這一輪我要使用哪一台 UART 設備。
目前是否已經接好、既有連線是否仍然有效,應該由真正管理 UART 的 serialwrap 判斷。
這才是正確的 Responsibility Boundary。
同一個 Setup 重跑時,已經成立的狀態不應該再被破壞一次。
這種特性就是 Idempotency(冪等性)。
名字很學術,先嚇嚇你,要求其實很直接:
已經做好了,就不要為了證明自己會做,再拆掉重做一次。
PR #18 因此修改了 session.bind。
serialwrap 收到 Bind Request 後,會先確認目前連著的是否仍然是同一台 UART 設備,以及既有連線是否還能正常使用。
如果狀態根本沒有改變,就不再拆除連線,也不重新建立 PTY,而是直接回傳:
{
"ok": true,
"already_bound": true
}
意思很簡單:
已經 Bind 好了,什麼都不用動。
只有設備真的換了,或原本的 UART 連線已經失效,serialwrap 才會執行真正的重新連線。
程式碼上只多了一個判斷。
我則可以少重開很多次 minicom。
終於不用拆座墊啦~
wal reset 與 Idempotent Bind 放在一起後,TestPilot 每次開跑前仍然可以照常:
重設本次 Run 的 UART 紀錄
→ 確認 UART 綁定
→ 開始執行 Test Case
差別在於,現在重設的是本次 Run 的資料,確認的是既有連線的現況。
不會再因為 Setup 重跑一次,就把旁邊的 minicom 清出去一次。
這次被錯誤拆掉的東西,是 Host 上的 UART 連線,以及 minicom 正在使用的 PTY。
不是 DUT 裡某一條 LLAPI Command 的行為。
所以 PR #18 沒有拿真板重新跑一輪 Wi-Fi Test Case,而是在 serialwrap Repo 裡加入 8 項 Python 自動化整合測試。
測試程式會建立一個可控制的 UART 端點,啟動真正的 serialwrap 背景服務,再透過 tmux 持續讀取 serialwrap 提供的 PTY,模擬工程師開著 minicom 看 UART。
接著依序執行:
清除本次測試紀錄
→ 重複 Bind 同一台設備
→ 連續送出多條 Command
測試程式會直接檢查:
清除紀錄後,原本的終端連線是否仍然存在。
重複 Bind 同一台設備後,serialwrap 是否回覆 already_bound。
Agent 執行 Command 時,原本的終端是否仍然看得到輸出。
模擬三條 Test Case 跑完後,UART 紀錄是否正常增加,而終端全程沒有斷線。
這不是工程師盯著 minicom,最後點頭說:
嗯,這次好像沒斷。
也不是 TestPilot 自己執行、自己判定,再自己宣布 TestPilot 沒有問題。
它是 serialwrap 的 Host-side Integration Test,用受控環境重現同一組操作,確認這次修正沒有再銷毀既有 PTY。
PR 記錄的測試結果為:
178 項既有測試
+ 8 項 Human/Agent 共存整合測試
= 186 項測試通過
這組測試能證明受控 Host 環境下的連線生命週期。
它沒有使用真實 DUT,也不能直接代表所有 USB Serial Adapter、所有部署環境,或完整 TestPilot 實機 Run 都已經驗證。
證據到哪裡,結論就寫到哪裡。
這是 Host-side Integration Test,不是真實 DUT 驗證。
到這裡,TestPilot 終於可以開始一輪測試,而不會因為清紀錄或重複 Bind,就把工程師的 minicom 踢下線。
Human Console 還在。
Agent 也還在。
但 serialwrap 現在只知道:
有人連著 UART。
它還不知道:
這個人只是看 Log,還是正在輸入 Command?
如果只要 minicom 開著,Agent 就一律不能做 Self-test 或 Probe,那麼連線雖然保住了,Human 與 Agent 還是不能真正共用這條 UART。
下一篇,我們開始分清楚:
人在看,和人正在操作,是兩件不同的事。
Have a nice day.