#test-framework #safety #compiler #test #test-suite

sleuth

极其有见地的测试框架,生成精确规范并简化代码到最小实现

7 个版本

0.2.1 2023年5月3日
0.2.0 2023年5月3日
0.1.4 2023年5月3日
0.1.3 2023年4月30日

#244 in 测试

Download history 94/week @ 2024-03-10 4/week @ 2024-03-17 2/week @ 2024-03-31

每月下载量 70

Zlib 许可证

25KB
367 代码行

Sleuth

极其有见地的测试框架,生成精确规范并简化代码到最小实现。

概述

此库将变异测试的理念推向极致。

变异测试认为你源代码中的小错误(如 a > b 而不是 a >= b)很可能是错误;当它们发生时,你的测试应该失败,变异测试框架确保它们这样做。当你的测试套件无法识别一个错误时,这意味着你需要添加一个新的测试。

Rust 已经有我非常钦佩且不打算取代的变异测试框架。此库是对变异 所有 代码的实验,通过蛮力证明没有比你所写的函数更短的函数能满足你的规范。它应该鼓励测试驱动开发,无需在事先编写详尽的测试套件;经过初步尝试后,你的测试和实现将随着你遇到所有可能的实现方式而共同进化,最终缩小到你最初想要的目标。

此库还受到了像 Haskell 的 QuickCheck 这样的属性测试的启发。你可以使用可重用的属性编写测试,并通过 ! 指示它不要通过,如下所示

use ::sleuth::sleuth;

fn roundtrip<T, U, F, G>(f: F, g: G, x: T) -> bool
  where
    T: PartialEq + Clone,
    F: Fn(U) -> T,
    G: Fn(T) -> U,
{
    x.clone() == f(g(x))
}

#[sleuth(
    roundtrip(sub_one, 42),
    !roundtrip(add_one, 42),
)]
fn add_one(x: u8) -> u8 {
    x + 1
}

#[sleuth(
    roundtrip(add_one, 42),
    !roundtrip(sub_one, 42),
)]
fn sub_one(x: u8) -> u8 {
    x - 1
}

注意以下几点

  • roundtrip 返回一个布尔值来指示成功或失败,而不是像常规测试一样 panic
  • sleuth 属性接受一个带有缺少第一个参数的函数(这里,roundtrip 没有提供 f),该参数在编译时用应用该属性到的函数(这里,add_onesub_one)填充。
  • 我们不向roundtrip发送大量随机输入(尽管你可以单独这样做!)。相反,你选择你的输入(在这里,两次都是42),库帮助你找到能够排除所有变异的最小集合。

库仍在开发中——我是一名大学生,经常缺乏空闲时间——但当前,它可以运行像上面的测试套件,并且不久之后应该能够简化以下常见的案例

fn is_true(b: bool) -> bool {
    if b { true } else { false }
}

到它们的最小实现

fn is_true(b: bool) -> bool {
    b
}

这通常是变异测试无法(也不旨在)修复的。Clippy可以基于白名单修复这些简单的错误,但这个库旨在从底层消除整个类别的错误,而不是玩击鼓传花。

它是如何工作的

给定一个像这样的函数

#[sleuth(
    does_nothing(false),
    does_nothing(true),
)]
const fn is_true(b: bool) -> bool {
    if b { true } else { false }
}

sleuth属性在编译时将其重写为以下内容

const fn is_true(b: bool) -> bool {
    if b { true } else { false }
}

#[cfg(test)]
mod is_true_sleuth {
    use super::*;

    // A struct with each scoped variable.
    struct Scope { pub b: bool }

    // A fully implemented AST node for each function argument.
    mod scoped_variables {
        pub struct B; // CamelCase by convention but referring to the argument `b`
        impl ::sleuth::Expr for B {
            type Return = bool;
            type Scope = super::Scope;
            const COMPLEXITY: usize = 1;
            fn eval(&self, scope: &mut Self::Scope) -> Self::Return {
                scope.b
            }
        }
    }

    // Instantiation of a unique type for the exact AST of `is_true`.
    type Ast = ::sleuth::expr::{... many lines, lots of generics ...};
    const AST: Ast = ::sleuth::expr::{... instantiation of the above ...};

    // Checks, for any function, whether it fulfills the test suite for `is_true`.
    pub fn check<_FnToCheck>(f: &_FnToCheck) -> Option<&'static str>
      where
        _FnToCheck: Fn(bool) -> bool + ::core::panic::RefUnwindSafe
    {
        match std::panic::catch_unwind(|| crate::sleuth::does_nothing(f, false)) {
            Ok(b) => if !b { Some("crate::sleuth::does_nothing(f, false)") } else { None },
            _ => Some("crate::sleuth::does_nothing(f, false) PANICKED (see two lines above)"),
        }
            .or_else(|| match std::panic::catch_unwind(|| crate::sleuth::does_nothing(f, true)) {
                Ok(b) => if !b { Some("crate::sleuth::does_nothing(f, true)") } else { None },
                _ => Some("crate::sleuth::does_nothing(f, true) PANICKED (see two lines above)"),
            })
    }

    #[test]
    fn test_original() {
        // check that the original function passes before mutating
        ::sleuth::testify(check(&is_true))
    }
    
    #[test]
    fn test_mutants() {
        use ::sleuth::Expr;

        // If there's something wrong with our AST, pass and don't mutate
        // And if it's an error in your logic, `test_original` won't pass
        if check(&(|b| AST.eval(&mut Scope { b }))).is_some() { return; }

        // breadth-first search
        for mutation_severity in 0usize..=AST::COMPLEXITY {
            // . . .
            // very long, not yet complete
            // . . .
            // eventually, with a different value in place of `AST` each time:
            ::sleuth::testify(check(&(|b| AST.eval(&mut Scope { b }))))
        }
    }
}

你可以通过运行EXPAND=1 cargo expand来查看这个展开。环境变量EXPAND#[cfg(test)]#[test]转换为无意义的内容,这样cargo expand就不会假设我们不是在测试,并删除整个模块。

注意,除非我们在测试中,否则该宏没有任何效果。即使在生产中也可以安全使用。

依赖关系

~0.6–11MB
~77K SLoC