# 语法覆盖率

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

[从语法](GrammarFuzzer.html)中产生输入时，规则的所有可能扩展都具有相同的可能性。 然而，对于产生一个全面的测试套件，使*品种*最大化是更有意义的-例如，不要一遍又一遍地重复相同的扩展。 在本章中，我们探索如何系统地*覆盖语法的*元素，以使我们最大限度地提高多样性并且不会漏掉单个元素。

**前提条件**

*   您应该阅读语法的[一章。](Grammars.html)
*   您应该已经阅读[关于有效语法模糊](GrammarFuzzer.html)的章节。

## 内容提要

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

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

```

然后利用以下功能。

本章介绍`GrammarCoverageFuzzer`，这是一种有效的语法模糊器，它是从[这一章中对有效语法模糊](GrammarFuzzer.html)进行扩展的。 它努力至少覆盖一次所有扩展。 例如，在下面的示例中，区号中的所有数字都不同，行号中的数字也都不同：

```py
>>> from [Grammars](Grammars.html) import US_PHONE_GRAMMAR
>>> phone_fuzzer = GrammarCoverageFuzzer(US_PHONE_GRAMMAR)
>>> phone_fuzzer.fuzz()
'(521)383-0695'

```

模糊处理后，`expansion_coverage()`方法返回所涵盖的语法扩展的映射。

```py
>>> phone_fuzzer.expansion_coverage()
{'<area> -> <lead-digit><digit><digit>',
 '<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 8',
 '<digit> -> 9',
 '<exchange> -> <lead-digit><digit><digit>',
 '<lead-digit> -> 3',
 '<lead-digit> -> 5',
 '<line> -> <digit><digit><digit><digit>',
 '<phone-number> -> (<area>)<exchange>-<line>',
 '<start> -> <phone-number>'}

```

随后对`fuzz()`的呼叫将进一步覆盖（例如，覆盖其他区号）。 重新调用`reset()`会清除记录的覆盖范围。

由于输入中的此类覆盖范围还会产生更高的代码覆盖范围，因此`GrammarCoverageFuzzer`是`GrammarFuzzer`的推荐扩展。

## 覆盖语法元素

测试生成的目的是涵盖程序的所有功能-当然希望包括失败的功能。 但是，此功能与输入的结构相关：如果我们无法生成某些输入元素，则也不会触发相关的代码和功能，从而减少了在其中查找错误的机会。

例如，请考虑语法[一章中的表达语法`EXPR_GRAMMAR`。](Grammars.html) 。 如果我们不产生负数，那么将不会测试负数。 如果我们不产生浮点数，那么将不会测试浮点数。 因此，我们的目标必须是*涵盖所有可能的扩展*。

最大化这种多样性的一种方法是*跟踪*在语法生成过程中发生的扩展：如果我们已经看到了某种扩展，则可以从可能的扩展集中选择其他可能的扩展候选。 在我们的表达语法中考虑以下规则：

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

```

```py
from [Grammars](Grammars.html) import EXPR_GRAMMAR, CGI_GRAMMAR, URL_GRAMMAR, START_SYMBOL
from [Grammars](Grammars.html) import is_valid_grammar, extend_grammar

```

```py
EXPR_GRAMMAR["<factor>"]

```

```py
['+<factor>', '-<factor>', '(<expr>)', '<integer>.<integer>', '<integer>']

```

假设我们已经在`<factor>`的第一个扩展中产生了`<integer>`。 在扩展下一个因子时，我们将标记`<integer>`扩展已被覆盖，并选择尚未发现的替代方法之一，例如`-<factor>`（负数）或`<integer>.<integer>`（浮点数） 。 只有涵盖了所有替代方案后，我们才可以重新考虑以前涵盖的扩展。

### 跟踪语法覆盖率

*语法覆盖率*的概念非常容易实现。 我们引入了一个`GrammarCoverageFuzzer`类，该类跟踪当前已实现的语法覆盖率：

```py
from [GrammarFuzzer](GrammarFuzzer.html) import GrammarFuzzer, all_terminals, nonterminals, display_tree

```

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

```

```py
class TrackingGrammarCoverageFuzzer(GrammarFuzzer):
    def __init__(self, *args, **kwargs):
        # invoke superclass __init__(), passing all arguments
        super().__init__(*args, **kwargs)
        self.reset_coverage()

    def reset_coverage(self):
        self.covered_expansions = set()

    def expansion_coverage(self):
        return self.covered_expansions

```

在这组`covered_expansions`中，我们使用函数`expansion_key()`为该对生成一个字符串表示形式，将各个扩展存储为成对的[*符号*，*扩展*）。

```py
def expansion_key(symbol, expansion):
    """Convert (symbol, children) into a key.  `children` can be an expansion string or a derivation tree."""
    if isinstance(expansion, tuple):
        expansion = expansion[0]
    if not isinstance(expansion, str):
        children = expansion
        expansion = all_terminals((symbol, children))
    return symbol + " -> " + expansion

```

```py
expansion_key(START_SYMBOL, EXPR_GRAMMAR[START_SYMBOL][0])

```

```py
'<start> -> <expr>'

```

除了*扩展*之外，我们还可以传递一个子代列表作为参数，然后将其自动转换为字符串。

```py
children = [("<expr>", None), (" + ", []), ("<term>", None)]
expansion_key("<expr>", children)

```

```py
'<expr> -> <expr> + <term>'

