<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Parser on Tarragon</title><link>https://tarrragon.github.io/blog/tags/parser/</link><description>Recent content in Parser on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 19 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/parser/index.xml" rel="self" type="application/rss+xml"/><item><title>BNF（Backus-Naur Form）</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/bnf/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/bnf/</guid><description>&lt;p>BNF（Backus-Naur Form）的核心概念是「&lt;strong>用產生式規則描述一個語言裡哪些字串合法&lt;/strong>」。它常用在程式語言、資料格式、parser 與 structured output grammar，讓人跟工具都能用同一份規則理解合法語法。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>BNF 是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">grammar&lt;/a> 的一種表示法，特別適合描述 context-free grammar。規則左邊是非終結符，右邊是它可以展開成的符號組合；終結符是實際會出現在字串中的 token，非終結符是中間抽象節點。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&amp;lt;expr&amp;gt; ::= &amp;lt;term&amp;gt; | &amp;lt;expr&amp;gt; &amp;#34;+&amp;#34; &amp;lt;term&amp;gt;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&amp;lt;term&amp;gt; ::= &amp;lt;number&amp;gt; | &amp;#34;(&amp;#34; &amp;lt;expr&amp;gt; &amp;#34;)&amp;#34;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段規則表示 expression 可以是 term，也可以是 expression 加 term；term 可以是 number，也可以是括號包住的 expression。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>看到 &lt;code>::=&lt;/code>、&lt;code>&amp;lt;name&amp;gt;&lt;/code>、多個展開選項，就是 BNF 或 BNF-like grammar。LLM structured output 文章裡提到 BNF，通常是在說「把合法輸出格式寫成形式語法，推論時用它限制生成」。llama.cpp 的 GBNF、部分 grammar engine 與 parser 文件都會使用類似記法。&lt;/p>
&lt;p>BNF 的限制是它描述語法，不描述語意。它能表示「括號必須成對」「欄位順序合法」，但不能直接表示「日期必須晚於今天」「使用者必須有權限讀這筆資料」這類外部約束。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>BNF 適合拿來讀懂 grammar-constrained sampling 的規則形狀。實作時要確認你使用的引擎支援的是標準 BNF、EBNF、GBNF，還是自家 dialect；不同 dialect 的 optional、repeat、token escaping 寫法會不同。下一步路由是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">Grammar&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/lark-grammar/" data-link-title="Lark Grammar" data-link-desc="Lark parser 使用的 EBNF-like grammar 格式，常被 structured output 工具拿來描述自訂輸出語法">Lark Grammar&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>BNF（Backus-Naur Form）的核心概念是「<strong>用產生式規則描述一個語言裡哪些字串合法</strong>」。它常用在程式語言、資料格式、parser 與 structured output grammar，讓人跟工具都能用同一份規則理解合法語法。</p>
<h2 id="概念位置">概念位置</h2>
<p>BNF 是 <a href="/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">grammar</a> 的一種表示法，特別適合描述 context-free grammar。規則左邊是非終結符，右邊是它可以展開成的符號組合；終結符是實際會出現在字串中的 token，非終結符是中間抽象節點。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">&lt;expr&gt; ::= &lt;term&gt; | &lt;expr&gt; &#34;+&#34; &lt;term&gt;
</span></span><span class="line"><span class="ln">2</span><span class="cl">&lt;term&gt; ::= &lt;number&gt; | &#34;(&#34; &lt;expr&gt; &#34;)&#34;</span></span></code></pre></div><p>這段規則表示 expression 可以是 term，也可以是 expression 加 term；term 可以是 number，也可以是括號包住的 expression。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>看到 <code>::=</code>、<code>&lt;name&gt;</code>、多個展開選項，就是 BNF 或 BNF-like grammar。LLM structured output 文章裡提到 BNF，通常是在說「把合法輸出格式寫成形式語法，推論時用它限制生成」。llama.cpp 的 GBNF、部分 grammar engine 與 parser 文件都會使用類似記法。</p>
<p>BNF 的限制是它描述語法，不描述語意。它能表示「括號必須成對」「欄位順序合法」，但不能直接表示「日期必須晚於今天」「使用者必須有權限讀這筆資料」這類外部約束。</p>
<h2 id="設計責任">設計責任</h2>
<p>BNF 適合拿來讀懂 grammar-constrained sampling 的規則形狀。實作時要確認你使用的引擎支援的是標準 BNF、EBNF、GBNF，還是自家 dialect；不同 dialect 的 optional、repeat、token escaping 寫法會不同。下一步路由是 <a href="/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">Grammar</a> 與 <a href="/blog/llm/knowledge-cards/lark-grammar/" data-link-title="Lark Grammar" data-link-desc="Lark parser 使用的 EBNF-like grammar 格式，常被 structured output 工具拿來描述自訂輸出語法">Lark Grammar</a>。</p>
]]></content:encoded></item><item><title>Lark Grammar</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/lark-grammar/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/lark-grammar/</guid><description>&lt;p>Lark grammar 的核心概念是「&lt;strong>Lark parser 使用的一種 EBNF-like 語法描述格式&lt;/strong>」。在 LLM structured output 文件中看到 lark grammar，通常是在說某個工具用 Lark 風格規則描述合法輸出，再把規則交給 parser 或 constrained decoding engine。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Lark 是 Python 生態的 parsing toolkit，Lark grammar 是它的規則語言。它比傳統 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/bnf/" data-link-title="BNF（Backus-Naur Form）" data-link-desc="用遞迴產生式描述語法的經典記法，是 CFG、parser 與 grammar-constrained sampling 常見的基礎表示">BNF&lt;/a> 更接近實作格式，常見元素包含 rule、terminal、literal、repeat、optional、ignore whitespace 與 start rule。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">start: query
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">query: FIELD OP VALUE
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">FIELD: &amp;#34;status&amp;#34; | &amp;#34;owner&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">OP: &amp;#34;=&amp;#34; | &amp;#34;!=&amp;#34;
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">VALUE: ESCAPED_STRING
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">%import common.ESCAPED_STRING
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">%ignore &amp;#34; &amp;#34;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這段規則描述一個很小的查詢語言，只允許固定欄位、固定運算子與 quoted string。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>看到 &lt;code>start:&lt;/code>、大寫 terminal、&lt;code>%import common.*&lt;/code>、&lt;code>%ignore&lt;/code>，通常就是 Lark grammar 或受它影響的格式。LLM 場景常用它描述 JSON 子集、SQL-like query、指令 DSL、分類輸出或固定格式報告。&lt;/p>
&lt;p>Lark grammar 的風險是把 parser 格式誤當跨引擎標準。某些 constrained decoding 工具支援 Lark-like 語法，某些只支援 JSON Schema、regex、GBNF 或自家格式；搬規則前要確認目標 server 能不能解析同一套語法。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>Lark grammar 適合需要清楚描述自訂格式、且工具鏈支援 Lark dialect 的場景。設計時先把合法範圍縮到應用真的需要的語法，再補 validator 處理外部狀態與權限。下一步路由是 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">Grammar&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/dsl/" data-link-title="DSL（Domain-Specific Language）" data-link-desc="為特定業務或技術領域設計的小語言，在 LLM 應用中常作為可解析、可驗證、可執行的中介輸出">DSL&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>Lark grammar 的核心概念是「<strong>Lark parser 使用的一種 EBNF-like 語法描述格式</strong>」。在 LLM structured output 文件中看到 lark grammar，通常是在說某個工具用 Lark 風格規則描述合法輸出，再把規則交給 parser 或 constrained decoding engine。</p>
<h2 id="概念位置">概念位置</h2>
<p>Lark 是 Python 生態的 parsing toolkit，Lark grammar 是它的規則語言。它比傳統 <a href="/blog/llm/knowledge-cards/bnf/" data-link-title="BNF（Backus-Naur Form）" data-link-desc="用遞迴產生式描述語法的經典記法，是 CFG、parser 與 grammar-constrained sampling 常見的基礎表示">BNF</a> 更接近實作格式，常見元素包含 rule、terminal、literal、repeat、optional、ignore whitespace 與 start rule。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">start: query
</span></span><span class="line"><span class="ln">2</span><span class="cl">query: FIELD OP VALUE
</span></span><span class="line"><span class="ln">3</span><span class="cl">FIELD: &#34;status&#34; | &#34;owner&#34;
</span></span><span class="line"><span class="ln">4</span><span class="cl">OP: &#34;=&#34; | &#34;!=&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">VALUE: ESCAPED_STRING
</span></span><span class="line"><span class="ln">6</span><span class="cl">%import common.ESCAPED_STRING
</span></span><span class="line"><span class="ln">7</span><span class="cl">%ignore &#34; &#34;</span></span></code></pre></div><p>這段規則描述一個很小的查詢語言，只允許固定欄位、固定運算子與 quoted string。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>看到 <code>start:</code>、大寫 terminal、<code>%import common.*</code>、<code>%ignore</code>，通常就是 Lark grammar 或受它影響的格式。LLM 場景常用它描述 JSON 子集、SQL-like query、指令 DSL、分類輸出或固定格式報告。</p>
<p>Lark grammar 的風險是把 parser 格式誤當跨引擎標準。某些 constrained decoding 工具支援 Lark-like 語法，某些只支援 JSON Schema、regex、GBNF 或自家格式；搬規則前要確認目標 server 能不能解析同一套語法。</p>
<h2 id="設計責任">設計責任</h2>
<p>Lark grammar 適合需要清楚描述自訂格式、且工具鏈支援 Lark dialect 的場景。設計時先把合法範圍縮到應用真的需要的語法，再補 validator 處理外部狀態與權限。下一步路由是 <a href="/blog/llm/knowledge-cards/grammar/" data-link-title="Grammar" data-link-desc="描述合法字串形狀的形式規則，在 structured output 中用來限制 LLM 每一步可輸出的 token">Grammar</a> 與 <a href="/blog/llm/knowledge-cards/dsl/" data-link-title="DSL（Domain-Specific Language）" data-link-desc="為特定業務或技術領域設計的小語言，在 LLM 應用中常作為可解析、可驗證、可執行的中介輸出">DSL</a>。</p>
]]></content:encoded></item><item><title>Test data 代表性</title><link>https://tarrragon.github.io/blog/testing/05-test-design-judgment/test-data-representativeness/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/05-test-design-judgment/test-data-representativeness/</guid><description>&lt;p>測試資料的代表性是指測試輸入能多大程度反映真實環境的輸入分佈。「測試資料能代表真實環境」是每個 test 的隱性假設 — 這個假設成立時 test 有效，不成立時 test 通過但問題仍在。&lt;/p>
&lt;h2 id="代表性問題的案例">代表性問題的案例&lt;/h2>
&lt;p>app_tunnel 的 ANSI parser 有 18 個 test，全部通過。測試資料是手寫的 SGR 色彩碼（&lt;code>\x1B[31mhello\x1B[0m&lt;/code>），parser 正確解析這類序列。&lt;/p>
&lt;p>真實 zsh 啟動後送出的控制序列包含 OSC 標題設定、CSI private mode、字元集指定等至少 5 種類型。Parser 只認識 SGR，其他全部透傳為亂碼（&lt;a href="https://tarrragon.github.io/blog/testing/cases/ansi-parser-test-data-blindspot/" data-link-title="T.C3 ANSI parser 測試資料不覆蓋真實 shell output" data-link-desc="ANSI parser 只處理基本 SGR 色彩碼、unit test 用手寫乾淨字串驗證 — 真實 zsh prompt 送出 OSC 標題設定、CSI private mode 游標隱藏、括號貼上模式等數十種控制序列，全部殘留為亂碼">T.C3&lt;/a>）。&lt;/p>
&lt;p>18 個 test 覆蓋了 1 種序列類型。測試資料的代表性假設（「SGR 就是主要的序列類型」）和真實環境不符。&lt;/p>
&lt;h2 id="三種測試資料來源">三種測試資料來源&lt;/h2>
&lt;h3 id="手寫">手寫&lt;/h3>
&lt;p>開發者根據對輸入格式的理解手動建構測試字串。&lt;/p>
&lt;p>優點：精確控制、容易理解、可以針對特定邊界條件設計。&lt;/p>
&lt;p>缺點：受限於開發者對輸入分佈的認知。如果開發者不知道真實環境有哪些輸入類型，手寫的測試資料就是開發者認知的子集 — T.C3 就是這個模式。&lt;/p>
&lt;p>適合場景：格式規格明確且有限（JSON schema、固定格式的設定檔）、邊界條件測試（空值、最大長度、特殊字元）。&lt;/p>
&lt;h3 id="錄製">錄製&lt;/h3>
&lt;p>從真實環境擷取實際的輸入資料，作為 test 的輸入。&lt;/p>
&lt;p>優點：直接反映真實環境的輸入分佈，包含開發者不知道的輸入類型。&lt;/p>
&lt;p>缺點：錄製的資料可能包含敏感資訊（需要脫敏）、資料量可能大（需要挑選代表性樣本）、真實環境的輸入可能隨時間改變（錄製的資料可能過時）。&lt;/p>
&lt;p>適合場景：輸入格式複雜且規格不完整（終端機 escape 序列、網路封包、使用者產生的內容）、parser 類的功能（需要知道「真實輸入長什麼樣」）。&lt;/p>
&lt;p>T.C3 如果用錄製的真實 zsh 啟動輸出作為測試資料，OSC 和 CSI private mode 會自然出現在輸入中。即使 parser 仍然不處理這些序列，test 至少能讓開發者看到「有 5 種序列類型，我只處理了 1 種」。&lt;/p>
&lt;h3 id="生成property-based-testing">生成（Property-based testing）&lt;/h3>
&lt;p>用 generator 自動產生大量隨機或半隨機的輸入，驗證 parser 的行為是否符合通用性質（不崩潰、輸出長度 &amp;lt;= 輸入長度、冪等性）。&lt;/p>
&lt;p>優點：覆蓋人類想不到的 edge case、發現意外的崩潰或無限迴圈。&lt;/p>
&lt;p>缺點：不針對特定功能驗證（驗證的是通用性質，不是「OSC 序列是否被正確處理」）、generator 本身需要維護。&lt;/p>
&lt;p>適合場景：parser、serializer、codec 等輸入格式複雜的功能。和手寫 test 互補 — 手寫驗證特定行為正確性，生成驗證通用穩定性。&lt;/p>
&lt;h2 id="兩類-test-的分工">兩類 test 的分工&lt;/h2>
&lt;p>T.C3 的策略建議是把 test 分成兩類：&lt;/p>
&lt;p>&lt;strong>功能正確性 test&lt;/strong>：用手寫乾淨字串驗證 parser 對已知序列的處理正確性。&lt;code>\x1B[31mhello\x1B[0m&lt;/code> 應該產生紅色 token — 這是功能規格的驗證。&lt;/p>
&lt;p>&lt;strong>環境相容性 test&lt;/strong>：用錄製的真實輸出驗證 parser 在真實環境中的表現。不斷言「每個序列都被正確處理」，而是斷言「沒有崩潰」「沒有未處理序列殘留在可見輸出中」。&lt;/p>
&lt;p>兩類 test 回答不同問題。功能正確性回答「parser 的邏輯對不對」，環境相容性回答「parser 在真實環境中夠不夠用」。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>Assertion 的品質判斷 → &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問&lt;/a>&lt;/li>
&lt;li>Mock 邊界的判斷 → &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/mock-boundary-decision/" data-link-title="Mock 邊界判斷決策表" data-link-desc="什麼時候 mock 夠用、什麼時候需要真實服務 — 從 API 層 / 協議層 / 環境層的斷裂點判斷 mock 的適用範圍">Mock 邊界判斷決策表&lt;/a>&lt;/li>
&lt;li>Protocol integration test 用真實服務輸出 → &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/websocket-protocol-test/" data-link-title="WebSocket 協議測試實作" data-link-desc="對真實 ttyd 驗證 frame type 和 auth handshake — 從 T.C1 和 T.C2 的教訓推導出的 protocol integration test 設計">testing 模組三 WebSocket 協議測試&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>測試資料的代表性是指測試輸入能多大程度反映真實環境的輸入分佈。「測試資料能代表真實環境」是每個 test 的隱性假設 — 這個假設成立時 test 有效，不成立時 test 通過但問題仍在。</p>
<h2 id="代表性問題的案例">代表性問題的案例</h2>
<p>app_tunnel 的 ANSI parser 有 18 個 test，全部通過。測試資料是手寫的 SGR 色彩碼（<code>\x1B[31mhello\x1B[0m</code>），parser 正確解析這類序列。</p>
<p>真實 zsh 啟動後送出的控制序列包含 OSC 標題設定、CSI private mode、字元集指定等至少 5 種類型。Parser 只認識 SGR，其他全部透傳為亂碼（<a href="/blog/testing/cases/ansi-parser-test-data-blindspot/" data-link-title="T.C3 ANSI parser 測試資料不覆蓋真實 shell output" data-link-desc="ANSI parser 只處理基本 SGR 色彩碼、unit test 用手寫乾淨字串驗證 — 真實 zsh prompt 送出 OSC 標題設定、CSI private mode 游標隱藏、括號貼上模式等數十種控制序列，全部殘留為亂碼">T.C3</a>）。</p>
<p>18 個 test 覆蓋了 1 種序列類型。測試資料的代表性假設（「SGR 就是主要的序列類型」）和真實環境不符。</p>
<h2 id="三種測試資料來源">三種測試資料來源</h2>
<h3 id="手寫">手寫</h3>
<p>開發者根據對輸入格式的理解手動建構測試字串。</p>
<p>優點：精確控制、容易理解、可以針對特定邊界條件設計。</p>
<p>缺點：受限於開發者對輸入分佈的認知。如果開發者不知道真實環境有哪些輸入類型，手寫的測試資料就是開發者認知的子集 — T.C3 就是這個模式。</p>
<p>適合場景：格式規格明確且有限（JSON schema、固定格式的設定檔）、邊界條件測試（空值、最大長度、特殊字元）。</p>
<h3 id="錄製">錄製</h3>
<p>從真實環境擷取實際的輸入資料，作為 test 的輸入。</p>
<p>優點：直接反映真實環境的輸入分佈，包含開發者不知道的輸入類型。</p>
<p>缺點：錄製的資料可能包含敏感資訊（需要脫敏）、資料量可能大（需要挑選代表性樣本）、真實環境的輸入可能隨時間改變（錄製的資料可能過時）。</p>
<p>適合場景：輸入格式複雜且規格不完整（終端機 escape 序列、網路封包、使用者產生的內容）、parser 類的功能（需要知道「真實輸入長什麼樣」）。</p>
<p>T.C3 如果用錄製的真實 zsh 啟動輸出作為測試資料，OSC 和 CSI private mode 會自然出現在輸入中。即使 parser 仍然不處理這些序列，test 至少能讓開發者看到「有 5 種序列類型，我只處理了 1 種」。</p>
<h3 id="生成property-based-testing">生成（Property-based testing）</h3>
<p>用 generator 自動產生大量隨機或半隨機的輸入，驗證 parser 的行為是否符合通用性質（不崩潰、輸出長度 &lt;= 輸入長度、冪等性）。</p>
<p>優點：覆蓋人類想不到的 edge case、發現意外的崩潰或無限迴圈。</p>
<p>缺點：不針對特定功能驗證（驗證的是通用性質，不是「OSC 序列是否被正確處理」）、generator 本身需要維護。</p>
<p>適合場景：parser、serializer、codec 等輸入格式複雜的功能。和手寫 test 互補 — 手寫驗證特定行為正確性，生成驗證通用穩定性。</p>
<h2 id="兩類-test-的分工">兩類 test 的分工</h2>
<p>T.C3 的策略建議是把 test 分成兩類：</p>
<p><strong>功能正確性 test</strong>：用手寫乾淨字串驗證 parser 對已知序列的處理正確性。<code>\x1B[31mhello\x1B[0m</code> 應該產生紅色 token — 這是功能規格的驗證。</p>
<p><strong>環境相容性 test</strong>：用錄製的真實輸出驗證 parser 在真實環境中的表現。不斷言「每個序列都被正確處理」，而是斷言「沒有崩潰」「沒有未處理序列殘留在可見輸出中」。</p>
<p>兩類 test 回答不同問題。功能正確性回答「parser 的邏輯對不對」，環境相容性回答「parser 在真實環境中夠不夠用」。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Assertion 的品質判斷 → <a href="/blog/testing/05-test-design-judgment/assertion-quality/" data-link-title="Assertion 品質三問" data-link-desc="斷言的是行為嗎？能區分正確和錯誤嗎？會 flaky 嗎？— 三個問題判斷 assertion 是否有效">Assertion 品質三問</a></li>
<li>Mock 邊界的判斷 → <a href="/blog/testing/05-test-design-judgment/mock-boundary-decision/" data-link-title="Mock 邊界判斷決策表" data-link-desc="什麼時候 mock 夠用、什麼時候需要真實服務 — 從 API 層 / 協議層 / 環境層的斷裂點判斷 mock 的適用範圍">Mock 邊界判斷決策表</a></li>
<li>Protocol integration test 用真實服務輸出 → <a href="/blog/testing/03-protocol-integration-test/websocket-protocol-test/" data-link-title="WebSocket 協議測試實作" data-link-desc="對真實 ttyd 驗證 frame type 和 auth handshake — 從 T.C1 和 T.C2 的教訓推導出的 protocol integration test 設計">testing 模組三 WebSocket 協議測試</a></li>
</ul>
]]></content:encoded></item><item><title>T.C3 ANSI parser 測試資料不覆蓋真實 shell output</title><link>https://tarrragon.github.io/blog/testing/cases/ansi-parser-test-data-blindspot/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/cases/ansi-parser-test-data-blindspot/</guid><description>&lt;p>這個案例的核心責任是說明 unit test 的輸入資料品質如何決定測試的有效性。Parser 邏輯正確、斷言正確、覆蓋率高 — 但測試資料是人工挑選的乾淨子集，跟真實環境的輸入分佈不同。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>app_tunnel 的 &lt;code>AnsiParser&lt;/code> 負責解析終端機輸出的 ANSI escape 序列，轉換為帶色彩的文字 token。unit test 用手寫字串驗證：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-dart" data-lang="dart">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 測試資料範例 — 乾淨的 SGR 色彩碼
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="n">test&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;紅色文字&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kd">final&lt;/span> &lt;span class="n">tokens&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parser&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;&lt;/span>&lt;span class="se">\x1B&lt;/span>&lt;span class="s1">[31mhello&lt;/span>&lt;span class="se">\x1B&lt;/span>&lt;span class="s1">[0m&amp;#39;&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="n">expect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tokens&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="n">first&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">isA&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">TextToken&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="p">());&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>真實 zsh prompt 啟動後送出的控制序列（擷取自實機 log）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">\x1B]0;user@host: ~\x07 ← OSC：設定終端機視窗標題
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">\x1B[?2004h ← CSI private mode：啟用括號貼上模式
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">\x1B[?1h ← CSI private mode：啟用應用程式游標鍵
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">\x1B(B ← 字元集指定：選擇 ASCII
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">\x1B[?25l ← CSI private mode：隱藏游標&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Parser 只認識 &lt;code>\x1B[{數字;數字}{字母}&lt;/code> 格式的標準 CSI，其他全部殘留在輸出中。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>值&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>測試案例數&lt;/td>
 &lt;td>18 個 AnsiParser test，全過&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>測試覆蓋的序列類型&lt;/td>
 &lt;td>SGR 色彩碼（&lt;code>\x1B[31m&lt;/code> 等）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>真實環境的序列類型&lt;/td>
 &lt;td>SGR + OSC + CSI private mode + 字元集指定 + 其他&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>實機表現&lt;/td>
 &lt;td>終端機畫面散佈 &lt;code>]0;user@host&lt;/code> 等亂碼片段&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>修復&lt;/td>
 &lt;td>新增 3 個 RegExp 過濾 OSC / CSI private / 其他 escape&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;ol>
&lt;li>
&lt;p>&lt;strong>測試資料的代表性是隱性假設&lt;/strong>。18 個 test 的斷言都正確 — &lt;code>\x1B[31m&lt;/code> 確實應該產生紅色 token。但「測試輸入能代表真實輸入」是一個未經驗證的假設。真實 zsh 的輸出包含 5+ 種 escape 序列類型，測試只覆蓋了 1 種。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Parser 的行為對未知序列是「透傳」而非「報錯」&lt;/strong>。這是合理的設計 — 不認識的序列不應該讓 parser 崩潰。但透傳的後果是亂碼靜默出現在畫面上，不觸發任何錯誤或 log，開發者在 unit test 環境完全不會察覺。&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>手寫測試資料 vs 錄製真實資料&lt;/strong>。如果測試資料是從真實 shell session 錄製的（capture 一次真實 zsh 啟動輸出），OSC 和 CSI private mode 會自然出現在測試輸入中，parser 的透傳行為會在 test 階段就被看到。&lt;/p>
&lt;/li>
&lt;/ol>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>從真實環境錄製測試資料&lt;/strong>：用 &lt;code>script&lt;/code> 命令或 WebSocket log 錄一次真實 shell session 的完整輸出，作為 integration test 的輸入。即使不改 parser 邏輯，至少能看到「哪些序列被透傳了」。&lt;/li>
&lt;li>&lt;strong>Parser 對未知序列記 warning log&lt;/strong>：透傳是合理的 fallback，但加一行 &lt;code>developer.log('Unknown escape: ${escape.codeUnits}')&lt;/code> 讓開發者知道有未處理的序列。&lt;/li>
&lt;li>&lt;strong>測試分兩類&lt;/strong>：「功能正確性」用手寫乾淨字串；「環境相容性」用錄製的真實輸出。兩類測試回答不同問題。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>想理解測試資料代表性 → &lt;a href="https://tarrragon.github.io/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">Test data 代表性&lt;/a>&lt;/li>
&lt;li>想建 protocol integration test 用真實 ttyd 輸出 → &lt;a href="https://tarrragon.github.io/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">模組三：協議整合測試&lt;/a>&lt;/li>
&lt;li>類似案例（mock 遮蔽） → &lt;a href="https://tarrragon.github.io/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type mock 遮蔽&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 unit test 的輸入資料品質如何決定測試的有效性。Parser 邏輯正確、斷言正確、覆蓋率高 — 但測試資料是人工挑選的乾淨子集，跟真實環境的輸入分佈不同。</p>
<h2 id="觀察">觀察</h2>
<p>app_tunnel 的 <code>AnsiParser</code> 負責解析終端機輸出的 ANSI escape 序列，轉換為帶色彩的文字 token。unit test 用手寫字串驗證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// 測試資料範例 — 乾淨的 SGR 色彩碼
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="n">test</span><span class="p">(</span><span class="s1">&#39;紅色文字&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="kd">final</span> <span class="n">tokens</span> <span class="o">=</span> <span class="n">parser</span><span class="p">.</span><span class="n">parse</span><span class="p">(</span><span class="s1">&#39;</span><span class="se">\x1B</span><span class="s1">[31mhello</span><span class="se">\x1B</span><span class="s1">[0m&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">tokens</span><span class="p">.</span><span class="n">first</span><span class="p">,</span> <span class="n">isA</span><span class="o">&lt;</span><span class="n">TextToken</span><span class="o">&gt;</span><span class="p">());</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">});</span></span></span></code></pre></div><p>真實 zsh prompt 啟動後送出的控制序列（擷取自實機 log）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">\x1B]0;user@host: ~\x07          ← OSC：設定終端機視窗標題
</span></span><span class="line"><span class="ln">2</span><span class="cl">\x1B[?2004h                      ← CSI private mode：啟用括號貼上模式
</span></span><span class="line"><span class="ln">3</span><span class="cl">\x1B[?1h                         ← CSI private mode：啟用應用程式游標鍵
</span></span><span class="line"><span class="ln">4</span><span class="cl">\x1B(B                           ← 字元集指定：選擇 ASCII
</span></span><span class="line"><span class="ln">5</span><span class="cl">\x1B[?25l                        ← CSI private mode：隱藏游標</span></span></code></pre></div><p>Parser 只認識 <code>\x1B[{數字;數字}{字母}</code> 格式的標準 CSI，其他全部殘留在輸出中。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>值</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>測試案例數</td>
          <td>18 個 AnsiParser test，全過</td>
      </tr>
      <tr>
          <td>測試覆蓋的序列類型</td>
          <td>SGR 色彩碼（<code>\x1B[31m</code> 等）</td>
      </tr>
      <tr>
          <td>真實環境的序列類型</td>
          <td>SGR + OSC + CSI private mode + 字元集指定 + 其他</td>
      </tr>
      <tr>
          <td>實機表現</td>
          <td>終端機畫面散佈 <code>]0;user@host</code> 等亂碼片段</td>
      </tr>
      <tr>
          <td>修復</td>
          <td>新增 3 個 RegExp 過濾 OSC / CSI private / 其他 escape</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<ol>
<li>
<p><strong>測試資料的代表性是隱性假設</strong>。18 個 test 的斷言都正確 — <code>\x1B[31m</code> 確實應該產生紅色 token。但「測試輸入能代表真實輸入」是一個未經驗證的假設。真實 zsh 的輸出包含 5+ 種 escape 序列類型，測試只覆蓋了 1 種。</p>
</li>
<li>
<p><strong>Parser 的行為對未知序列是「透傳」而非「報錯」</strong>。這是合理的設計 — 不認識的序列不應該讓 parser 崩潰。但透傳的後果是亂碼靜默出現在畫面上，不觸發任何錯誤或 log，開發者在 unit test 環境完全不會察覺。</p>
</li>
<li>
<p><strong>手寫測試資料 vs 錄製真實資料</strong>。如果測試資料是從真實 shell session 錄製的（capture 一次真實 zsh 啟動輸出），OSC 和 CSI private mode 會自然出現在測試輸入中，parser 的透傳行為會在 test 階段就被看到。</p>
</li>
</ol>
<h2 id="策略">策略</h2>
<ol>
<li><strong>從真實環境錄製測試資料</strong>：用 <code>script</code> 命令或 WebSocket log 錄一次真實 shell session 的完整輸出，作為 integration test 的輸入。即使不改 parser 邏輯，至少能看到「哪些序列被透傳了」。</li>
<li><strong>Parser 對未知序列記 warning log</strong>：透傳是合理的 fallback，但加一行 <code>developer.log('Unknown escape: ${escape.codeUnits}')</code> 讓開發者知道有未處理的序列。</li>
<li><strong>測試分兩類</strong>：「功能正確性」用手寫乾淨字串；「環境相容性」用錄製的真實輸出。兩類測試回答不同問題。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想理解測試資料代表性 → <a href="/blog/testing/05-test-design-judgment/test-data-representativeness/" data-link-title="Test data 代表性" data-link-desc="手寫 vs 錄製 vs 生成三種測試資料來源 — 測試資料的代表性是一個隱性假設，決定了 test 能發現什麼問題">Test data 代表性</a></li>
<li>想建 protocol integration test 用真實 ttyd 輸出 → <a href="/blog/testing/03-protocol-integration-test/" data-link-title="模組三：協議整合測試" data-link-desc="對真實服務驗證 WebSocket / gRPC / HTTP 協議契約 — unit test 和 E2E test 之間的一層">模組三：協議整合測試</a></li>
<li>類似案例（mock 遮蔽） → <a href="/blog/testing/cases/ws-text-binary-frame-mock-blindspot/" data-link-title="T.C1 WebSocket text/binary frame 被 FakeWebSocketChannel 遮蔽" data-link-desc="Flutter app 用 Uint8List 發送 WS 資料走 binary frame，ttyd 期望 text frame 靜默忽略 — FakeWebSocketChannel 的 sink.add 接受 dynamic 不區分 frame type，192 個 test 全過但實機無回應">T.C1 WS frame type mock 遮蔽</a></li>
</ul>
]]></content:encoded></item></channel></rss>