Small. Fast. Reliable.
Choose any three.
如何测试SQLite

1.简介

SQLite的可靠性和健壮性部分是通过全面而仔细的测试来实现的。

版本3.33.0(2020-08-14)开始,SQLite库包含大约143.4 KSLOC的C代码。(KSLOC表示成千上万的“源代码行”,或者换句话说,不含空行和注释的代码行。)相比之下,该项目的测试代码和测试脚本的数量是640倍-91911.0 KSLOC。

1.1。执行摘要

2.测试线束

有四个独立的测试工具可用于测试核心SQLite库。每个测试工具均独立于其他工具进行设计,维护和管理。

  1. TCL测试是最古老的一套SQLite的测试。它们包含在与SQLite核心相同的源树中,就像SQLite核心在公共域中一样。TCL测试是开发期间使用的主要测试。TCL测试是使用 TCL脚本语言编写的。TCL测试工具本身包含27.0 KSLOC的C代码,用于创建TCL接口。测试脚本包含在1272个文件中,总大小为21.1MB。有46363个不同的测试用例,但是许多测试用例已参数化并多次运行(使用不同的参数),因此在一个完整的测试运行中,要执行数百万个单独的测试。

  2. 所述TH3测试工具是一组专有的测试中,用C语言编写的是提供100%的分支测试覆盖率(和100%MC / DC测试覆盖)到核心SQLite库。TH3测试旨在在无法轻松支持TCL或其他工作站服务的嵌入式和专用平台上运行。TH3测试仅使用已发布的SQLite接口。TH3包含约71.5 MB或978.3 KSLOC的C代码,实现了46622个不同的测试用例。不过,TH3测试是经过严格参数化的,因此,一个全覆盖测试将运行大约190万个不同的测试实例。提供100%分支测试覆盖率的案例构成了总TH3测试套件的一部分。发布之前的浸泡测试会进行数亿次测试。关于TH3的更多信息是单独提供

  3. SQL逻辑测试 或SLT测试工具是用来运行对阵双方SQLite和其他几个SQL数据库引擎的SQL语句的数量巨大,并确认他们都得到同样的答案。SLT当前将SQLite与PostgreSQL,MySQL,Microsoft SQL Server和Oracle 10g进行了比较。SLT运行720万查询,包括1.12GB的测试数据。

  4. dbsqlfuzz引擎是一种专有的模糊测试。SQLite的其他模糊器会 更改SQL输入或数据库文件。Dbsqlfuzz同时更改SQL和数据库文件,因此能够达到新的错误状态。Dbsqlfuzz是使用LLVM的 libFuzzer框架和自定义变量构建的。从252个种子文件开始,dbsqlfuzz模糊测试器实际上尝试了数十亿个突变,从而产生了58497个不同的测试用例,这些测试用例构成了每个新的模糊测试会话的基础。Dbsqlfuzz帮助确保SQLite具有强大的抵御恶意SQL或数据库输入的攻击的能力。

除了四个主要的测试工具之外,还有其他几个用于实现专门测试的小程序。

  1. “ speedtest1.c”程序估计典型工作负载下SQLite的性能。
  2. “ mptester.c”程序是对多个进程同时进行读写单个数据库的压力测试。
  3. “ threadtest3.c”程序是同时使用SQLite对多个线程进行的压力测试。
  4. “ fuzzershell.c”程序用于运行一些模糊测试

在每个SQLite版本之前,以上所有测试必须在多个平台上和多个编译时配置下成功运行。

在每次检入SQLite源树之前,开发人员通常会运行Tcl测试的子集(称为“非常快速”),该子集包含大约24.85万个测试用例。非常快速的测试包括除异常,绒毛和浸泡测试以外的大多数测试。非常快速的测试背后的想法是,它们足以捕获大多数错误,而且运行仅需几分钟而不是几个小时。

3.异常测试

异常测试是旨在在出现问题时验证SQLite正确行为的测试。(相对)容易构建一个SQL数据库引擎,该引擎在功能齐全的计算机上的格式正确的输入上可以正常运行。建立一个对无效输入做出理性响应并在系统故障后继续运行的系统,将更加困难。异常测试旨在验证后者的行为。

3.1。内存不足测试