```

我们可以通过列举所有扩展来计算语法中可能的扩展集。 方法`max_expansion_coverage()`从给定符号（默认：语法开始符号）开始递归遍历语法，并将所有扩展累积在`expansions`中。 使用`max_depth`参数（默认值：$ \ infty $），我们可以控制语法探索的深度。 在本章的后面，我们将需要它。

```py
class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def _max_expansion_coverage(self, symbol, max_depth):
        if max_depth <= 0:
            return set()

        self._symbols_seen.add(symbol)

        expansions = set()
        for expansion in self.grammar[symbol]:
            expansions.add(expansion_key(symbol, expansion))
            for nonterminal in nonterminals(expansion):
                if nonterminal not in self._symbols_seen:
                    expansions |= self._max_expansion_coverage(
                        nonterminal, max_depth - 1)

        return expansions

    def max_expansion_coverage(self, symbol=None, max_depth=float('inf')):
        """Return set of all expansions in a grammar starting with `symbol`"""
        if symbol is None:
            symbol = self.start_symbol

        self._symbols_seen = set()
        cov = self._max_expansion_coverage(symbol, max_depth)

        if symbol == START_SYMBOL:
            assert len(self._symbols_seen) == len(self.grammar)

        return cov

```

我们可以使用`max_expansion_coverage()`来计算表达式语法中的所有扩展：

```py
expr_fuzzer = TrackingGrammarCoverageFuzzer(EXPR_GRAMMAR)
expr_fuzzer.max_expansion_coverage()

```

```py
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9',
 '<expr> -> <term>',
 '<expr> -> <term> + <expr>',
 '<expr> -> <term> - <expr>',
 '<factor> -> (<expr>)',
 '<factor> -> +<factor>',
 '<factor> -> -<factor>',
 '<factor> -> <integer>',
 '<factor> -> <integer>.<integer>',
 '<integer> -> <digit>',
 '<integer> -> <digit><integer>',
 '<start> -> <expr>',
 '<term> -> <factor>',
 '<term> -> <factor> * <term>',
 '<term> -> <factor> / <term>'}

```

在扩展过程中，我们可以跟踪看到的扩展。 为此，我们使用`choose_node_expansion()`方法，在[语法模糊器](GrammarFuzzer.html)中扩展单个节点。

```py
class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def add_coverage(self, symbol, new_children):
        key = expansion_key(symbol, new_children)

        if self.log and key not in self.covered_expansions:
            print("Now covered:", key)
        self.covered_expansions.add(key)

    def choose_node_expansion(self, node, possible_children):
        (symbol, children) = node
        index = super().choose_node_expansion(node, possible_children)
        self.add_coverage(symbol, possible_children[index])
        return index

```

方法`missing_expansion_coverage()`是一个辅助方法，它返回仍然必须涵盖的扩展：

```py
class TrackingGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def missing_expansion_coverage(self):
        return self.max_expansion_coverage() - self.expansion_coverage()

```

让我们展示跟踪的工作原理。 为简单起见，让我们仅关注`<digit>`扩展。

```py
digit_fuzzer = TrackingGrammarCoverageFuzzer(
    EXPR_GRAMMAR, start_symbol="<digit>", log=True)
digit_fuzzer.fuzz()

```

```py
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 9
Tree: 9
'9'

```

```py
'9'

```

```py
digit_fuzzer.fuzz()

```

```py
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 0
Tree: 0
'0'

```

```py
'0'

```

```py
digit_fuzzer.fuzz()

```

```py
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 5
Tree: 5
'5'

```

```py
'5'

```

到目前为止，这是一组涵盖的扩展：

```py
digit_fuzzer.expansion_coverage()

```

```py
{'<digit> -> 0', '<digit> -> 5', '<digit> -> 9'}

```

这是我们可以涵盖的所有扩展的集合：

```py
digit_fuzzer.max_expansion_coverage()

```

```py
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9'}

```

这是缺少的覆盖范围：

```py
digit_fuzzer.missing_expansion_coverage()

```

```py
{'<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8'}

```

平均来说，在涵盖所有扩展之后，我们必须产生多少个字符？

```py
def average_length_until_full_coverage(fuzzer):
    trials = 50

    sum = 0
    for trial in range(trials):
        # print(trial, end=" ")
        fuzzer.reset_coverage()
        while len(fuzzer.missing_expansion_coverage()) > 0:
            s = fuzzer.fuzz()
            sum += len(s)

    return sum / trials

```

```py
digit_fuzzer.log = False
average_length_until_full_coverage(digit_fuzzer)

```

```py
28.4

```

对于完整表达式，这需要更长的时间：

```py
expr_fuzzer = TrackingGrammarCoverageFuzzer(EXPR_GRAMMAR)
average_length_until_full_coverage(expr_fuzzer)

```

```py
138.12

```

### 覆盖语法扩展

现在让我们不仅跟踪覆盖范围，而且实际上*产生*覆盖范围。 这个想法如下：

1.  我们确定尚未发现的孩子（在`uncovered_children`中）
2.  如果所有孩子都被覆盖，我们将退回到原始方法（即，随机选择一个扩展）
3.  否则，我们从未发现的孩子中选择一个孩子，并将其标记为覆盖。

为此，我们引入了一个新的模糊器`SimpleGrammarCoverageFuzzer`，它在`choose_node_expansion()`方法中实现了该策略。

```py
class SimpleGrammarCoverageFuzzer(TrackingGrammarCoverageFuzzer):
    def choose_node_expansion(self, node, possible_children):
        # Prefer uncovered expansions
        (symbol, children) = node
        uncovered_children = [c for (i, c) in enumerate(possible_children)
                              if expansion_key(symbol, c) not in self.covered_expansions]
        index_map = [i for (i, c) in enumerate(possible_children)
                     if c in uncovered_children]

        if len(uncovered_children) == 0:
            # All expansions covered - use superclass method
            return self.choose_covered_node_expansion(node, possible_children)

        # Select from uncovered nodes
        index = self.choose_uncovered_node_expansion(node, uncovered_children)

        return index_map[index]

