How SQLite Is Tested
How SQLite Is Tested
1.介绍
1.1.执行摘要
2.测试线束
3.异常测试
3.1.内存不足测试
3.2.I / O错误测试
3.3.碰撞测试
3.4.复合失效测试
4.模糊测试
4.1.SQL Fuzz
4.1.1.SQL模糊使用美国模糊Lop模糊器
4.1.2.Google OSS Fuzz
4.2.畸形数据库文件
4.3.边界值测试
5.回归测试
6.自动资源泄漏检测
7.测试覆盖率
7.1.声明与分行覆盖率
7.2.防御性代码的覆盖测试
7.3.强制覆盖边界值和布尔向量测试
7.4.分支机构覆盖率与MC / DC相比
7.5.测量分支机构覆盖
7.6.突变检测
7.7.全面测试覆盖的经验
8.动态分析
8.1.断言
8.2.Valgrind的
8.3.Memsys2
8.4.互斥体声明
8.5.期刊测试
8.6.未定义的行为检查
9.禁用优化测试
10.检查清单
11.静态分析
12.总结
1.介绍
SQLite的可靠性和健壮性部分通过彻底和仔细的测试来实现。
从版本3.20.0(2017-08-01)开始,SQLite库由大约125.4个C代码的KSLOC组成。(KSLOC意味着成千上万的“代码源代码行”,换言之,代码行不包括空白行和注释)。相比之下,该项目的测试代码和测试脚本的数量是730多倍 - 91616.0 KSLOC。
1.1.执行摘要
- 三个独立开发的测试线束
- 部署配置中的100%分支测试覆盖率
- 数以百万计的测试案例
- 内存不足测试
- I / O错误测试
- 崩溃和功率损耗测试
- 模糊测试
- 边界值测试
- 禁用优化测试
- 回归测试
- 畸形的数据库测试
- 广泛使用assert()和运行时检查
- Valgrind分析
- 未定义的行为检查
- 清单
2.测试线束
有三个独立的测试工具用于测试核心SQLite库。每个测试工具都是相互独立地设计,维护和管理的。
- 在
TCL测试
是最古老的一套SQLite的测试。它们与SQLite核心包含在同一个源代码树中,并且像SQLite核心在公共领域。TCL测试
是开发过程中使用的主要测试。TCL测试
是使用TCL脚本语言编写的。TCL测试
工具本身由用于创建TCL接口的C代码的25.3 KSLOC组成。测试脚本包含在总共13.3MB大小的1135个文件中。有39747个不同的测试用例,但许多测试用例都被参数化并运行多次(使用不同的参数),以便在完整测试中运行数百万个单独的测试。
- 所述
TH3
测试工具是一组专有的测试中,用C语言编写的是提供100%的分支测试覆盖率(和100%MC / DC测试覆盖)到核心SQLite库。TH3
测试旨在运行在不易支持TCL或其他工作站服务的嵌入式和专用平台上。TH3
测试只使用已发布的SQLite接口。TH3
包含大约57.3 MB或782.3个实现42213个不同测试用例的C代码的KSLOC。不过,TH3
测试的参数很多,因此全覆盖测试运行了大约170万个不同的测试实例。提供100%分支测试覆盖率的案例构成TH3
全套测试套件的一个子集。在发布之前进行浸泡测试会进行数亿次测试。有关TH3
的更多信息可单独提供。
- 在
SQL逻辑测试
或SLT测试工具是用来运行对阵双方SQLite和其他几个SQL数据库引擎的SQL语句的数量巨大,并确认他们都得到同样的答案。SLT目前将SQLite与PostgreSQL,MySQL,Microsoft SQL Server和Oracle 10g进行比较。SLT运行720万个包含1.12GB测试数据的查询。
除了三个主要的测试工具之外,还有其他几个实施专门测试的小程序。
- “speedtest1.c”程序估计一个典型工作负载下SQLite的性能。
- “mptester.c”程序是对多个进程同时读取和写入单个数据库的压力测试。
- “threadtest3.c”程序是同时使用SQLite对多个线程进行压力测试的。
- “fuzzershell.c”程序用于运行一些模糊测试。
上述所有测试必须在每个版本的SQLite之前,在多个平台上和多个编译时配置下成功运行。
在每次签入SQLite源代码树之前,开发人员通常运行Tcl测试的子集(称为“veryquick”),其中包含大约14.26万个测试用例。非常快的测试包括除异常,模糊和浸泡测试以外的大多数测试。非常快的测试背后的想法是,他们足以捕捉大多数错误,但也只需几分钟而不是几个小时。
3.异常测试
异常测试是旨在验证SQLite在出现问题时的正确行为的测试。建立一个SQL数据库引擎(相对而言)是很容易的,该引擎在全功能计算机上的格式良好的输入上行为正确。建立一个对无效输入作出明确回应并在系统故障后继续运行的系统会更加困难。异常测试旨在验证后者的行为。
3.1.内存不足测试
与所有SQL数据库引擎一样,SQLite广泛使用malloc()(有关更多详细信息,请参阅SQLite中动态内存分配的单独报告。)在服务器和工作站上,malloc()在实践中永远不会失败,因此正确处理out-内存(OOM)错误并不特别重要。但在嵌入式设备上,OOM错误非常普遍,而且由于SQLite经常用于嵌入式设备,所以SQLite能够正常处理OOM错误是非常重要的。
OOM测试是通过模拟OOM错误来完成的。SQLite允许应用程序使用sqlite3_config(SQLITE_CONFIG_MALLOC,...)接口替代另一个malloc()实现。TCL和TH3测试工具都能够插入修改后的malloc()版本,在经过一定次数的分配后,可能会被操纵失败。这些instrumented malloc可以设置为仅失败一次,然后再次开始工作,或者在第一次失败后继续失败。OOM测试是循环完成的。在循环的第一次迭代中,被检测的malloc在第一次分配时被操纵失败。然后执行一些SQLite操作并执行检查以确保SQLite正确处理了OOM错误。然后,仪器化的malloc上的时间失败计数器将增加1,并重复测试。循环继续,直到整个操作运行完成,而不会遇到模拟的OOM故障。像这样的测试运行两次,一次使用仪器化的malloc设置为仅失败一次,并且再次将仪器化的malloc设置为在第一次失败后连续失败。
3.2.I / O错误测试
I / O错误测试试图验证SQLite对失败的I / O操作做出明智的响应。I / O错误可能是由磁盘驱动器,磁盘硬件故障,使用网络文件系统时的网络中断,SQL操作中发生的系统配置或权限更改,或其他硬件或操作系统故障导致的。无论是什么原因,重要的是SQLite能够正确地响应这些错误,并且I / O错误测试试图证实它的确存在。
I / O错误测试在概念上与OOM测试类似; 模拟I / O错误并进行检查以验证SQLite是否正确响应模拟错误。通过插入一个新的虚拟文件系统对象来模拟TCL和TH3测试线束中的I / O错误,该虚拟文件系统对象在经过一定数量的I / O操作后专门用于模拟I / O错误。与OOM错误测试一样,I / O错误模拟器可以设置为仅失败一次,或者在第一次失败后连续失败。测试循环运行,慢慢增加故障点直到测试用例运行完成而没有错误。循环运行两次,一次I / O错误模拟器设置为仅模拟一次故障,第二次设置为第一次故障后所有I / O操作失败。
在I / O错误测试中,在禁用I / O错误模拟失败机制后,使用PRAGMA integrity_check检查数据库以确保I / O错误未引入数据库损坏。
3.3.碰撞测试
碰撞测试试图证明,如果应用程序或操作系统崩溃或数据库更新过程中出现电源故障,SQLite数据库不会损坏。标题为SQLite中的Atomic Commit的单独白皮书描述了SQLite为防止崩溃后数据库损坏而采取的防御措施。碰撞测试努力验证这些防御措施是否正常工作。
当然,使用真正的电源故障进行碰撞测试是不切实际的,因此碰撞测试在模拟中完成。插入备用虚拟文件系统,允许测试工具模拟崩溃后数据库文件的状态。
在TCL测试工具中,碰撞模拟是在一个单独的过程中完成的。主要的测试过程产生一个子进程,它运行一些SQLite操作,并在写操作过程中的某个地方随机崩溃。一个特殊的VFS会随机地重新排序和破坏未同步的写操作来模拟缓冲文件系统的效果。孩子死后,原始测试过程打开并读取测试数据库,并验证孩子尝试的更改是成功完成还是完全回滚。integrity_check PRAGMA用于确保不发生数据库损坏。
TH3测试工具需要在嵌入式系统上运行,这些嵌入式系统不一定具备生成子进程的能力,因此它使用内存中的VFS来模拟崩溃。内存中的VFS可以在一定数量的I / O操作之后进行操作以创建整个文件系统的快照。碰撞测试循环运行。在循环的每次迭代中,快照的创建点都会被提前,直到被测试的SQLite操作运行完成而没有创建快照。在循环中,在测试完成后,SQLite操作完成后,文件系统将恢复为快照,并引入随机文件损坏,这是故障发生后损坏类型的特征。然后打开数据库并进行检查,以确保数据库格式正确,并且事务运行完成或完全回滚。循环的内部对每次快照重复多次,每次都有不同的随机损坏。
3.4.复合失效测试
SQLite的测试套件还探讨堆叠多个故障的结果。例如,运行测试以确保在尝试从先前崩溃中恢复时发生I / O错误或OOM错误时的正确行为。
4.模糊测试
模糊测试旨在确定SQLite对无效,超出范围或格式错误的输入进行正确响应。
4.1.SQL Fuzz
SQL模糊测试包括创建语法正确但狂妄无常的SQL语句,并将它们提供给SQLite以查看它将如何处理它们。通常会返回某种错误(例如“没有这样的表”)。有时候,纯粹偶然的情况下,SQL语句也恰好在语义上是正确的。在这种情况下,运行结果准备好的声明以确保它给出合理的结果。
SQL fuzz生成器测试是TCL测试套件的一部分。在完整的测试运行期间,会生成和测试大约11.33万条模糊SQL语句。
4.1.1。SQL模糊使用美国模糊Lop模糊器
在美国的模糊罗布泊或“AFL”的fuzzer是米哈尔扎莱夫斯基最近(约2014)的创新。与大多数盲目生成随机输入的其他模糊器不同,AFL模糊器可以测试正在测试的程序(通过修改C编译器的汇编语言输出),并使用该工具检测输入何时导致程序执行不同的操作 - 一个新的控制路径或循环不同的次数。引发新行为的输入被保留并进一步发生变异。通过这种方式,AFL能够“发现”被测程序的新行为,包括设计师从未设想过的行为。
AFL已经证明非常善于在SQLite中发现神秘的bug。大多数调查结果都声称()在有条件的情况下,在模糊的情况下是错误的。但是,AFL在SQLite中也发现了相当多的崩溃错误,甚至在SQLite计算出错误结果的情况下也是如此。
由于其过去的成功,AFL成为了版本3.8.10(2015-05-07)开始的SQLite测试策略的标准部分。SQL语句和数据库文件都模糊不清。已经尝试了数十亿和数十亿的突变,但AFL的仪器已将它们缩小到少于50,000个覆盖所有不同行为的测试案例。定期捕获新发现的测试用例,并将其添加到TCL测试套件中,使用“make fuzztest”或“make valgrindfuzz”命令重新运行测试用例。
4.1.2.Google OSS Fuzz
从2016年开始,Google的一个工程师团队启动了OSS Fuzz项目。OSS Fuzz使用在Google基础架构上运行的AFL风格的引导式模糊器。Fuzzer会自动下载参与项目的最新签到,模糊他们,并向开发者发送报告任何问题的电子邮件。检入修复程序后,模糊程序会自动检测到此问题,并向开发人员发送确认邮件。
SQLite是OSS Fuzz测试的许多开源项目之一。SQLite存储库中的test / ossfuzz.c源文件是SQLite与OSS fuzz的接口。
4.2.畸形数据库文件
有许多测试用例验证SQLite能够处理格式错误的数据库文件。这些测试首先构建一个格式良好的数据库文件,然后通过除SQLite之外的某种方式更改文件中的一个或多个字节来添加损坏。然后用SQLite读取数据库。在某些情况下,字节更改位于数据中间。这会导致数据库的内容发生更改,同时保持数据库格式良好。在其他情况下,文件的未使用字节被修改,这对数据库的完整性没有影响。有趣的情况是当定义数据库结构的文件的字节被改变时。格式错误的数据库测试验证SQLite发现文件格式错误,并使用SQLITE_CORRUPT返回代码报告它们,而不会溢出缓冲区,取消引用NULL指针,
4.3.边界值测试
SQLite为其操作定义了一些限制,例如表中的最大列数,SQL语句的最大长度或整数的最大值。TCL和TH3测试套件都包含大量测试,将SQLite推到其定义的限制边缘并验证它是否正确地执行所有允许的值。额外的测试超出了定义的限制,并验证SQLite正确返回错误。源代码包含测试用例宏,以验证每个边界的两侧都已经过测试。
5.回归测试
无论何时报告针对SQLite的错误,该错误都不会被认为是固定的,除非将新的测试用例显示错误添加到TCL或TH3测试套件中。多年来,这导致了成千上万的新测试。这些回归测试确保过去修复的错误不会重新引入未来版本的SQLite中。
6.自动资源泄漏检测
资源泄漏发生在分配系统资源并且永不释放时。在许多应用程序中最麻烦的资源泄漏是内存泄漏 - 当使用malloc()分配内存但从未使用free()释放时。但是其他类型的资源也可能泄露:文件描述符,线程,互斥锁等。
TCL和TH3测试仪器都会自动跟踪系统资源并在每次测试运行时报告资源泄漏。不需要特殊的配置或设置。测试线束对于内存泄漏特别警惕。如果更改导致内存泄漏,测试工具将很快识别出这一点。SQLite旨在永远不会泄漏内存,即使发生OOM错误或磁盘I / O错误等异常。测试用具热衷于执行此操作。
7.测试覆盖率
包括unix VFS在内的SQLite核心在默认配置下TH3下的100%分支测试覆盖率由gcov衡量。本分析不包括FTS3和RTree等扩展。
7.1.声明与分行覆盖率
有很多方法可以衡量测试覆盖率。最受欢迎的指标是“声明范围”。当你听到有人说他们的节目没有进一步的解释时称为“XX%测试报道”,他们通常意味着声明报道。语句覆盖率测试测试套件至少执行一次代码行的百分比。
分支机构的覆盖范围比声明范围更严格。分支机构覆盖率测量在两个方向上至少评估一次的机器代码分支指令的数量。
为了说明语句覆盖率和分支覆盖率之间的差异,请考虑以下假设的C代码行:
if( a>b && c!=25 ){ d++; }
这样一行C代码可能会生成一打独立的机器代码指令。如果这些指示中的任何一条都经过评估,那么我们说该声明已经过测试。因此,例如,情况可能是条件表达式始终为假,并且“d”变量从不增加。即使如此,语句覆盖率也将这行代码视为已经过测试。
分支机构更加严格。通过分支覆盖,每个测试和声明中的每个子块都是分开考虑的。为了在上面的例子中实现100%的分支覆盖率,必须至少有三个测试用例:
- a<=b
- a>b && c==25 =b>
- a>b && c!=25
上述任何一个测试案例都将提供100%的报表覆盖率,但所有三个测试用例都需要100%的分支报道。一般来说,100%分支覆盖率意味着100%的报表覆盖率,但反过来是不正确的。再次强调,SQLite的TH3测试工具提供了更强大的测试覆盖范围--100%分支测试覆盖率。
7.2.防御性代码的覆盖测试
一个写得很好的C程序通常会包含一些防御性条件,这些条件在实践中总是为真或者总是为假。这导致编程困境:为了获得100%的分支覆盖率,是否需要移除防御性代码?
在SQLite中,上一个问题的答案是“否”。出于测试目的,SQLite源代码定义了称为ALWAYS()和NEVER()的宏。ALWAYS()宏围绕预期始终评估为true的条件,而NEVER()围绕始终评估为false的条件。这些宏用作注释来表明条件是防御性代码。在发布版本中,这些宏是传递:
#define ALWAYS(X) (X)
#define NEVER(X) (X)
然而,在大多数测试中,如果这些宏的参数不具有预期的真值,则这些宏将引发断言错误。这很快提醒开发人员不正确的设计假设。
#define ALWAYS(X) ((X)?1:assert(0),0)
#define NEVER(X) ((X)?assert(0),1:0)
测量测试覆盖率时,将这些宏定义为常量真值,以便它们不生成汇编语言分支指令,因此在计算分支覆盖范围时不会发挥作用:
#define ALWAYS(X) (1)
#define NEVER(X) (0)
测试套件设计为运行三次,每次对于上面显示的每个ALWAYS()和NEVER()定义。所有三个测试运行应该产生完全相同的结果。有一个使用sqlite3_test_control(SQLITE_TESTCTRL_ALWAYS,...)接口的运行时测试,可用于验证宏是否正确设置为第一种形式(传递表单)以进行部署。
7.3.强制覆盖边界值和布尔向量测试
与测试覆盖测量结合使用的另一个宏是testcase()
宏。这个论点是我们希望测试用例既能评估真假的条件。在非覆盖构建中(也就是说,在发布构建中)testcase()
宏是无操作的:
#define testcase(X)
但是在覆盖测量构建中,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_Subtract
/* ... */
break;
}
/* ... */
}
对于位掩码测试,使用testcase()
宏来验证位掩码的每一位都会影响结果。例如,在下面的代码块中,如果掩码包含指示MAIN_DB或TEMP_DB正在打开的两位中的任一位,则条件为真。在testcase()
它之前的if语句验证这两种情况下进行测试的宏:
testcase( mask & SQLITE_OPEN_MAIN_DB
testcase( mask & SQLITE_OPEN_TEMP_DB
if( (mask & (SQLITE_OPEN_MAIN_DB|SQLITE_OPEN_TEMP_DB))!=0 ){ ... }
SQLite源代码包含846个testcase()
宏的使用。
7.4.分支机构覆盖率与MC / DC相比
上面描述了测量覆盖率的两种方法:“声明”和“分支”覆盖率。除了这两者之外,还有许多其他的测试覆盖度量。另一个流行的度量是“修改条件/决策覆盖率”或MC / DC。维基百科定义MC / DC如下:
- 每个决定都尝试每一个可能的结果
- 决策中的每个条件都会采取每种可能的结果。
- 每个入口和出口点都被调用。
- 显示决策中的每个条件独立地影响决策的结果。
在C程序设计语言在那里&&
和||
有“短路”的运营商,MC / DC和分支覆盖是非常接近同样的事情。主要区别在于布尔向量测试。即使MC / DC的第二个元素 - 决策中的每个条件对每个可能结果的要求 - 都可能不被满足,人们可以测试位矢量中的任何位,并仍能获得100%的分支测试覆盖率。
SQLite使用testcase()
前面小节中描述的宏来确保位向量决策中的每个条件都会带来每种可能的结果。通过这种方式,除了100%的分支覆盖之外,SQLite还可以实现100%的MC / DC。
7.5.测量分支机构覆盖
SQLite中的分支覆盖率目前使用带有“-b”选项的gcov进行测量。首先使用选项“-g -fprofile-arcs -ftest-coverage”编译测试程序,然后运行测试程序。然后运行“gcov -b”以生成覆盖报告。覆盖率报告冗长且不方便阅读,因此gcov生成的报告使用一些简单的脚本进行处理,以便将其转换为更加人性化的格式。当然,整个过程都是使用脚本自动完成的。
请注意,使用gcov运行SQLite不是SQLite的测试 - 它是测试套件的测试。gcov运行不测试SQLite,因为-fprofile-args和-ftest-coverage选项会导致编译器生成不同的代码。gcov运行只验证测试套件提供100%分支测试覆盖率。gcov运行是对测试的测试 - 一个元测试。
在运行gcov以验证100%分支测试覆盖率后,然后使用交付编译器选项(没有特殊的-fprofile-arcs和-ftest-coverage选项)重新编译测试程序,然后重新运行测试程序。第二次运行是SQLite的实际测试。
验证gcov测试运行和第二次真正的测试运行都给出相同的输出是非常重要的。输出中的任何差异都表示在SQLite代码中使用未定义或不确定的行为(并因此存在错误)或者编译器中的错误。请注意,在过去的十年中,SQLite在GCC,Clang和MSVC中都遇到了错误。编译器错误虽然罕见,但确实发生了,这就是为什么在交付配置中测试代码非常重要的原因。
7.6.突变检测
使用gcov(或类似的)来显示每个分支指令在两个方向上至少取得一次是衡量测试套件质量的好方法。但更好的是显示每个分支指令在输出中都有所不同。换句话说,我们不仅要显示每条分支指令都跳转和落空,还要显示每个分支都在做有用的工作,并且测试套件能够检测并验证工作。当发现一个分支对输出结果没有影响时,这表明与该分支相关的代码可以被删除(减少了库的大小并且可能使其运行得更快),或者测试套件没有充分测试该特征该分支实施。
SQLite努力验证每个分支指令使用变异测试有所作为。脚本首先将SQLite源代码编译为汇编语言(例如,使用gcc的-S选项)。然后,脚本逐步执行生成的汇编语言,并逐个将每个分支指令更改为无条件跳转或无操作,编译结果,并验证测试套件是否捕获到突变。
不幸的是,SQLite包含很多分支指令,这些指令可以帮助代码更快地运行而不会改变输出。这些分支在突变测试中产生假阳性。作为一个例子,考虑下面用于加速表名查找的散列函数:
55 static unsigned int strHash(const char *z){
56 unsigned int h = 0;
57 unsigned char c;
58 while( (c = (unsigned char)*z++)!=0 ){ /*OPTIMIZATION-IF-TRUE*/
59 h = (h<<3) ^ h ^ sqlite3UpperToLower[c];
60 }
61 return h;
62 }
如果在第58行执行“c!= 0”测试的分支指令改变为no-op,那么while循环将永远循环,并且测试套件将失败并伴随超时。但是,如果该分支变成无条件跳转,那么散列函数将始终返回0.问题是0是一个有效的散列。总是返回0的散列函数仍然有效,因为SQLite仍然总是得到正确的答案。表名散列表退化为链表,因此解析SQL语句时发生的表名查找可能会稍微慢一些,但最终结果将是相同的。
为了解决这个问题,将表单“ /*OPTIMIZATION-IF-TRUE*/
”和“ /*OPTIMIZATION-IF-FALSE*/
”的注释插入到SQLite源代码中,告诉突变测试脚本忽略一些分支指令。
7.7.全面测试覆盖的经验
SQLite的开发人员发现,全面覆盖测试是一种非常有效的定位和防止错误的方法。由于SQLite核心代码中的每个分支指令都被测试用例覆盖,因此开发人员可以确信,对代码的一部分进行的更改不会在代码的其他部分产生意想不到的后果。如果没有全面覆盖测试的可用性,近年来添加到SQLite中的许多新功能和性能改进将不可能实现。
保持100%MC / DC是费力且耗时的。维持全覆盖测试所需的工作量对于典型应用来说可能不具成本效益。然而,我们认为全覆盖测试对于像SQLite这样非常广泛部署的基础架构库是合理的,特别是对于其本质上“记住”过去错误的数据库库。
8.动态分析
动态分析是指在代码正式运行时执行的SQLite代码的内部和外部检查。动态分析已被证明对于维护SQLite的质量非常有帮助。
8.1.断言
SQLite核心包含5285个assert()
验证函数前置条件和后置条件以及循环不变式的语句。Assert()是一个宏,它是ANSI-C的标准部分。该参数是一个假定始终为真的布尔值。如果断言是错误的,程序将打印一条错误消息并暂停。
通过编译与定义的NDEBUG宏,Assert()宏被禁用。在大多数系统中,断言是默认启用的。但是在SQLite中,断言非常多,并且处于这样的性能关键位置,因此在启用断言时数据库引擎的运行速度会降低三倍。因此,SQLite的默认(生产)版本会禁用断言。断言语句仅在SQLite使用定义的SQLITE_DEBUG预处理器宏进行编译时启用。
8.2.Valgrind
Valgrind也许是世界上最令人惊叹和有用的开发者工具。Valgrind是一个模拟器 - 它模拟运行Linux二进制文件的x86。(Linux以外的平台Valgrind正在开发中,但在撰写本文时,Valgrind只能在Linux上可靠地工作,SQLite开发人员认为Linux应该成为所有软件开发的首选平台。)作为Valgrind运行一个Linux二进制文件,它会查找各种有趣的错误,如数组超载,从未初始化的内存中读取,堆栈溢出,内存泄漏等等。Valgrind发现的问题可以轻松地通过对SQLite运行的所有其他测试。而且,当Valgrind发现错误时,它可以将开发人员直接转储到发生错误的确切点的符号调试器中,以便于快速修复。
因为它是一个模拟器,所以在Valgrind中运行二进制比在本机硬件上运行更慢。(首先,工作站上运行在Valgrind上的应用程序的性能与在智能手机上本地运行的性能大致相同。)因此,通过Valgrind运行完整的SQLite测试套件是不切实际的。然而,TH3测试的快速测试和覆盖率在每次发布之前都会通过Valgrind进行。
8.3. Memsys2
SQLite包含一个可插入的内存分配子系统。默认实现使用系统malloc()和free()。但是,如果使用SQLITE_MEMDEBUG编译SQLite,则会插入一个替代内存分配包装(memsys2),用于在运行时查找内存分配错误。当然,memsys2包装会检查内存泄漏,但也会查找缓冲区溢出,未初始化内存的使用以及在释放内存后尝试使用内存。这些相同的检查也是由valgrind完成的(实际上,Valgrind做得更好),但是memsys2的优势在于比Valgrind快得多,这意味着可以更频繁地进行检查并进行更长的测试。
8.4.互斥体声明
SQLite包含一个可插拔的互斥体子系统。根据编译时选项,默认的互斥体系统包含接口sqlite3_mutex_held()和sqlite3_mutex_notheld(),用于检测调用线程是否保持特定的互斥体。这两个接口广泛用于SQLite的assert()语句中,以验证互斥体在所有正确的时刻被保持和释放,以便仔细检查SQLite是否在多线程应用程序中正常工作。
8.5.日志测试
SQLite确保事务在系统崩溃和电源故障期间处于原子状态的一件事是在更改数据库之前将所有更改写入回滚日志文件。TCL测试工具包含一个备用操作系统后端实现,有助于验证这种情况是否正确发生。“journal-test VFS”监视数据库文件和回滚日志之间的所有磁盘I / O流量,检查以确保没有任何内容写入数据库文件,该文件没有先写入并同步到回滚日志。如果发现任何差异,则会引发断言错误。
日志测试是在崩溃测试之上进行的另一次双重检查,以确保SQLite事务在系统崩溃和电源故障时是原子的。
8.6.未定义的行为检查
在C编程语言中,编写具有“未定义”或“实现定义”行为的代码非常容易。这意味着代码可能在开发过程中工作,但在不同的系统上给出不同的答案,或者使用不同的编译器选项重新编译时。ANSI C中未定义和实现定义的行为的示例包括:
- 有符号整数溢出。(像大多数人所期望的那样,带符号的整数溢出不一定包含在内)。
- 将N位整数移位N位以上。
- 转移负数。
- 转移负数。
- 在重叠缓冲区上使用memcpy()函数。
- 函数参数的评估顺序。
- “char”变量是否有符号或无符号。
- 等等......
由于未定义的和实现定义的行为是不可移植的,并且很容易导致不正确的答案,所以SQLite很难避免它。例如,当将两个整数列值作为SQL语句的一部分添加在一起时,SQLite不会简单地使用C语言“+”运算符将它们相加。相反,它首先检查以确保添加不会溢出,并且如果它会,则使用浮点进行添加。
为了帮助确保SQLite不使用未定义的或实现定义的行为,使用检测的构建重新运行测试套件,以尝试检测未定义的行为。例如,测试套件使用GCC的“-ftrapv”选项运行。然后使用Clang上的“-fsanitize = undefined”选项再次运行它们。再次使用MSVC中的“/ RTC1”选项。然后使用“-funsigned-char”和“-fsigned-char”等选项重新运行测试套件,以确保实现差异无关紧要。然后在32位和64位系统以及使用各种CPU架构的大端系统和小端系统上重复测试。此外,测试套件还增加了许多故意设计用于激发未定义行为的测试用例。例如:”。
9.禁用优化测试
sqlite3_test_control(SQLITE_TESTCTRL_OPTIMIZATIONS,...)接口允许在运行时禁用选定的SQL语句优化。SQLite应该始终生成完全相同的答案,并启用优化并禁用优化; 开启优化后答案会更快。因此,在生产环境中,总是会启用优化(默认设置)。
SQLite使用的一种验证技术是运行整个测试套件两次,一次优化保持开启状态,第二次优化关闭,并验证两次获得相同的输出。这表明优化不会引入错误。
并非所有的测试用例都可以这样处理。某些测试用例通过计算磁盘访问次数,排序操作,全扫描步骤或查询过程中发生的其他处理步骤来检查以确认优化是否真的减少了计算量。这些测试用例在禁用优化时似乎失败。但是大多数测试用例只是简单地检查是否获得了正确的答案,并且所有这些情况都可以在优化和不进行优化的情况下成功运行,以显示优化不会导致故障。
10.检查清单
SQLite开发人员使用在线清单来协调测试活动并验证所有测试是否在每个SQLite版本之前通过。过去的清单保留以供历史参考。(这些清单对于匿名的互联网浏览者来说是只读的,但开发人员可以在他们的Web浏览器中登录和更新清单项目。)SQLite测试和其他开发活动的清单的使用受“清单宣言”的
启发 。
最新清单包含大约200个项目,每个版本都经过单独验证。一些清单项目只需要几秒钟来验证和标记。其他一些涉及运行数小时的测试套件。
发布清单不是自动的:开发人员手动运行清单中的每个项目。我们发现保持人类循环是非常重要的。即使测试本身已通过,但运行核对清单项目时有时会发现问题。让一个人在最高级别上检查测试结果是非常重要的,并且不断地问“这真的是对的吗?”
发布清单不断发展。当发现新问题或潜在问题时,会添加新的清单项目以确保这些问题不会出现在后续版本中。发布清单已被证明是帮助确保在发布过程中不会被忽视的宝贵工具。
11.静态分析
静态分析意味着在编译时分析源代码来检查正确性。静态分析包括编译器警告消息和更多深入的分析引擎,如Clang静态分析器。SQLite使用Linux和Mac上的-Wall和-Wextra标志以及Windows上的MSVC在GCC和Clang上编译时没有警告。Clang Static Analyzer工具“scan-build”也没有生成有效的警告(虽然最近版本的clang似乎会产生很多误报)。然而,其他静态分析器可能会产生一些警告。鼓励用户不要强调这些警告,而要在上述SQLite的强烈测试中获得慰借。
静态分析对于在SQLite中查找错误没有帮助。静态分析在SQLite中发现了一些错误,但这些例外。SQLite中引入了更多的错误,同时试图让它在没有警告的情况下进行编译,而不是通过静态分析发现。
12.总结
SQLite是开源的。这给很多人提供了这样一个想法,即它没有像商业软件那样得到很好的测试,并且可能不可靠。但这种印象是错误的。SQLite在该领域表现出非常高的可靠性和非常低的缺陷率,特别是考虑到它的发展速度。SQLite的质量部分通过仔细的代码设计和实现来实现。但是广泛的测试对于维护和改进SQLite的质量也起着至关重要的作用。本文档概述了SQLite的每个版本都经历的测试过程,希望能够激发人们对SQLite适用于任务关键型应用程序的信心。
SQLite在公共领域。