与所有SQL数据库引擎一样,SQLite广泛使用了malloc()(有关其他详细信息,请参见SQLite中有关动态内存分配的单独报告 。)在服务器和工作站上,malloc()在实践中绝不会失败,因此正确处理外出-内存错误(OOM)并不是特别重要。但是在嵌入式设备上,OOM错误非常普遍,并且由于SQLite经常在嵌入式设备上使用,因此SQLite能够妥善处理OOM错误非常重要。

OOM测试是通过模拟OOM错误来完成的。SQLite允许应用程序使用sqlite3_configSQLITE_CONFIG_MALLOC,...) 界面。TCL和TH3测试工具都能够插入修改后的malloc()版本,该版本可以操纵一定数量的分配后失败。可以将这些检测到的malloc设置为仅失败一次,然后再次开始工作,或者在第一次失败后继续失败。OOM测试是循环完成的。在循环的第一次迭代中,检测到的malloc被装配为在第一次分配时失败。然后执行一些SQLite操作,并进行检查以确保SQLite正确处理了OOM错误。然后,已检测的malloc上的故障时间计数器增加一,并重复测试。循环一直进行到整个操作完成为止,而从未遇到过模拟的OOM故障。这样的测试运行两次,

3.2。I / O错误测试

I / O错误测试旨在验证SQLite是否对失败的I / O操作做出了合理的响应。I / O错误可能是由于磁盘驱动器已满,磁盘硬件故障,使用网络文件系统时网络中断,SQL操作中间发生的系统配置或权限更改或其他硬件或操作系统故障引起的。不管是什么原因,SQLite都能正确响应这些错误,并且I / O错误测试试图验证它是很重要的。

I / O错误测试在概念上与OOM测试类似;模拟I / O错误,并进行检查以验证SQLite是否正确响应了模拟错误。通过插入一个新的虚拟文件系统对象(在经过一定数量的I / O操作后专门模拟I / O错误),可以在TCL和TH3测试工具 中模拟I / O错误。与OOM错误测试一样,可以将I / O错误模拟器设置为仅失败一次,或者在首次失败后连续失败。测试以循环的方式运行,缓慢增加故障点,直到测试用例运行无误为止。该循环运行两次,一次将I / O错误模拟器设置为仅模拟单个故障,第二次将循环设置为在第一次故障后使所有I / O操作失败。

在I / O错误测试中,禁用I / O错误模拟失败机制后,将使用PRAGMA integrity_check检查数据库 ,以确保I / O错误未引起数据库损坏。

3.3。碰撞测试

崩溃测试旨在证明,如果应用程序或操作系统崩溃,或者在数据库更新过程中出现电源故障,则SQLite数据库不会损坏。另一本名为《SQLite中的原子提交》的白皮书 介绍了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模糊

SQL模糊测试包括创建语法正确但极其荒谬的SQL语句,并将其提供给SQLite以查看将如何处理它们。通常会返回某种错误(例如“ no such table”)。有时,纯粹出于偶然,SQL语句在语义上也是正确的。在这种情况下,将运行结果准备好的语句以确保给出合理的结果。

4.1.1。使用American Fuzzy Lop Fuzzer的SQL Fuzz

模糊测试的概念已经存在了数十年,但是直到2014年Michal Zalewski发明了第一个实用的轮廓引导模糊器,American Fuzzy Lop或“ AFL”之后,模糊测试才是发现错误的有效方法 。与以前的模糊器盲目生成随机输入不同,AFL会检测正在测试的程序(通过修改C编译器的汇编语言输出),并使用该检测方法来检测输入何时导致程序执行不同的操作-遵循新的控件路径或循环不同的次数。引发新行为的输入将保留并进一步突变。通过这种方式,AFL能够“发现”被测程序的新行为,包括设计人员从未想到的行为。

事实证明,AFL擅长在SQLite中发现奥术错误。大多数发现是assert()语句,其中在模糊情况下条件为假。但是AFL还发现SQLite中有相当多的崩溃错误,甚至在少数情况下SQLite计算的结果也不正确。

由于过去的成功,AFL从3.8.10(2015-05-07)版本开始成为SQLite测试策略的标准部分,直到被3.29.0(2019-07-10)版本中更好的模糊测试所取代。

4.1.2。Google OSS模糊测试

从2016年开始,Google的工程师团队启动了 OSS Fuzz项目。OSS Fuzz使用在Google基础架构上运行的AFL样式的指导性模糊器。Fuzzer会自动为参与的项目下载最新的签入内容,对它们进行模糊处理,然后向开发人员发送电子邮件,报告任何问题。检入修复程序后,模糊器将自动检测到此问题,并通过电子邮件将确认发送给开发人员。