```

为子类提供了两种方法`choose_covered_node_expansion()`和`choose_uncovered_node_expansion()`：

```py
class SimpleGrammarCoverageFuzzer(SimpleGrammarCoverageFuzzer):
    def choose_uncovered_node_expansion(self, node, possible_children):
        return TrackingGrammarCoverageFuzzer.choose_node_expansion(
            self, node, possible_children)

    def choose_covered_node_expansion(self, node, possible_children):
        return TrackingGrammarCoverageFuzzer.choose_node_expansion(
            self, node, possible_children)

```

通过返回到目前为止涵盖的扩展集，我们可以多次调用模糊器，每次都增加语法覆盖率。 例如，使用`EXPR_GRAMMAR`语法产生数字，模糊器产生的数字比另一数字大：

```py
f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR, start_symbol="<digit>")
f.fuzz()

```

```py
'5'

```

```py
f.fuzz()

```

```py
'2'

```

```py
f.fuzz()

```

```py
'1'

```

Here's the set of covered expansions so far:

```py
f.expansion_coverage()

```

```py
{'<digit> -> 1', '<digit> -> 2', '<digit> -> 5'}

```

让我们再模糊一些。 我们看到，每次迭代都覆盖了另一个扩展：

```py
for i in range(7):
    print(f.fuzz(), end=" ")

```

```py
0 9 7 4 8 3 6 

```

最后，涵盖所有扩展：

```py
f.missing_expansion_coverage()

```

```py
set()

```

让我们将其应用于更复杂的语法-例如，完整表达语法。 我们看到，经过几次迭代，我们涵盖了每个数字，运算符和扩展：

```py
f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR)
for i in range(10):
    print(f.fuzz())

```

```py
+(0.31 / (5) / 9 + 4 * 6 / 3 - 8 - 7) * -2
+++2 / 87360
((4) * 0 - 1) / -9.6 + 7 / 6 + 1 * 8 + 7 * 8
++++26 / -64.45
(8 / 1 / 6 + 9 + 7 + 8) * 1.1 / 0 * 1
7.7
++(3.5 / 3) - (-4 + 3) / (8 / 0) / -4 * 2 / 1
+(90 / --(28 * 8 / 5 + 5 / (5 / 8))) - +9.36 / 2.5 * (5 * (7 * 6 * 5) / 8)
9.11 / 7.28
1 / (9 - 5 * 6) / 6 / 7 / 7 + 1 + 1 - 7 * -3

```

同样，所有扩展都包括在内：

```py
f.missing_expansion_coverage()

```

```py
set()

```

我们看到，与随机方法相比，我们的策略在实现覆盖率方面更为有效：

```py
average_length_until_full_coverage(SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR))

```

```py
52.28

```

## 远见卓识

为单个规则选择扩展是一个好的开始； 但是，如下面的示例所示，这还不够。 我们将覆盖范围的[一章应用于CGI语法的覆盖范围模糊器：](Coverage.html)

```py
CGI_GRAMMAR

```

```py
{'<start>': ['<string>'],
 '<string>': ['<letter>', '<letter><string>'],
 '<letter>': ['<plus>', '<percent>', '<other>'],
 '<plus>': ['+'],
 '<percent>': ['%<hexdigit><hexdigit>'],
 '<hexdigit>': ['0',
  '1',
  '2',
  '3',
  '4',
  '5',
  '6',
  '7',
  '8',
  '9',
  'a',
  'b',
  'c',
  'd',
  'e',
  'f'],
 '<other>': ['0', '1', '2', '3', '4', '5', 'a', 'b', 'c', 'd', 'e', '-', '_']}

```

```py
f = SimpleGrammarCoverageFuzzer(CGI_GRAMMAR)
for i in range(10):
    print(f.fuzz())

```

```py
c
+%a6++
+-
+
++
%18%b7
+e
_
d2+%e3
%d0

```

经过10次迭代，我们仍然发现了许多扩展：

```py
f.missing_expansion_coverage()

```

```py
{'<hexdigit> -> 2',
 '<hexdigit> -> 4',
 '<hexdigit> -> 5',
 '<hexdigit> -> 9',
 '<hexdigit> -> c',
 '<hexdigit> -> f',
 '<other> -> 0',
 '<other> -> 1',
 '<other> -> 3',
 '<other> -> 4',
 '<other> -> 5',
 '<other> -> a',
 '<other> -> b'}

```

为什么会这样？ 问题在于，在CGI语法中，`hexdigit`规则中要涵盖的变体数量最多。 但是，我们首先需要*达到*这种扩展。 扩展`<letter>`符号时，我们可以在三种可能的扩展之间进行选择：

```py
CGI_GRAMMAR["<letter>"]

```

```py
['<plus>', '<percent>', '<other>']

