小巧。快速。可靠。
任選三項。
SQLite 中 assert() 的使用

1. SQLite 中的 Assert() 及類似巨集

assert(X) 巨集是 C 標準的一部分,在 <assert.h> 標頭檔中。SQLite 新增了三個其他類似的 assert() 巨集,分別稱為 NEVER(X)、ALWAYS(X) 和 testcase(X)。

SQLite 版本 3.22.0 (2018-01-22) 包含 5290 個 assert() 巨集、839 個 testcase() 巨集、88 個 ALWAYS() 巨集和 63 個 NEVER() 巨集。

1.1. assert() 的理念

在 SQLite 中,assert(X) 的存在表示開發人員有證據證明 X 永遠為真。讀者可以依賴 X 為真來幫助他們推論程式碼。assert(X) 是關於 X 為真的強烈聲明。毫無疑問。

ALWAYS(X) 和 NEVER(X) 巨集是關於 X 為真的較弱聲明。ALWAYS(X) 或 NEVER(X) 的存在表示開發人員相信 X 永遠或從不為真,但沒有證據,或證據複雜且容易出錯,或證據取決於系統中其他看似可能變更的方面。

其他系統有時會以類似於 SQLite 中使用 ALWAYS(X) 或 NEVER(X) 的方式使用 assert(X)。開發人員會新增 assert(X) 作為 默認承認他們並不完全相信 X 永遠為真。我們認為這種使用 assert(X) 的方式是錯誤的,違反了 C 中提供 assert(X) 的初衷和目的。assert(X) 不應視為用於防止錯誤的安全網或頂繩。assert(X) 也不適合深度防禦。在這些情況下應使用 ALWAYS(X) 或 NEVER(X) 巨集,或類似巨集,因為 ALWAYS(X) 或 NEVER(X) 後面會接續實際處理問題的程式碼,當程式設計師的推論結果錯誤時。由於接續 ALWAYS(X) 或 NEVER(X) 的程式碼未經測試,因此應為非常簡單的程式碼,例如「return」陳述式,可透過檢查輕鬆驗證。

由於 assert() 經常被誤用,一些程式語言理論家和設計者對它持負面看法。例如,Go 程式語言 的設計者故意 省略內建 assert()。他們認為 assert() 誤用所造成的危害大於將其納入語言內建的優點。SQLite 開發者不同意。事實上,本文的原始目的是反駁 assert() 有害的普遍觀念。根據我們的經驗,沒有 assert(),SQLite 的開發、測試和維護將困難得多。

1.2. 根據建置類型而有不同的行為

使用三個不同的建置來驗證 SQLite 軟體。

  1. 功能測試建置用於驗證原始碼。
  2. 覆蓋率測試建置用於驗證測試組,以確認測試組提供 100% 的 MC/DC。
  3. 發行版建置用於驗證產生的機器碼。

所有測試在三個建置中都必須給出相同的答案。有關更多詳細資訊,請參閱 "如何測試 SQLite" 文件。

各種類似 assert() 的巨集會根據 SQLite 的建置方式而有不同的行為。

功能測試覆蓋率測試發行版
assert(X) 如果 X 為 false,則中止 無動作 無動作
ALWAYS(X) 如果 X 為 false,則中止 總是為真 傳遞值 X
NEVER(X) 如果 X 為真,則中止 總是為假 傳遞值 X
testcase(X) 無動作 如果 X 為真,則執行一些無害的工作 無動作

標準 C 中 assert(X) 的預設行為是針對釋出版本啟用。這是合理的預設值。然而,SQLite 程式碼庫在效能敏感的程式碼區域中有很多 assert() 陳述式。讓 assert(X) 保持開啟會導致 SQLite 執行速度變慢約三倍。此外,SQLite 致力於在交付的組態中提供 100% MC/DC,如果啟用 assert(X) 陳述式,這顯然是不可能的。基於這些原因,assert(X) 對於 SQLite 中的釋出版本是不執行任何操作的。