SQLite是OSS Fuzz测试的众多开源项目之一。SQLite存储库中的 test / ossfuzz.c源文件是SQLite与OSS模糊测试的接口。

OSS Fuzz不再在SQLite中发现历史错误。但是它仍在运行,偶尔会在新开发签入中发现问题。示例: [1] [2] [3]

4.1.3。dbsqlfuzz模糊器

从2018年末开始,SQLite已使用名为“ dbsqlfuzz”的专有模糊器进行模糊测试。Dbsqlfuzz使用LLVM的 libFuzzer框架构建。

dbsqlfuzz模糊器同时更改SQL输入和数据库文件。Dbsqlfuzz 在专门的输入文件上使用自定义的结构感知突变 ,该变量定义了输入数据库和要针对该数据库运行的SQL文本。由于dbsqlfuzz同时更改了输入数据库和输入SQL,因此dbsqlfuzz能够在SQLite中找到一些模糊的错误,而以前的仅模糊更改SQL输入或仅更改数据库文件的模糊器会忽略这些错误。SQLite开发人员通常在离开办公室很长时间(例如整夜)时,会在sqlite的最新中继代码上运行一两个dbsqlfuzz实例。

dbsqlfuzz模糊测试器在加固SQLite代码库以抵抗恶意攻击方面非常成功,因此现在被认为是SQLite的四个主要测试工具之一。

请注意,dbsqlfuzz不是Chromium使用的,基于Protobuf的SQLite基于结构的模糊检测器,并在Structure-Aware Mutator文章中进行了描述 。这两个模糊器之间没有任何联系,只是它们都基于libFuzzer。SQLite 的Protobuf模糊器由Google的Chromium团队编写和维护,而dbsqlfuzz由原始SQLite开发人员编写和维护。对于SQLite拥有多个独立开发的模糊测试器是很好的,因为这意味着更可能发现隐蔽的问题。

4.1.4。其他第三方模糊器

对于第三方而言,SQLite似乎是一个受欢迎的目标。开发人员听说过许多尝试模糊SQLite的尝试,并且偶尔会得到独立的模糊器发现的错误报告。所有此类报告均已得到及时修复,因此产品得到了改进,整个SQLite用户社区都将从中受益。具有许多独立测试人员的这种机制类似于 Linus的定律:“眼球够大,所有错误都是浅浅的”。

特别令人关注的一个令人困惑的研究人员是 曼努埃尔·里格Manuel Rigger),目前(本段写于2019-12-21)在苏黎世联邦理工学院。大多数模糊测试器仅查找断言错误,崩溃,未定义行为(UB)或其他容易检测到的异常。另一方面,Rigger博士的模糊处理程序能够发现SQLite计算出错误答案的情况。瑞格(Rigger)发现了 很多这样的情况。这些发现大多数都是模糊的,涉及类型转换和亲和力转换的极端情况,并且很多发现都针对未发布的功能。尽管如此,他的发现仍然很重要,因为它们是真正的错误,并且SQLite开发人员很感激能够识别并解决潜在的问题。Rigger的工作目前尚未公开。当它发布时,它的影响力可能与Zalewski发明的AFL和配置文件引导的模糊测试一样有影响力。

4.1.5。模糊检查测试工具

来自AFLOSS Fuzzdbsqlfuzz的历史测试用例收集在主SQLite源树中的一组数据库文件中,然后每当运行“ make test”时由“ fuzzcheck”实用程序重新运行。多年来,各种模糊测试人员检查的数十亿个案例中,Fuzzcheck仅运行了数千个“有趣”案例。“有趣”的案例是表现出以前看不见的行为的案例。模糊测试人员发现的实际错误始终包含在有趣的测试用例中,但是fuzzcheck运行的大多数情况绝不是实际错误。

4.1.6。模糊测试和100%MC / DC测试之间的张力

模糊测试和100%MC / DC测试相互矛盾。也就是说,经过100%MC / DC测试的代码将更容易受到模糊测试发现的问题的影响,而在模糊测试过程中执行良好的代码往往具有(远远)低于100%MC / DC的代码。这是因为MC / DC测试会阻止具有无法到达的分支的防御性代码,但如果没有防御性代码,则模糊测试者更有可能找到引起问题的路径。MC / DC测试对于构建在正常使用过程中很健壮的代码似乎效果很好,而模糊测试对于构建对恶意攻击而言很健壮的代码非常有用。