```

如果已经涵盖了所有三个扩展，则上面的`choose_node_expansion()`将随机选择一个-即使选择`<percent>`时可能需要覆盖更多扩展。

我们需要的是一个更好的策略，如果后续有更多未发现的扩展，即使`<percent>`被覆盖，也将选择`<percent>`。 W. Burkhardt [ [Burkhardt *等人*，1967。](https://doi.org/10.1007/BF02235512)]首先以“最短路径选择”的名称讨论了这种策略：

> 此版本从几种开发选择中选择了该语法单元，该语法单元从最短路径开始仍然有一个未使用的单元。

这是我们将在后续步骤中实现的。

### 确定每个符号的最大覆盖范围

为了解决此问题，我们引入了一个基于`SimpleGrammarCoverageFuzzer`的新类`GrammarCoverageFuzzer`，但具有更好的策略。 首先，我们需要计算从特定符号可以达到的*最大扩展集*，正如我们已经在`max_expansion_coverage()`中实现的那样。 我们的想法是稍后计算该集合的*交集*和已经涵盖的展开，以便我们可以偏爱带有非空交集的那些展开。

第一步-计算符号可以达到的最大扩展集-已经实现。 通过将`symbol`参数传递给`max_expansion_coverage()`，我们可以计算每个符号的可能扩展：

```py
f = SimpleGrammarCoverageFuzzer(EXPR_GRAMMAR)
f.max_expansion_coverage('<integer>')

```

```py
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9',
 '<integer> -> <digit>',
 '<integer> -> <digit><integer>'}

```

```py
f.max_expansion_coverage('<digit>')

```

```py
{'<digit> -> 0',
 '<digit> -> 1',
 '<digit> -> 2',
 '<digit> -> 3',
 '<digit> -> 4',
 '<digit> -> 5',
 '<digit> -> 6',
 '<digit> -> 7',
 '<digit> -> 8',
 '<digit> -> 9'}

```

### 确定尚未发现的孩子

现在我们可以开始实现`GrammarCoverageFuzzer`。 计算`max_expansion_coverage()`可以让我们确定每个孩子的*失踪覆盖率*。 为此，我们*从可以获得的覆盖范围中减去*已经看到的覆盖范围（`expansion_coverage()`）。

```py
class GrammarCoverageFuzzer(SimpleGrammarCoverageFuzzer):
    def _new_child_coverage(self, children, max_depth):
        new_cov = set()
        for (c_symbol, _) in children:
            if c_symbol in self.grammar:
                new_cov |= self.max_expansion_coverage(
                    c_symbol, max_depth)
        return new_cov

    def new_child_coverage(self, symbol, children, max_depth=float('inf')):
        """Return new coverage that would be obtained by expanding (symbol, children)"""
        new_cov = self._new_child_coverage(children, max_depth)
        new_cov.add(expansion_key(symbol, children))
        new_cov -= self.expansion_coverage()   # -= is set subtraction
        return new_cov

```

让我们说明`new_child_coverage()`。 我们再次开始模糊测试，随机选择扩展。

```py
f = GrammarCoverageFuzzer(EXPR_GRAMMAR, start_symbol="<digit>", log=True)
f.fuzz()

```

```py
Tree: <digit>
Expanding <digit> randomly
Now covered: <digit> -> 2
Tree: 2
'2'

```

```py
'2'

```

这是我们目前的报道：

```py
f.expansion_coverage()

```

```py
{'<digit> -> 2'}

```

当我们查看`<digit>`的单个扩展可能性时，我们看到所有扩展都提供了额外的覆盖范围，*除了*才适用。

```py
for expansion in EXPR_GRAMMAR["<digit>"]:
    children = f.expansion_to_children(expansion)
    print(expansion, f.new_child_coverage("<digit>", children))

```

```py
0 {'<digit> -> 0'}
1 {'<digit> -> 1'}
2 set()
3 {'<digit> -> 3'}
4 {'<digit> -> 4'}
5 {'<digit> -> 5'}
6 {'<digit> -> 6'}
7 {'<digit> -> 7'}
8 {'<digit> -> 8'}
9 {'<digit> -> 9'}

```

这意味着无论何时选择扩展，我们都可以使用`new_child_coverage()`并在提供最大新（看不见）覆盖范围的扩展中进行选择。

### 自适应前瞻

当选择一个孩子时，我们不会期望获得最大的总体覆盖范围，因为这会导致扩展，而许多未发现的可能性将完全主导其他扩展。 相反，我们的目标是采用*广度优先*策略，首先涵盖到达给定深度的所有扩展，然后才寻找更大的深度。 方法`new_coverages()`是此策略的核心：从最大深度（`max_depth`）为零开始，它会增加深度，直到找到至少一个未发现的扩展为止。

```py
class GrammarCoverageFuzzer(GrammarCoverageFuzzer):
    def new_coverages(self, node, possible_children):
        """Return coverage to be obtained for each child at minimum depth"""
        (symbol, children) = node
        for max_depth in range(len(self.grammar)):
            new_coverages = [
                self.new_child_coverage(
                    symbol, c, max_depth) for c in possible_children]
            max_new_coverage = max(len(new_coverage)
                                   for new_coverage in new_coverages)
            if max_new_coverage > 0:
                # Uncovered node found
                return new_coverages

        # All covered
        return None

```

### 全部在一起

现在，我们可以定义`choose_node_expansion()`来使用此策略：首先，我们确定要获得的可能覆盖范围（使用`new_coverages()`）； 然后，我们（随机地）选择运动范围最大的孩子。

```py
class GrammarCoverageFuzzer(GrammarCoverageFuzzer):
    def choose_node_expansion(self, node, possible_children):
        (symbol, children) = node
        new_coverages = self.new_coverages(node, possible_children)

        if new_coverages is None:
            # All expansions covered - use superclass method
            return self.choose_covered_node_expansion(node, possible_children)

        max_new_coverage = max(len(cov) for cov in new_coverages)

        children_with_max_new_coverage = [c for (i, c) in enumerate(possible_children)
                                          if len(new_coverages[i]) == max_new_coverage]
        index_map = [i for (i, c) in enumerate(possible_children)
                     if len(new_coverages[i]) == max_new_coverage]

        # Select a random expansion
        new_children_index = self.choose_uncovered_node_expansion(
            node, children_with_max_new_coverage)
        new_children = children_with_max_new_coverage[new_children_index]

        # Save the expansion as covered
        key = expansion_key(symbol, new_children)

        if self.log:
            print("Now covered:", key)
        self.covered_expansions.add(key)

        return index_map[new_children_index]

