Small. Fast. Reliable.
Choose any three.
在SQLite中使用assert()

1. SQLite中的Assert()和类似宏

assert(X)宏是 <assert.h>头文件中标准C的一部分。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始终为true。读者可以依靠X为真来​​帮助他们推理代码。assert(X)是有关X真相的有力说明。毫无疑问。

ALWAYS(X)和NEVER(X)宏对X的真实性的陈述较弱。ALWAYS(X)或NEVER(X)的存在意味着开发人员认为X始终为真或从不为真,但没有证据,或者证明很复杂且容易出错,或者证明取决于系统中可能发生变化的其他方面。

其他系统有时以类似于SQLite中ALWAYS(X)或NEVER(X)的方式使用assert(X)。开发人员将添加断言(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的构建方式而有所不同。

功能测试覆盖率测试释放
断言(X) 如果X为假,则abort() 无操作 无操作
总是(X) 如果X为假,则abort() 永远是真的 通过值X
从来没有(X) 如果X为true,则abort() 永远是假的 通过值X
测试用例(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均为真且不存在的测试用例。错误的。

2.例子

assert()语句通常用于验证内部函数和方法的前提条件。示例:https : //sqlite.org/src/artifact/c1e97e4c6f?ln=1048。因为assert()实际上是执行的,所以认为这比在标题注释中简单地说明前提条件要好。在像SQLite这样经过高度测试的程序中,读者知道先决条件对于针对SQLite运行的数亿个测试用例都是正确的,因为该条件已经由assert()进行了验证。相反,标题注释中的文本前提条件语句未经测试。编写代码时可能确实如此,但是谁又说它现在仍然正确呢?

有时,SQLite使用编译时可评估的assert()语句。考虑以下代码,网址https://sqlite.org/src/artifact/c1e97e4c6f?ln=2130-2138。四个assert()语句验证编译时常量的值,以便读者可以快速检查随后的if语句的有效性,而不必在单独的头文件中查找常量值。

有时,编译时assert()语句用于验证SQLite是否已正确编译。例如,https ://sqlite.org/src/artifact/c1e97e4c6f?ln = 157上的代码 可验证是否为目标体系结构正确设置了SQLITE_PTRSIZE预处理程序宏。

CORRUPT_DB宏用于许多assert()语句中。在功能测试版本中,CORRUPT_DB引用一个全局变量,如果数据库文件可能包含损坏,则该变量为true。默认情况下,此变量为true,因为我们通常不知道数据库是否损坏,但是在测试已知格式正确的数据库时,可以将全局变量设置为false。然后可以在assert()语句中使用CORRUPT_DB宏,例如 https://sqlite.org/src/artifact/18a53540aa3?ln=1679-1680。这些assert()指定例程的前提条件,这些前提条件对于一致的数据库文件为true,但如果数据库文件已损坏,则可能为false。对试图独立理解代码块的读者来说,了解这类条件非常有帮助。

即使开发人员认为X的值始终为true或false,我们总是希望进行测试的地方使用ALWAYS(X)和NEVER(X)函数。例如,显示的sqlite3BtreeCloseCursor()例程必须从所有游标的链接列表中删除结束游标。我们知道游标在列表中,因此循环必须以“ break”语句终止,但是在https://sqlite.org/src/artifact/18a53540aa3上使用ALWAYS(X)测试很方便 吗? ln = 4371可以防止在链表损坏的代码的其他部分出现错误的情况下运行链表的末尾。

如果以微妙的方式修改了代码的其他部分,则ALWAYS(X)或NEVER(X)有时会验证可能会更改的前提条件。在https://sqlite.org/src/artifact/18a53540aa3?ln=5512-5516, 我们对两个前提条件进行了测试,这些前提条件仅由于sqlite3BtreeRowCountEst()函数的使用范围有限而为真。将来对SQLite的增强可能会以新的方式使用sqlite3BtreeRowCountEst()来满足这些前提条件,并且NEVER()宏会在情况出现时迅速提醒开发人员该事实。但是,如果由于某种原因,在发行版本中不满足前提条件,则程序仍将保持理智的行为,并且不会进行未定义的内存访问。

testcase()宏通常用于验证是否检查了不等式比较的边界情况。例如,在 https://sqlite.org/src/artifact/18a53540aa3?ln=5766。这些检查有助于防止一次失误。