系列說明 >> 本系列大部分 PR 來自私人或公司專案,部分程式碼、環境設定與實作細節不便公開。
我只能保證文中提到的 PR 與問題皆為真實案例,但會經過必要的匿名化與內容調整。
本系列主要分享問題如何被發現、背後的思考方式,以及解法如何形成,不會深入討論完整實作與部署細節,敬請見諒。
serialwrapd 直接持有實體 UART,統一管理 Command、Console 寫入與 Recovery;Target 只接收原始 Command 與 Raw Keystrokes。OK,並驗證一般 Command、Background Output、Interactive、Recovery 與多個 Minicom Console。技術債,技術債,一時快樂一時債,一直快樂一堆債。
之前 serialwrap 用得有多爽,現在接進測試就有多痛。
上一篇為了讓 Full Run 真的跑完,TestPilot 補了 Command Extraction、Readback Fallback、RC 判斷,最後還得把 Report 裡的 BEGIN、RC、END、Shell Prompt 與 Command Echo 全部清掉。
原本是 serialwrap 用來辨認 Command 的格式,最後卻變成 TestPilot 也必須理解的格式。
UART Log 看不懂,還可以先怪 Marker 太多。
但當 TestPilot 必須知道 Marker 長什麼樣子,才能把它們清掉時,問題就不只是 Log 難看了。
上一篇已經把結論講得很清楚:
不再補 Regex,也不再多寫一層 Sanitizer。
葵花寶典記載,欲練神功,必先自宮!
長痛不如短痛,脖子一伸,手起刀落。
Marker,砍了吧!
不過在正式殺死 Marker 以前,還是先替它說句公道話。
這東西當初不是亂塞的,而且在 Day 1 的使用情境裡,確實很好用。
在沒有 serialwrap 的情況下,Agent 可以透過 tmux 對 minicom 下 Command,但 UART Log 是一段持續增加的文字。
裡面可能同時包含前一條 Command 的 Output、Shell Prompt、Command Echo、Kernel Log、Background Message,以及工程師剛才手動輸入的內容。
serialwrap 必須知道:哪一段才屬於剛剛送出的 Command?
最直接的方法,就是在 Command 前後加上唯一 Marker,再把 Return Code 一起印出來:
echo __BEGIN_abcd1234__
real_command
rc=$?
echo __RC_abcd1234__=$rc
echo __END_abcd1234__
接著從 minicom Log 裡找到 BEGIN 與 END,把中間那一段切出來。
Agent
↓
tmux send-keys
↓
minicom
↓
BEGIN + Command + RC + END
↓
DUT UART
↓
minicom log
↓
Marker Extraction
↓
Command Result
對一般 Shell Command 而言,這套方法很有效。
每次產生不同的 ID,Marker 幾乎不會撞名;BEGIN 與 END 之間就是這次的 Output,RC 則提供成功或失敗的判斷。
重點是,Parser 很好寫。
serialwrap 不需要辨認每一種 Prompt,也不用從一大段 Log 裡猜 Command 到底在哪裡結束。
Day 1 做到這裡時,我確實覺得問題差不多解完了。
直到接進 TestPilot,開始遇到那些不照著:
Command
→ Output
→ Prompt
→ Done
這條路走的非典型操作。
BEGIN 與 END 最先遇到的問題,是它們只能表示字串邊界。
例如:
echo __BEGIN__
false
echo __END__
中間的 false 已經失敗,但 END 還是會出現,所以後來又補了 RC Marker。
再來是:
reboot
如果 reboot 真的成功,原本的 Shell 會直接消失,RC 與 END 根本沒有機會印出來。最成功的一次 Reboot,最後反而會被判成 Timeout。
還有 Background Command:
(sleep 1; echo TOKEN)&
Prompt 很快就回來,但 TOKEN 一秒後才出現。看到 Prompt 就結束,晚到的 Output 可能會被算進下一條 Command。
更麻煩的是 Interactive:
啟動工具
→ 等待輸入
→ 輸入文字
→ 再等待
→ Ctrl-C
Bootloader Menu、Password Prompt、menuconfig,甚至單純開一個 cat,都不是送出一行文字後等待 END 的操作。
BEGIN、RC、END 並不是完全沒用。
它只是把「會正常回到 Shell 的 Line Command」當成了所有 UART 操作的共同形狀。
單獨執行幾條 Command 時,這個假設很少出事。Full Run 把 Reboot、Background、Interactive 與不同 Target State 全部放進來後,例外就開始比正常流程還忙。
既然 Marker 會污染 UART,最直接的替代方案,就是送出原始 Command,等 Shell Prompt 回來,再把中間的 Output 當成 Result。
Command
→ Output
→ Prompt
→ Result
一般的 pwd、cat 或 Query Command 確實可以這樣處理。
問題是,前面那些 Case 一個都沒有消失。
reboot 不會回到原本的 Prompt;Background Output 會在 Prompt 後繼續出現;Interactive 操作則根本沒有單一 Prompt 可以代表完成。
所以「等 Prompt」可以成為一種 Command Mode,但不能取代整套 Marker Model。
至少必須分清楚:
到這裡,問題已經不是怎麼把 stdout 切得更準。
這些都是 Command 在 Host 端的生命週期,必須有地方記住目前正在做什麼。
舊架構裡,實體 UART 由 minicom 開啟。
Agent 透過 tmux 對 minicom 下 Command,再從 minicom Log 取回結果。
如果只是擔心兩個 Agent 同時輸入,看起來可以在 Agent 的操作路徑外面加一把 Lock:
Agent A ─┐
├─> Lock ─> tmux ─> minicom ─> UART
Agent B ─┘
Agent 之間確實可以排隊。
但工程師仍然可以直接在 minicom 裡輸入。
有時候我看到 Agent 剛下完一條 Command,覺得方向不太對,便順手補上一條:
logread
同一時間,Agent 又送出:
dmesg -n 1
最後 DUT 可能收到:
logredmesg -n 1ad
這條 Command 不是我下的,也不是 Agent 下的。
是我們一起完成的。
是的,我就是那個來添亂的工程師,我就手賤咩……(搓手)
原本實體 UART 是由 minicom 開啟,Agent 再透過 tmux 間接操作 minicom。工程師坐在 minicom 前面時,輸入根本不會經過 Agent 使用的 Lock。
Human ──────────────> minicom ─> UART
Agent ─> Lock ─> tmux ─┘
問題不在 Lock 本身。
問題在於這把 Lock 只管得到 Agent。
當然,也可以繼續替每一個入口補 Lock。Agent 一把、CLI 一把、測試流程一把,未來再多一個 Tool,就再多補一把。
但只要這些 Lock 不在實體 UART 的共同入口,它們就只能保證自己的 Caller 很守規矩,無法保證 UART 最後收到的資料沒有和別人混在一起。
到這裡,幾個比較便宜的方向差不多都走過了:
拿掉 Marker,改等 Prompt
→ 只能處理一般 Command
替 Agent 操作加 Lock
→ Human Input 仍然可以繞過
Marker 可以處理字串切割,卻無法表示所有 UART 操作的生命週期。
Lock 可以限制某一條操作路徑,卻無法管理整條 UART。
真正缺少的不是另一種 Parser,也不是另一把 Lock。
是 UART 必須有一個共同的 Owner。
我只是想偷個懶,怎麼就這麼難啊?
原本我的設想,是讓 serialwrap 當一個中繼輔助的角色,越感覺不到它的存在,我就越舒服。
我不想改掉自己使用 minicom 的習慣,也不想為了 Agent 再維護另一套 UART 操作方式。
但所有 TX 如果都必須經過同一個入口,那乾脆一不做二不休:
實體 UART 全部收回來。
你們都不准再自己拿。
我的 UART 我來管,我命由我不由天!
新的方向變成:
Human Minicom ─┐
Agent CLI ├──> serialwrapd ───> Physical UART ───> DUT
其他 Client ┘
這次由 serialwrapd 直接持有實體 UART。
Human 使用的 minicom 不再直接連到 TTY,而是連到 Broker 提供的 Console;Agent 與其他 Client 也從同一個入口提交操作。
UART RX 可以 Fan-out。
工程師、Agent 與 Log Collector 都能看到相同的資料。
但 TX 必須全部經過同一個寫入點,再由 Broker 決定現在輪到誰。
RX:一份資料,可以分給很多人看
TX:所有輸入,必須排成同一條隊伍
Command ID、目前正在執行哪一種操作、是否 Timeout,以及失敗後進行了哪些 Recovery,也都留在 Host 端管理。
Target 只接收真正要執行的 Command,或 Interactive 操作需要的 Raw Keystrokes。
不再收到 BEGIN、RC 與 END。
也不需要替 serialwrap 維護一套與 DUT 本身無關的控制格式。
看起來很完美。
我可以繼續使用 minicom,Agent 也能透過同一個入口操作,TestPilot 則不用再理解 Marker。
我果然是天才……
當時的我確實是這樣想的。
完成重構後,除了 Unit Tests,也使用安裝版 daemon 驗證 COM0、COM1 與 COM2。
三個 Session 的 Self-test 最後都回到:
OK
一般 Command 分別執行了 echo 與 pwd;Background Command 驗證 Prompt 回來後,延遲 Output 仍然能被取得;Interactive 則使用 cat 測試文字輸入、Enter 與 Ctrl-C。
COM0 與 COM2 也實際透過 Minicom Console,確認工程師原本的操作方式仍然能用。
其中 COM1 曾回報:
TARGET_UNRESPONSIVE
接著經過:
session recover
才恢復正常。
這次驗證的重點,不只是拿掉 Marker 後仍然能下 Command。
一般 Command、Background、Interactive、Human Console 與 Recovery,確實都走過同一條 UART Path。
水啦,這條路看起來行得通。
至少 Broker 不是只存在於架構圖裡。
到這裡,serialwrap 已經可以對一條 UART 操作負責。
TestPilot 不必再理解 Marker Grammar,也不用先清掉 serialwrap 的控制字串,才能讓人看懂 UART Output。
但 Test Case 需要的資訊,仍然不只是一條 Command 的 stdout 與 RC。
Case A 觸發的 Kernel Log,可能在 Command 完成後才出現;DUT 與 STA 也可能在同一段測試期間,各自產生需要保留的 UART 訊息。
只保存整理過的 Result,還是不知道整個 Case 的現場從哪裡開始,又在哪裡結束。
今日工事: serialwrap_lib.py 正式退場,下台一鞠躬。
舊時代 Marker 的眼淚,就讓它留在舊時代。
歷史,已經翻頁了。
下一篇,TestPilot 不再只接 stdout。
我們開始讓每一個 Test Case 對應到真正的 UART Evidence。
Have a nice day.