1. SQLite 中的 Assert() 和类似的宏
assert(X) 宏是 标准 C 的一部分,在 <assert.h> 头文件中。SQLite 添加了另外三个类似于 assert() 的宏,名为 NEVER(X)、ALWAYS(X) 和 testcase(X)。
assert(X) → assert(X) 语句表示条件 X 始终为真。换句话说,X 是不变量。assert(X) 宏像一个过程一样工作,因为它没有返回值。
ALWAYS(X) → ALWAYS(X) 函数表示就开发人员所知,条件 X 始终为真,但没有证据证明 X 为真,或者证明复杂且容易出错,或者证明取决于将来可能会更改的实施细节。ALWAYS(X) 的行为类似于返回布尔值 X 的函数,旨在用于“if”语句的条件中。
NEVER(X) → NEVER(X) 函数表示条件 X 永远不会为真。这是 ALWAYS(X) 函数的负模拟。
testcase(X) → testcase(X) 语句表明 X 有时为真,有时为假。换句话说,testcase(X) 表示 X 绝对不是不变量。由于 SQLite 使用 100% MC/DC 测试,testcase(X) 宏的存在表明 X 不仅可能为真或为假,而且有测试用例来证明这一点。
SQLite 版本 3.22.0 (2018-01-22) 包含 5290 个 assert() 宏、839 个 testcase() 宏、88 个 ALWAYS() 宏和 63 个 NEVER() 宏。
1.1. assert() 的哲学
在 SQLite 中,assert(X) 的存在意味着开发人员可以证明 X 始终为真。读者可以依靠 X 为真来帮助他们推理代码。assert(X) 是关于 X 真实性的有力陈述。毫无疑问。
ALWAYS(X) 和 NEVER(X) 宏是关于 X 真实性的较弱陈述。ALWAYS(X) 或 NEVER(X) 的存在意味着开发人员相信 X 永远或永远不为真,但没有证据,或者证明很复杂且容易出错,或者证明取决于系统的其他方面,这些方面似乎可能会发生变化。
其他系统有时使用 assert(X) 的方式类似于在 SQLite 中使用 ALWAYS(X) 或 NEVER(X)。开发人员将添加一个 assert(X) 作为 他们不完全相信 X 永远为真的默认确认. 我们认为这种对 assert(X) 的使用是错误的,并且违背了最初在 C 中使用 assert(X) 的意图和目的。assert(X) 不应被视为用于防止错误的安全网或顶绳。assert(X) 也不适合纵深防御。在这些情况下应该使用 ALWAYS(X) 或 NEVER(X) 宏或类似的东西,因为当程序员的推理结果为是错的。由于 ALWAYS(X) 或 NEVER(X) 之后的代码未经测试,因此它应该非常简单,如“return”语句,很容易通过检查验证。
因为 assert() 可能并且经常被滥用,所以一些编程语言理论家和设计者不喜欢它。例如,Go 编程语言的设计者 有意省略了一个内置的 assert()。他们认为滥用 assert() 造成的危害超过了将其作为内置语言包含在内的好处。SQLite 开发人员不同意。事实上,本文的最初目的是反驳 assert() 有害的普遍观念。根据我们的经验,如果没有 assert(),SQLite 的开发、测试和维护会困难得多。
1.2. 根据构建类型的不同行为
三个单独的构建用于验证 SQLite 软件。
- 功能测试构建用于验证源代码。
- 覆盖测试构建用于验证测试套件,以确认测试套件提供 100% MC/DC。
- 发布版本用于验证生成的机器代码。
所有测试必须在所有三个构建中给出相同的答案。有关详细信息,请参阅“如何测试 SQLite”文档。
各种类似 assert() 的宏根据 SQLite 的构建方式表现不同。
功能测试 | 覆盖率测试 | 发布 | |
---|---|---|---|
断言(X) | 如果 X 为假,则中止() | 空操作 | 空操作 |
总是(X) | 如果 X 为假,则中止() | 总是正确的 | 传递值 X |
从不(X) | 如果 X 为真,则中止() | 总是假的 | 传递值 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,因为我们通常不知道数据库是否损坏,但是在测试已知格式良好的数据库时,该全局变量可以设置为 false。然后可以在 assert() 语句中使用 CORRUPT_DB 宏,例如 https://sqlite.org/src/artifact/18a53540aa3?ln=1679-1680. 这些 assert() 为例程指定前提条件,这些前提条件对于一致的数据库文件为真,但如果数据库文件已损坏则可能为假。了解这些条件对于试图孤立地理解代码块的读者非常有帮助。
ALWAYS(X) 和 NEVER(X) 函数用在我们总是希望测试发生的地方,即使开发人员认为 X 的值总是 true 或 false。例如,所示的 sqlite3BtreeCloseCursor() 例程必须从所有游标的链表中删除关闭游标。我们知道游标在列表上,因此循环必须由“break”语句终止,但是使用 ALWAYS(X) 测试在 https://sqlite.org/src/artifact/18a53540aa3 上很方便? 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。这些类型的检查有助于防止差一错误。