iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0

這一天的內容是我在這整個系列裡錯得最徹底的一次,而且錯了三次。所以寫法會跟其他天不一樣:照時間順序,把每一個錯誤結論留在原位。

標的是 DB Browser for SQLite,Qt。


在我自己的機器上,Qt 是四個標的裡最好的

先講我一開始量到什麼。Qt 把 QAccessible 橋接到 UIA,覆蓋率意外地豐富:38 個 Button、11 個 Tab、4 個 Tree、6 個 ToolBar、19 個 Group。

而最大的收穫是 automation_id

Application.MainWindow.centralwidget.mainTab.qt_tabwidget_stackedwidget
  .browser.tabBrowsers.dockBrowse1.TableBrowser.dataTable

一個完整的物件路徑。 與語言無關、結構清楚、跨版本大致穩定。分頁的名稱是在地化的(瀏覽資料 而不是 Browse Data),但 automation_id 不是——這正是第四章講的東西,而 Qt 做得最好。

我在筆記裡寫下:「三個標的裡只有 Qt 有這個優勢。」


然後 CI 上全紅

AssertionError: the main tab bar never reached 4 tabs (saw 0)

七個測試,六個掛在同一個 fixture 上。等了 60 秒還是 0 個分頁。

錯誤結論一:「Qt 的 accessibility 橋接沒起來」

這是我的第一個念頭,而且聽起來很合理:Qt 的 QAccessible惰性初始化的,要有 client 來問才會建起來。CI 環境沒有螢幕閱讀器,也許就沒起來。

驗證的方法不是想,是量。 我讓 fixture 在失敗的時候 dump 整棵樹:

UIA descendants: 201
control types: button=41, list item=33, header=19, tree item=18, group=16,
               tab item=11, text=8, window=8, menu item=6, tool bar=6, ...
automation ids (71 total): [...]

tab item=11——跟我在本機量到的一模一樣。樹是完整的,71 個元素有 automation_id。橋接完全正常。

第一個結論錯了,而糾正它的是在失敗訊息裡加一份 census

錯誤結論二:「是 Windows Server 的差別」

windows-latestWindows Server 2025 Datacenter,我的 VM 是 Windows 11。這個 repo 本來就有 desktop_onlyserver_only 兩個 pytest marker,代表 SKU 差異踩過。所以我把這個寫進了 module docstring。

也錯。 繼續 dump,這次印分頁的完整 automation id 而不只是尾段:

TabItem name='Browse Data'        id=''
TabItem name='Database Structure' id=''
TabItem name='Execute SQL'        id=''

id=''。全空。而本機是完整物件路徑。

第二個結論錯了,而糾正它的是印出完整的 id 而不是我以為夠用的那一段

真正的原因:視窗類別早就說了

runner:  class='Qt5152QWindowIcon'
我的 VM: class='Qt683QWindowIcon'

Qt 5.15.2 對 Qt 6.8.3。

去看官方 release 的壓縮檔裡有什麼:

DB.Browser.for.SQLite-v3.13.1-win64.zip → Qt5Core.dll
DB.Browser.for.SQLite-v3.13.1-win32.zip → Qt5Core.dll
DB.Browser.for.SQLite-v3.13.1-arm64.zip → Qt6Core.dll

同一個應用程式版本,每個架構配不同的 Qt。

我的 VM 是 ARM64,所以我裝到的是 Qt6 那份。而一般 x64 使用者拿到的是 Qt5。

換句話說:我筆記裡那句「Qt 的物件路徑是三個標的裡最好的優勢」,是在一個不具代表性的建置上量出來的。

錯誤結論三:「那 x64 就別測了,只跑 arm64 的 Qt6」

這是我確認 Qt 版本之後的第一個提案。也錯——那等於放掉絕大多數真實使用者。兩個都要測。


兩個 Qt 的實際差異

Qt 5.15.2 Qt 6.8.3
視窗類別 Qt5152QWindowIcon Qt683QWindowIcon
分頁物件路徑 有, Application. 前綴 有,前綴
分頁項目 id '' 分頁列的路徑
分頁項目 patterns Invoke, Value + SelectionItem
bar.get_selection() [] 可用
item.is_selected None 可用
工具按鈕 id 37 個中 21 個空 38 個中 19 個共用 QToolButton