```

现在我们的模糊器已经完成。 让我们将其应用于一系列示例。 在表达式上，它可以快速覆盖所有数字和运算符：

```py
f = GrammarCoverageFuzzer(EXPR_GRAMMAR, min_nonterminals=3)
f.fuzz()

```

```py
'-4.02 / (1) * +3 + 5.9 / 7 * 8 - 6'

```

```py
f.max_expansion_coverage() - f.expansion_coverage()

```

```py
set()

```

平均而言，它比简单策略还快：

```py
average_length_until_full_coverage(GrammarCoverageFuzzer(EXPR_GRAMMAR))

```

```py
50.74

```

在CGI语法上，只需几次迭代即可覆盖所有字母和数字：

```py
f = GrammarCoverageFuzzer(CGI_GRAMMAR, min_nonterminals=5)
while len(f.max_expansion_coverage() - f.expansion_coverage()) > 0:
    print(f.fuzz())

```

```py
%18%d03
%c3%94%7f+cd
%a6%b5%e2%5e%4c-54e01a2
%5eb%7cb_ec%a0+

```

通过比较CGI语法的随机，仅扩展和深度预见策略，也可以看到这种改进：

```py
average_length_until_full_coverage(TrackingGrammarCoverageFuzzer(CGI_GRAMMAR))

```

```py
211.34

```

```py
average_length_until_full_coverage(SimpleGrammarCoverageFuzzer(CGI_GRAMMAR))

```

```py
68.64

```

```py
average_length_until_full_coverage(GrammarCoverageFuzzer(CGI_GRAMMAR))

```

```py
40.38

```

## 上下文[的覆盖范围](#Coverage-in-Context)

有时，语法元素会在多个地方使用。 例如，在我们的表达式语法中，`<integer>`符号用于整数以及浮点数：

```py
EXPR_GRAMMAR["<factor>"]

```

```py
['+<factor>', '-<factor>', '(<expr>)', '<integer>.<integer>', '<integer>']

```

如上所定义，我们的覆盖产品将确保覆盖所有`<integer>`扩展（即所有`<digit>`扩展）。 但是，在语法中所有出现的`<integer>`时，单个数字都是*分布*。 如果我们基于覆盖的模糊器产生`1234.56`和`7890`，那么我们将完全覆盖所有数字扩展。 但是，上述`<factor>`扩展中的`<integer>.<integer>`和`<integer>`仅单独覆盖了一部分数字。 如果浮点数和整数具有不同的读取功能，则我们希望对所有这些功能进行全数字测试； 也许我们还希望对浮点数的整个和小数部分进行测试，每个数字都包含数字。

如果我们可以假定该符号的所有出现都得到相同的对待，则忽略使用符号的上下文（在我们的示例中，`<factor>`上下文中`<integer>`和`<digit>`的各种用法）可能很有用。 但是，如果不是，则一种确保符号的出现独立于其他出现而被系统地覆盖的方法是将该出现分配给新符号，该符号是旧符号的*副本*。 我们将首先展示如何手动创建此类重复项，然后介绍一种自动执行此操作的专用功能。

### 手动扩展文法以覆盖上下文

如上所述，一种实现上下文覆盖的简单方法是通过*复制*符号及其引用的规则。 例如，我们可以将`<integer>.<integer>`替换为`<integer-1>.<integer-2>`，并赋予`<integer-1>`和`<integer-2>`与原始`<integer>`相同的定义。 这意味着不仅将覆盖`<integer>`的所有扩展，而且还将覆盖`<integer-1>`和`<integer-2>`的所有扩展。

让我们用实际代码说明这一点：

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR,
                                  {
                                      "<factor>": ["+<factor>", "-<factor>", "(<expr>)", "<integer-1>.<integer-2>", "<integer>"],
                                      "<integer-1>": ["<digit-1><integer-1>", "<digit-1>"],
                                      "<integer-2>": ["<digit-2><integer-2>", "<digit-2>"],
                                      "<digit-1>":
                                      ["0", "1", "2", "3", "4",
                                          "5", "6", "7", "8", "9"],
                                      "<digit-2>":
                                      ["0", "1", "2", "3", "4",
                                          "5", "6", "7", "8", "9"]
                                  }
                                  )

```

```py
assert is_valid_grammar(dup_expr_grammar)

```

如果现在在扩展语法上运行基于覆盖率的模糊器，那么我们将覆盖正整数的所有数字，以及浮点数的整数和小数部分的所有数字：

```py
f = GrammarCoverageFuzzer(dup_expr_grammar, start_symbol="<factor>")
for i in range(10):
    print(f.fuzz())

```

```py
-(43.76 / 8.0 * 5.5 / 6.9 * 6 / 4 + +03)
(90.1 - 1 * 7.3 * 9 + 5 / 8 / 7)
2.8
1.2
10.4
2
4386
7
0
08929.4302

```