当然,用户希望使用既能正常使用又能抵抗恶意攻击的代码。SQLite开发人员致力于提供这一点。本部分的目的仅是指出同时执行这两个操作很困难。

SQLite的大部分历史一直专注于100%MC / DC测试。仅在2014年引入AFL时,对模糊攻击的抵抗才成为关注的问题。在那里,一段时间以来,模糊者在SQLite中发现了许多问题。近年来,SQLite的测试策略已经发展为更加重视模糊测试。我们仍保留核心SQLite代码的100%MC / DC,但是现在大多数测试CPU周期都专用于模糊测试。

虽然模糊测试和100%MC / DC测试处于紧张状态,但它们并不完全是交叉用途。SQlite测试套件可以对100%的MC / DC进行测试的事实意味着,当模糊测试者确实发现问题时,可以快速解决这些问题,并且几乎不会引入新错误。

4.2。格式错误的数据库文件

有许多测试用例可以验证SQLite是否能够处理格式错误的数据库文件。这些测试首先构建格式正确的数据库文件,然后通过SQLite以外的其他方式通过更改文件中的一个或多个字节来添加损坏。然后使用SQLite读取数据库。在某些情况下,字节更改位于数据中间。这将导致数据库的内容发生更改,同时保持数据库的格式正确。在其他情况下,文件的未使用字节会被修改,这对数据库的完整性没有影响。有趣的情况是定义数据库结构的文件的字节被更改。格式错误的数据库测试将验证SQLite是否找到文件格式错误并使用SQLITE_CORRUPT报告错误 返回代码,而不会溢出缓冲区,取消引用NULL指针或执行其他有害操作。

dbsqlfuzz模糊器也确实验证了SQLite的三立响应畸形数据库文件的出色。

4.3。边界值检验

SQLite对其操作定义了某些限制,例如表中的最大列数,SQL语句的最大长度或整数的最大值。TCL和TH3测试套件都包含大量测试,这些测试将SQLite推到其定义的限制的边缘,并验证它对于所有允许的值均能正确执行。其他测试超出了定义的限制,并验证SQLite是否正确返回错误。源代码包含测试用例宏,以验证每个边界的两侧均已被测试。

5.回归测试

每当报告针对SQLite的错误时,直到将显示该错误的新测试用例添加到TCL或TH3测试套件中之前,该错误才被视为已修复。多年来,这导致了成千上万的新测试。这些回归测试可确保不会将以前已修复的错误重新引入SQLite的未来版本中。

6.自动资源泄漏检测

当分配系统资源并且从不释放系统资源时,就会发生资源泄漏。在许多应用程序中,最麻烦的资源泄漏是内存泄漏-当使用malloc()分配内存但从未使用free()释放内存时。但是其他类型的资源也可能泄漏:文件描述符,线程,互斥体等。

TCL和TH3测试工具都会自动跟踪系统资源,并在每次测试运行时报告资源泄漏。无需特殊配置或设置。测试工具特别注意内存泄漏。如果更改导致内存泄漏,则测试工具将迅速识别出这种情况。SQLite的设计即使在发生OOM错误或磁盘I / O错误之类的异常之后也不会泄漏内存。测试工具热衷于执行此操作。

7.测试范围

gcov衡量,SQLite内核(包括unix VFS)在其默认配置 下在TH3下具有100%的分支测试覆盖率。此分析不包括诸如FTS3和RTree的扩展。

7.1。声明与分支机构覆盖率

有很多方法可以衡量测试覆盖率。最受欢迎的指标是“声明覆盖率”。当您听到有人说他们的程序为“ XX%测试覆盖率”而没有进一步说明时,通常是指声明覆盖率。语句覆盖率衡量测试套件至少执行一次代码行的百分比。

分支机构的覆盖范围比语句的覆盖范围更为严格。分支覆盖率用于衡量在两个方向上至少评估一次的机器代码分支指令的数量。

为了说明语句覆盖率和分支覆盖率之间的区别,请考虑以下假设的C代码行:

if(a> b && c!= 25){d ++; }

这样的C代码行可能会生成十几个单独的机器代码指令。如果对这些说明中的任何一条进行了评估,那么我们说该语句已经过测试。因此,例如,可能出现条件表达式始终为false且“ d”变量从不递增的情况。即使这样,语句覆盖范围也将这一行代码视为经过测试。