只依賴兩邊都有的東西

定位分頁列——對分頁列本身(不是項目)的物件路徑做 endswith

MAIN_TAB_BAR = "centralwidget.mainTab.qt_tabwidget_tabbar"

Qt5 少了 Application. 前綴,所以 endswith 剛好兩種拼法都命中。我原本的 filter 掛在分頁「項目」上,而那個 Qt5 沒有——這就是原始的 bug。

備援(完全不依賴 id):三個分頁列裡,主文件區與 Remote dock 的是 QTabBar,dock 標題列是 QMainWindowTabBar。所以主分頁列 = 祖先鏈裡沒有 *Dock 的那個 QTabBar。Qt 的類別名稱不會被翻譯,兩邊都在。

選取——有 SelectionItem 就用 select_verified()(透過 pattern 驗證比較強),沒有就 click()。Qt5 上實測 click()Table 型別數量從 0 變 1。

驗證選取——這是最漂亮的一塊。兩個 build 都有的東西是 分頁列的 name,而它就是目前選中分頁的標籤

點 [1] '瀏覽資料'    -> bar.name='瀏覽資料'      吻合
點 [3] '執行 SQL'    -> bar.name='執行 SQL'      吻合
點 [0] '資料庫結構'  -> bar.name='資料庫結構'    吻合
點 [2] '編輯 Pragmas' -> bar.name='編輯 Pragmas'  吻合

四次全中。而且這個比較與語言無關——兩個字串都來自同一個來源。


一個視窗,三個看起來無關的失敗

同一個主題還有第二個版本,而這一次我猜了兩次,兩次都錯。

arm64 job 第一版用套件管理員裝 x64 建置,在 ARM 上跑模擬。三個症狀:

  • Notepad++:打進去的字完全沒有到 Scintillalength=0lines=1is_modified=Falseget_value()==''——而每一個元素都解析得出來、視窗看起來完全正常。11 個測試裡 7 個倒。
  • WinMerge:9 個裡 8 個過,只有 Next Difference 的點擊讓狀態列沒有變化。
  • Filesfocus_content_island()False,7 個測試全部倒在 fixture 上。

三個不同的應用程式、三種不同的症狀。而「模擬下的行為不同」聽起來完全合理,而且跟 Qt 那一題是同一個形狀——兩者都有官方 arm64 安裝檔,所以我換成原生建置。

Notepad++ 換了之後還是一樣紅。

去查我自己 VM 上那份(測試在那裡全過)的 PE 標頭:

notepad++.exe machine = ARM64
version = 8.9.8.0

它本來就是 ARM64 原生。 所以「模擬」從來不是 Notepad++ 那組失敗的原因——同一個原生建置在我的 VM 上完全正常,在 runner 上完全收不到輸入。

真正的差別在別的地方,而症狀早就指向它了:length=0 加上「Ctrl+F 沒有開出對話框」不是「編輯器壞了」,是輸入根本沒有到這個應用程式。fixture 裡那一行是這樣寫的:

with win.foreground(verify=False):

verify=False。這個 suite 到處都這樣寫,因為前景請求會跟桌面上其他事情搶——而在這裡它讓「有另一個視窗擋在前面」變成了一個沒有訊息的失敗。

修法不是加 sleep,是讓失敗說出是誰握著前景

assert _foreground_settled(win), (
    "Notepad++ never became the foreground window; "
    f"{_describe_foreground()} has it instead, so every keystroke "
    "below would have gone there"
)

下一次 CI 就把答案印出來了:

AssertionError: Notepad++ never became the foreground window;
<hwnd=0x10206 class='Windows.UI.Core.CoreWindow' title='Microsoft account'>
has it instead, so every keystroke below would have gone there

一個「Microsoft account」的系統提示視窗,從開機就握著前景,而且不會自己走。 它在我的 VM 上不存在,所以在那裡什麼問題都沒有。

