[翻譯] How SQLite Is Tested

作者: adxis (Acquire higher)   2013-03-29 22:39:33
慶賀開版,嘗試翻譯一篇許願文,有建議或修改的話
歡迎直接在 github 上發 pull request 給我 :-)
https://github.com/yangacer/translation/blob/master/sqlite-test.md
====
SQLite 是如何測試的
1.0 介紹
通透且謹慎的測試,是SQLite 的可靠性與強固性的一環。以 3.7.14 版來說,SQLite 函
式庫內含近 8.13 萬行的原始碼(不含空白與註解). 相較來說,這個專案對應的測試碼與
指令稿高達 1124 倍─相當於 914.211 萬行的程式。
1.1 執行總論
- 三個各自開發的自動化測試框架
- 倚照佈署設定前提下,百分百的測試涵蓋度
- 近百萬個測試案例
- 記憶體不足測試
- 系統崩潰與跳電測試
- 模糊測試
- 邊界值測試
- 關閉最佳化測試
- 迴歸測試
- 崎異(malformed)資料庫測試
- 大量使用斷言 (assert) 與執行期檢驗
- Valgrind (堆積分析工具) 分析
- 有號數溢位檢驗
2.0 自動化測式框架
有三個獨立開發的自動化測式框架被使用於 SQLite 的核心函式庫。各自被分開來設計、
維護,與管理。
TCL 是最早期的測試;他附帶在 SQLite 的原碼樹之中,並使用跟 SQLite 一樣的公開授
權。他是在開發 SQLite 的過程中主要的測試工具。TCL 自動化測試框架是以 TCL 指令
語言寫成,大約由 2.51 萬行的 C 原碼構成 TCL 介面。這些測試指令碼分布在 711 個
檔案中,大小共約 10.0 MB。其中有 29,226 個不同的測試案例,不過許多參數化的測試
,會以不同參數進行多次的測試,因此一次完整的測試會執行約 3 百萬個不同的案例。
TH3 是一個有專利的框架,以 C 寫成,提供對 SQLite 核心函式庫百分百的測試涵蓋度(
與百分白的 MC/DC 測試涵蓋度)。此框架是為了嵌入式系統、特化平台,這些無法輕易獲
得 TCL 支援的環境,以及工作站所設計。TH3 只使用公開的 SQLite 介面。TH3 對
SQLite 加盟會員是免費的,同時也可透過授權方式供他人使用。TH3 由近 50.1 mb 或
66.69 萬行 C 程式碼實作朱 34,229 個不同的測試案例。然而 TH3 是重度參數化的,
因此,完全涵蓋的測試會執行約 80 萬個不同的測試實例。該提供 100% 分支測試涵蓋度
構成一個 TH3 測試套件的子集。一個發佈前的浸入式測試會進行數億個測試。額外的
TH3 資訊另外提供於此。
SQL Logic Test 或 SLT 框架用來對 SQLite 與數個其他 SQL 資料庫執行大量的 SQL 語
句,並驗證結果是否相同。SLT 目前將 SQLite 與 PostgreSQL, MySQL, Microsoft SQL
Server 與 Oracle 10g 進行比對。 SLT 執行逾 7 百萬個查詢,相當於 1.12 GB 的測資

