#测试工具 #变异 #Python #模块 #pytest #变异体

bin+lib pymute

PyMute:用 Rust 编写的 Python/Pytest 变异测试工具

3 个版本

0.1.2 2024 年 4 月 30 日
0.1.1 2024 年 4 月 30 日
0.1.0 2024 年 4 月 30 日

#148 in 测试

无许可证

62KB
1K SLoC

PyMute:用 Rust 编写的 Python/Pytest 变异测试工具

PyMute 启发于我使用 cargo mutants 的经验。我在一个 Rust 项目中使用了它,非常喜欢,因此我不得不为 Python/pytest 项目寻找类似的东西。很快在几种解决方案中发现了 mut.pymut.py 的 pynguin 分支,但它们似乎在不同 Python 版本之间不够稳定。

PyMute 采用了一种相当简单的方法,为每个变异体创建一个临时目录,然后在那个工作目录中独立运行 pytest(注意,如果 pymute 被中断,可能需要手动清理 /tmp)。变异体通过简单地操作 *.py 文件中的文本插入,而不是操作 AST,因此应该能在大多数版本上工作。

安装

要安装 pymute,请确保您的系统已安装 Rust 和 cargo(检查 cargo --version)。您可以根据此处提供的说明安装 Rust 及其工具链:https://rust-lang.net.cn/tools/install

您可以通过 cargo 从 crates.io 安装 pymute

cargo install pymute

或者,您可以通过 cargo 从 GitHub 安装 pymute

cargo install --git https://github.com/LeSasse/pymute.git

使用 pymute --versionpymute --help 验证安装的正确性。

如何运行它

PyMute 允许您使用两种不同的运行器在变异体上运行测试

  1. Pytest
  2. Tox

如果您使用 pytest(也是默认运行器),则 PyMute 假设您的 pytest 测试可以从 Python 项目的根目录运行,并且可以在独立的 Python 项目的副本中运行而不会失败。例如,如果您有一个位于

~/projects/my_project

的项目,并将其复制为

cp -r ~/projects/my_project /tmp/my_project

那么您应该能够运行复制中的测试,如下所示

cd /tmp/my_project
python -m pytest .