分支机构的覆盖范围更加严格。在分支覆盖范围内,语句中的每个测试和每个子块都被单独考虑。为了在上面的示例中实现100%的分支覆盖率,必须至少有三个测试用例:

上述测试用例中的任何一个都将提供100%的语句覆盖率,但是这三个条件都是100%分支覆盖率所必需的。一般而言,100%的分支覆盖率意味着100%的语句覆盖率,但反之则不成立。再次强调,用于SQLite的 TH3测试工具提供了更强大的测试覆盖率形式-100%分支测试覆盖率。

7.2。防御性代码的覆盖率测试

编写良好的C程序通常将包含一些防御条件,实际上这些防御条件始终为true或false。这导致了编程难题:是否为了获得100%的分支覆盖率而删除了防御性代码?

在SQLite中,上一个问题的答案为“否”。为了进行测试,SQLite源代码定义了称为ALWAYS()和NEVER()的宏。ALWAYS()宏包围期望始终评估为true的条件,而NEVER()包围总是评估为false的条件。这些宏用作注释,以指示条件是防御性代码。在发行版中,这些宏是直通的:

#定义总是(X)(X)
#定义永不(X)(X)

但是,在大多数测试中,如果这些宏的参数不具有预期的真值,则它们将引发断言错误。这会迅速提醒开发人员注意错误的设计假设。

#定义总是(X)((X)?1:断言(0),0)
#定义NEVER(X)((X)?assert(0),1:0)

在测量测试覆盖率时,这些宏被定义为恒定的真值,以便它们不生成汇编语言分支指令,因此在计算分支覆盖率时不起作用:

#定义总是(X)(1)
#定义从不(X)(0)

该测试套件设计为可以运行3次,对于上面显示的每个ALWAYS()和NEVER()定义一次。所有三个测试运行都应产生完全相同的结果。有一个使用sqlite3_test_controlSQLITE_TESTCTRL_ALWAYS,...)接口的运行时测试,可用于验证宏是否正确设置为用于部署的第一种形式(传递形式)。

7.3。强制覆盖边界值和布尔向量测试

与测试覆盖率度量结合使用的另一个宏是testcase()宏。该参数是我们希望测试用例的评估结果为true和false的条件。在非覆盖版本中(也就是说,在发行版本中), testcase()宏是禁止操作的:

#定义测试用例(X)

但是在覆盖率测量版本中,testcase()宏会生成用于评估其参数中条件表达式的代码。然后,在分析过程中,将进行检查以确保存在将条件评估为真和假的测试。 例如,使用Testcase()宏来帮助验证是否已测试了边界值。例如:

测试用例(a == b);
测试用例(a == b + 1);
if(a> b && c!= 25){d ++; }

当两个或两个以上switch语句的案例进入同一代码块时,也要使用Testcase宏,以确保所有案例都到达了代码:

开关(op){
  案例OP_Add:
  案例OP_Subtract:{
    测试用例(op == OP_Add);
    测试用例(op == OP_Subtract);
    / * ... * /
    休息;
  }
  / * ... * /
}

对于位掩码测试,使用testcase()宏来验证位掩码的每一位都会影响结果。例如,在下面的代码块中,如果掩码包含两个位中的任意一位,则表示该条件已成立,这两个位指示正在打开MAIN_DB或TEMP_DB。该测试用例() 宏先于if语句验证这两种情况下进行测试:

测试用例(mask&SQLITE_OPEN_MAIN_DB);
测试用例(mask&SQLITE_OPEN_TEMP_DB);
if((掩码&(SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_TEMP_DB))!= 0){...}

SQLite源代码包含testcase()宏的1084个用法。

7.4。分支机构覆盖率与MC / DC

上面描述了两种测量测试覆盖率的方法:“声明”和“分支”覆盖率。除了这两个以外,还有许多其他测试覆盖率指标。另一个流行的度量标准是“修改的条件/决策覆盖率”或MC / DC。 维基百科 对MC / DC的定义如下:

在C编程语言中,&&|| 是“短路”运算符,MC / DC和分支覆盖范围几乎是同一件事。主要区别在于布尔向量测试。即使可能不满足MC / DC的第二个元素(决定中的每个条件都考虑所有可能结果的要求),也可以测试位向量中的任何几位,并且仍然获得100%的分支测试覆盖率。