而它同時解釋了最早那次模擬環境的失敗——也就是說,我對那組失敗的兩個解釋(先是模擬、後是原生建置)都不是原因。兩次都是同一個視窗擋在前面。

(CI 上的處理是在測試前把它關掉,而且是按視窗類別而不是處理程序名稱去找——一個 CoreWindow 背後的處理程序不會是你猜的那一個。)

這跟 Qt 那一題的結構完全一樣:我有一個合理的解釋,它解釋得通所有已知症狀,而它是錯的。糾正它的不是更好的推理,是讓失敗訊息多說一句話。

而它解釋了全部三個症狀

症狀 為什麼
Notepad++ 什麼都沒打進去 按鍵全部送到那個視窗
WinMerge 的按鈕點擊無效 合成點擊被前景視窗攔走
Files 的 focus_content_island()False 另一個視窗握著前景時,OS 拒絕對背景視窗的元素 SetFocus

關掉它之後,三個 job 全綠。一個原因,三個看起來完全不同的症狀。

第三列是最有意思的一列,因為它連「我以為跟輸入無關的東西」都涵蓋了:SetFocus 不是輸入操作,但作業系統對背景視窗的元素會拒絕它——而拒絕的方式是回報失敗,不是拋出可辨識的錯誤。

那「模擬不等於原生」還成立嗎

對 WinMerge 成立——原生 arm64 建置確實修好了它那一個失敗。而且「架構決定你拿到哪個建置」在 DB Browser 那一題是硬事實:

x64 與 arm64 不是「同一個測試在兩台機器上跑」,是兩個不同的程式。

只是它不會是每一個 arm64 失敗的原因。而我在只有三個症狀、沒有任何診斷輸出的情況下,就把它當成了唯一的解釋。


所以測試環境要釘到什麼程度

這一天的結論不是關於 Qt,是關於「一個測試的環境要指定到多細」。答案比我原本以為的細:

要釘 為什麼
應用程式版本 顏色值、automation id、分頁數都是對著一個 build 量的
架構 決定你拿到哪一個建置
原生或模擬 模擬下的行為可以完全不同
client / server SKU 這個 repo 早就有兩個 marker 在處理它

而且要釘在兩個地方並且互相檢查:CI 的安裝步驟指定版本,測試模組裡也宣告一個 VERIFIED_VERSION,再加一個測試比對實際安裝的版本。不然這兩件事會默默漂開。

(版本從 file version 的數值欄位讀,不要讀 FileVersion 字串——DB Browser 自己寫的是 '3.13.1.',尾巴一個點、沒有第四段。)


小結

  • 我在這一題上錯了三次,而每一次糾正我的都是更多的量測,不是更多的推理
  • 「Qt 橋接沒起來」→ 在失敗訊息裡 dump 一份 census 就推翻了
  • 「是 Windows Server 的差別」→ 印出完整的 automation id 就推翻了
  • 同一個應用程式版本可以是兩個不同的程式:v3.13.1 的 x64 是 Qt5、arm64 是 Qt6
  • 我本機量到的「Qt 的物件路徑最好」是在不具代表性的建置上量的
  • 兩邊都成立的寫法:對分頁列做 endswith、用 Qt 類別名稱備援、用 name 驗證選取
  • 一個原因可以有三個看起來無關的症狀:一個「Microsoft account」提示視窗握著前景,同時讓 Notepad++ 收不到按鍵、WinMerge 的點擊無效、Files 的 SetFocus 被拒絕。我對它猜了兩次(模擬、原生建置),兩次都錯
  • 「模擬不等於原生」對 WinMerge 那一個失敗成立,但它不是那一組的原因
  • 測試環境要釘住版本、架構、原生/模擬、SKU——而且要釘在兩個地方互相檢查

第五章剩下三天:兩天回到 UIA 的複雜控制項,最後一天用這四個 app 去抓四個真實的 bug。


上一篇
Day 23:UIA 完全看不到的控制項,像素是唯一的證據
系列文
Windows 桌面軟體 CI 實戰:從系統工具開發到 Zero-RDP 自動化測試錄影24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言