重要的是,由于这种方法不负责设置环境或安装您的软件包,因此需要使用python -m pytest调用测试本地副本而不是已安装版本,以确保每个突变体都能正确运行测试。如果您使用的是没有src布局(请参阅https://blog.ionelmc.ro/2014/05/25/python-packaging/了解src布局的定义),那么这通常是正确的,根据pytest文档,使用python -m pytest .调用从项目根目录运行实际上将运行针对本地模块的测试,而不是已安装的版本。

然而,pytest和导入可能相当令人困惑,如果您不确定,您可以使用tox来运行测试。这将创建一个虚拟环境并为每个突变体单独安装您的软件包,这样您就可以确保测试正确地针对您软件包的每个突变体版本运行。重要的是,您不需要运行所有的tox环境,但使用--environment选项,您可以运行特定的tox环境。当然,由于需要设置所有的tox环境,这种方法将大大减慢速度。

示例

此存储库包含一个带有一些基本测试的小型Python项目示例。如果您喜欢,可以克隆此存储库进行测试

# set up an environment with pytest and tox if you dont have it already
python -m venv .env
source .env/bin/activate
pip install tox pytest

git clone https://github.com/LeSasse/pymute.git
cd pymute
pymute example

这将给出以下输出

[MISSED] Mutant Survived:  +  replaced by  -  in file example/src/model.py on line 6
[MISSED] Mutant Survived: 5 replaced by 6 in file example/src/model.py on line 16
[MISSED] Mutant Survived: 0 replaced by 1 in file example/src/model.py on line 17
[MISSED] Mutant Survived: 0 replaced by 1 in file example/src/model.py on line 22
[MISSED] Mutant Survived: == replaced by != in file example/src/model.py on line 27

默认情况下,pymute只显示遗漏的突变体,即所有测试都通过的那些突变体。这非常有用,因为它告诉您可能已经将类似这些的缺陷引入到您的程序中,而您的测试却没有警告您。这些替换改变了程序的行为,即代表一个错误/回归,但测试仍然通过。换句话说,测试无法保护项目免受这些缺陷的影响,并且它们可能已被检入主分支。

然而,还有两个输出级别,即caughtprocess。您可以指定它们为

pymute example --output-level caught
pymute example --output-level process

caught级别将打印出您的测试成功捕获的突变体,这样就不会引入错误。而process级别将打印出底层pytesttox过程的全部输出。这对于验证过程是否真正运行正确非常有用(例如,您可能忘记激活正确的环境,并且pytesttox实际上未安装)。这很重要,因为pymute只会检查过程是否成功。

在更大的项目中

让我们使用一个更大的Python项目来测试pymute:julearn

使用pymute的方法运行长时间测试套件,可能对数百个突变体来说可能不可行。因此,pymute提供了仅对程序的部分子集进行突变以及仅运行测试的部分子集的选项。

让我们先尝试在整个项目上运行pymute。我们可以使用pytest,因为julearn使用src无布局,因此因此python -m pytest .调用应该在本地模块上运行测试,而不是在已安装的一个上。

# set up an environment with pytest and tox if you dont have it already
python -m venv .env
source .env/bin/activate
pip install tox pytest
git clone https://github.com/juaml/julearn.git
cd julearn
# we can install it to install all the dependencies
pip install ".[docs,deslib,viz,skopt,dev]"

我们可以设置线程数来控制并行运行的突变体数量。请注意,每个线程都需要你 /tmp 目录下的一些磁盘空间,因此你应该考虑到这一点,以免因为 /tmp 目录下空间不足而导致线程失败。我们可以将输出级别设置为 caught 来获取更多有关正在发生的事情的信息,然后运行

pymute . --output-level caught --num-threads 4

然而,这找到了一千多个突变体,似乎在 docs 和其他不属于包的文件夹中的文件上进行了突变。 pymute 将会在项目根目录(即 pymute 的第一个位置参数)下的任何位置查找突变体。相反,我们可以通过提供 --modules 选项来更加具体一些。这是一个 glob 表达式,指定 pymute 只在匹配它的文件中查找突变体。重要的是,pymute 会自动过滤掉以 "test_" 开头的文件和以 *_test.py 结尾的文件,以避免为 pytest 测试创建突变。同样重要的是,将 glob 表达式用字符串括起来,这样它就不会被你的 shell 实际解释为一个 glob 表达式,而是作为字符串传递给 pymute

pymute . --output-level caught --num-threads 4 --modules "julearn/**/*.py"

output for pymute . --output-level caught --num-threads 4 --modules "julearn/**/*.py"

然而,这仍然找到了大约 600 个突变体,并且运行速度相当慢。上面的输出运行了大约 10 分钟(gif 被加速)。有许多方法可以进一步细分突变体,或者细分运行的测试,以便进行更具体的测试,这不需要花费太多时间。

在整个包上运行突变体的随机子集

你可以通过指定 --max-mutants 选项在整个包上运行突变体的随机样本子集。每个单独的测试运行仍然会很慢,但总体上要少做,这样 pymute 就会更快地完成

pymute . --output-level caught --num-threads 4 --modules "julearn/**/*.py" --max-mutants 10

output for pymute . --output-level caught --num-threads 4 --modules "julearn/**/*.py" --max-mutants 10

这个命令花费了不到 5 分钟(gif 被加速),虽然它找到了一些有趣的 MISSED 突变,但每次运行仍然需要相当多的时间。

通常,你只想专注于提高特定模块的测试,因此运行整个测试套件是浪费时间。你创建或更改一些模块,然后你想对这些旨在捕捉这些模块回归的测试执行突变测试。你可以通过指定 --tests 选项来这样做,这将仅运行 python -m pytest 这些测试。例如,我们可能关注 julearn/model_selection 中的模块。我们可以这样运行

pymute . \
	--output-level caught \
	--num-threads 4 \
	--modules "julearn/model_selection/*.py" \
	--tests julearn/model_selection/tests

output specific tests

这次运行耗时 20 秒,gif 最终不必被加速来显示一些有趣的输出。我们现在可以轻松快速地检查 MISSED 突变体,并研究它们会如何改变该模块的一些公共 API 的行为,以及我们是否可以更好地测试这种改变的行为。这种方法在使用 pymute 时是推荐的,因为它允许进行快速的突变运行迭代。你可以改进测试,然后使用相同的命令再次运行 pymute,它应该会非常快。

细分突变类型

进一步细分 pymute 将运行的突变体的另一种方法是指定 --mutation-types 选项。这是一个由逗号分隔的类型列表。帮助文本(pymute --help)提供了以下选项

--mutation-types <MUTATION_TYPES>
	Mutation types
          
    [default: math-ops conjunctions booleans control-flow comp-ops numbers]

	Possible values:
		- math-ops:     Mutate mathematical operators (e.g. "*,+,-,/")
        - conjunctions: Mutate conjunctions in boolean expressions (e.g. "and/or")
        - booleans:     Mutate booleans (e.g. "True/False")
        - control-flow: Mutate control flow statements (e.g. if statements)
        - comp-ops:     Mutate comparison operators (e.g. "<,>,==,!=")
        - numbers:      Mutate numbers (e.g. off-by-one errors)

例如,如果要仅对数字和比较运算符进行变异,我们可以使用以下 --mutation-types 选项来运行之前的命令(gif 也未加速)

pymute . \
	--output-level caught \
	--num-threads 4 \
	--modules "julearn/model_selection/*.py" \
	--tests julearn/model_selection/tests \
	--mutation-types numbers,comp-ops

output mutation types

依赖项

~10-21MB
~296K SLoC