SQLite使用上一小节中所述的testcase()宏来确保位向量决策中的每个条件都具有所有可能的结果。这样,除了100%的分支覆盖率之外,SQLite还可以实现100%的MC / DC。

7.5。测量分支机构覆盖率

当前使用gcov和“ -b”选项来测量SQLite中的分支覆盖率。首先使用选项“ -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静态无符号int strHash(const char * z){
56 unsigned int h = 0;
57个未签名的字符c;
58 while(((c =(unsigned char)* z ++)!= 0){/ * OPTIMIZATION-IF-TRUE * /
59 h =(h << 3)^ h ^ sqlite3UpperToLower [c];
60}
61返回h;
62}

如果将第58行上执行“ c!= 0”测试的分支指令更改为无操作,则while循环将永远循环,并且测试套件将因超时而失败。但是,如果将该分支更改为无条件跳转,则哈希函数将始终返回0。问题是0是有效哈希。始终返回0的哈希函数在SQLite始终始终获得正确答案的意义上仍然有效。表名哈希表会退化为链接列表,因此在解析SQL语句时发生的表名查找可能会稍慢一些,但最终结果将是相同的。

要解决此问题,请在SQLite源代码中插入“ /*OPTIMIZATION-IF-TRUE*/”和“ /*OPTIMIZATION-IF-FALSE*/”形式的注释,以告知变异测试脚本忽略某些分支指令。

7.7。全面的测试经验

SQLite的开发人员发现,全面覆盖测试是查找和防止错误的极其有效的方法。由于SQLite核心代码中的每条分支指令都包含在测试用例中,因此开发人员可以确信,对代码的一部分进行的更改不会对代码的其他部分造成意外的后果。如果没有可用性全面覆盖测试,SQLite近年来已添加的许多新功能和性能改进将是不可能的。

维持100%的MC / DC既费力又费时。对于典型的应用程序而言,维持全覆盖测试所需的工作水平可能不符合成本效益。但是,我们认为完全覆盖的测试对于像SQLite这样广泛部署的基础结构库是有道理的 ,尤其是对于数据库库,由于其本质,它可以“记住”过去的错误。

8.动态分析

动态分析是指对SQLite代码进行内部和外部检查,这些检查是在代码运行和运行时执行的。实践证明,动态分析对保持SQLite的质量非常有帮助。

8.1。断言

SQLite核心包含5855条assert() 语句,用于验证函数的前提条件和后置条件以及循环不变量。Assert()是一个宏,它是ANSI-C的标准部分。该参数是一个布尔值,假定始终为true。如果断言为假,程序将显示一条错误消息并暂停。

通过使用定义的NDEBUG宏进行编译,可以禁用Assert()宏。在大多数系统中,断言默认情况下处于启用状态。但是在SQLite中,断言是如此之多,并且处在性能至关重要的位置,以致于启用断言时,数据库引擎的运行速度要慢大约三倍。因此,SQLite的默认(生产)构建会禁用断言。只有在使用定义的SQLITE_DEBUG预处理程序宏编译SQLite时,才启用断言语句。

有关SQLite如何使用assert()的其他信息,请参见SQLite文档中的Use Of assert

8.2。瓦尔格朗德

瓦尔格朗德是世界上最神奇,最有用的开发人员工具。Valgrind是一个模拟器-它模拟运行Linux二进制文件的x86。(用于Linux以外的平台的Valgrind的端口正在开发中,但是在撰写本文时,Valgrind仅在Linux上可靠地工作,SQLite开发人员认为这意味着Linux应该是所有软件开发的首选平台。)运行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测试工具包含一个可选的 OS后端实现,可帮助验证此操作是否正确发生。“ journal-test VFS”监视数据库文件和回滚日志之间的所有磁盘I / O流量,检查以确保没有任何内容写入数据库文件,而该文件尚未被首先写入并同步到回滚日志。如果发现任何差异,则会引发断言错误。

日志测试是崩溃测试之外的另一项双重检查,以确保SQLite事务在系统崩溃和电源故障时是原子的。

8.6。未定义的行为检查

在C编程语言中,编写具有“未定义”或“实现定义”行为的代码非常容易。这意味着代码可能在开发过程中起作用,但随后会在不同的系统上或使用不同的编译器选项重新编译时给出不同的答案。ANSI C中未定义和实现定义的行为的示例包括:

由于未定义和实现定义的行为是不可移植的,并且很容易导致错误的答案,因此SQLite努力避免这种行为。例如,当将两个整数列值作为SQL语句的一部分加在一起时,SQLite不会简单地使用C语言“ +”运算符将它们加在一起。相反,它首先检查以确保添加项不会溢出,如果确实会溢出,它将改为使用浮点数进行添加。

为了帮助确保SQLite不使用未定义或实现定义的行为,将使用试图检测未定义行为的检测过的内部版本重新运行测试套件。例如,使用GCC的“ -ftrapv”选项运行测试套件。然后使用Clang上的“ -fsanitize = undefined”选项再次运行它们。再次使用MSVC中的“ / RTC1”选项。然后,使用“ -funsigned-char”和“ -fsigned-char”之类的选项重新运行测试套件,以确保实现差异也无关紧要。然后,使用各种CPU架构,在32位和64位系统以及big-endian和little-endian系统上重复测试。此外,在测试套件中增加了许多经过精心设计以激发未定义行为的测试用例。例如:SELECT -1 *(-9223372036854775808); ”。

9.禁用的优化测试

sqlite3_test_controlSQLITE_TESTCTRL_OPTIMIZATIONS,...)接口允许选择SQL语句的优化,以在运行时被禁用。在启用优化和禁用优化的情况下,SQLite应始终生成完全相同的答案。启用优化后,答案很快就会到达。因此,在生产环境中,始终使优化处于打开状态(默认设置)。

