36 个版本
| 新增 0.6.4 | 2024 年 8 月 20 日 |
|---|---|
| 0.6.3 | 2024 年 7 月 23 日 |
| 0.6.1 | 2024 年 6 月 28 日 |
| 0.4.6 | 2024 年 3 月 19 日 |
| 0.1.0-beta.6 | 2022 年 12 月 5 日 |
34 在 #remove
每月 330 次下载
用于 3 个 包
81KB
2.5K SLoC
Necessist
通过移除语句和方法调用运行测试,以帮助识别损坏的测试
Necessist 目前支持 Anchor (TS)、Foundry、Go、Hardhat (TS) 和 Rust。
Necessist 的论文(《Test Harness Mutilation》)将出现在 Mutation 2024。 (Test Harness Mutilation)。 (幻灯片, 预印本)
内容
安装
系统要求
在您的系统上安装 pkg-config 和 sqlite3 开发文件,例如在 Ubuntu 上
sudo apt install pkg-config libsqlite3-dev
从 crates.io 安装 Necessist
cargo install necessist
从 github.com 安装 Necessist
cargo install --git https://github.com/trailofbits/necessist --branch release
概述
Necessist 逐步从测试中移除语句和方法调用,然后运行它们。如果移除语句或方法调用后测试仍然通过,这可能表明测试存在问题。或者更糟糕的是,这可能表明正在测试的代码存在问题。
示例
此示例来自 rust-openssl。 verify_untrusted_callback_override_ok 测试检查失败的证书验证是否可以通过回调覆盖。但如果回调从未被调用(例如,由于失败的连接),测试仍然会通过。Necessist 通过显示测试在调用 set_verify_callback 的情况下通过来揭示这一事实
#[test]
fn verify_untrusted_callback_override_ok() {
let server = Server::builder().build();
let mut client = server.client();
client
.ctx()
.set_verify_callback(SslVerifyMode::PEER, |_, x509| { //
assert!(x509.current_cert().is_some()); // Test passes without this call
true // to `set_verify_callback`.
}); //
client.connect();
}
在此发现之后,测试中增加了一个标志,以记录是否调用了回调。必须设置该标志才能使测试通过
#[test]
fn verify_untrusted_callback_override_ok() {
static CALLED_BACK: AtomicBool = AtomicBool::new(false); // Added
let server = Server::builder().build();
let mut client = server.client();
client
.ctx()
.set_verify_callback(SslVerifyMode::PEER, |_, x509| {
CALLED_BACK.store(true, Ordering::SeqCst); // Added
assert!(x509.current_cert().is_some());
true
});
client.connect();
assert!(CALLED_BACK.load(Ordering::SeqCst)); // Added
}
与传统突变测试的比较
点击展开
传统的突变测试旨在识别测试覆盖率缺口,而Necessist旨在识别现有测试中的错误。
传统的突变测试工具(例如universalmutator)会随机将故障注入源代码中,并观察代码的测试是否仍然通过。如果它们通过了,这可能意味着代码的测试不足。
值得注意的是,传统的突变测试是关于找到测试集合中的缺陷,而不是单个测试中的缺陷。也就是说,对于任何给定的测试,随机向代码中注入故障不太可能揭示该测试中的错误。这是不幸的,因为有些测试比其他测试更重要,例如,确保代码某些部分的正确性比其他部分更重要。
相比之下,Necessist通过迭代删除语句和方法调用的方法确实针对单个测试,因此可以揭示单个测试中的错误。
当然,这两种方法可以发现的问题集合存在重叠,例如,未能找到注入的故障可能表明测试中存在错误。然而,正如前面所述的原因,我们认为这两种方法是互补的,而不是相互竞争的。
可能的理论基础
点击展开
以下标准(*)几乎描述了Necessist旨在删除的语句
- (
*)语句S的最弱前件P与S的后置条件Q具有相同的上下文(例如,作用域内的变量),并且P不意味着Q。
(*)试图捕捉的概念是:影响后续断言的语句。在本节中,我们将解释和论证这个选择。为了简洁起见,我们关注语句,但本节中的注释也适用于方法调用。
回顾两种谓词变换语义:最弱前件和最强后件。前者是,在给定语句之后的后件成立的情况下,推理在语句之前可能成立的最弱前件。后者是,在给定语句之前的前件成立的情况下,推理在语句之后可能成立的最强后件。一般来说,前者更常见(参见Aldrich 2013的解释),并且这是我们在这里使用的。
通过这个视角考虑一个测试。测试是一个没有输入或输出的函数。因此,确定测试是否通过的替代程序如下。从True开始,迭代地通过测试的语句向后工作,计算每个语句的最弱前件。如果得到测试的第一个语句的前件是True,则测试通过。如果前件是False,则测试失败。
现在,想象我们应用这个程序,考虑一个违反(*)的语句S。我们争辩说,移除S可能没有意义
情况1:S添加或删除作用域中的变量(例如,S是声明),或者S更改变量的类型。那么移除S可能会导致编译失败。(此外,由于S的前件和后件具有不同的上下文,因此不清楚如何比较它们。)
案例2:S的前置条件比其后置条件更强(例如,S是一个断言)。在这种情况下,S对其执行环境施加了约束。换句话说,S正在测试某些内容。因此,移除S可能会损害测试的总体目的。
相反,考虑一个满足(*)的语句S。以下是为什么可能需要移除S的原因。将S视为转换有效环境集,而不是对其进行约束。更准确地说,如果S的最弱前置条件P不蕴含Q,并且如果Q是可满足的,那么存在一个将P和Q的自由变量赋值的分配,该分配满足P和Q。如果这种分配来自S实际执行的所有环境,那么S的必要性就值得怀疑。
*的主要效用在于帮助选择Necessist忽略的函数、宏和方法调用。Necessist默认忽略其中的一些。假设对于某个框架,我们正在考虑Necessist是否应该忽略某些函数foo。如果我们想象框架测试语言的谓词转换语义,我们可以问:如果语句S是调用foo,那么S是否会满足(*)?如果答案是“否”,那么Necessist可能应该忽略foo。
以Rust的clone方法为例。对clone的调用可能是多余的。然而,如果我们想象Rust的谓词转换语义,对clone的调用不太可能满足(*)。因此,Necessist不尝试移除clone调用。
除了帮助选择Necessist忽略的函数、宏等之外,(*)还有其他一些很好的后果。例如,测试中最后一条语句应该被忽略的规则来源于(*)。要看到这一点,请注意此类语句的后置条件Q始终是True。因此,如果该语句没有改变上下文,那么其最弱前置条件必然蕴含Q。
虽然如此,(*)并不完全捕捉Necessist实际上做什么。考虑一个像x -= 1;这样的语句。Necessist将无条件地移除此类语句,但(*)表示可能不应该这样做。假设启用了溢出检查,则计算此语句的最弱前置条件的计算可能如下所示
{ Q[(x - 1)/x] ^ x >= 1 }
x -= 1;
{ Q }
请注意,x -= 1;不会改变上下文,并且Q[(x - 1) / x]^x >= 1可能意味着Q。例如,如果Q不包含x,则Q[(x - 1) / x] = Q和Q ^ x >= 1意味着Q。
鉴于*与Necessist当前行为之间的差异,人们可能会问:应该调整哪一个?换句话说,Necessist是否应该无条件地删除像x -= 1;这样的语句?
一种看待这个问题的方式是:哪些语句值得删除,即哪些语句是“有趣的”?如上所述,*认为如果一个语句的删除可能会影响后续的断言,那么这个语句是“有趣的”。但还有其他可能的、有用的“有趣”语句的定义。例如,可以考虑上述的最强后置条件,或者除了Hoare逻辑之外的其他框架。
为了明确起见,Necessist并不正式地应用*,例如,Necessist实际上并不计算最弱前置条件。当前*的角色是帮助指导Necessist应该忽略哪些语句,并且*似乎在这个角色中做得很好。因此,我们将解决上述差异的工作留待将来进行。
用法
Usage: necessist [OPTIONS] [TEST_FILES]... [-- <ARGS>...]
Arguments:
[TEST_FILES]... Test files to mutilate (optional)
[ARGS]... Additional arguments to pass to each test command
Options:
--allow <WARNING> Silence <WARNING>; `--allow all` silences all warnings
--default-config Create a default necessist.toml file in the project's root directory
--deny <WARNING> Treat <WARNING> as an error; `--deny all` treats all warnings as errors
--dump Dump sqlite database contents to the console
--dump-candidates Dump removal candidates and exit (for debugging)
--framework <FRAMEWORK> Assume testing framework is <FRAMEWORK> [possible values: anchor, auto, foundry, go, hardhat, rust]
--no-dry-run Do not perform dry runs
--no-sqlite Do not output to an sqlite database
--quiet Do not output to the console
--reset Discard sqlite database contents
--resume Resume from the sqlite database
--root <ROOT> Root directory of the project under test
--timeout <TIMEOUT> Maximum number of seconds to run any test; 60 is the default, 0 means no timeout
--verbose Show test outcomes besides `passed`
-h, --help Print help
-V, --version Print version
输出
默认情况下,Necessist仅在测试通过时向控制台输出。使用--verbose将导致Necessist输出以下所有删除结果。
| 结果 | 意义(删除语句/方法调用后...) |
|---|---|
| 通过 | 构建并通过了测试。 |
| 超时 | 构建了测试但超时。 |
| 失败 | 构建了测试但失败。 |
| 无法构建 | 测试未构建。 |
默认情况下,Necessist会将输出同时发送到控制台和sqlite数据库。对于后者,可以使用sqlitebrowser等工具来过滤/排序结果。
详细信息
一般来说,Necessist不会尝试删除以下类型的语句
- 包含其他语句的语句(例如,一个
for循环) - 声明(例如,局部或
let绑定) break、continue或return- 测试中的最后一个语句
同样,如果以下条件成立,Necessist也不会尝试删除方法调用
- 它是包围语句的主要效果(例如,
x.foo();)。 - 它出现在被忽略的函数、方法或宏的参数列表中(见下文)。
对于某些框架,某些语句和方法会被忽略。点击框架查看其具体信息。
锚点TS
忽略的函数
assert- 以
assert.开头的内容(例如,assert.equal) - 以
console.开头的内容(例如,console.log) expect
被忽略的方法
toNumbertoString
Foundry
除了以下内容外,Foundry 框架还会忽略
- 紧随
vm.prank或任何形式的vm.expect(例如,vm.expectRevert)之后的一个语句 emit语句
忽略的函数
- 以
assert开头的内容(例如,assertEq) - 以
vm.expect开头的内容(例如,vm.expectCall) - 以
console.log开头的内容(例如,console.log,console.logInt) - 以
console2.log开头的内容(例如,console2.log,console2.logInt) vm.getLabelvm.label
Go
除了以下内容外,Go 框架还会忽略
defer语句
忽略的函数
- 以
assert.开头的内容(例如,assert.Equal) - 以
require.开头的内容(例如,require.Equal) panic
被忽略的方法*
CloseErrorErrorfFailFailNowFatalFatalfLogLogfParallelSkipSkipfSkipNow
* 此列表主要基于 testing.T 的方法。然而,为了防止与其他类型的冲突,省略了一些常见名称的方法。
Hardhat TS
被忽略的函数和方法与上述 Anchor TS 相同。
Rust
被忽略的宏
assertassert_eqassert_matchesassert_neeprinteprintlnpanicprintprintlnunimplementedunreachable
被忽略的方法*
as_bytesas_encoded_bytesas_mutas_mut_os_stras_mut_os_stringas_mut_sliceas_mut_stras_os_stras_pathas_refas_sliceas_strborrowborrow_mutcloneclonedcopiedderefderef_mutexpectexpect_errinto_boxed_bytesinto_boxed_os_strinto_boxed_pathinto_boxed_sliceinto_boxed_strinto_bytesinto_encoded_bytesinto_os_stringinto_ownedinto_path_bufinto_stringinto_veciteriter_mutsuccessto_os_stringto_ownedto_path_bufto_stringto_vecunwrapunwrap_err
* 此列表基本上是 Dylint 的 unnecessary_conversion_for_trait lint 的监视特性和固有方法,以下为添加项
clone(例如std::clone::Clone::clone)cloned(例如:std::iter::Iterator::cloned)copied(例如:std::iter::Iterator::copied)expect(例如:std::option::Option::expect)expect_err(例如:std::result::Result::expect_err)into_owned(例如:std::borrow::Cow::into_owned)success(例如:assert_cmd::assert::Assert::success)unwrap(例如:std::option::Option::unwrap)unwrap_err(例如:std::result::Result::unwrap_err)
配置文件
配置文件允许用户根据项目定制Necessist的行为。该文件必须命名为necessist.toml,出现在项目的根目录中,并且是toml编码。该文件可以包含以下列出的一个或多个选项。
-
ignored_functions,ignored_methods,ignored_macros:字符串列表,被解释为模式。如果函数、方法或宏(分别)的路径与列表中的模式匹配,则会被忽略。请注意,ignored_macros目前只由Rust框架使用。 -
ignored_path_disambiguation:字符串Either、Function或Method之一。对于可能指代函数或方法的路径(见下文),此选项影响是否忽略函数或方法。-
Either(默认):如果路径匹配ignored_functions或ignored_methods模式,则忽略。 -
Function:只有当路径匹配ignored_functions模式时才忽略。 -
Method:只有当路径匹配ignored_methods模式时才忽略。
-
-
ignored_tests:一个字符串列表。如果一个测试的名称与列表中的某个字符串完全匹配,则该测试将被忽略。对于基于Mocha的框架(例如Anchor和Hardhat),测试名称被视为传递给it的消息。
模式
模式是由字母、数字、.、_或*组成的字符串。除了*之外的每个字符都被当作文字处理,并且仅匹配自身。一个*可以匹配任何字符串,包括空字符串。
以下是一些模式的示例
assert:仅匹配自身assert_eq:仅匹配自身assertEqual:仅匹配自身assert.Equal:仅匹配自身assert.*:匹配assert.Equal,但不匹配assert、assert_eq或assertEqualassert*:匹配assert、assert_eq、assertEqual和assert.Equal*.Equal:匹配assert.Equal,但不匹配Equal
注意
路径
路径是由.分隔的标识符序列。以下是一个示例(来自Chainlink)
operator.connect(roles.oracleNode).signer.sendTransaction({
to: operator.address,
data,
}),
在上面的例子中,operator.connect和signer.sendTransaction是路径。
但是请注意,像operator.connect这样的路径是不确定的
- 如果
operator指的是包或模块,那么operator.connect指的是一个函数。 - 如果
operator指的是一个对象,那么operator.connect指的是一个方法。
默认情况下,Necessist忽略这样的路径,如果它匹配一个ignored_functions或ignored_methods模式。将上述ignored_path_disambiguation选项设置为Function或Method会导致Necessist仅在该路径匹配一个ignored_functions或ignored_methods模式时忽略它(分别对应)。
限制
-
慢。修改测试需要重新构建它们。在即使是中等规模的代码库上运行Necessist可能需要几个小时。
-
三诊需要深入了解源代码。一般来说,Necessist不会产生“明显”的bug。根据我们的经验,决定一个语句/方法调用是否是必要的需要深入了解正在测试的代码。Necessist最适合在具有(或打算拥有)这种知识的代码库上运行。
语义版本控制策略
我们保留更改以下内容并视为非破坏性更改的权利
- Necessist默认忽略的语法
以下更改将伴随至少Necessist的次要版本号的升级
- 输出移除候选者的顺序
- 在necessist.db中存储记录的顺序
目标
- 如果项目使用支持框架,那么在项目目录下使用
cd命令进入并输入necessist(不带参数)应该会产生有意义的输出。
反目标
- 成为一个通用的突变测试工具。已经存在一些优秀的此类工具(例如,
universalmutator)。
参考文献
- Groce, A., Ahmed, I., Jensen, C., McKenney, P.E., Holmes, J.: 我的代码如何经过验证(或测试)?基于错误驱动的验证和测试。Autom. Softw. Eng. 25, 917–960 (2018)。预印本可在此处找到。参见第2.3节。
许可
Necessist在AGPLv3许可下授权和分发。如果您需要关于条款的例外情况,请联系我们:联系我们。
依赖项
~40–54MB
~1M SLoC