我们将看到我们的“有远见”的覆盖率模糊器是如何专门生成浮点数的，该浮点数覆盖整个和小数部分的所有数字。

### 以编程方式扩展文法覆盖上下文

如果我们想增强上下文的覆盖范围，则手动调整语法可能不是理想的选择，因为对语法的任何更改都必须复制在所有重复项中。 取而代之的是，我们引入了一个将为我们做重复的功能。

函数`duplicate_context()`接受语法，语法中的符号以及该符号的扩展名（`None`或未提供：符号的所有扩展名），并且它将扩展名更改为引用所有原始引用规则的副本 。 这个想法是我们以

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<factor>", "<integer>.<integer>")

```

并获得与上述手动更改类似的结果。

这是代码：

```py
from [Grammars](Grammars.html) import new_symbol, unreachable_nonterminals
from [GrammarFuzzer](GrammarFuzzer.html) import expansion_to_children

```

```py
def duplicate_context(grammar, symbol, expansion=None, depth=float('inf')):
    """Duplicate an expansion within a grammar.

 In the given grammar, take the given expansion of the given symbol
 (if expansion is omitted: all symbols), and replace it with a
 new expansion referring to a duplicate of all originally referenced rules.

 If depth is given, limit duplication to `depth` references (default: unlimited)
 """
    orig_grammar = extend_grammar(grammar)
    _duplicate_context(grammar, orig_grammar, symbol,
                       expansion, depth, seen={})

    # After duplication, we may have unreachable rules; delete them
    for nonterminal in unreachable_nonterminals(grammar):
        del grammar[nonterminal]

```

大部分工作都在此辅助功能中进行。 附加参数`seen`跟踪已扩展的符号，并避免无限递归。

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

```

```py
def _duplicate_context(grammar, orig_grammar, symbol, expansion, depth, seen):
    for i in range(len(grammar[symbol])):
        if expansion is None or grammar[symbol][i] == expansion:
            new_expansion = ""
            for (s, c) in expansion_to_children(grammar[symbol][i]):
                if s in seen:                 # Duplicated already
                    new_expansion += seen[s]
                elif c == [] or depth == 0:   # Terminal symbol or end of recursion
                    new_expansion += s
                else:                         # Nonterminal symbol - duplicate
                    # Add new symbol with copy of rule
                    new_s = new_symbol(grammar, s)
                    grammar[new_s] = copy.deepcopy(orig_grammar[s])

                    # Duplicate its expansions recursively
                    # {**seen, **{s: new_s}} is seen + {s: new_s}
                    _duplicate_context(grammar, orig_grammar, new_s, expansion=None,
                                       depth=depth - 1, seen={**seen, **{s: new_s}})
                    new_expansion += new_s

            grammar[symbol][i] = new_expansion

```

这是我们上面的`duplicate_context()`工作原理示例，现在有了结果。 我们让它在表达式语法中复制`<integer>.<integer>`扩展名，并获得一个带有`<integer-1>.<integer-2>`扩展名的新语法，其中`<integer-1>`和`<integer-2>`都引用原始规则的副本：

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<factor>", "<integer>.<integer>")
dup_expr_grammar

```

```py
{'<start>': ['<expr>'],
 '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'],
 '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'],
 '<factor>': ['+<factor>',
  '-<factor>',
  '(<expr>)',
  '<integer-1>.<integer-2>',
  '<integer>'],
 '<integer>': ['<digit><integer>', '<digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integer-1>': ['<digit-1><integer-1>', '<digit-2>'],
 '<digit-1>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<digit-2>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integer-2>': ['<digit-3><integer-2>', '<digit-4>'],
 '<digit-3>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<digit-4>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']}

```

就像上面一样，使用这样的语法进行覆盖模糊处理现在将覆盖许多上下文中的数字。 准确地说，有五个上下文：正则整数，以及浮点数的一位和多位整数和小数部分。

```py
f = GrammarCoverageFuzzer(dup_expr_grammar, start_symbol="<factor>")
for i in range(10):
    print(f.fuzz())

```

```py
(57.5)
2
+-(1 / 3 + 6 / 0 - 7 * 59 * 3 + 8 * 4)
374.88
5.709
0.93
01.1
892.27
219.50
6.636

```

`depth`参数控制复制应进行的深度。 将`depth`设置为1只会复制下一条规则：

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<factor>", "<integer>.<integer>", depth=1)
dup_expr_grammar

```

```py
{'<start>': ['<expr>'],
 '<expr>': ['<term> + <expr>', '<term> - <expr>', '<term>'],
 '<term>': ['<factor> * <term>', '<factor> / <term>', '<factor>'],
 '<factor>': ['+<factor>',
  '-<factor>',
  '(<expr>)',
  '<integer-1>.<integer-2>',
  '<integer>'],
 '<integer>': ['<digit><integer>', '<digit>'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integer-1>': ['<digit><integer-1>', '<digit>'],
 '<integer-2>': ['<digit><integer-2>', '<digit>']}

```

```py
assert is_valid_grammar(dup_expr_grammar)

```

默认情况下，`depth`设置为$ \ infty $，表示无限复制。 真正的无界重复可能会导致诸如`EXPR_GRAMMAR`之类的递归语法出现问题，因此一旦将`duplicate_context()`设置为不再重复的符号。 尽管如此，如果我们将其应用于所有 `<expr>`扩展的*重复项，我们将获得不少于296条规则的语法：*

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<expr>")

```

```py
assert is_valid_grammar(dup_expr_grammar)
len(dup_expr_grammar)

```

```py
292