在SQLite上使用的一种验证技术是运行整个测试套件两次,一次运行启用优化,另一次运行关闭优化,并验证两次均获得相同的输出。这表明优化不会引入错误。

并非所有测试用例都可以用这种方式处理。一些测试用例通过计算磁盘访问,排序操作,全扫描步骤或查询期间发生的其他处理步骤的数量,来检查优化是否确实在减少计算量。禁用优化后,这些测试用例似乎将失败。但是大多数测试用例只是检查是否获得了正确的答案,并且所有这些用例都可以在有或没有优化的情况下成功运行,以表明优化不会引起故障。

10.清单

SQLite开发人员使用在线检查表来协调测试活动并验证每个SQLite版本之前所有测试均通过。 保留过去的清单以供历史参考。(清单对于匿名Internet查看者是只读的,但开发人员可以在其Web浏览器中登录和更新清单项目。)清单用于SQLite测试和其他开发活动的灵感来自于清单宣言

最新的清单包含大约200个项目,每个版本均经过单独验证。一些清单项目仅需几秒钟即可进行验证和标记。其他则涉及运行多个小时的测试套件。

发布清单不是自动的:开发人员手动运行清单上的每个项目。我们发现重要的是要让一个人处于循环中。即使通过测试本身,有时在运行清单项目时也会发现问题。至关重要的是,要让人员在最高级别上查看测试输出,并不断问“这是真的吗?”

发布清单不断发展。发现新问题或潜在问题后,将添加新的清单项目,以确保这些问题不会在后续发行版中出现。事实证明,发行清单是帮助确保发行过程中没有被忽视的宝贵工具。

11.静态分析

静态分析意味着在编译时分析源代码以检查其正确性。静态分析包括编译器警告消息和更深入的分析引擎,例如 Clang Static Analyzer。在Linux和Mac以及Windows的MSVC上,SQLite使用-Wall和-Wextra标志在GCC和Clang上进行编译时不会发出警告。Clang静态分析器工具“ scan-build”也不会生成任何有效的警告(尽管最新版本的clang似乎会生成许多误报。)但是,其他静态分析器可能会生成一些警告。鼓励用户不要对这些警告施加压力,而应在上述SQLite的严格测试中寻求安慰。

静态分析对查找SQLite中的错误没有帮助。静态分析在SQLite中发现了一些错误,但这是例外。与静态分析发现的错误相比,SQLite在尝试使其不发出警告的情况下进行编译时引入了更多错误。

12.总结

SQLite是开源的。这给许多人以为它没有作为商业软件进行过很好的测试并且可能不可靠。但是那种印象是错误的。SQLite在该领域表现出很高的可靠性,而且缺陷率非常低,尤其是考虑到它的发展速度。SQLite的质量部分是通过精心设计和实现的代码来实现的。但是,广泛的测试在维护和提高SQLite的质量方面也起着至关重要的作用。本文档总结了每个SQLite版本都经过的测试程序,以期激发人们对SQLite适用于关键任务应用程序的信心。