WinMerge 是四個標的裡最「乾淨」的一個:純 Win32、零外部相依、秒級啟動。它的工具列也很好測——44 個按鈕都有名字,而名字就是 tooltip:
'Next File (Ctrl+F8)'
'Refresh (F5)'
'Copy to Right and Advance (Ctrl+Alt+Right)'
狀態列也讀得到:'找到 1 個差異'。
然後是編輯窗格:
UIA: <Pane class='Afx:00007FF6C8380000:8' name='' id='59648' patterns=[]>
patterns: []
get_value(): ''
兩個問題疊在一起。
Afx:00007FF6C8380000:8——中間那串十六進位是 MFC 把模組的載入基底位址編進了視窗類別名稱。
我量了兩次,兩次都一樣,因為這台機器上這個模組的基底位址是固定的。但這不能依賴。 換機器、換版本、開啟 ASLR 重定位,那串數字就變了。原理上它不是一個識別碼。
WM_GETTEXT 都不回應這跟昨天的 Scintilla 不同。Scintilla 雖然也是 patterns=[],但 WM_GETTEXT 有回應,所以 get_value() 能用。WinMerge 的 CrystalEdit 連那條都沒有——get_value() 回傳空字串。
四個同 class 的窗格裡兩個 218x18、兩個 545x81,只能靠尺寸分辨,沒有語意識別。
find_text_input() 會「成功」但拿到錯的東西find_text_input() -> class='Edit' id=1133
value='C:\Users\tester\a.txt'
那是檔案路徑輸入框。肇因是階梯裡的 {"class_name": "Edit"}——WinMerge 頂端那兩個「選擇檔案」的欄位就是標準 Edit,它們排在編輯窗格前面被找到。
這正是第二章講的「找到錯的東西」,而且不會拋例外。
最容易想到的驗證方式是:合併、儲存、讀輸出檔案、比對內容。
這會漏掉整個重點。WinMerge 是一個視覺工具。「差異算對了,但沒有渲染出來」是一個真實的回歸,而它會完整通過「輸出檔案內容正確」這個斷言。
差異標色在像素層面偵測得出來。做法是十行算術:
量測結果(16 行的檔案,每隔 4 行改一行):
| 改幾行 | 標色列數 | 連續帶狀區塊 |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 15 | 1 |
| 2 | 31 | 2 |
| 3 | 47 | 3 |
標色是 (239, 203, 5)。相同檔案時 0 個像素。四組全中。
關鍵是不做截圖比對。 基準圖會因為字型更新、DPI 變動、主題變更、反鋸齒而失效並需要重新產生;而「這個顏色在不在、有幾條連續帶」對這四項全部免疫。
上面那張表是第三版。前兩版都錯,而錯法都值得記。
第一版我改的是第 0、1、2 行——連續三行。結果只數到 2 條帶。
標色列數是 47,所以三行都有標色,只是它們在像素上是連在一起的。帶是「連續列的 run」,相鄰的差異中間沒有未標色的列可以分開它們。
我先前的筆記寫「3 條差異 = 3 條帶」,那是用不相鄰的行量的,而我沒有記下這個前提。改成每隔 4 行就對了。
顏色比對要不要容差?我憑感覺設了 30。實測:
| 改幾行 | tol=0 | tol=5 | tol=10 | tol=20 | tol=30 |
|---|---|---|---|---|---|
| 1 | 1 ✓ | 1 ✓ | 1 ✓ | 2 ✗ | 2 ✗ |
| 2 | 2 ✓ | 2 ✓ | 2 ✓ | 4 ✗ | 3 ✗ |
20 以上開始把選取色也算成標色。填色本身很平,容差只需要吸收擷取時的捨入誤差,所以最後定 10。
WinMerge 啟動後大約 100ms 就能被 launch_and_discover 找到,而標色更晚才畫出來。第一版在三條差異的情境數到 0 條帶——而 1 條與 2 條那兩個情境過關純粹是時機湊巧。
改成輪詢:
bands = settled(lambda: _band_count(win), lambda n: n == changed_lines, timeout=20.0)
settled 會回傳它看到的最後一個值,所以一個真正錯誤的帶數還是會出現在斷言訊息裡——不會變成一句沒有資訊的 timeout。
這是這一組裡最微妙的一點:0 條帶同時也是「還沒畫」的值。 所以沒有一個狀態可以「等待它出現」。
那個情境獨立成一個測試,就緒條件改成「連續多次讀到同一個值」,再加上一個外部理由——同一組裡的其他測試證明了標色在這個環境下畫得出來。並且在 docstring 裡寫明:
如果 WinMerge 哪天把渲染延後到超過這個視窗,這個測試會因為錯誤的理由通過。
把限制寫出來,而不是藏起來。
數同色列是十行算術、沒有陷阱。但一旦把它做成 API:
先是有人要容差參數,然後要限定區域,然後要「大致相似」,然後就是感知雜湊與影像比對——沒有天然的停止點。
wintegrate 的判準是:只編碼難以重新發現的 Windows 知識。「WM_GETTEXT 是系統訊息所以會被 marshal」很難重新發現。「數連續同色列」不難,難的是知道該去數它。
所以這個技術寫進了 docs/pitfalls.md,測試裡有一個 40 行的 tests_support_pixels.py,而 library 本身沒有任何影像 API。
/noprefs 確保乾淨設定)Next Difference 之後狀態列文字要改變(找到 3 個差異 → 3 個差異中 第 1 個)第 2 項的斷言不解析文字內容,只比較「整串清單變了沒」——狀態列是在地化的,而兩個字串都來自同一個來源,所以比較它們與語言無關。
第 3 項能抓到只讀狀態列會漏掉的回歸:差異算對了但沒有渲染出來。
WM_GETTEXT 都不回應,get_value() 是空字串find_text_input() 會拿到檔案路徑框——成功回傳,錯的元素明天是這一章最反直覺的一天:同一個應用程式版本,在兩個架構上是兩個不同的程式。