```

这使我们可以扩展将近2000个范围：

```py
f = GrammarCoverageFuzzer(dup_expr_grammar)
len(f.max_expansion_coverage())

```

```py
1981

```

再重复一遍，既使语法又提高了覆盖率要求：

```py
dup_expr_grammar = extend_grammar(EXPR_GRAMMAR)
duplicate_context(dup_expr_grammar, "<expr>")
duplicate_context(dup_expr_grammar, "<expr-1>")
len(dup_expr_grammar)

```

```py
594

```

```py
f = GrammarCoverageFuzzer(dup_expr_grammar)
len(f.max_expansion_coverage())

```

```py
3994

```

在这一点上，可以单独涵盖很多上下文，例如，加法中元素的乘法：

```py
dup_expr_grammar["<expr>"]

```

```py
['<term-1> + <expr-4>', '<term-5> - <expr-8>', '<term-9>']

```

```py
dup_expr_grammar["<term-1-1>"]

```

```py
['<factor-1-1> * <term-1-1>', '<factor-2-1> / <term-1-1>', '<factor-3-1>']

```

```py
dup_expr_grammar["<factor-1-1>"]

```

```py
['+<factor-1-1>',
 '-<factor-1-1>',
 '(<expr-1-1>)',
 '<integer-1-1>.<integer-2-1>',
 '<integer-3-1>']

```

产生的语法可能不再对人类维护有用； 但是运行覆盖率驱动的模糊器（例如`GrammarCoverageFuzzer()`）将可以覆盖所有情况下的所有这些扩展。 如果您想覆盖大量上下文中的元素，那么`duplicate_context()`和覆盖率驱动的模糊器是您的朋友。

## 通过覆盖语法来覆盖代码

有无上下文：通过系统地覆盖所有输入元素，我们可以在输入中获得更大的种类-但这是否会转化为更广泛的程序行为？ 毕竟，这些行为是我们要涵盖的，包括意外行为。

在语法中，有些元素直接对应于程序功能。 处理算术表达式的程序将具有直接由各个元素触发的功能-例如，由`+`存在触发的加法功能，由`-`存在触发的减法和由`-`存在触发的浮点算术 输入中的浮点数。

输入结构和功能之间的这种联系导致语法覆盖范围和代码覆盖范围之间具有很强的*相关性。 换句话说：如果我们可以实现较高的语法覆盖率，那么这也将导致较高的代码覆盖率。*

### CGI语法

让我们在我们的一种语法中探索这种关系，例如，关于覆盖范围的[章中的CGI解码器。 我们计算映射`coverages`，其中在`coverages[x]` = `{y_1, y_2, ...}`中，`x`是获得的语法覆盖率，`y_n`是从第`n`次运行获得的代码覆盖率。](Coverage.html)

我们首先计算最大覆盖率，如关于覆盖率的[一章：](Coverage.html)

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

```

```py
with Coverage() as cov_max:
    cgi_decode('+')
    cgi_decode('%20')
    cgi_decode('abc')
    try:
        cgi_decode('%?a')
    except:
        pass

```

现在，我们进行实验：

```py
f = GrammarCoverageFuzzer(CGI_GRAMMAR, max_nonterminals=2)
coverages = {}

trials = 100
for trial in range(trials):
    f.reset_coverage()
    overall_cov = set()
    max_cov = 30

    for i in range(10):
        s = f.fuzz()
        with Coverage() as cov:
            cgi_decode(s)
        overall_cov |= cov.coverage()

        x = len(f.expansion_coverage()) * 100 / len(f.max_expansion_coverage())
        y = len(overall_cov) * 100 / len(cov_max.coverage())
        if x not in coverages:
            coverages[x] = []
        coverages[x].append(y)

```

我们计算`y`值的平均值：

```py
xs = list(coverages.keys())
ys = [sum(coverages[x]) / len(coverages[x]) for x in coverages]

```

并创建散点图：

```py
%matplotlib inline

```

```py
import [matplotlib.pyplot](https://docs.python.org/3/library/matplotlib.pyplot.html) as [plt](https://docs.python.org/3/library/plt.html)

```

```py
import [matplotlib.ticker](https://docs.python.org/3/library/matplotlib.ticker.html) as [mtick](https://docs.python.org/3/library/mtick.html)

```

```py
ax = plt.axes(label="coverage")
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
ax.xaxis.set_major_formatter(mtick.PercentFormatter())

plt.xlim(0, max(xs))
plt.ylim(0, max(ys))

plt.title('Coverage of cgi_decode() vs. grammar coverage')
plt.xlabel('grammar coverage (expansions)')
plt.ylabel('code coverage (lines)')
plt.scatter(xs, ys);

```

![](data:image/png;base64, 
)

我们看到语法覆盖率越高，代码覆盖率越高。

这也将转化为约0.9的相关系数，表明相关性很强：

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

```

```py
np.corrcoef(xs, ys)

```

```py
array([[1\.        , 0.81663071],
       [0.81663071, 1\.        ]])