在功能測試期間,ALWAYS(X) 和 NEVER(X) 巨集的行為就像 assert(X),因為開發人員希望在 X 的值與預期不同時立即收到問題警示。但是對於交付,ALWAYS(X) 和 NEVER(X) 是簡單的直通巨集,提供深度防禦。對於覆蓋率測試,ALWAYS(X) 和 NEVER(X) 是硬編碼的布林值,因此它們不會導致無法到達的機器碼產生。

testcase(X) 巨集通常是不執行任何操作的,但對於覆蓋率測試版本,它會產生少量額外的程式碼,其中包含至少一個分支,以驗證存在 X 為 true 和 false 的測試案例。

2. 範例

assert() 陳述式通常用於驗證內部函數和方法的先決條件。範例:https://sqlite.dev.org.tw/src/artifact/c1e97e4c6f?ln=1048。這被認為比單純在標頭註解中陳述先決條件更好,因為 assert() 實際上會執行。在像 SQLite 這樣經過高度測試的程式中,讀者知道先決條件對執行於 SQLite 的數億個測試案例而言皆為真,因為它已由 assert() 驗證過。相反地,標頭註解中的文字先決條件陳述式並未經過測試。它在撰寫程式碼時可能是真的,但誰能保證它現在仍然是真的?

SQLite 有時會使用編譯時期可評估的 assert() 陳述式。考量 https://sqlite.dev.org.tw/src/artifact/c1e97e4c6f?ln=2130-2138 的程式碼。四個 assert() 陳述式驗證編譯時期常數的值,以便讀者能夠快速檢查後續的 if 陳述式的有效性,而無需在個別標頭檔案中查詢常數值。

有時會使用編譯時期 assert() 陳述式來驗證 SQLite 已正確編譯。例如,https://sqlite.dev.org.tw/src/artifact/c1e97e4c6f?ln=157 的程式碼驗證 SQLITE_PTRSIZE 預處理器巨集是否已針對目標架構正確設定。

CORRUPT_DB 巨集用於許多 assert() 陳述中。在功能測試建置中,CORRUPT_DB 參照一個全域變數,如果資料庫檔案可能包含損毀,則該變數為 true。此變數預設為 true,因為我們通常不知道資料庫是否損毀,但測試時處理已知格式良好的資料庫,該全域變數可以設定為 false。然後,CORRUPT_DB 巨集可以用於 assert() 陳述中,例如在 https://sqlite.dev.org.tw/src/artifact/18a53540aa3?ln=1679-1680 中看到的。這些 assert() 指定常規資料庫檔案為 true 的常式,但如果資料庫檔案損毀,則可能為 false。了解這些類型的條件對於嘗試獨立了解程式碼區塊的讀者非常有幫助。

ALWAYS(X) 和 NEVER(X) 函數用於我們始終希望測試發生的位置,即使開發人員認為 X 的值始終為 true 或 false。例如,顯示的 sqlite3BtreeCloseCursor() 常式必須從所有游標的連結清單中移除關閉的游標。我們知道游標在清單中,因此迴圈必須以「break」陳述終止,但使用 https://sqlite.dev.org.tw/src/artifact/18a53540aa3?ln=4371 中的 ALWAYS(X) 測試很方便,以防止在程式碼其他部分有錯誤損毀連結清單時執行連結清單的結尾。

ALWAYS(X) 或 NEVER(X) 有時候會驗證預設條件,而這些條件會在程式碼其他部分以微妙的方式修改時受到變更。在 https://sqlite.dev.org.tw/src/artifact/18a53540aa3?ln=5512-5516 中,我們有一個測試,針對兩個預設條件進行測試,而這兩個預設條件僅因為 sqlite3BtreeRowCountEst() 函式的使用範圍有限而為真。SQLite 的未來增強功能可能會以新的方式使用 sqlite3BtreeRowCountEst(),而這些預設條件不再成立,而且 NEVER() 巨集會在情況發生時快速提醒開發人員。但是,如果出於某種原因,預設條件在發行版本中不成立,程式仍然會正常運作,而且不會執行未定義的記憶體存取。

testcase() 巨集通常用於驗證不等式比較邊界案例已受檢查。例如,在 https://sqlite.dev.org.tw/src/artifact/18a53540aa3?ln=5766。這類檢查有助於防止差一錯誤。

此頁面最後修改於 2022-01-08 05:02:57 UTC