[翻译] 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