```

Spearman等级相关性也证实了这一点：

```py
from [scipy.stats](https://docs.python.org/3/library/scipy.stats.html) import spearmanr

```

```py
spearmanr(xs, ys)

```

```py
SpearmanrResult(correlation=0.937547293248041, pvalue=1.0928720949027369e-09)

```

### URL语法

让我们在URL语法上重复此实验。 除了交换语法和功能外，我们使用与上述相同的代码：

```py
try:
    from [urlparse](https://docs.python.org/3/library/urlparse.html) import urlparse      # Python 2
except ImportError:
    from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse  # Python 3

```

再次，我们首先计算最大覆盖率，如关于覆盖率的[一章所述，进行有根据的猜测：](Coverage.html)

```py
with Coverage() as cov_max:
    urlparse("http://foo.bar/path")
    urlparse("https://foo.bar#fragment")
    urlparse("ftp://user:password@foo.bar?query=value")
    urlparse("ftps://127.0.0.1/?x=1&y=2")

```

这是实际的实验：

```py
f = GrammarCoverageFuzzer(URL_GRAMMAR, max_nonterminals=2)
coverages = {}

trials = 100
for trial in range(trials):
    f.reset_coverage()
    overall_cov = set()

    for i in range(20):
        s = f.fuzz()
        with Coverage() as cov:
            urlparse(s)
        overall_cov |= cov.coverage()

        x = len(f.expansion_coverage()) * 100 / len(f.max_expansion_coverage())
        y = len(overall_cov) * 100 / len(cov_max.coverage())
        if x not in coverages:
            coverages[x] = []
        coverages[x].append(y)

```

```py
xs = list(coverages.keys())
ys = [sum(coverages[x]) / len(coverages[x]) for x in coverages]

```

```py
ax = plt.axes(label="coverage")
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
ax.xaxis.set_major_formatter(mtick.PercentFormatter())

plt.xlim(0, max(xs))
plt.ylim(0, max(ys))

plt.title('Coverage of urlparse() vs. grammar coverage')
plt.xlabel('grammar coverage (expansions)')
plt.ylabel('code coverage (lines)')
plt.scatter(xs, ys);

```

![](data:image/png;base64, 
)

在这里，我们的相关系数甚至超过了0.95：

```py
np.corrcoef(xs, ys)

```

```py
array([[1\.        , 0.94110679],
       [0.94110679, 1\.        ]])

```

This is also confirmed by the Spearman rank correlation:

```py
spearmanr(xs, ys)

```

```py
SpearmanrResult(correlation=1.0, pvalue=0.0)

```

我们得出的结论是：如果要获得较高的代码覆盖率，则首先争取较高的语法覆盖率是一个好主意。

### 这会一直有效吗？

对于CGI和URL示例观察到的相关性并不适用于每个程序和每个结构。

#### 等效元素

首先，即使某个语法元素将它们视为不同的符号，它们也会被程序统一处理。 例如，在URL的主机名中，我们可以有许多不同的字符，尽管URL处理程序将它们完全相同。 同样，单个数字一旦组合成一个数字，其影响就小于数字本身的值。 因此，实现数字或字符的多样性不一定会在功能上产生很大的差异。

如上所述，可以通过*取决于元素的上下文*区分元素并覆盖每种上下文的替代方案来解决此问题。 关键是确定需要多样性的环境，而不是不需要多样性的环境。

#### 深度数据处理

其次，数据的处理方式可能会有很大的不同。 考虑到*媒体播放器*的输入，该输入由压缩的媒体数据组成。 在处理媒体数据时，媒体播放器将显示行为上的差异（特别是在其输出中），但是无法通过媒体数据的各个元素直接触发这些差异。 同样，在大量输入上训练的*机器学习器*通常不会通过输入的单个语法元素来控制其行为。 （嗯，可以，但是然后，我们将不需要机器学习者。）在“深度”数据处理的这些情况下，在语法上实现结构覆盖不一定会导致代码覆盖。

解决此问题的一种方法不仅是实现*语法*，而且实际上是*语义*变体。 在[关于带约束的模糊化](GeneratorGrammarFuzzer.html)的章节中，我们将看到如何专门生成和过滤输入值，尤其是数值。 这样的生成器也可以在上下文中应用，从而可以分别控制输入的每个方面。 同样，在以上示例中，*输入的某些*部分仍可以在结构上覆盖：*元数据*（例如媒体播放器的作者姓名或作曲者）或*配置数据[* （例如机器学习者的设置）可以并且应该被系统地涵盖； 我们将在“配置模糊” 一章中看到如何完成此操作。

## 经验教训

*   快速实现*语法覆盖率*会产生各种各样的输入。
*   复制语法规则允许覆盖特定*上下文*中的元素。
*   实现语法覆盖率可以帮助获得*代码覆盖率*。

## 后续步骤

从这里，您可以学习如何

*   [使用语法覆盖系统地测试配置](ConfigurationFuzzer.html)。

## 背景

确保语法中的每个扩展至少使用一次的想法可以追溯到Burkhardt [ [Burkhardt *等人*，1967。](https://doi.org/10.1007/BF02235512)]，稍后由Paul Purdom [[重新发现。 Purdom *等*，1972。](https://doi.org/10.1007/BF01932308)。 语法覆盖率和代码覆盖率之间的关系是由Nikolas Havrikov发现的，他在其博士学位论文中对此进行了探讨。

## 练习

### 练习1：测试ls

考虑用于列出目录内容的Unix `ls`程序。 创建用于调用`ls`的语法：

```py
LS_EBNF_GRAMMAR = {
    '<start>': ['-<options>'],
    '<options>': ['<option>*'],
    '<option>': ['1', 'A', '@',
                 # many more
                 ]
}

```

```py
assert is_valid_grammar(LS_EBNF_GRAMMAR)

```

使用`GrammarCoverageFuzzer`测试所有选项。 确保在每个选项集中都调用`ls`。

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

### 练习2：缓存

`max_expansion_coverage()`的值仅取决于语法。 更改实现，以便在初始化时为每个符号和深度预先计算值（`__init__()`）； 这样，`max_expansion_coverage()`可以简单地在表中查找值。

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