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() 宏。
在 SQLite 中,assert(X) 的存在意味着开发人员拥有 X 始终为真的证明。读者可以依赖 X 为真来帮助他们理解代码。assert(X) 是关于 X 真实的强烈声明。毫无疑问。
ALWAYS(X) 和 NEVER(X) 宏是对 X 真实性的较弱声明。ALWAYS(X) 或 NEVER(X) 的存在意味着开发人员认为 X 始终为真或永远不为真,但没有证明,或者证明很复杂且容易出错,或者证明依赖于系统其他可能发生变化的方面。
其他系统有时以与 SQLite 中使用 ALWAYS(X) 或 NEVER(X) 类似的方式使用 assert(X)。开发人员会添加 assert(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 的开发、测试和维护将会变得更加困难。
使用三种不同的构建来验证 SQLite 软件。
所有测试必须在所有三种构建中给出相同的答案。有关更多详细信息,请参阅 "如何测试 SQLite" 文档。
各种类似 assert() 的宏的行为会根据 SQLite 的构建方式而有所不同。
功能测试 | 覆盖测试 | 发布 | |
---|---|---|---|
assert(X) | 如果 X 为假,则 abort() | 无操作 | 无操作 |
ALWAYS(X) | 如果 X 为假,则 abort() | 始终为真 | 传递值 X |
NEVER(X) | 如果 X 为真,则 abort() | 始终为假 | 传递值 X |
testcase(X) | 无操作 | 如果 X 为真,则执行一些无害的操作 | 无操作 |
标准 C 中 assert(X) 的默认行为是在发布构建中启用它。这是一个合理的默认值。但是,SQLite 代码库在代码的性能敏感区域中包含许多 assert() 语句。保持 assert(X) 启用会导致 SQLite 的运行速度降低约三倍。此外,SQLite 努力在交付配置中提供 100% 的 MC/DC,如果启用 assert(X) 语句,这显然是不可能的。由于这些原因,在 SQLite 中,发布构建的 assert(X) 是一个无操作。
ALWAYS(X) 和 NEVER(X) 宏在功能测试期间的行为类似于 assert(X),因为开发人员希望在 X 的值与预期值不同时立即收到警报。但是对于交付,ALWAYS(X) 和 NEVER(X) 只是简单的直通宏,提供了纵深防御。对于覆盖测试,ALWAYS(X) 和 NEVER(X) 是硬编码的布尔值,因此它们不会导致生成无法访问的机器码。
testcase(X) 宏通常是一个无操作,但对于覆盖测试构建,它确实会生成少量额外的代码,其中至少包含一个分支,以验证存在 X 既为真又为假的测试用例。
assert() 语句通常用于验证内部函数和方法的先决条件。例如:https://sqlite.ac.cn/src/artifact/c1e97e4c6f?ln=1048。这被认为比在头文件注释中简单地陈述先决条件更好,因为 assert() 实际上是执行的。在一个经过高度测试的程序(如 SQLite)中,读者知道先决条件对于针对 SQLite 运行的数亿个测试用例来说都是正确的,因为它已通过 assert() 验证。相反,头文件注释中的文本先决条件语句是未经测试的。它在编写代码时可能为真,但现在谁又能说它仍然为真呢?
有时 SQLite 使用编译时可评估的 assert() 语句。考虑 https://sqlite.ac.cn/src/artifact/c1e97e4c6f?ln=2130-2138 中的代码。四个 assert() 语句验证编译时常量的值,以便读者可以快速检查 if 语句的有效性,而无需在单独的头文件中查找常量值。
有时编译时 assert() 语句用于验证 SQLite 是否已正确编译。例如,https://sqlite.ac.cn/src/artifact/c1e97e4c6f?ln=157 中的代码验证了 SQLITE_PTRSIZE 预处理器宏是否为目标体系结构正确设置。
CORRUPT_DB 宏用于许多 assert() 语句。在功能测试构建中,CORRUPT_DB 引用一个全局变量,如果数据库文件可能包含损坏,则该变量为真。该变量默认情况下为真,因为我们通常不知道数据库是否损坏,但在测试期间(在处理已知格式良好的数据库时),该全局变量可以设置为假。然后可以在 assert() 语句中使用 CORRUPT_DB 宏,如 https://sqlite.ac.cn/src/artifact/18a53540aa3?ln=1679-1680 中所示。这些 assert() 指定了该例程的先决条件,这些条件对于一致的数据库文件是正确的,但如果数据库文件已损坏,则可能为假。了解这类条件对于试图孤立地理解代码块的读者来说非常有用。
ALWAYS(X) 和 NEVER(X) 函数用于我们始终希望执行测试的位置,即使开发人员认为 X 的值始终为真或为假。例如,显示的 sqlite3BtreeCloseCursor() 例程必须从所有游标的链表中删除关闭的游标。我们知道该游标位于链表上,因此循环必须通过 "break" 语句终止,但使用 https://sqlite.ac.cn/src/artifact/18a53540aa3?ln=4371 中的 ALWAYS(X) 测试很方便,以防止在代码其他部分发生错误而损坏链表的情况下运行到链表末尾。
ALWAYS(X) 或 NEVER(X) 有时会验证先决条件,如果代码的其他部分以细微的方式修改,这些先决条件可能会发生变化。在 https://sqlite.ac.cn/src/artifact/18a53540aa3?ln=5512-5516 中,我们对两个先决条件进行了测试,这些先决条件仅由于 sqlite3BtreeRowCountEst() 函数的使用范围有限而为真。对 SQLite 的未来增强可能会以新的方式使用 sqlite3BtreeRowCountEst(),在这种情况下,这些先决条件不再成立,并且 NEVER() 宏会在这种情况出现时快速提醒开发人员。但是,如果由于某种原因,在发布构建中未满足先决条件,程序仍将正常运行,并且不会执行未定义的内存访问。
testcase() 宏通常用于验证不等式比较的边界情况是否已检查。例如,在 https://sqlite.ac.cn/src/artifact/18a53540aa3?ln=5766 中。这类检查有助于防止出现溢出错误。
此页面上次修改时间为 2022-01-08 05:02:57 UTC