# 变异分析

> 原文： [https://www.fuzzingbook.org/html/MutationAnalysis.html](https://www.fuzzingbook.org/html/MutationAnalysis.html)

在有关的[一章中，我们展示了如何识别程序的哪些部分由程序执行，从而了解一组测试用例在覆盖程序结构方面的有效性。 但是，仅覆盖可能不是衡量测试有效性的最佳方法，因为无需检查结果是否正确，覆盖范围就可以很大。 在本章中，我们介绍了另一种评估测试套件有效性的方法：将*突变* – *人为错误* –注入代码后，我们检查测试套件是否可以检测到这些人为的 故障。 这个想法是，如果它无法检测到此类突变，那么它也会错过真正的错误。](Coverage.html)

**前提条件**

*   您需要对程序的执行方式有所了解。
*   您应该已经阅读[关于覆盖](Coverage.html)的章节。

## 内容提要

要使用本章中提供的代码来[，请编写](Importing.html)

```py
>>> from [fuzzingbook.MutationAnalysis](MutationAnalysis.html) import <identifier>

```

然后利用以下功能。

本章介绍在主题程序上运行*突变分析*的两种方法。 第一类`MuFunctionAnalyzer`针对各个功能。 给定一个函数`gcd`并评估两个测试用例，一个可以对测试用例进行突变分析，如下所示：

```py
>>> for mutant in MuFunctionAnalyzer(gcd, log=True):
>>>     with mutant:
>>>         assert gcd(1, 0) == 1, "Minimal"
>>>         assert gcd(0, 1) == 1, "Mirror"
>>> mutant.pm.score()
->  gcd_1
<-  gcd_1

->  gcd_2
<-  gcd_2

->  gcd_3
<-  gcd_3

->  gcd_4
<-  gcd_4
Detected gcd_4 <class 'AssertionError'> Minimal

0.25

```

第二类`MuProgramAnalyzer`针对具有测试套件的独立程序。 给定一个程序`gcd`，其源代码在`gcd_src`中提供，而测试套件由`TestGCD`提供，则可以如下评估`TestGCD`的突变评分：

```py
>>> class TestGCD(unittest.TestCase):
>>>     def test_simple(self):
>>>         assert cfg.gcd(1, 0) == 1
>>> 
>>>     def test_mirror(self):
>>>         assert cfg.gcd(0, 1) == 1
>>> for mutant in MuProgramAnalyzer('gcd', gcd_src):
>>>     mutant[test_module].runTest('TestGCD')
>>> mutant.pm.score()
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-99-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

1.0

```

这样获得的突变评分比纯覆盖范围更好地指示了给定测试套件的质量。

## 为什么结构覆盖不足

[结构覆盖率](Coverage.html)措施的问题之一是，它无法检查测试套件生成的程序执行是否实际上*是正确的*。 也就是说，产生测试套件未注意到的错误输出的执行与产生正确覆盖率的执行的计数完全相同。 的确，如果删除典型测试用例中的断言，则新测试套件的覆盖范围不会改变，但是新测试套件的功能远不如原始测试套件有用。 例如，考虑以下“测试”：

```py
def ineffective_test():
    execute_the_program_as_a_whole()
    assert True

```

无论`execute_the_program_as_a_whole()`做什么，这里的最终断言总是会通过。 好的，如果`execute_the_program_as_a_whole()`引发异常，则测试将失败，但是我们也可以解决该问题：

```py
def ineffective_test():
    try:
        execute_the_program_as_a_whole()
    except:
        pass
    assert True

```

但是，这些“测试”的问题在于`execute_the_program_as_a_whole()`可能实现100％的代码覆盖率（或任何其他结构覆盖率度量标准的100％）。 但是，这个100％的数字并不反映测试发现错误的能力，实际上是0％。

实际上，这不是最佳状态。 我们如何验证我们的测试确实有用？ 一种选择（在有关覆盖率的章节中提供了提示）是将错误注入程序，并评估测试套件在捕获这些注入的错误方面的有效性。 但是，这带来了另一个问题。 我们如何首先产生这些错误？ 开发人员对错误可能发生的位置以及产生的影响的先入之见可能会使任何手动工作都产生偏差。 此外，编写非常好的bug可能会花费大量时间，这是非常间接的好处。 因此，这样的解决方案是不够的。

## 通过突变分析[播种人工故障](#Seeding-Artificial-Faults-with-Mutation-Analysis)

变异分析提供了另一种解决方案，用于评估测试套件的有效性。 突变分析的想法是将*人为错误*（称为*突变*）植入程序代码，并检查测试套件是否找到了它们。 例如，这种突变可以用`execute_the_program_as_a_whole()`中某处的`-`代替`+`。 当然，上述无效的测试不会检测到这一点，因为它们不会检查任何结果。 然而，有效的测试将； 假设测试在发现*人为*错误中越有效，则在发现*实际*错误中越有效。

变异分析的见解是从程序员的角度考虑错误插入的可能性。 如果假设程序中每个程序元素所收到的注意力足够相似，则可以进一步假定程序中的每个令牌都具有相似的被错误转录的可能性。 当然，程序员将纠正编译器（或其他静态分析工具）检测到的所有错误。 因此，与经过编译阶段之后的原始令牌不同的有效令牌集被认为是它的*突变*的可能集，它们表示程序中的*可能的错误*。 然后，通过测试套件检测（从而防止）此类突变的能力来对其进行判断。 在产生的所有*有效*突变体中检测到的此类突变体的比例作为突变得分。 在这一章中，我们将了解如何在Python程序中实现变异分析。 所获得的变异分数代表任何程序分析工具预防错误的能力，并且可以用于判断静态测试套件，测试生成器（例如模糊器）以及静态和符号执行框架。

考虑一个稍微不同的观点可能很直观。 测试套件是可以视为接受测试的程序作为其输入的程序。 评估此类程序（测试套件）的最佳方法是什么？ 通过对输入程序应用小的变异，并验证所涉及的测试套件不会产生意外行为，我们实质上可以*对*进行模糊测试。 测试套件应该只允许原始版本通过。 因此，任何未被检测为有缺陷的突变体都表示测试套件中的错误。

## 示例的结构覆盖范围充分性

让我们介绍一个更详细的示例，以说明覆盖问题以及突变分析的工作原理。 下面的`triangle()`程序将边长为$ a $，$ b $和$ c $的三角形分类为适当的三角形类别。 我们要验证程序是否正常运行。

```py
def triangle(a, b, c):
    if a == b:
        if b == c:
            return 'Equilateral'
        else:
            return 'Isosceles'
    else:
        if b == c:
            return "Isosceles"
        else:
            if a == c:
                return "Isosceles"
            else:
                return "Scalene"

```

这里有一些测试用例，以确保程序正常运行。

```py
def strong_oracle(fn):
    assert fn(1, 1, 1) == 'Equilateral'

    assert fn(1, 2, 1) == 'Isosceles'
    assert fn(2, 2, 1) == 'Isosceles'
    assert fn(1, 2, 2) == 'Isosceles'

    assert fn(1, 2, 3) == 'Scalene'

```

运行它们实际上会使所有测试通过。

```py
strong_oracle(triangle)

```

但是，只有当我们知道我们的测试有效时，“所有测试都通过”的声明才有价值。 我们的测试套件的功效是什么？ 正如我们在关于的[一章中所看到的，可以使用结构性覆盖技术（例如语句覆盖）来获得测试用例有效性的度量。](Coverage.html)

```py
import [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils)

```

```py
from [Coverage](Coverage.html) import Coverage

```

```py
import [inspect](https://docs.python.org/3/library/inspect.html)

```

我们添加了一个函数`show_coverage()`以可视化获得的覆盖范围。

```py
class Coverage(Coverage):
    def show_coverage(self, fn):
        src = inspect.getsource(fn)
        name = fn.__name__
        covered = set([lineno for method,
                       lineno in self._trace if method == name])
        for i, s in enumerate(src.split('\n')):
            print('%s  %2d: %s' % ('#' if i + 1 in covered else ' ', i + 1, s))

```

```py
with Coverage() as cov:
    strong_oracle(triangle)

```

```py
cov.show_coverage(triangle)

```

```py
   1: def triangle(a, b, c):
#  2:     if a == b:
#  3:         if b == c:
#  4:             return 'Equilateral'
   5:         else:
#  6:             return 'Isosceles'
   7:     else:
#  8:         if b == c:
#  9:             return "Isosceles"
  10:         else:
# 11:             if a == c:
# 12:                 return "Isosceles"
  13:             else:
# 14:                 return "Scalene"
  15: 

```

我们的`strong_oracle()`似乎已充分涵盖了所有可能的条件。 也就是说，根据结构覆盖范围，我们的测试用例集相当不错。 但是，获得的报道能说明全部情况吗？ 请考虑以下测试套件：

```py
def weak_oracle(fn):
    assert fn(1, 1, 1) == 'Equilateral'

    assert fn(1, 2, 1) != 'Equilateral'
    assert fn(2, 2, 1) != 'Equilateral'
    assert fn(1, 2, 2) != 'Equilateral'

    assert fn(1, 2, 3) != 'Equilateral'

```

我们这里要检查的只是边不相等的三角形不是等边的。 获得了什么保障？

```py
with Coverage() as cov:
    weak_oracle(triangle)

```

```py
cov.show_coverage(triangle)

```

```py
   1: def triangle(a, b, c):
#  2:     if a == b:
#  3:         if b == c:
#  4:             return 'Equilateral'
   5:         else:
#  6:             return 'Isosceles'
   7:     else:
#  8:         if b == c:
#  9:             return "Isosceles"
  10:         else:
# 11:             if a == c:
# 12:                 return "Isosceles"
  13:             else:
# 14:                 return "Scalene"
  15: 

```

实际上，覆盖范围似乎没有*任何*差异。 `weak_oracle()`获得的覆盖范围与`strong_oracle()`完全相同。 然而，片刻的反思应该使人相信`weak_oracle()`不如`strong_oracle()`有效。 但是，*的覆盖范围*无法区分这两个测试套件。 我们在覆盖范围上缺少什么？ 这里的问题是覆盖范围无法评估我们断言的*质量*。 实际上，覆盖范围根本不关心断言。 但是，如上所述，断言是测试套件有效性的极其重要的部分。 因此，我们需要一种评估断言质量的方法。

## 注入人为故障

注意，在有关的[一章中，覆盖范围是*代理*的形式，表示测试套件可能会发现错误。 如果实际上试图评估测试套件发现错误的可能性怎么办？ 我们所需要做的就是一次将一个错误注入程序，并计算测试套件检测到的此类错误的数量。 检测频率将为我们提供测试套件发现漏洞的实际可能性。 此技术称为*故障注入*。 这是*故障注入*的示例。](Coverage.html)

```py
def triangle_m1(a, b, c):
    if a == b:
        if b == c:
            return 'Equilateral'
        else:
            # return 'Isosceles'
            return None  # <-- injected fault
    else:
        if b == c:
            return "Isosceles"
        else:
            if a == c:
                return "Isosceles"
            else:
                return "Scalene"

```

让我们看看我们的测试套件是否足以应付此故障。 我们首先检查`weak_oracle()`是否可以检测到此更改。

```py
from [ExpectError](ExpectError.html) import ExpectError

```

```py
with ExpectError():
    weak_oracle(triangle_m1)

```

`weak_oracle()`无法检测到任何更改。 那我们的`strong_oracle()`呢？

```py
with ExpectError():
    strong_oracle(triangle_m1)

```

```py
Traceback (most recent call last):
  File "<ipython-input-18-e38642a77f76>", line 2, in <module>
    strong_oracle(triangle_m1)
  File "<ipython-input-4-90155baa3232>", line 5, in strong_oracle
    assert fn(2, 2, 1) == 'Isosceles'
AssertionError (expected)

```

我们的`strong_oracle()`能够检测到此故障，这证明`strong_oracle()`可能是更好的测试套件。

*故障注入*可以很好地衡量测试套件的有效性，前提是我们提供了可能的故障列表。 问题是收集这样的*无偏*故障集是相当昂贵的。 难以创建难以检测的良好故障，这是一个手动过程。 鉴于这是一个手动过程，因此所产生的错误将受到创建它的开发人员的先入为主的偏见。 即使存在此类经过策划的错误，它们也不大可能是详尽无遗的，并且可能会错过重要的错误类别以及程序的某些部分。 因此，*故障注入*不足以替代覆盖范围。 我们可以做得更好吗？

变异分析提供了一组精选故障的替代方法。 关键的见解是，如果假设程序员理解所讨论的程序，则大多数错误很可能是小的转录错误（少量的令牌）。 编译器很可能会捕获其中的大多数错误。 因此，程序中的大多数残余故障可能是由于正确程序的程序结构中某些点上的小（单个令牌）变化（此特定假设称为*合格程序员假说*或*有限邻里假说*）。 由程序的多个更改组成的较大故障又如何呢？ 此处的关键洞察力是，对于此类故障中的绝大部分，可以检测到隔离中的单个更改的测试用例很有可能检测到包含该故障的较大复合故障。 （此假设称为*耦合效应*。）我们如何在实践中使用这些假设？ 这个想法是简单地生成程序的所有可能有效的*有效*变体，这些变体与原始版本之间的差别很小（例如单个令牌更改）（这种变体称为 *突变体*）。 接下来，将给定的测试套件应用于由此生成的每个变体。 据说测试套件检测到的任何突变体均已被测试套件杀死*。 测试套件的有效性由杀死的突变体与产生的有效突变体的比例确定。*

接下来，我们实现一个简单的突变分析框架，并使用它来评估我们的测试套件。

## 突变Python代码

要处理Python程序，我们需要使用*抽象语法树*（AST）表示形式-这是内部表示形式，编译器和解释器在读取程序文本后便会进行处理。

简而言之，我们将程序转换为树，然后*更改此树的一部分*-例如，通过将`+`运算符更改为`-`或反之亦然，或将实际语句更改为`pass`语句 什么也没做。 然后可以进一步处理生成的变异树。 它可以传递给Python解释器以执行，或者我们可以*将其解析为文本形式。*

我们首先导入AST操作模块。

```py
import [ast](https://docs.python.org/3/library/ast.html)
import [astor](https://docs.python.org/3/library/astor.html)
import [inspect](https://docs.python.org/3/library/inspect.html)

```

我们可以使用`inspect.getsource()`获得Python函数的源代码。 （请注意，这不适用于其他笔记本中定义的功能。）

```py
triangle_source = inspect.getsource(triangle)
triangle_source

```

```py
'def triangle(a, b, c):\n    if a == b:\n        if b == c:\n            return \'Equilateral\'\n        else:\n            return \'Isosceles\'\n    else:\n        if b == c:\n            return "Isosceles"\n        else:\n            if a == c:\n                return "Isosceles"\n            else:\n                return "Scalene"\n'

```

为了以令人愉悦的形式查看这些内容，我们的函数`print_content(s, suffix)`格式化并突出显示了字符串`s`，就像它是一个以`suffix`结尾的文件一样。 因此，我们可以像查看Python文件一样查看（并突出显示）源：

```py
from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import print_content

```

```py
print_content(triangle_source, '.py')

```

```py
def triangle(a, b, c):
    if a == b:
        if b == c:
            return 'Equilateral'
        else:
            return 'Isosceles'
    else:
        if b == c:
            return "Isosceles"
        else:
            if a == c:
                return "Isosceles"
            else:
                return "Scalene"

```

对此进行解析可以为我们提供抽象语法树（AST）-以树形式表示程序。

```py
triangle_ast = ast.parse(triangle_source)

```

这个AST是什么样的？ 辅助功能`astor.dump_tree()`（文本输出）和`showast.show_ast()`（带有 [showast](https://github.com/hchasestevens/show_ast) 的图形输出）使我们可以检查树的结构。 我们看到函数以带有名称和参数的`FunctionDef`开始，后跟一个主体，该主体是`Expr`类型（文档字符串），`Assign`类型（赋值），`While`类型的语句列表 环回自己的身体），最后`Return`。

```py
print(astor.dump_tree(triangle_ast))

```

```py
Module(
    body=[
        FunctionDef(name='triangle',
            args=arguments(
                args=[arg(arg='a', annotation=None), arg(arg='b', annotation=None), arg(arg='c', annotation=None)],
                vararg=None,
                kwonlyargs=[],
                kw_defaults=[],
                kwarg=None,
                defaults=[]),
            body=[
                If(test=Compare(left=Name(id='a'), ops=[Eq], comparators=[Name(id='b')]),
                    body=[
                        If(test=Compare(left=Name(id='b'), ops=[Eq], comparators=[Name(id='c')]),
                            body=[Return(value=Str(s='Equilateral'))],
                            orelse=[Return(value=Str(s='Isosceles'))])],
                    orelse=[
                        If(test=Compare(left=Name(id='b'), ops=[Eq], comparators=[Name(id='c')]),
                            body=[Return(value=Str(s='Isosceles'))],
                            orelse=[
                                If(test=Compare(left=Name(id='a'), ops=[Eq], comparators=[Name(id='c')]),
                                    body=[Return(value=Str(s='Isosceles'))],
                                    orelse=[Return(value=Str(s='Scalene'))])])])],
            decorator_list=[],
            returns=None)])

```

文字太多了吗？ 这种图形表示可以使事情变得更简单。

```py
from [fuzzingbook_utils](https://github.com/uds-se/fuzzingbook/tree/master/notebooks/fuzzingbook_utils) import rich_output

```

```py
if rich_output():
    import [showast](https://docs.python.org/3/library/showast.html)
    showast.show_ast(triangle_ast)

```

<svg height="476pt" viewBox="0.00 0.00 1810.50 476.00" width="1811pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g class="graph" id="graph0" transform="scale(1 1) rotate(0) translate(4 472)"><title>%3</title> <g class="node" id="node1"><title>0</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="119" y="-447.3">FunctionDef</text></g> <g class="node" id="node2"><title>1</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="50.5" y="-374.3">"triangle"</text></g> <g class="edge" id="edge1"><title>0--1</title></g> <g class="node" id="node3"><title>2</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="127.5" y="-375.3">arguments</text></g> <g class="edge" id="edge2"><title>0--2</title></g> <g class="node" id="node10"><title>9</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="481" y="-375.3">If</text></g> <g class="edge" id="edge9"><title>0--9</title></g> <g class="node" id="node4"><title>3</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="44.5" y="-303.3">arg</text></g> <g class="edge" id="edge3"><title>2--3</title></g> <g class="node" id="node6"><title>5</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="116.5" y="-303.3">arg</text></g> <g class="edge" id="edge5"><title>2--5</title></g> <g class="node" id="node8"><title>7</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="188.5" y="-303.3">arg</text></g> <g class="edge" id="edge7"><title>2--7</title></g> <g class="node" id="node5"><title>4</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="57.5" y="-230.3">"a"</text></g> <g class="edge" id="edge4"><title>3--4</title></g> <g class="node" id="node7"><title>6</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="129.5" y="-230.3">"b"</text></g> <g class="edge" id="edge6"><title>5--6</title></g> <g class="node" id="node9"><title>8</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="201.5" y="-230.3">"c"</text></g> <g class="edge" id="edge8"><title>7--8</title></g> <g class="node" id="node11"><title>10</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="352" y="-303.3">Compare</text></g> <g class="edge" id="edge10"><title>9--10</title></g> <g class="node" id="node19"><title>18</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="643" y="-303.3">If</text></g> <g class="edge" id="edge18"><title>9--18</title></g> <g class="node" id="node34"><title>33</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1192" y="-303.3">If</text></g> <g class="edge" id="edge33"><title>9--33</title></g> <g class="node" id="node12"><title>11</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="256.5" y="-231.3">Name</text></g> <g class="edge" id="edge11"><title>10--11</title></g> <g class="node" id="node15"><title>14</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="345.5" y="-230.3">Eq</text></g> <g class="edge" id="edge14"><title>10--14</title></g> <g class="node" id="node16"><title>15</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="400.5" y="-231.3">Name</text></g> <g class="edge" id="edge15"><title>10--15</title></g> <g class="node" id="node13"><title>12</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="201.5" y="-158.3">"a"</text></g> <g class="edge" id="edge12"><title>11--12</title></g> <g class="node" id="node14"><title>13</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="273.5" y="-158.3">Load</text></g> <g class="edge" id="edge13"><title>11--13</title></g> <g class="node" id="node17"><title>16</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="345.5" y="-158.3">"b"</text></g> <g class="edge" id="edge16"><title>15--16</title></g> <g class="node" id="node18"><title>17</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="417.5" y="-158.3">Load</text></g> <g class="edge" id="edge17"><title>15--17</title></g> <g class="node" id="node20"><title>19</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="555" y="-231.3">Compare</text></g> <g class="edge" id="edge19"><title>18--19</title></g> <g class="node" id="node28"><title>27</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="660" y="-231.3">Return</text></g> <g class="edge" id="edge27"><title>18--27</title></g> <g class="node" id="node31"><title>30</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="793" y="-231.3">Return</text></g> <g class="edge" id="edge30"><title>18--30</title></g> <g class="node" id="node21"><title>20</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="472.5" y="-159.3">Name</text></g> <g class="edge" id="edge20"><title>19--20</title></g> <g class="node" id="node24"><title>23</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="561.5" y="-158.3">Eq</text></g> <g class="edge" id="edge23"><title>19--23</title></g> <g class="node" id="node25"><title>24</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="616.5" y="-159.3">Name</text></g> <g class="edge" id="edge24"><title>19--24</title></g> <g class="node" id="node22"><title>21</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="417.5" y="-86.3">"b"</text></g> <g class="edge" id="edge21"><title>20--21</title></g> <g class="node" id="node23"><title>22</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="489.5" y="-86.3">Load</text></g> <g class="edge" id="edge22"><title>20--22</title></g> <g class="node" id="node26"><title>25</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="561.5" y="-86.3">"c"</text></g> <g class="edge" id="edge25"><title>24--25</title></g> <g class="node" id="node27"><title>26</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="633.5" y="-86.3">Load</text></g> <g class="edge" id="edge26"><title>24--26</title></g> <g class="node" id="node29"><title>28</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="717.5" y="-159.3">Str</text></g> <g class="edge" id="edge28"><title>27--28</title></g> <g class="node" id="node30"><title>29</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="741.5" y="-86.3">"Equilateral"</text></g> <g class="edge" id="edge29"><title>28--29</title></g> <g class="node" id="node32"><title>31</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="835.5" y="-159.3">Str</text></g> <g class="edge" id="edge31"><title>30--31</title></g> <g class="node" id="node33"><title>32</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="877.5" y="-86.3">"Isosceles"</text></g> <g class="edge" id="edge32"><title>31--32</title></g> <g class="node" id="node35"><title>34</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1109" y="-231.3">Compare</text></g> <g class="edge" id="edge34"><title>33--34</title></g> <g class="node" id="node43"><title>42</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1220" y="-231.3">Return</text></g> <g class="edge" id="edge42"><title>33--42</title></g> <g class="node" id="node46"><title>45</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1531" y="-231.3">If</text></g> <g class="edge" id="edge45"><title>33--45</title></g> <g class="node" id="node36"><title>35</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1032.5" y="-159.3">Name</text></g> <g class="edge" id="edge35"><title>34--35</title></g> <g class="node" id="node39"><title>38</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1121.5" y="-158.3">Eq</text></g> <g class="edge" id="edge38"><title>34--38</title></g> <g class="node" id="node40"><title>39</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1176.5" y="-159.3">Name</text></g> <g class="edge" id="edge39"><title>34--39</title></g> <g class="node" id="node37"><title>36</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="977.5" y="-86.3">"b"</text></g> <g class="edge" id="edge36"><title>35--36</title></g> <g class="node" id="node38"><title>37</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1049.5" y="-86.3">Load</text></g> <g class="edge" id="edge37"><title>35--37</title></g> <g class="node" id="node41"><title>40</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1121.5" y="-86.3">"c"</text></g> <g class="edge" id="edge40"><title>39--40</title></g> <g class="node" id="node42"><title>41</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1193.5" y="-86.3">Load</text></g> <g class="edge" id="edge41"><title>39--41</title></g> <g class="node" id="node44"><title>43</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1266.5" y="-159.3">Str</text></g> <g class="edge" id="edge43"><title>42--43</title></g> <g class="node" id="node45"><title>44</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1293.5" y="-86.3">"Isosceles"</text></g> <g class="edge" id="edge44"><title>43--44</title></g> <g class="node" id="node47"><title>46</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1445" y="-159.3">Compare</text></g> <g class="edge" id="edge46"><title>45--46</title></g> <g class="node" id="node55"><title>54</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1579" y="-159.3">Return</text></g> <g class="edge" id="edge54"><title>45--54</title></g> <g class="node" id="node58"><title>57</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1702" y="-159.3">Return</text></g> <g class="edge" id="edge57"><title>45--57</title></g> <g class="node" id="node48"><title>47</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1376.5" y="-87.3">Name</text></g> <g class="edge" id="edge47"><title>46--47</title></g> <g class="node" id="node51"><title>50</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1465.5" y="-86.3">Eq</text></g> <g class="edge" id="edge50"><title>46--50</title></g> <g class="node" id="node52"><title>51</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1520.5" y="-87.3">Name</text></g> <g class="edge" id="edge51"><title>46--51</title></g> <g class="node" id="node49"><title>48</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1321.5" y="-14.3">"a"</text></g> <g class="edge" id="edge48"><title>47--48</title></g> <g class="node" id="node50"><title>49</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1393.5" y="-14.3">Load</text></g> <g class="edge" id="edge49"><title>47--49</title></g> <g class="node" id="node53"><title>52</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1465.5" y="-14.3">"c"</text></g> <g class="edge" id="edge52"><title>51--52</title></g> <g class="node" id="node54"><title>53</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1537.5" y="-14.3">Load</text></g> <g class="edge" id="edge53"><title>51--53</title></g> <g class="node" id="node56"><title>55</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1610.5" y="-87.3">Str</text></g> <g class="edge" id="edge55"><title>54--55</title></g> <g class="node" id="node57"><title>56</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1637.5" y="-14.3">"Isosceles"</text></g> <g class="edge" id="edge56"><title>55--56</title></g> <g class="node" id="node59"><title>58</title> <text fill="#004080" font-family="Courier,monospace" font-size="14.00" font-weight="bold" text-anchor="start" x="1724.5" y="-87.3">Str</text></g> <g class="edge" id="edge58"><title>57--58</title></g> <g class="node" id="node60"><title>59</title> <text fill="#008040" font-family="Courier,monospace" font-size="14.00" text-anchor="middle" x="1756.5" y="-14.3">"Scalene"</text></g> <g class="edge" id="edge59"><title>58--59</title></g></g></svg>

函数`astor.to_source()`将这种树转换回更熟悉的文本Python代码表示形式。

```py
print_content(astor.to_source(triangle_ast), '.py')

```

```py
def triangle(a, b, c):
    if a == b:
        if b == c:
            return 'Equilateral'
        else:
            return 'Isosceles'
    elif b == c:
        return 'Isosceles'
    elif a == c:
        return 'Isosceles'
    else:
        return 'Scalene'

```

## 一个简单的函数突变器

现在让我们去修改`triangle()`程序。 产生此程序的有效变异版本的一种简单方法是将其某些语句替换为`pass`。

`MuFunctionAnalyzer`是负责测试套件突变分析的主要类。 它接受要测试的功能。 使用上面讨论的功能，它将通过解析和解析一次给出的源代码标准化。 这是必需的，以确保原始和突变体之间的`diff`之后不会因空白注释等的差异而脱轨。

```py
class MuFunctionAnalyzer:
    def __init__(self, fn, log=False):
        self.fn = fn
        self.name = fn.__name__
        src = inspect.getsource(fn)
        self.ast = ast.parse(src)
        self.src = astor.to_source(self.ast)  # normalize
        self.mutator = self.mutator_object()
        self.nmutations = self.get_mutation_count()
        self.un_detected = set()
        self.mutants = []
        self.log = log

    def mutator_object(self, locations=None):
        return StmtDeletionMutator(locations)

    def register(self, m):
        self.mutants.append(m)

    def finish(self):
        pass

```

`get_mutation_count()`获取可用的可能突变数。 稍后我们将看到如何实现。

```py
class MuFunctionAnalyzer(MuFunctionAnalyzer):
    def get_mutation_count(self):
        self.mutator.visit(self.ast)
        return self.mutator.count

```

`Mutator`提供了实现单个突变的基类。 它接受要突变的位置列表。 假定方法`mutable_visit()`在子类确定的所有感兴趣的节点上被调用。 当调用`Mutator`而没有要突变的位置列表时，它仅循环遍历所有可能的突变点，并在`self.count`中保留计数。 如果使用要突变的特定位置列表调用它，则`mutable_visit()`方法将调用`mutation_visit()`，该节点将在节点上执行突变。 请注意，单个位置可以产生多个突变。 （因此，哈希图）。

```py
class Mutator(ast.NodeTransformer):
    def __init__(self, mutate_location=-1):
        self.count = 0
        self.mutate_location = mutate_location

    def mutable_visit(self, node):
        self.count += 1  # statements start at line no 1
        if self.count == self.mutate_location:
            return self.mutation_visit(node)
        return self.generic_visit(node)

```

`StmtDeletionMutator`只是挂接到所有处理语句的访问者中。 它通过将给定的语句替换为`pass`来执行变异。 如您所见，它访问各种语句。

```py
class StmtDeletionMutator(Mutator):
    def visit_Return(self, node): return self.mutable_visit(node)
    def visit_Delete(self, node): return self.mutable_visit(node)

    def visit_Assign(self, node): return self.mutable_visit(node)
    def visit_AnnAssign(self, node): return self.mutable_visit(node)
    def visit_AugAssign(self, node): return self.mutable_visit(node)

    def visit_Raise(self, node): return self.mutable_visit(node)
    def visit_Assert(self, node): return self.mutable_visit(node)

    def visit_Global(self, node): return self.mutable_visit(node)
    def visit_Nonlocal(self, node): return self.mutable_visit(node)

    def visit_Expr(self, node): return self.mutable_visit(node)

    def visit_Pass(self, node): return self.mutable_visit(node)
    def visit_Break(self, node): return self.mutable_visit(node)
    def visit_Continue(self, node): return self.mutable_visit(node)

```

实际的突变包括用`pass`语句替换节点：

```py
class StmtDeletionMutator(StmtDeletionMutator):
    def mutation_visit(self, node): return ast.Pass()    

```

对于`triangle()`，此访问者产生五个突变-即用`pass`替换五个`return`语句：

```py
MuFunctionAnalyzer(triangle).nmutations

```

```py
5

```

我们需要一种获得单个突变体的方法。 为此，我们将`MuFunctionAnalyzer`转换为*可迭代的*。

```py
class MuFunctionAnalyzer(MuFunctionAnalyzer):
    def __iter__(self):
        return PMIterator(self)

```

`PMIterator`定义为`MuFunctionAnalyzer`的*迭代器*类。

```py
class PMIterator:
    def __init__(self, pm):
        self.pm = pm
        self.idx = 0

```

`next()`方法返回相应的`Mutant`：

```py
class PMIterator(PMIterator):
    def __next__(self):
        i = self.idx
        if i >= self.pm.nmutations:
            self.pm.finish()
            raise StopIteration()
        self.idx += 1
        mutant = Mutant(self.pm, self.idx, log=self.pm.log)
        self.pm.register(mutant)
        return mutant

```

`Mutant`类包含用于在给定要突变的位置时生成突变体的逻辑。

```py
class Mutant:
    def __init__(self, pm, location, log=False):
        self.pm = pm
        self.i = location
        self.name = "%s_%s" % (self.pm.name, self.i)
        self._src = None
        self.tests = []
        self.detected = False
        self.log = log

```

使用方法如下：

```py
for m in MuFunctionAnalyzer(triangle):
    print(m.name)

```

```py
triangle_1
triangle_2
triangle_3
triangle_4
triangle_5

```

这些名称还有些通用。 让我们看看是否可以对产生的突变有更多的了解。

`generate_mutant()`只需调用`mutator()`方法，并向mutator传递AST的副本。

```py
class Mutant(Mutant):
    def generate_mutant(self, location):
        mutant_ast = self.pm.mutator_object(
            location).visit(ast.parse(self.pm.src))  # copy
        return astor.to_source(mutant_ast)

```

`src()`方法返回变异的源。

```py
class Mutant(Mutant):
    def src(self):
        if self._src is None:
            self._src = self.generate_mutant(self.i)
        return self._src

```

这是获取突变体并显示与原始突变的区别的方式：

```py
import [difflib](https://docs.python.org/3/library/difflib.html)

```

```py
for mutant in MuFunctionAnalyzer(triangle):
    shape_src = mutant.pm.src
    for line in difflib.unified_diff(mutant.pm.src.split('\n'),
                                     mutant.src().split('\n'),
                                     fromfile=mutant.pm.name,
                                     tofile=mutant.name, n=3):
        print(line)

```

```py
--- triangle

+++ triangle_1

@@ -1,7 +1,7 @@

 def triangle(a, b, c):
     if a == b:
         if b == c:
-            return 'Equilateral'
+            pass
         else:
             return 'Isosceles'
     elif b == c:
--- triangle

+++ triangle_2

@@ -3,7 +3,7 @@

         if b == c:
             return 'Equilateral'
         else:
-            return 'Isosceles'
+            pass
     elif b == c:
         return 'Isosceles'
     elif a == c:
--- triangle

+++ triangle_3

@@ -5,7 +5,7 @@

         else:
             return 'Isosceles'
     elif b == c:
-        return 'Isosceles'
+        pass
     elif a == c:
         return 'Isosceles'
     else:
--- triangle

+++ triangle_4

@@ -7,7 +7,7 @@

     elif b == c:
         return 'Isosceles'
     elif a == c:
-        return 'Isosceles'
+        pass
     else:
         return 'Scalene'

--- triangle

+++ triangle_5

@@ -9,5 +9,5 @@

     elif a == c:
         return 'Isosceles'
     else:
-        return 'Scalene'
+        pass

```

在此`diff`输出中，添加以`+`前缀的行，而以`-`前缀的行被删除。 我们看到五个突变体的每一个确实用`pass`语句替换了return语句。

我们将`diff()`方法添加到`Mutant`，以便可以直接调用它。

```py
class Mutant(Mutant):
    def diff(self):
        return '\n'.join(difflib.unified_diff(self.pm.src.split('\n'),
                                              self.src().split('\n'),
                                              fromfile='original',
                                              tofile='mutant',
                                              n=3))

```

## 评估突变

我们现在准备实施实际评估。 我们将变量定义为*上下文管理器*，它可以验证给定的所有断言是否成功。 这个想法是我们可以编写如下代码

```py
for mutant in MuFunctionAnalyzer(function):
    with mutant:
        assert function(x) == y

```

当`mutant`处于活动状态时（即`with:`下的代码块），原始功能被替换为替换功能。

输入`with`块时，将调用`__enter__()`功能。 它将突变体创建为Python函数并将其放置在全局名称空间中，以使`assert`语句执行变异函数而不是原始函数。

```py
class Mutant(Mutant):
    def __enter__(self):
        if self.log:
            print('->\t%s' % self.name)
        c = compile(self.src(), '<mutant>', 'exec')
        eval(c, globals())

```

`__exit__()`函数检查是否发生了异常（即断言失败或引发了其他错误）； 如果是这样，则将其标记为`detected`。 最后，它将恢复原始函数定义。

```py
class Mutant(Mutant):
    def __exit__(self, exc_type, exc_value, traceback):
        if self.log:
            print('<-\t%s' % self.name)
        if exc_type is not None:
            self.detected = True
            if self.log:
                print("Detected %s" % self.name, exc_type, exc_value)
        globals()[self.pm.name] = self.pm.fn
        if self.log:
            print()
        return True

```

`finish()`方法仅在突变体上调用该方法，检查是否发现了该突变体，然后返回结果。

```py
from [ExpectError](ExpectError.html) import ExpectTimeout

```

```py
class MuFunctionAnalyzer(MuFunctionAnalyzer):
    def finish(self):
        self.un_detected = {
            mutant for mutant in self.mutants if not mutant.detected}

```

突变分数-由测试套件检测到的突变体比率-由`score()`计算。 分数为1.0表示发现了所有突变体。 得分为0.1表示仅检测到10％的突变体。

```py
class MuFunctionAnalyzer(MuFunctionAnalyzer):
    def score(self):
        return (self.nmutations - len(self.un_detected)) / self.nmutations

```

这是我们如何使用我们的框架。

```py
import [sys](https://docs.python.org/3/library/sys.html)

```

```py
for mutant in MuFunctionAnalyzer(triangle, log=True):
    with mutant:
        assert triangle(1, 1, 1) == 'Equilateral', "Equal Check1"
        assert triangle(1, 0, 1) != 'Equilateral', "Equal Check2"
        assert triangle(1, 0, 2) != 'Equilateral', "Equal Check3"
mutant.pm.score()

```

```py
->	triangle_1
<-	triangle_1
Detected triangle_1 <class 'AssertionError'> Equal Check1

->	triangle_2
<-	triangle_2

->	triangle_3
<-	triangle_3

->	triangle_4
<-	triangle_4

->	triangle_5
<-	triangle_5

```

```py
0.2

```

五分之二的突变中只有一个导致断言失败。 因此，`weak_oracle()`测试套件的突变得分为20％。

```py
for mutant in MuFunctionAnalyzer(triangle):
    with mutant:
        weak_oracle(triangle)
mutant.pm.score()

```

```py
0.2

```

由于我们正在修改全局名称空间，因此我们不必直接在mutant的for循环内引用该函数。

```py
def oracle():
    strong_oracle(triangle)

```

```py
for mutant in MuFunctionAnalyzer(triangle, log=True):
    with mutant:
        oracle()
mutant.pm.score()

```

```py
->	triangle_1
<-	triangle_1
Detected triangle_1 <class 'AssertionError'> 

->	triangle_2
<-	triangle_2
Detected triangle_2 <class 'AssertionError'> 

->	triangle_3
<-	triangle_3
Detected triangle_3 <class 'AssertionError'> 

->	triangle_4
<-	triangle_4
Detected triangle_4 <class 'AssertionError'> 

->	triangle_5
<-	triangle_5
Detected triangle_5 <class 'AssertionError'> 

```

```py
1.0

```

也就是说，我们可以通过`strong_oracle()`测试套件获得`100%`突变评分。

这是另一个例子。 `gcd()`计算两个数字的最大公约数。

```py
def gcd(a, b):
    if a < b:
        c = a
        a = b
        b = c

    while b != 0:
        c = a
        a = b
        b = c % b
    return a

```

这是一个测试。 效果如何？

```py
for mutant in MuFunctionAnalyzer(gcd, log=True):
    with mutant:
        assert gcd(1, 0) == 1, "Minimal"
        assert gcd(0, 1) == 1, "Mirror"
mutant.pm.score()

```

```py
->	gcd_1
<-	gcd_1
Detected gcd_1 <class 'UnboundLocalError'> local variable 'c' referenced before assignment

->	gcd_2
<-	gcd_2
Detected gcd_2 <class 'AssertionError'> Mirror

->	gcd_3
<-	gcd_3

->	gcd_4
<-	gcd_4

->	gcd_5
<-	gcd_5

->	gcd_6
<-	gcd_6

->	gcd_7
<-	gcd_7
Detected gcd_7 <class 'AssertionError'> Minimal

```

```py
0.42857142857142855

```

我们看到，我们的`TestGCD`测试套件能够获得42％的突变评分。

## 模块和测试套件的变量

考虑我们前面讨论的`triangle()`程序。 正如我们所讨论的，产生此程序的有效变异版本的一种简单方法是将其某些语句替换为`pass`。

出于演示目的，我们希望程序看起来像在另一个文件中一样。 我们可以通过在Python中生成`Module`对象并将函数附加到该对象来实现。

```py
import [imp](https://docs.python.org/3/library/imp.html)

```

```py
def import_code(code, name):
    module = imp.new_module(name)
    exec(code, module.__dict__)
    return module

```

我们将`triangle()`功能附加到`shape`模块。

```py
shape = import_code(shape_src, 'shape')

```

现在我们可以通过模块`shape`调用三角形。

```py
shape.triangle(1, 1, 1)

```

```py
'Equilateral'

```

我们要测试`triangle()`功能。 为此，我们定义了一个`StrongShapeTest`类，如下所示。

```py
import [unittest](https://docs.python.org/3/library/unittest.html)

```

```py
class StrongShapeTest(unittest.TestCase):

    def test_equilateral(self):
        assert shape.triangle(1, 1, 1) == 'Equilateral'

    def test_isosceles(self):
        assert shape.triangle(1, 2, 1) == 'Isosceles'
        assert shape.triangle(2, 2, 1) == 'Isosceles'
        assert shape.triangle(1, 2, 2) == 'Isosceles'

    def test_scalene(self):
        assert shape.triangle(1, 2, 3) == 'Scalene'

```

我们定义了一个帮助程序功能`suite()`，它可以查看给定的类并标识测试功能。

```py
def suite(test_class):
    suite = unittest.TestSuite()
    for f in test_class.__dict__:
        if f.startswith('test_'):
            suite.addTest(test_class(f))
    return suite

```

可以使用不同的测试运行程序调用`TestTriangle`类中的测试。 最简单的方法是直接调用`TestCase`的`run()`方法。

```py
suite(StrongShapeTest).run(unittest.TestResult())

```

```py
<unittest.result.TestResult run=3 errors=0 failures=0>

```

`TextTestRunner`类提供了控制执行的详细程度的能力。 它还允许在第一个*故障时返回*。**

```py
runner = unittest.TextTestRunner(verbosity=0, failfast=True)
runner.run(suite(StrongShapeTest))

```

```py
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

```

```py
<unittest.runner.TextTestResult run=3 errors=0 failures=0>

```

在覆盖范围内运行该程序的操作如下：

```py
with Coverage() as cov:
    suite(StrongShapeTest).run(unittest.TestResult())

```

获得的覆盖范围如下：

```py
cov.show_coverage(triangle)

```

```py
   1: def triangle(a, b, c):
#  2:     if a == b:
#  3:         if b == c:
#  4:             return 'Equilateral'
   5:         else:
#  6:             return 'Isosceles'
#  7:     else:
#  8:         if b == c:
#  9:             return "Isosceles"
# 10:         else:
  11:             if a == c:
# 12:                 return "Isosceles"
  13:             else:
  14:                 return "Scalene"
  15: 

```

```py
class WeakShapeTest(unittest.TestCase):
    def test_equilateral(self):
        assert shape.triangle(1, 1, 1) == 'Equilateral'

    def test_isosceles(self):
        assert shape.triangle(1, 2, 1) != 'Equilateral'
        assert shape.triangle(2, 2, 1) != 'Equilateral'
        assert shape.triangle(1, 2, 2) != 'Equilateral'

    def test_scalene(self):
        assert shape.triangle(1, 2, 3) != 'Equilateral'

```

它获得多少覆盖率？

```py
with Coverage() as cov:
    suite(WeakShapeTest).run(unittest.TestResult())

```

```py
cov.show_coverage(triangle)

```

```py
   1: def triangle(a, b, c):
#  2:     if a == b:
#  3:         if b == c:
#  4:             return 'Equilateral'
   5:         else:
#  6:             return 'Isosceles'
#  7:     else:
#  8:         if b == c:
#  9:             return "Isosceles"
# 10:         else:
  11:             if a == c:
# 12:                 return "Isosceles"
  13:             else:
  14:                 return "Scalene"
  15: 

```

`MuProgramAnalyzer`是负责测试套件突变分析的主要类。 它接受要测试的模块的名称及其源代码。 它通过解析和解析一次来标准化源代码。 这是必需的，以确保原始和突变体之间的`diff`之后不会因空白注释等的差异而脱轨。

```py
class MuProgramAnalyzer(MuFunctionAnalyzer):
    def __init__(self, name, src):
        self.name = name
        self.ast = ast.parse(src)
        self.src = astor.to_source(self.ast)
        self.changes = []
        self.mutator = self.mutator_object()
        self.nmutations = self.get_mutation_count()
        self.un_detected = set()

    def mutator_object(self, locations=None):
        return AdvStmtDeletionMutator(self, locations)

```

The `Mutator` provides the base class for implementing individual mutations. It accepts a list of locations to mutate. It assumes that the method `mutable_visit()` is invoked on all nodes of interest as determined by the subclass. When the `Mutator` is invoked without a list of locations to mutate, it simply loops through all possible mutation points and retains a count in `self.count`. If it is invoked with a specific list of locations to mutate, the `mutable_visit()` method calls the `mutation_visit()` which performs the mutation on the node. Note that a single location can produce multiple mutations. (Hence the hashmap).

```py
class AdvMutator(Mutator):
    def __init__(self, analyzer, mutate_locations=None):
        self.count = 0
        self.mutate_locations = [] if mutate_locations is None else mutate_locations
        self.pm = analyzer

    def mutable_visit(self, node):
        self.count += 1  # statements start at line no 1
        return self.mutation_visit(node)

```

`AdvStmtDeletionMutator`只是挂接到所有处理语句的访问者中。 它通过将给定的语句替换为`pass`来执行变异。

```py
class AdvStmtDeletionMutator(AdvMutator, StmtDeletionMutator):
    def __init__(self, analyzer, mutate_locations=None):
        AdvMutator.__init__(self, analyzer, mutate_locations)

    def mutation_visit(self, node):
        index = 0  # there is only one way to delete a statement -- replace it by pass
        if not self.mutate_locations:  # counting pass
            self.pm.changes.append((self.count, index))
            return self.generic_visit(node)
        else:
            # get matching changes for this pass
            mutating_lines = set((count, idx)
                                 for (count, idx) in self.mutate_locations)
            if (self.count, index) in mutating_lines:
                return ast.Pass()
            else:
                return self.generic_visit(node)

```

Aagin，我们可以如下获得`triangle()`产生的突变数。

```py
MuProgramAnalyzer('shape', shape_src).nmutations

```

```py
5

```

我们需要一种获得单个突变体的方法。 为此，我们将`MuProgramAnalyzer`转换为*可迭代的*。

```py
class MuProgramAnalyzer(MuProgramAnalyzer):
    def __iter__(self):
        return AdvPMIterator(self)

```

`AdvPMIterator`定义为`MuProgramAnalyzer`的*迭代器*类。

```py
class AdvPMIterator:
    def __init__(self, pm):
        self.pm = pm
        self.idx = 0

```

`next()`方法返回相应的`Mutant`

```py
class AdvPMIterator(AdvPMIterator):
    def __next__(self):
        i = self.idx
        if i >= len(self.pm.changes):
            raise StopIteration()
        self.idx += 1
        # there could be multiple changes in one mutant
        return AdvMutant(self.pm, [self.pm.changes[i]])

```

The `Mutant` class contains logic for generating mutants when given the locations to mutate.

```py
class AdvMutant(Mutant):
    def __init__(self, pm, locations):
        self.pm = pm
        self.i = locations
        self.name = "%s_%s" % (self.pm.name,
                               '_'.join([str(i) for i in self.i]))
        self._src = None

```

Here is how it can be used:

```py
shape_src = inspect.getsource(triangle)

```

```py
for m in MuProgramAnalyzer('shape', shape_src):
    print(m.name)

```

```py
shape_(1, 0)
shape_(2, 0)
shape_(3, 0)
shape_(4, 0)
shape_(5, 0)

```

The `generate_mutant()` simply calls the `mutator()` method, and passes the mutator a copy of the AST.

```py
class AdvMutant(AdvMutant):
    def generate_mutant(self, locations):
        mutant_ast = self.pm.mutator_object(
            locations).visit(ast.parse(self.pm.src))  # copy
        return astor.to_source(mutant_ast)

```

The `src()` method returns the mutated source.

```py
class AdvMutant(AdvMutant):
    def src(self):
        if self._src is None:
            self._src = self.generate_mutant(self.i)
        return self._src

```

同样，我们将突变体可视化为与原始突变体的区别：

```py
import [difflib](https://docs.python.org/3/library/difflib.html)

```

We add the `diff()` method to `Mutant` so that it can be called directly.

```py
class AdvMutant(AdvMutant):
    def diff(self):
        return '\n'.join(difflib.unified_diff(self.pm.src.split('\n'),
                                              self.src().split('\n'),
                                              fromfile='original',
                                              tofile='mutant',
                                              n=3))

```

```py
for mutant in MuProgramAnalyzer('shape', shape_src):
    print(mutant.name)
    print(mutant.diff())
    break

```

```py
shape_(1, 0)
--- original

+++ mutant

@@ -1,7 +1,7 @@

 def triangle(a, b, c):
     if a == b:
         if b == c:
-            return 'Equilateral'
+            pass
         else:
             return 'Isosceles'
     elif b == c:

```

我们现在准备实施实际评估。 为此，我们需要能够接受定义了测试套件的模块，并在其上调用测试方法。 方法`getitem`接受测试模块，将测试模块上的导入条目固定为正确指向变异模块，然后将其传递给测试运行器`MutantTestRunner`。

```py
class AdvMutant(AdvMutant):
    def __getitem__(self, test_module):
        test_module.__dict__[
            self.pm.name] = import_code(
            self.src(), self.pm.name)
        return MutantTestRunner(self, test_module)

```

`MutantTestRunner`只需调用测试模块上的所有`test_`方法，检查是否发现了突变体，然后返回结果。

```py
from [ExpectError](ExpectError.html) import ExpectTimeout

```

```py
class MutantTestRunner:
    def __init__(self, mutant, test_module):
        self.mutant = mutant
        self.tm = test_module

    def runTest(self, tc):
        suite = unittest.TestSuite()
        test_class = self.tm.__dict__[tc]
        for f in test_class.__dict__:
            if f.startswith('test_'):
                suite.addTest(test_class(f))
        runner = unittest.TextTestRunner(verbosity=0, failfast=True)
        try:
            with ExpectTimeout(1):
                res = runner.run(suite)
                if res.wasSuccessful():
                    self.mutant.pm.un_detected.add(self)
                return res
        except SyntaxError:
            print('Syntax Error (%s)' % self.mutant.name)
            return None
        raise Exception('Unhandled exception during test execution')

```

突变得分通过`score()`计算。

```py
class MuProgramAnalyzer(MuProgramAnalyzer):
    def score(self):
        return (self.nmutations - len(self.un_detected)) / self.nmutations

```

Here is how we use our framework.

```py
import [sys](https://docs.python.org/3/library/sys.html)

```

```py
test_module = sys.modules[__name__]
for mutant in MuProgramAnalyzer('shape', shape_src):
    mutant[test_module].runTest('WeakShapeTest')
mutant.pm.score()

```

```py
======================================================================
FAIL: test_equilateral (__main__.WeakShapeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-67-bde838eb5078>", line 3, in test_equilateral
    assert shape.triangle(1, 1, 1) == 'Equilateral'
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK

```

```py
0.2

```

`WeakShape`测试套件仅导致`20%`突变评分。

```py
for mutant in MuProgramAnalyzer('shape', shape_src):
    mutant[test_module].runTest('StrongShapeTest')
mutant.pm.score()

```

```py
======================================================================
FAIL: test_equilateral (__main__.StrongShapeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-61-6ba9e9735b80>", line 4, in test_equilateral
    assert shape.triangle(1, 1, 1) == 'Equilateral'
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_isosceles (__main__.StrongShapeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-61-6ba9e9735b80>", line 8, in test_isosceles
    assert shape.triangle(2, 2, 1) == 'Isosceles'
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_isosceles (__main__.StrongShapeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-61-6ba9e9735b80>", line 9, in test_isosceles
    assert shape.triangle(1, 2, 2) == 'Isosceles'
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_isosceles (__main__.StrongShapeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-61-6ba9e9735b80>", line 7, in test_isosceles
    assert shape.triangle(1, 2, 1) == 'Isosceles'
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
======================================================================
FAIL: test_scalene (__main__.StrongShapeTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-61-6ba9e9735b80>", line 12, in test_scalene
    assert shape.triangle(1, 2, 3) == 'Scalene'
AssertionError

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)

```

```py
1.0

```

另一方面，我们可以通过`StrongShapeTest`测试套件获得`100%`突变评分。

这是另一个示例`gcd()`。

```py
gcd_src = inspect.getsource(gcd)

```

```py
class TestGCD(unittest.TestCase):
    def test_simple(self):
        assert cfg.gcd(1, 0) == 1

    def test_mirror(self):
        assert cfg.gcd(0, 1) == 1

```

```py
for mutant in MuProgramAnalyzer('cfg', gcd_src):
    mutant[test_module].runTest('TestGCD')
mutant.pm.score()

```

```py
======================================================================
ERROR: test_mirror (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-93-9157b5e48dd8>", line 6, in test_mirror
    assert cfg.gcd(0, 1) == 1
  File "<string>", line 5, in gcd
UnboundLocalError: local variable 'c' referenced before assignment

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (errors=1)
======================================================================
FAIL: test_mirror (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-93-9157b5e48dd8>", line 6, in test_mirror
    assert cfg.gcd(0, 1) == 1
AssertionError

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK
======================================================================
FAIL: test_simple (__main__.TestGCD)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<ipython-input-93-9157b5e48dd8>", line 3, in test_simple
    assert cfg.gcd(1, 0) == 1
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)

```

```py
0.42857142857142855

```

我们看到我们的`TestGCD`测试套件能够获得`42%`突变得分。

## 等价突变体[的问题](#The-Problem-of--Equivalent-Mutants)

突变分析的问题之一是，并非所有产生的突变体都必须是有缺陷的。 例如，考虑下面的`new_gcd()`程序。

```py
def new_gcd(a, b):
    if a < b:
        a, b = b, a
    else:
        a, b = a, b

    while b != 0:
        a, b = b, a % b
    return a

```

可以对该程序进行突变以产生以下突变体。

```py
def gcd(a, b):
    if a < b:
        a, b = b, a
    else:
        pass

    while b != 0:
        a, b = b, a % b
    return a

```

```py
for i, mutant in enumerate(MuFunctionAnalyzer(new_gcd)):
    print(i,mutant.src())

```

```py
0 def new_gcd(a, b):
    if a < b:
        pass
    else:
        a, b = a, b
    while b != 0:
        a, b = b, a % b
    return a

1 def new_gcd(a, b):
    if a < b:
        a, b = b, a
    else:
        pass
    while b != 0:
        a, b = b, a % b
    return a

2 def new_gcd(a, b):
    if a < b:
        a, b = b, a
    else:
        a, b = a, b
    while b != 0:
        pass
    return a

3 def new_gcd(a, b):
    if a < b:
        a, b = b, a
    else:
        a, b = a, b
    while b != 0:
        a, b = b, a % b
    pass

```

尽管其他突变体与原始变异体相比有缺陷，但是`mutant 1`在语义上与原始变异没有区别，因为它删除了无关紧要的分配。 这表示`mutant 1`不代表故障。 这些不代表故障的突变体称为*等效突变体*。 等效突变体的问题在于，在存在等效突变体的情况下，很难判断突变得分。 例如，突变分数为70％时，0％至30％的突变体可能是等效的。 因此，在不知道等效突变体的实际数目的情况下，不可能判断测试可以提高多少。 我们讨论了两种处理等效突变体的方法。

### 等价突变体数量的统计估计

如果活着的突变体的数量足够少，则可以依靠简单地手动检查它们。 但是，如果突变体的数量足够大（例如> 1000），则可以从存活的突变体中随机选择较少数量的突变体，然后手动评估它们是否代表缺陷。 样本大小的确定由以下公式控制，该公式用于二项式分布（近似于正态分布）：

$$ n \ ge \ hat {p}（1- \ hat {p}）\ bigg（\ frac {Z _ {\ frac {\ alpha} {2}}} {\ Delta} \ bigg）^ 2 $$

其中，$ n $是样本数，$ p $是概率分布的参数，$ \ alpha $是所需的精度，$ \ Delta $是精度。 对于$ 95 \％$的准确性，$ Z_ {0.95} = 1.96 $。 我们有以下值（$ \ hat {p}（1- \ hat {p}）的最大值= 0.25 $），而$ Z $是正态分布的临界值：

$$ n \ ge 0.25 \ bigg（\ frac {1.96} {\ Delta} \ bigg）^ 2 $$

对于$ Delta = 0.01 $，（最大误差为1％），我们需要评估$ 9604 $突变体的等效性。 如果将约束放宽到$ \ Delta = 0.1 $（这是$ 10 \％$的误差），则只需要评估$ 96 $突变体的等效性即可。

### 统计量估计由不朽的估计

尽管仅采样有限数量的突变体的想法很吸引人，但它仍然受到限制，因为必须进行手动分析。 如果计算能力很便宜，另一种估算真实突变体数量（以及等效突变体数量）的方法是使用Chao的估算器。 正如我们将在[一章中看到的何时停止模糊](WhenToStopFuzzing.html)的公式一样，公式为：

$$ \ hat S_ \ text {Chao1} = \ begin {cases} S（n）+ \ frac {f_1 ^ 2} {2f_2} & \ text {if $ f_2 > 0 $} \\ S（ n）+ \ frac {f_1（f_1-1）} {2} & \ text {否则} \ end {cases} $$

基本思想是针对每个突变体计算每个测试的完整测试矩阵$ T \ M $的结果。 变量$ f_1 $代表被精确杀死一次的突变体的数量，变量$ f_2 $代表被精确杀死两次的变量的数量。 $ S（n）$是被杀死的突变体总数。 在这里，$ \ hat {S} _ {Chao1} $提供了对突变体真实数量的估计。 如果$ M $是生成的总突变体，则$ M-\ hat {S} _ {Chao1} $表示**不朽**突变体的数量。 请注意，这些**不朽**突变体与传统的等效突变体有些不同，因为**死亡率**取决于用于区分变体行为的预言。 也就是说，如果使用依赖于抛出的错误来检测杀戮的模糊器，它将不会检测到产生不同输出但不会抛出错误的突变体。 因此， *Chao1* 估计值将实质上是模糊器可以检测到的突变体的渐近线值，如果该突变体被赋予了无限的时间。 当使用的预言家足够强大时，**不朽的**突变体估算值将接近真实的**等效**突变体估算值。 有关更多详细信息，请参见[何时停止模糊](WhenToStopFuzzing.html)的章节。

## 经验教训

*   我们已经了解了为什么结构覆盖范围不足以评估测试套件的质量。
*   我们已经了解了如何使用变异分析来评估测试套件的质量。
*   我们已经了解了突变分析的局限性-等效和冗余突变体，以及如何估算它们。

## 后续步骤

*   虽然幼稚的模糊测试会产生质量较差的预言，但诸如[符号](SymbolicFuzzer.html)和[缩略语](ConcolicFuzzer.html)之类的技术可以增强模糊测试中使用的预言质量。
*   [动态不变量](DynamicInvariants.html)对提高Oracle的质量也有很大的帮助。
*   [何时停止模糊](WhenToStopFuzzing.html)的章节详细介绍了Chao估算器。

## 背景

突变分析的想法最早由Lipton等人提出。 [Lipton *等人*，1971。]。 贾等人发表了一份出色的突变分析研究综述。 [Jia *等*，2011。]。 Papadakis等人[Papadakis *等人*，2019年。有关突变分析的章节是对突变分析当前趋势的又一出色概述。

## 练习

### 练习1：算术表达变量

我们简单的语句删除突变只是可以对程序进行突变的一种方法。 另一类突变体是*表达突变*，其中算术运算符（例如`{+,-,*,/}`等）彼此替换。 例如，给定一个表达式，例如

```py
x = x + 1
```

可以将其变异为

```py
x = x - 1
```

和

```py
x = x * 1
```

and

```py
x = x / 1
```

首先，我们需要找出要突变的节点类型。 我们通过ast函数获得这些，并发现节点类型名为BinOp

```py
print(astor.dump_tree(ast.parse("1 + 2 - 3 * 4 / 5")))

```

```py
Module(
    body=[
        Expr(
            value=BinOp(left=BinOp(left=Num(n=1), op=Add, right=Num(n=2)),
                op=Sub,
                right=BinOp(left=BinOp(left=Num(n=3), op=Mult, right=Num(n=4)), op=Div, right=Num(n=5))))])

```

要使树变异，因此您需要更改`op`属性（其值为`Add`，`Sub`，`Mult`和`Div`之一）

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions.

要使树变异，我们需要更改`op`属性（其值为`Add`，`Sub`，`Mult`和`Div`之一）。 编写一个进行必要突变的类`BinOpMutator`，然后创建一个类`MuBinOpAnalyzer`作为`MuFunctionAnalyzer`的子类，该类使用`BinOpMutator`。

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions.

### 练习2：优化突变分析

我们的突变分析技术效率低下，因为我们即使在代码中包含测试案例未涵盖的突变的突变体上也可以运行测试。 测试用例无法检测未涵盖的部分代码中的错误。 因此，最简单的优化方法之一是首先从给定的测试用例中恢复覆盖率信息，然后仅在突变位于测试用例所覆盖的代码中的突变体上运行测试用例。 您可以修改`MuFunctionAnalyzer`以纳入恢复覆盖范围作为第一步吗？

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions.

### 练习3：字节码突变器

我们已经看到了在给定源的情况下如何突变AST。 这种方法的缺点之一是Python字节码也被其他语言作为目标。 在这种情况下，源可能不容易转换为Python AST，因此希望更改字节码。 您是否可以为Python函数实现字节码更改器，该字节器更改字节码而不是获取源代码然后对其进行更改？

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions.

### 练习4：估计残余缺陷密度

程序的缺陷密度是程序中发布之前检测到的缺陷数除以程序大小。 残余缺陷密度是逃避检测的缺陷百分比。 尽管很难估计实际的残余缺陷密度，但突变分析可以提供一个上限。 仍未检测到的突变体数量是程序中剩余缺陷数量的合理上限。 但是，此上限可能太宽。 原因是一些剩余的故障可以相互影响，并且如果一起出现，则可以由可用的测试套件检测到。 因此，更严格的界限是在给定程序中可以被*并存*却没有被给定测试套件检测到的突变体的数量。 这可以通过从可能的完整突变集开始，并应用[的delta-debugging来完成，该章在减少](Reducer.html)的章节中确定了需要删除的最小数目的突变，以使突变体无法被检测到。 测试套件。 您可以通过扩展使用此技术来估计残余缺陷密度上限的`MuFunctionAnalyzer`来产生新的`RDDEstimator`吗？

[Use the notebook](https://mybinder.org/v2/gh/uds-se/fuzzingbook/master?filepath=docs/notebooks/MutationAnalysis.ipynb#Exercises) to work on the exercises and see solutions.