在 SQLite 發佈前,在多個平台、多個編譯選項的狀況下,以上的所有測試都必須成功。
在提交程式碼到 SQLite 的原碼樹之前,開發者通常得通過一個快速的 TCL 測試,約
12.91 萬個測試案例。這個快速測試不包含異常、模糊與浸入式測試。快速測試足以捕
捉常見的錯誤,同時也只需要幾分鐘的時間而不是幾小時。
3.0 異常測試
異常測試設計用來驗證當事情出錯時行為是否正確。在功能正常的電腦上建立一個 SQL
資料庫引擎相對地容易。而在有問題的系統上建立一個資料庫;可以正常地回絕無效輸入
並維持運作就較為困難了。異常測試乃為後者而設計。
3.1 記憶體不足(Out-Of-Memory, OOM) 測試
跟所有的 SQL 資料庫相同,SQLite 使用大量的 malloc() (詳見關於SQLite 的動態記憶
體配置文件。在伺服器或工作站上,malloc() 實務上從不失敗,因此 OOM 錯誤的處理並
不特別重要。然而在嵌入式系統上,OOM 錯誤常見地驚人,而 SQLite 又常用於嵌入式系
統上。為此,SQLite 要能夠優雅地處理 OOM 錯誤是很重要的。
記憶體不足測試以模擬記憶體錯誤的方式完成。透過
sqlite3_config(SQLITE_CONFIG_MALLOC, ...) 介面 SQLite 允許用戶端以不同方案置
換 malloc() 的實作。TCL 及 TH3 框架都允許置入一個修改過的 malloc() ,讓記憶體
配置在一定數量後失敗。這個修改過的 malloc() 可以僅失敗一次後就恢復正常,又或者
不斷地配置失敗。OOM 測試以一個迴圈完成。當第一次佚代迴圈時,該修改過的
malloc() 會使第一次配置失敗,接著會進行一些 SQLite 操作並檢驗結果以確認
SQLite 正確處理 OOM 錯誤。之後便遞增該 malloc 中的計數器並重複迴圈。此迴圈會
持續到所有 SQLite 操作都沒有遇到 OOM 錯誤時。像這樣的測式會進行兩次,第一次讓
該 malloc() 只會失敗一次,第二次則讓 malloc() 連續失敗。
3.2 I/O 錯誤測試
I/O 錯誤測試旨在驗證 SQLite 能正常處理 I/O 操作的失敗。I/O 錯誤可能肇因於磁碟
滿載、磁碟硬體損壞、使用網路檔案系統時斷線、在 SQL運作中變更系統設定或權限,或
其他硬體與作業系統的異常。不論原因,重要的是SQLite 能夠正確處理這些錯誤,而
I/O 錯誤測試希冀能驗證這一點。
I/O 錯誤測試概念上與 MMO 測試類似:I/O 錯誤以模擬的方式產生,用來驗證 SQLite
能正確回應這些模擬的錯誤。I/O 錯誤在 TCL 與 TH3 框架中皆以插入虛擬檔案系統物件
被模擬,此物件會在一定數量的 I/O 操作後失敗。與 MMO 相同,I/O 錯誤模擬也能設定
為失敗一次或連續失敗。測試在迴圈中執行,漸進式地增加失敗直到測試案例能正常執行
完畢。迴圈會執行兩次,第一次讓 I/O 只錯誤一次,第二次則會連續失敗。
在 I/O 錯誤測試中,會在關閉錯誤模擬之後, PRAGMA integrity_check會被用來驗證資
料庫,以確保 I/O 錯誤並未損毀資料。
3.3 崩潰測試
崩潰測試目的在於展現 SQLite 即使面臨客戶端崩潰、系統崩潰或者是更新資料庫時斷電
等悲劇,都能避免資料庫損毀。另一篇名為SQLte 單步遞交的文件描述 SQLite 採取了哪
些防禦性措施,防止資料庫因崩潰而損毀。崩潰測試將驗證這些防禦工事是否正常運作。
當然,使用真正的斷電進行崩潰測試不太實際,所以這項測試也是以模擬的方式進行。透
過增添一個修改過的虛擬檔案系統 ,崩潰測試得以模擬崩潰後的資料庫檔案。
TCL 測試框架中,崩潰測試在另外的行程執行。主行程會產生一個子行程來執行某些
SQLite 操作並於寫入動作中隨機產生崩毀。一個特殊的虛擬檔案系統會隨機重排並損毀
未同步的寫入動作,此舉意在模擬有緩衝機制的檔案系統發生崩潰時連帶產生的影響。該
子行程結束後,主行程會讀取同一個資料庫,並檢驗子行程嘗試進行的修改要麼是成功完
成,要麼是被完整的回溯。PRAGMA integrity_check會被用來驗證資料庫,以確保資料完
整性。
3.4 綜合失敗測試
這個測試套件會檢視多種以上提到的失敗一起發生時造成的結果。舉例來說,測試會被用
來確認一個 I/O 錯誤或 MMO 錯誤發生在回復崩潰後的資料庫時,SQLite 的行為是否正
常。
4.0 模糊測試
模糊測試旨在建立 SQLite 對無效、越界、或崎異的輸入正確回應的能力。
4.1 模糊 SQL
SQL 模糊測試會把文法正確,但廣泛來說沒有意義的 SQL 語句輸入 SQLite 來觀察它會
如何應對。通常會傳回某些錯誤(像是“沒有這個表“)。某些時候,這些 SQL 語句偶
然是語意正確的,這種狀況下預先準備好的語句會被執行,以確認得到合理的結果。
模糊 SQL 產生器是 TCL 測試套件的一部分。在完整的測試執行過程中,將近 12.63 萬
模糊 SQL 語句會被產生並被測試。
4.2 崎異資料庫檔案
無數個測試案例用來驗證 SQLite 處理崎異資料庫檔案的能力。這些測試首先建立一個正
常的資料庫檔案,接著藉由改變一些檔案中的位元組來產生一些扭曲,再以 SQLite 讀取
這個檔案。在某些狀況,被改變的位元組可能在欄位資料中間,這會讓資料庫檔案的內容
發生改變,但不影響資料庫檔案本身的格式正確:在其他狀況,未被使用的位元組被修改
則不會影響資料庫的完整性。有趣的情況是修改到定義資料庫結構的資料時。崎異資料庫
測試會驗證 SQLite 能找到這些檔案格式的錯誤並以 SQLITE_CORRUPT 錯誤碼回報給客戶
端,而不繪影發任何緩衝溢位、對空指標解參照或執行其他不當行為。
4.3 邊界值測試
SQLite 定義了 一些操作的限制,像是資料表的行數上限、SQL 語句長度上限或是整數的
上限。TCL 與 TH3 套件都含有許多測試能將 SQLite 塞滿到這些定義內的上限值並驗證
他能正確地處理所有允許的值。額外的測試會以超過定義上限的方式來驗證 SQLite 會正
常回報錯誤。原始碼中含有測試案例巨集來驗證邊界值得兩端都能被測試。
5.0 迴歸測試
當一個臭蟲被回報時,直到新的測試案例加入 TCL 測試套件,並在一個未修改的
SQLite 版本重現該臭蟲,此臭蟲才算是被修正。數年來,這導致數百萬個新的測試案例
被加入 TCL 測試套件。這些迴歸能保證被修復的臭蟲不會出現在未來的 SQLite 版本。
6.0 自動資源洩漏偵測
資源洩漏會在系統資源被配置了卻從未被釋放時發生,許多應用程式理,最惱人的資源洩
漏莫過於記憶體洩漏─以 malloc() 配置記憶體後未以 free() 釋放。不過其他種類的資
源也可能洩漏,像是檔案記述子、執行緒、互斥器等。
TCL 與 TH3 在每次進行測試時會自動追蹤系統資源並回報資源洩漏,不需要特殊的設定
。這些測試框架對記憶體洩漏尤其關注;如果一個更動引起了記憶體洩漏,測試框架會快
速地發現這個狀況。SQLite 被設計為從不洩漏記憶體,即使發生了 OOM 錯誤或磁碟錯誤
這樣的例外情境─測試框架勤於強化這個特點。
7.0 測試涵蓋度
在 2009-07-25 版本的 TH3,使用預設設定在 SuSE Linux 10.1 的 x86 平台上以 GCC
4.0.1 編譯產生的SQLite ,以 gcov 檢測,分支測試涵蓋度達到 100%。
7.1 語句 v.s. 分支涵蓋度
有許多方法測量測試的涵蓋度。最普遍的方法是"語句涵蓋度"。當你聽到有人說他們的程
式有多少百分比的涵蓋度而缺少進一步的解說時,他門通常指的是與具涵蓋度。語句涵蓋
度量測的是程式碼中至少被測試一次的百分比。
分支涵蓋度就嚴苛多了,他會量測機器碼中的分支指令,其兩條支線是否至少被執行過一
次。考慮以下 C 程式碼:
if( a>b && c!=25 ){ d++; }
這樣的一行可能會產生一打機器語言。〈譯:以分支涵蓋度來看〉如果其中的任一個指令
都被運算過,我們才能說這個語句被測試過。那麼舉例來說,可能這個條件句總是為假,
而 d 變數從未被遞增。這時,語句涵蓋度仍會將這一行程式碼列入已測試的計算。
分支涵蓋度是更嚴謹的,有了它,每個測試與語句的子區段都會被獨立考慮。為了達到
100% 的分支涵蓋度,上面的範例必須要有至少三個測試案例:
a <= b
a > b && c == 25
a> b && c!= 25
以上任何一個測試案例都能提供 100% 的語句涵蓋度,但是分支涵蓋度則是三個都需要。
一般來說,100% 分支涵蓋度可引申為 100% 的語句涵蓋度,但反之則不然。再強調一次
,TH3 測式框架有 100% 分支涵蓋度,我超強˙。
7.2 防衛性程式碼的涵蓋度測試
寫作良好的 C 程式通常含有一些防衛性的測試,測試結果實務上是永遠為真或為假。這
會導致一個程式設計的兩難:有人為了 100% 的分支測試移除防衛性測試嗎?
在 SQLite ,這個答案是否定的。為了測試需求,SQLite 定義了 ALWAYS() 與 NEVER 兩
個巨集。ALWAYS() 巨集包裹預期為真的條件而 NEVER() 則相反。這兩個巨集可視為防衛
性測試的註解。以標準建置的環境來說,這些巨集是被略過的:
#define ALWAYS(X) (X)
#define NEVER(X) (X)
不過在大多數測試,如果參數與預期結果不同,這兩個巨集會丟出一個斷言失敗。這會快
速地警告開發者使用了不正確的設計假設。
#define ALWAYS(X) ((X)?1:assert(0),))
#define NEVER(X) ((X)?assert(0), 1:0)
當量測涵蓋度時,這兩個巨集會被定義為常數,因此不會產生分支指令,也就不會影響分
支涵蓋度的計算。
#define ALWAYS(X) (1)
#define NEVER(X) (0)
此測試套件被設計為執行三次,每次使用一種上面列出的巨集定義。三次執行都應該產出
完全一樣的結果。執行期可透過 sqlite3_test_control(SQLITE_TESTCTRL_ALWAYS,
...) 介面來驗證巨集被正確設定為佈署用的設定(略過斷言)。
7.3 強制涵蓋邊界值與布林向量(布林遮罩)測試
另一個巨集,與涵蓋量測共同使用的是 testcase() 巨集。參數是我們希望真、假兩種結
果都被評估的的條件句。在非涵蓋型建置時〈發佈型建置〉,此巨集為無作用操作。
不過在涵蓋度量測模式時,此巨集會運算它的參數。然後在分析階段檢查對真假兩種狀態
都有進行測試。舉例來說 testcase() 會被用來驗證邊界值已被測試:
testcase( a == b );
testcase( a == b+1);
if( a>b && c!=25 ){ d++; }
該巨集也會被用在有多個條件落入相同區段的 switch 語句,來確認所有條件都有被(測
試)執行過:
switch( op ){
case OP_Add:
case OP_Subtract: {
testcase( op == OP_Add );
testcase( op == OP_Substract );
break;
}
}
對位元遮罩的測試,testcase() 巨集可驗證所有遮罩中的位元都有影響到測試。例如下
面的程式碼,若遮罩中 MAIN_DB 或 TEMP_DB 任一位元被打開,則條件為真,前置的
testcase() 巨集會驗證兩種狀況都被測試過:
testcase( mask & SQLITE_OPEN_MAIN_DB );
testcase( mask & SQLITE_OPTN_TEMP_DB);
if( (mask & (SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_TEMP_DB)) !=0 )
{ ... }
SQLite 的原始碼中使用了 667 個 testcase() 巨集。
7.5 關於完全涵蓋測試的經驗
SQLite 的開發者發現完全涵蓋測試是個極具生產力的方式,它能避免在系統進化時引入
新的臭蟲。由於每一個分支指令都被測試實例涵蓋,開發者能自信其修改不會讓其他程式
碼發生預期外的影響。沒有這個保險措施,SQLite 的品質將難以維護。
8.0 動態分析
動態分析意指 SQLite 執行期的內部與外部檢驗。這項分析已證明對 SQLite 品質的維護
有很大幫助。
8.1 斷言
SQLite 核心包含 3531 個 assert() 語句,用以驗證函式的前置條件、後置條件與迴圈
的不變性。assert() 是一個 ANSI-C 標準中的巨集。其參數為一個預期為真的布林值。
若這個斷言失敗,程式會印出錯誤訊息並終止。
此巨集在定義了 NDBUG 巨集的編譯狀況下會被忽略。在大部分系統,斷言預設是開啟的
。不過在 SQLite,斷言數量極多且在校能瓶頸區段,造成資料庫引擎在斷言開啟的狀況
下速度降低了三倍。因此,預設的〈產品級〉SQLite 建置會關閉斷言。斷言語句只有在
SQLite 建置過程中,SQLITE_DEBUG 預處理巨集被定義的狀況下才會開啟。
8.2 Valgrind
Valgrind 可能是世上最令人驚艷、最有用的開發者工具。Valgrind 是一個模擬器─模擬
一個執行 Linux 的 x86 〈移植 Valgrind 到不同系統的開發正在進行,然而在寫這篇文
章的當下,只有 Linux 上的 Valgrind 是可靠的,對 SQLite 的開發者來說,這代表
Linux 會是一個偏好的軟體開發平台〉。當 Valgrind 執行時 ,它會監看所有有趣的錯
誤,像是陣列逾界存取、讀取未初始化的記憶體、堆疊溢位、記憶體洩漏等。Valgrind
能找出逃過其他 SQLite 測試的錯誤,而且,當 Valgrind 發現錯誤時,它能將確切的出
錯位置傾印至一個符號除錯器,得以快速進行修復。
由於它是一個模擬器,在 Valgrind 中執行的速度會較原生硬體上〈應用程式跑在工作站
上的 Valgrind 時,速度大約等於在智慧型手機原生執行的速度〉。因此用 Valgrind 來
執行所有 SQLite 的測試是不切實際的。然而,在每一次發佈新版前,快速測試與 TH3
的涵蓋度測試會在 Valgrind 上進行。
8.3 Memsys2
SQLite 內建一個可插拔的記憶體配置子系統。預設的實作使用系統函式 malloc() 與
free()。然而,若 SQLite 以 SQLITE_MEMDEBUG 選項編譯時,一個替代的記憶體配置包
裝器(memsys2)會被用來監看執行期的記憶體配置錯誤。memsys2 包裝器除了檢查記憶
體洩漏外也會追蹤緩衝區逾界存取、存取未初始化的記憶體,
以及操作已被釋放的記憶體。同樣的測試也會以 Valgrind 進行(而的確, Valgrind 做
得比較好),不過 memsys2 擁有速度較快的優勢,這代表可以常常進行檢驗或執行較久
的測試。
8.4 互斥器斷言
SQLite 內建一個可插拔的互斥器子系統。依編譯選項,預設的互制器系統有兩個介面
sqlite3_mutex_held()與 sqlite3_mutex_notheld()用於偵測某個互斥器是否被呼叫的
執行序所持有。在 SQLite 中,為了雙重檢驗在多序環境運作的正確性,這兩個介面被大
量地與 assert() 一起使用,藉以驗證互斥器在對的時機被持有與釋放。
8.5 旅記測試
SQLite 會在開始變更資料庫前,將所有修改預先寫入一個回溯旅記,藉此確認跨越系統
崩潰與斷電的異動(transaction)是單步的。TCL 測試框架含有一個作業系統後端實作
,可輔助驗證這項功能是否被正確觸發。此旅記測試虛擬檔案系統監視所有資料庫檔與回
溯旅記之間的磁碟 I/O 流程,確認在資料寫入回溯旅記之前,沒有任何資料被寫入到資
料庫檔案。若任何違背以上原則的狀況發生,會發出一個斷言失敗。
旅記測試是在崩潰測試之上額外的雙重檢驗,可確認 SQLite 的異動能單步執行,即使中
間發生了系統崩潰與斷電。
8.6 有號整數溢位檢驗
C 語言標準對有號整數溢位後的行為未有定義,換句話說,當你對一個有號整數做加法運
算而運算結果超過了該整數的存放空間,這個數值不見得像多數程式設計者認為的那樣,
被處理為負數。這只是可能的現象之一,但完全不同的情況也可能發生,可參考這裡與這
裡。即使是同一個編譯器也會因程式碼的位置不同或最佳化的選項不同而對溢位採用不同
的處理方式。
SQLite 從不溢位一個有號數。為了驗證這一點,測試套件至少會執行一次以 GCC 的
-ftrapv 參數編成的執行檔。該參數讓 GCC 遇到有號數溢位時自動呼叫 panic() 。此
外,有許多測試會嘗試造成有號數溢位,像是 "SELECT -1*(-9223372036854775808);"。
9.0 關閉最佳化測試
sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS,...) 允許 SQL 語句最佳化在
執行其關閉。不管最佳化開啟與否,SQLite 的回應應該都是一致的;最佳化只是讓回敬
速度較快。因此在產品使用上,預設是開啟最佳化設定的。
為了驗證最佳化不會帶來臭蟲,SQLite 的測試包含兩次測試,一次在開啟最佳化的情況
下,一次則在關的情況下。
並非所有測試都能這麼搞。有些測試本身是為了驗證最佳化的有效性,比如藉由磁碟操作
計數降低運算量、排序操作、全掃描,或者其他查詢中的處理。這些測試在關閉最佳化的
狀況下當然會失敗。不過主要的測試項目在於驗證回應是否正確,這些項目就與最佳化無
關了。
10.0 靜態分析
靜態分析指的是在編譯中或編譯後檢查正確性。靜態檢查包括編譯器警告訊息以及用分析
引擎,像是 Clang Static Analyzer 深入檢查。SQLite 以 GCC/Clang -Wall -Wextra
在 Linux, Mac, Windows 上的 MSVC 編譯時,是完全沒有警告訊息的。以 Clang 的
Clang Static Analyzer 工具 scan-build 也沒有任何警告。不用說,可能其他的靜態
分析工具會提出警告。我們鼓勵使用者不要對這些警告太過緊張,可以用本文提到的測試
安慰一下自己。
靜態分析還沒被證明對除錯很有用。他是有找到一些臭蟲,但這些算是例外;更多臭蟲的
產生是由於嘗試移除靜態分析提出警告。
11.0 總結
SQLite 是開放原碼專案。這讓很多人覺得他並未像商用軟體般被良好測試,因而可能不
太可靠。然而這是個錯誤印象。SQLite 在這個領域呈現高度的可靠性與稀少的損毀率,
尤其考慮到他是這麼頻繁的被使用。品質的達成一部分是由謹慎的程式碼設計與實作;然
而全面性的測試,在 SQLite 的維護與改進中也佔了重要的一席之地。這份文件總結了每
次 SQLite 發布前的測試的過程,冀望讀者能理解 SQLite 能適用於嚴謹的應用程式。
作者: PsMonkey (痞子軍團團長)   2013-03-29 23:01:00
好強大... 有神快拜阿..... Orz

Links booklink

Contact Us: admin [ a t ] ucptt.com