# 测试配置

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

程序的行为不仅受其数据支配。 程序的*配置*（即由选项或配置文件设置的用于控制程序在其（常规）输入数据上执行的设置）也会影响行为，因此可以并且 应该进行测试。 在本章中，我们探索如何系统地*测试*和*涵盖*软件配置。 通过*自动推断配置选项*，我们可以立即使用这些技术，而无需编写语法。 最后，我们展示了如何系统地涵盖配置选项的*组合*，从而快速检测到不必要的干扰。

**前提条件**

*   您应该阅读语法的[一章。](Grammars.html)
*   您应该已经阅读[一章，内容涉及语法](GrammarCoverageFuzzer.html)。

## 内容提要

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

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

```

然后利用以下功能。

本章提供了两个类：

*   `OptionRunner`自动从Python程序中提取命令行选项；
*   `OptionFuzzer`使用这些选项自动测试具有多种选项的Python程序。

`OptionRunner`运行程序直至解析其参数，然后提取描述其调用的语法：

```py
>>> autopep8_runner = OptionRunner("autopep8", "foo.py")

```

可以通过`ebnf_grammar()`方法提取语法：

```py
>>> option_ebnf_grammar = autopep8_runner.ebnf_grammar()
>>> print(option_ebnf_grammar)
{'<start>': ['(<option>)*<arguments>'], '<option>': [' -h', ' --help', ' --version', ' -v', ' --verbose', ' -d', ' --diff', ' -i', ' --in-place', ' --global-config <filename>', ' --ignore-local-config', ' -r', ' --recursive', ' -j <n>', ' --jobs <n>', ' -p <n>', ' --pep8-passes <n>', ' -a', ' --aggressive', ' --experimental', ' --exclude <globs>', ' --list-fixes', ' --ignore <errors>', ' --select <errors>', ' --max-line-length <n>', ' --line-range <line> <line>', ' --range <line> <line>', ' --indent-size <int>', ' --hang-closing'], '<arguments>': [' foo.py'], '<str>': ['<char>+'], '<char>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~'], '<filename>': ['<str>'], '<int>': ['(-)?<digit>+'], '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'], '<n>': ['<int>'], '<globs>': ['<str>'], '<errors>': ['<str>'], '<line>': ['<int>']}

```

语法可立即用于模糊测试。 `GrammarCoverageFuzzer`将确保涵盖所有选项：

```py
>>> from [Grammars](Grammars.html) import convert_ebnf_grammar
>>> fuzzer = GrammarCoverageFuzzer(convert_ebnf_grammar(option_ebnf_grammar))
>>> [fuzzer.fuzz() for i in range(3)]
[' foo.py',
 ' --indent-size 54 --diff --global-config k --select &, --list-fixes -a --hang-closing --range 0 72 --ignore-local-config -p 8 --version -d --experimental foo.py',
 ' --ignore i --jobs -16 --verbose -v --line-range -3 9 -r --help --max-line-length 8 -h --aggressive --recursive --exclude qE" --in-place -j -979 -i --pep8-passes 4 --version --in-place --aggressive --version foo.py']

```

`OptionFuzzer`类总结了这些步骤。 它的构造函数使用`OptionRunner`自动提取语法。 它执行必要的步骤来提取语法并对其进行模糊测试。

```py
>>> autopep8_runner = OptionRunner("autopep8", "foo.py")
>>> autopep8_fuzzer = OptionFuzzer(autopep8_runner)
>>> [autopep8_fuzzer.fuzz() for i in range(3)]
[' foo.py',
 ' --range 46 -1 --recursive -d --select <6 --exclude :" --global-config UVE --help --aggressive --experimental -r --line-range -7 -9 --version -i -h --indent-size -05 --max-line-length 8 --in-place --verbose --jobs -32 --ignore-local-config -v -p -1 --hang-closing -j 38 -a --list-fixes --pep8-passes 67 --diff --ignore v --select I --ignore (1NJ --ignore Km --ignore ? --select ^kZ --global-config y --select ia]9 --exclude o --ignore R!4GP.x8/ --ignore D --exclude 7 --exclude Bd -a --recursive --verbose foo.py',
 " --ignore \\ --global-config l --global-config @ --ignore ,CM~& --ignore nb --select c --global-config zgW --ignore $`s{H --global-config - --exclude 2| --select O --exclude 0 --exclude * --ignore qA'F}X --global-config p>_r+ --global-config eQ --exclude [ --ignore t --select h) --select %f --exclude u#3;=TL --global-config w --ignore j5 --exclude Y --ignore S --ignore ]J --global-config 1 --ignore-local-config --max-line-length 36693 -i foo.py"]

```

现在测试的最后一步是使用这些参数调用程序。

请注意，`OptionRunner`是实验性的：它假设所讨论的Python程序使用了`argparse`模块； 并非所有`argparse`功能都受支持。 尽管如此，即使在非平凡的程序上，它也做得很好。

## 配置选项

当我们谈论程序的输入时，通常会想到它处理的*数据*。 这也是我们在过去几章中一直在进行的模糊处理-使用[随机输入](Fuzzer.html)，基于[突变的模糊化](MutationFuzzer.html)或基于[语法的模糊化](GrammarFuzzer.html)。 但是，程序通常具有多个输入源，所有这些源都可以并且应该进行测试-并包含在测试生成中。

输入的一个重要来源是程序的*配置* –也就是说，一组输入通常在开始处理数据时设置一次，然后在处理数据时，程序运行时甚至在运行时保持不变。 该程序已部署。 通常在*配置文件*中设置这种配置（例如，作为键/值对）； 但是，命令行工具中最普遍使用的方法是命令行上的*配置选项*。

例如，考虑使用`grep`实用程序在文件中查找文本模式。 `grep`的确切工作方式由众多选项决定，可以通过提供`--help`选项列出这些选项：

```py
!grep --help

```

```py
usage: grep [-abcDEFGHhIiJLlmnOoqRSsUVvwxZ] [-A num] [-B num] [-C[num]]
	[-e pattern] [-f file] [--binary-files=value] [--color=when]
	[--context[=num]] [--directories=action] [--label] [--line-buffered]
	[--null] [pattern] [file ...]

```

所有这些选项都需要测试它们是否正确运行。 在安全测试中，任何此类选项也可能触发未知的漏洞。 因此，这些选项可以自己成为*模糊目标*。 在本章中，我们将分析如何系统地测试这些选项，更好的是，如何从给定的程序文件中提取可能的配置，这样我们就不必指定任何内容。

## Python [中的选项](#Options-in-Python)

让我们在这里坚持我们的通用编程语言，并检查如何在Python中处理选项。 `argparse`模块为命令行参数（和选项）提供了一个功能强大且复杂度很高的解析器。 首先，定义一个解析器（`argparse.ArgumentParser()`），在其中添加具有各种功能的单个参数，一个接一个。 每个参数的附加参数可以指定参数的类型（`type`）（例如整数或字符串），或参数的数量（`nargs`）。

默认情况下，参数以其名称存储在来自`parse_args()`的`args`对象中–因此，`args.integers`保留先前添加的`integers`参数。 特殊操作（`actions`）允许在给定变量中存储特定值； `store_const`操作将给定的`const`存储在以`dest`命名的属性中。 下面的示例采用多个整数参数（`integers`）和一个运算符（`--sum`，`--min`或`--max`）应用于这些整数。 所有运算符都在`accumulate`属性中存储一个函数引用，该引用最终在解析的整数上调用：

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

```

```py
def process_numbers(args=[]):
    parser = argparse.ArgumentParser(description='Process some integers.')
    parser.add_argument('integers', metavar='N', type=int, nargs='+',
                        help='an integer for the accumulator')
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument('--sum', dest='accumulate', action='store_const',
                       const=sum,
                       help='sum the integers')
    group.add_argument('--min', dest='accumulate', action='store_const',
                       const=min,
                       help='compute the minimum')
    group.add_argument('--max', dest='accumulate', action='store_const',
                       const=max,
                       help='compute the maximum')

    args = parser.parse_args(args)
    print(args.accumulate(args.integers))

```

`process_numbers()`的工作方式如下。 例如，我们可以在给定参数上调用`--min`选项以计算最小值：

```py
process_numbers(["--min", "100", "200", "300"])

```

```py
100

```

或计算三个数字的总和：

```py
process_numbers(["--sum", "1", "2", "3"])

```

```py
6

```

通过`add_mutually_exclusive_group()`（如上所述）定义时，选项是互斥的。 因此，我们只能有一个运算符：

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

```

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

```

```py
with ExpectError(print_traceback=False):
    process_numbers(["--sum", "--max", "1", "2", "3"])

```

```py
usage: ipykernel_launcher.py [-h] (--sum | --min | --max) N [N ...]
ipykernel_launcher.py: error: argument --max: not allowed with argument --sum
SystemExit: 2 (expected)

```

## 配置语法

我们如何测试具有多个选项的系统？ 最简单的答案是为此编写语法。 语法`PROCESS_NUMBERS_EBNF_GRAMMAR`反映了选项和参数的可能组合：

```py
from [Grammars](Grammars.html) import crange, srange, convert_ebnf_grammar, extend_grammar, is_valid_grammar
from [Grammars](Grammars.html) import START_SYMBOL, new_symbol

```

```py
PROCESS_NUMBERS_EBNF_GRAMMAR = {
    "<start>": ["<operator> <integers>"],
    "<operator>": ["--sum", "--min", "--max"],
    "<integers>": ["<integer>", "<integers> <integer>"],
    "<integer>": ["<digit>+"],
    "<digit>": crange('0', '9')
}

assert is_valid_grammar(PROCESS_NUMBERS_EBNF_GRAMMAR)

```

```py
PROCESS_NUMBERS_GRAMMAR = convert_ebnf_grammar(PROCESS_NUMBERS_EBNF_GRAMMAR)

```

我们可以将此语法输入到[语法覆盖率测试工具](GrammarCoverageFuzzer.html)中，然后逐个覆盖它：

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

```

```py
f = GrammarCoverageFuzzer(PROCESS_NUMBERS_GRAMMAR, min_nonterminals=10)
for i in range(3):
    print(f.fuzz())

```

```py
--max 9 5 8 210 80 9756431
--sum 9 4 99 1245 612370
--min 2 3 0 46 15798 7570926

```

当然，我们也可以使用这些参数来调用`process_numbers()`。 为此，我们需要使用`split()`将语法产生的字符串转换回单个参数列表：

```py
f = GrammarCoverageFuzzer(PROCESS_NUMBERS_GRAMMAR, min_nonterminals=10)
for i in range(3):
    args = f.fuzz().split()
    print(args)
    process_numbers(args)

```

```py
['--max', '8', '9', '3067', '44', '13852967057']
13852967057
['--sum', '9', '8', '63', '9278111', '59206197798']
59215475989
['--min', '4', '1', '4864', '33342', '7827970808951']
1

```

以类似的方式，我们可以为要测试的任何程序定义语法。 以及为配置文件定义语法。 但是，必须对程序进行每次更改才能更新语法，这会带来维护负担。 既然语法所需的信息已经在程序中全部编码了，就会出现问题：*我们不能首先从程序中直接提取配置选项吗？*

## 挖掘配置选项

在本节中，我们尝试从程序中提取选项和参数信息，这样就不必指定配置语法。 目的是要有一个配置模糊器，它可以对任意程序的选项和参数起作用，只要它遵循用于处理其参数的特定约定即可。 对于Python程序，这意味着使用`argparse`模块。

我们的想法如下：我们执行给定程序直到实际解析参数为止-即调用`argparse.parse_args()`。 到目前为止，我们将所有调用都跟踪到参数解析器中，特别是定义了参数和选项（`add_argument()`）的那些调用。 由此，我们构建语法。

### 跟踪参数

让我们通过一个简单的实验来说明这种方法：我们定义一个跟踪函数（有关详细信息，请参见关于覆盖的章节[），该函数在调用`process_numbers`时处于活动状态。 如果调用方法`add_argument`，则将访问并打印局部变量（此时是该方法的参数）。](Coverage.html)

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

```

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

```

```py
def traceit(frame, event, arg):
    if event != "call":
        return
    method_name = frame.f_code.co_name
    if method_name != "add_argument":
        return
    locals = frame.f_locals
    print(method_name, locals)

```

我们得到的是对`add_argument()`的所有调用的列表，以及传递的方法参数：

```py
sys.settrace(traceit)
process_numbers(["--sum", "1", "2", "3"])
sys.settrace(None)

```

```py
add_argument {'kwargs': {'action': 'help', 'default': '==SUPPRESS==', 'help': 'show this help message and exit'}, 'args': ('-h', '--help'), 'self': ArgumentParser(prog='ipykernel_launcher.py', usage=None, description='Process some integers.', formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)}
add_argument {'kwargs': {'metavar': 'N', 'type': <class 'int'>, 'nargs': '+', 'help': 'an integer for the accumulator'}, 'args': ('integers',), 'self': ArgumentParser(prog='ipykernel_launcher.py', usage=None, description='Process some integers.', formatter_class=<class 'argparse.HelpFormatter'>, conflict_handler='error', add_help=True)}
add_argument {'kwargs': {'dest': 'accumulate', 'action': 'store_const', 'const': <built-in function sum>, 'help': 'sum the integers'}, 'args': ('--sum',), 'self': <argparse._MutuallyExclusiveGroup object at 0x109c3bd30>}
add_argument {'kwargs': {'dest': 'accumulate', 'action': 'store_const', 'const': <built-in function min>, 'help': 'compute the minimum'}, 'args': ('--min',), 'self': <argparse._MutuallyExclusiveGroup object at 0x109c3bd30>}
add_argument {'kwargs': {'dest': 'accumulate', 'action': 'store_const', 'const': <built-in function max>, 'help': 'compute the maximum'}, 'args': ('--max',), 'self': <argparse._MutuallyExclusiveGroup object at 0x109c3bd30>}
6

```

从`args`参数，我们可以访问要定义的各个选项和参数：

```py
def traceit(frame, event, arg):
    if event != "call":
        return
    method_name = frame.f_code.co_name
    if method_name != "add_argument":
        return
    locals = frame.f_locals
    print(locals['args'])

```

```py
sys.settrace(traceit)
process_numbers(["--sum", "1", "2", "3"])
sys.settrace(None)

```

```py
('-h', '--help')
('integers',)
('--sum',)
('--min',)
('--max',)
6

```

我们看到每个参数都是一个元组，具有一个（例如`integers`或`--sum`）或两个成员（`-h`和`--help`），它们表示同一选项的替代形式。 我们的工作将是遍历`add_arguments()`的参数，并不仅检测选项和参数的名称，而且还要检测它们是否接受其他参数以及参数的类型。

### 用于选项和参数的语法挖掘器

现在，让我们建立一个收集所有这些信息以创建语法的类。

在收集所有参数和选项之后，我们使用`ParseInterrupt`异常来中断程序执行：

```py
class ParseInterrupt(Exception):
    pass

```

`OptionGrammarMiner`类采用一个可执行函数，该函数将挖掘选项和参数的语法：

```py
class OptionGrammarMiner(object):
    def __init__(self, function, log=False):
        self.function = function
        self.log = log

```

方法`mine_ebnf_grammar()`是一切发生的地方。 它创建形式的语法

```py
<start> ::= <option>* <arguments>
<option> ::= <empty>
<arguments> ::= <empty>
```

其中将收集选项和参数。 然后，它会设置一个跟踪功能（有关详细信息，请参见本章中的章节[），该函数在调用先前定义的`function`时将处于活动状态。 引发`ParseInterrupt`（在调用`parse_args()`时）结束执行。](Coverage.html)

```py
class OptionGrammarMiner(OptionGrammarMiner):
    OPTION_SYMBOL = "<option>"
    ARGUMENTS_SYMBOL = "<arguments>"

    def mine_ebnf_grammar(self):
        self.grammar = {
            START_SYMBOL: ["(" + self.OPTION_SYMBOL + ")*" + self.ARGUMENTS_SYMBOL],
            self.OPTION_SYMBOL: [],
            self.ARGUMENTS_SYMBOL: []
        }
        self.current_group = self.OPTION_SYMBOL

        old_trace = sys.settrace(self.traceit)
        try:
            self.function()
        except ParseInterrupt:
            pass
        sys.settrace(old_trace)

        return self.grammar

    def mine_grammar(self):
        return convert_ebnf_grammar(self.mine_ebnf_grammar())

```

跟踪函数检查四种方法：`add_argument()`是最重要的函数，导致处理参数。 `frame.f_locals`还是局部变量的集合，这时主要是`add_argument()`的参数。 由于互斥组也具有方法`add_argument()`，因此我们将标志`in_group`设置为可区分。

请注意，我们没有做出具体努力来区分多个解析器或组。 我们仅假设有一个解析器，并且在任何时候最多只有一个互斥的组。

```py
class OptionGrammarMiner(OptionGrammarMiner):
    def traceit(self, frame, event, arg):
        if event != "call":
            return

        if "self" not in frame.f_locals:
            return
        self_var = frame.f_locals["self"]

        method_name = frame.f_code.co_name

        if method_name == "add_argument":
            in_group = repr(type(self_var)).find("Group") >= 0
            self.process_argument(frame.f_locals, in_group)
        elif method_name == "add_mutually_exclusive_group":
            self.add_group(frame.f_locals, exclusive=True)
        elif method_name == "add_argument_group":
            # self.add_group(frame.f_locals, exclusive=False)
            pass
        elif method_name == "parse_args":
            raise ParseInterrupt

        return None

```

`process_arguments()`现在分析传递的参数并将其添加到语法中：

*   如果参数以`-`开头，则将其作为可选元素添加到`<option>`列表中
*   否则，它将被添加到`<argument>`列表中。

可选的`nargs`参数指定可以跟随的参数数量。 如果是数字，则在语法中添加适当数量的元素。 如果它是抽象说明符（例如`+`或`*`），我们将其直接用作EBNF运算符。

考虑到大量的参数和可选的行为，这是一个有点杂乱的函数，但确实可以做到。

```py
class OptionGrammarMiner(OptionGrammarMiner):
    def process_argument(self, locals, in_group):
        args = locals["args"]
        kwargs = locals["kwargs"]

        if self.log:
            print(args)
            print(kwargs)
            print()

        for arg in args:
            self.process_arg(arg, in_group, kwargs)

```

```py
class OptionGrammarMiner(OptionGrammarMiner):
    def process_arg(self, arg, in_group, kwargs):
        if arg.startswith('-'):
            if not in_group:
                target = self.OPTION_SYMBOL
            else:
                target = self.current_group
            metavar = None
            arg = " " + arg
        else:
            target = self.ARGUMENTS_SYMBOL
            metavar = arg
            arg = ""

        if "nargs" in kwargs:
            nargs = kwargs["nargs"]
        else:
            nargs = 1

        param = self.add_parameter(kwargs, metavar)
        if param == "":
            nargs = 0

        if isinstance(nargs, int):
            for i in range(nargs):
                arg += param
        else:
            assert nargs in "?+*"
            arg += '(' + param + ')' + nargs

        if target == self.OPTION_SYMBOL:
            self.grammar[target].append(arg)
        else:
            self.grammar[target].append(arg)

```

方法`add_parameter()`处理选项的可能参数。 如果自变量定义为`action`，则不接受任何参数。 否则，我们确定参数的类型（如`int`或`str`），并使用适当的规则扩展语法。

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

```

```py
class OptionGrammarMiner(OptionGrammarMiner):
    def add_parameter(self, kwargs, metavar):
        if "action" in kwargs:
            # No parameter
            return ""

        type_ = "str"
        if "type" in kwargs:
            given_type = kwargs["type"]
            # int types come as '<class int>'
            if inspect.isclass(given_type) and issubclass(given_type, int):
                type_ = "int"

        if metavar is None:
            if "metavar" in kwargs:
                metavar = kwargs["metavar"]
            else:
                metavar = type_

        self.add_type_rule(type_)
        if metavar != type_:
            self.add_metavar_rule(metavar, type_)

        param = " <" + metavar + ">"

        return param

```

方法`add_type_rule()`将参数类型的规则添加到语法中。 如果参数是由元变量（例如`N`）标识的，则我们也会为此添加一条规则以提高可读性。

```py
class OptionGrammarMiner(OptionGrammarMiner):
    def add_type_rule(self, type_):
        if type_ == "int":
            self.add_int_rule()
        else:
            self.add_str_rule()

    def add_int_rule(self):
        self.grammar["<int>"] = ["(-)?<digit>+"]
        self.grammar["<digit>"] = crange('0', '9')

    def add_str_rule(self):
        self.grammar["<str>"] = ["<char>+"]
        self.grammar["<char>"] = srange(
            string.digits
            + string.ascii_letters
            + string.punctuation)

    def add_metavar_rule(self, metavar, type_):
        self.grammar["<" + metavar + ">"] = ["<" + type_ + ">"]

```

方法`add_group()`将新的互斥组添加到语法中。 我们为添加到组中的选项定义了一个新符号（例如`<group>`），并使用`required`和`exclusive`标志来定义适当的扩展运算符。 然后将该组作为语法的前缀，如

```py
<start> ::= <group><option>* <arguments>
<group> ::= <empty>
```

并充满了组中对`add_argument()`的下一个调用。

```py
class OptionGrammarMiner(OptionGrammarMiner):
    def add_group(self, locals, exclusive):
        kwargs = locals["kwargs"]
        if self.log:
            print(kwargs)

        required = kwargs.get("required", False)
        group = new_symbol(self.grammar, "<group>")

        if required and exclusive:
            group_expansion = group
        if required and not exclusive:
            group_expansion = group + "+"
        if not required and exclusive:
            group_expansion = group + "?"
        if not required and not exclusive:
            group_expansion = group + "*"

        self.grammar[START_SYMBOL][0] = group_expansion + \
            self.grammar[START_SYMBOL][0]
        self.grammar[group] = []
        self.current_group = group

```

而已！ 这样，我们现在可以从`process_numbers()`程序中提取语法。 再次打开日志记录会显示我们使用的变量。

```py
miner = OptionGrammarMiner(process_numbers, log=True)
process_numbers_grammar = miner.mine_ebnf_grammar()

```

```py
('-h', '--help')
{'action': 'help', 'default': '==SUPPRESS==', 'help': 'show this help message and exit'}

('integers',)
{'metavar': 'N', 'type': <class 'int'>, 'nargs': '+', 'help': 'an integer for the accumulator'}

{'required': True}
('--sum',)
{'dest': 'accumulate', 'action': 'store_const', 'const': <built-in function sum>, 'help': 'sum the integers'}

('--min',)
{'dest': 'accumulate', 'action': 'store_const', 'const': <built-in function min>, 'help': 'compute the minimum'}

('--max',)
{'dest': 'accumulate', 'action': 'store_const', 'const': <built-in function max>, 'help': 'compute the maximum'}

```

这是提取的语法：

```py
process_numbers_grammar

```

```py
{'<start>': ['<group>(<option>)*<arguments>'],
 '<option>': [' -h', ' --help'],
 '<arguments>': ['( <integers>)+'],
 '<int>': ['(-)?<digit>+'],
 '<digit>': ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
 '<integers>': ['<int>'],
 '<group>': [' --sum', ' --min', ' --max']}

```

语法正确识别找到的组：

```py
process_numbers_grammar["<start>"]

```

```py
['<group>(<option>)*<arguments>']

```

```py
process_numbers_grammar["<group>"]

```

```py
[' --sum', ' --min', ' --max']

```

它还标识了`--help`选项，该选项不是我们提供的，而是`argparse`模块提供的：

```py
process_numbers_grammar["<option>"]

```

```py
[' -h', ' --help']

```

语法还可以正确识别参数的类型：

```py
process_numbers_grammar["<arguments>"]

```

```py
['( <integers>)+']

```

```py
process_numbers_grammar["<integers>"]

```

```py
['<int>']

```

`int`的规则由`add_int_rule()`定义

```py
process_numbers_grammar["<int>"]

```

```py
['(-)?<digit>+']

```

我们可以采用这种语法并将其转换为BNF，这样我们就可以立即对其进行模糊处理：

```py
assert is_valid_grammar(process_numbers_grammar)

```

```py
grammar = convert_ebnf_grammar(process_numbers_grammar)
assert is_valid_grammar(grammar)

```

```py
f = GrammarCoverageFuzzer(grammar)
for i in range(10):
    print(f.fuzz())

```

```py
 --sum 9
 --max -h --help --help -16 -0
 --min --help 2745341 8
 --min 1 27
 --sum --help --help -2
 --sum --help 0 3 -77
 --sum -3
 --sum --help 429 8 10 0295 -694 1
 --max -h 91 -1425 99
 --sum -795 -94 8 -44

```

每次调用都遵循`argparse`调用中列出的规则。 通过挖掘现有程序中的选项和参数，我们现在可以即开即用地模糊这些选项-无需指定语法。

## 测试Autopep8

让我们在现实世界的Python程序上试用选项语法挖掘器。 `autopep8`是自动将Python代码转换为 [PEP 8 Python代码](https://www.python.org/dev/peps/pep-0008/)样式指南的工具。 （实际上，本书中的所有Python代码在生产过程中都会通过`autopep8`运行。）`autopep8`提供了多种选择，可以通过使用`--help`进行调用来看到：

```py
!autopep8 --help

```

```py
usage: autopep8 [-h] [--version] [-v] [-d] [-i] [--global-config filename]
                [--ignore-local-config] [-r] [-j n] [-p n] [-a]
                [--experimental] [--exclude globs] [--list-fixes]
                [--ignore errors] [--select errors] [--max-line-length n]
                [--line-range line line] [--hang-closing]
                [files [files ...]]

Automatically formats Python code to conform to the PEP 8 style guide.

positional arguments:
  files                 files to format or '-' for standard in

optional arguments:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  -v, --verbose         print verbose messages; multiple -v result in more
                        verbose messages
  -d, --diff            print the diff for the fixed source
  -i, --in-place        make changes to files in place
  --global-config filename
                        path to a global pep8 config file; if this file does
                        not exist then this is ignored (default:
                        /Users/zeller/.config/pep8)
  --ignore-local-config
                        don't look for and apply local config files; if not
                        passed, defaults are updated with any config files in
                        the project's root directory
  -r, --recursive       run recursively over directories; must be used with
                        --in-place or --diff
  -j n, --jobs n        number of parallel jobs; match CPU count if value is
                        less than 1
  -p n, --pep8-passes n
                        maximum number of additional pep8 passes (default:
                        infinite)
  -a, --aggressive      enable non-whitespace changes; multiple -a result in
                        more aggressive changes
  --experimental        enable experimental fixes
  --exclude globs       exclude file/directory names that match these comma-
                        separated globs
  --list-fixes          list codes for fixes; used by --ignore and --select
  --ignore errors       do not fix these errors/warnings (default:
                        E226,E24,W503)
  --select errors       fix only these errors/warnings (e.g. E4,W)
  --max-line-length n   set maximum allowed line length (default: 79)
  --line-range line line, --range line line
                        only fix errors found within this inclusive range of
                        line numbers (e.g. 1 99); line numbers are indexed at
                        1
  --hang-closing        hang-closing option passed to pycodestyle

```

### Autopep8设置

我们要系统地测试这些选项。 为了部署我们的配置语法挖掘器，我们需要找到可执行文件的源代码：

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

```

```py
def find_executable(name):
    for path in os.get_exec_path():
        qualified_name = os.path.join(path, name)
        if os.path.exists(qualified_name):
            return qualified_name
    return None

```

```py
autopep8_executable = find_executable("autopep8")
assert autopep8_executable is not None
autopep8_executable

```

```py
'/Users/zeller/anaconda3/bin/autopep8'

```

接下来，我们构建一个函数来读取文件的内容并执行它。

```py
def autopep8():
    executable = find_executable("autopep8")

    # First line has to contain "/usr/bin/env python" or like
    first_line = open(executable).readline()
    assert first_line.find("python") >= 0

    contents = open(executable).read()
    exec(contents)

```

### 挖掘Autopep8语法

我们可以在语法挖掘器中使用`autopep8()`函数：

```py
autopep8_miner = OptionGrammarMiner(autopep8)

```

并为其提取语法：

```py
autopep8_ebnf_grammar = autopep8_miner.mine_ebnf_grammar()

```

之所以起作用，是因为`autopep8`不是一个单独的进程（和一个单独的Python解释器），但是我们在当前的Python解释器中运行`autopep8()`函数（和`autopep8`代码）–直到对`parse_args()`的调用 ]，我们再次中断执行。 至此，`autopep8`代码除了设置参数解析器之外什么也没有做-这是我们感兴趣的。

挖掘的语法选项准确反映了提供`--help`时看到的选项：

```py
print(autopep8_ebnf_grammar["<option>"])

```

```py
[' -h', ' --help', ' --version', ' -v', ' --verbose', ' -d', ' --diff', ' -i', ' --in-place', ' --global-config <filename>', ' --ignore-local-config', ' -r', ' --recursive', ' -j <n>', ' --jobs <n>', ' -p <n>', ' --pep8-passes <n>', ' -a', ' --aggressive', ' --experimental', ' --exclude <globs>', ' --list-fixes', ' --ignore <errors>', ' --select <errors>', ' --max-line-length <n>', ' --line-range <line> <line>', ' --range <line> <line>', ' --indent-size <int>', ' --hang-closing']

```

像`<n>`或`<line>`这样的元变量都是整数的占位符。 我们假设所有同名的元变量都具有相同的类型：

```py
autopep8_ebnf_grammar["<line>"]

```

```py
['<int>']

```

语法挖掘者推断`autopep8`的参数是文件列表：

```py
autopep8_ebnf_grammar["<arguments>"]

```

```py
['( <files>)*']

```

依次是字符串：

```py
autopep8_ebnf_grammar["<files>"]

```

```py
['<str>']

```

由于我们仅对测试选项感兴趣，而对参数不感兴趣，因此将参数固定为单个强制输入。 （否则，我们将生成大量随机文件名。）

```py
autopep8_ebnf_grammar["<arguments>"] = [" <files>"]
autopep8_ebnf_grammar["<files>"] = ["foo.py"]
assert is_valid_grammar(autopep8_ebnf_grammar)

```

### 创建Autopep8选项

现在让我们使用推断的语法进行模糊测试。 再次，我们将EBNF语法转换为常规BNF语法：

```py
autopep8_grammar = convert_ebnf_grammar(autopep8_ebnf_grammar)
assert is_valid_grammar(autopep8_grammar)

```

我们可以使用语法模糊所有选项：

```py
f = GrammarCoverageFuzzer(autopep8_grammar, max_nonterminals=4)
for i in range(20):
    print(f.fuzz())

```

```py
 -r foo.py
 --hang-closing --experimental --aggressive foo.py
 --ignore-local-config -d -h -p 9 --version --list-fixes foo.py
 -a --verbose foo.py
 -v --indent-size 7 --global-config { foo.py
 --in-place --help --select ~s --max-line-length 1 foo.py
 --pep8-passes 8 --diff foo.py
 -i --recursive foo.py
 -r --hang-closing foo.py
 --jobs 0 -i foo.py
 --exclude k --line-range 3 6 --verbose foo.py
 -v -i foo.py
 --version -a --list-fixes foo.py
 --ignore x -r foo.py
 -j 4 --in-place -a foo.py
 --range 5 2 --list-fixes foo.py
 --indent-size 5 --indent-size 3 foo.py
 --indent-size 0 --indent-size 8 foo.py
 --indent-size 7 --indent-size 3 foo.py
 --indent-size 9 --verbose foo.py

```

让我们将这些选项应用于实际程序。 我们需要一个文件`foo.py`作为输入：

```py
def create_foo_py():
    open("foo.py", "w").write("""
def twice(x = 2):
 return  x  +  x
""")

```

```py
create_foo_py()

```

```py
print(open("foo.py").read(), end="")

```

```py
def twice(x = 2):
    return  x  +  x

```

我们看到`autopep8`如何固定间距：

```py
!autopep8 foo.py

```

```py
def twice(x=2):
    return x + x

```

现在让我们放在一起。 我们定义了一个`ProgramRunner`，它将使用从`autopep8`语法中提取的参数来运行`autopep8`可执行文件。

```py
from [Fuzzer](Fuzzer.html) import ProgramRunner

```

在运行带有已挖掘选项的`autopep8`时，显示了惊人的大量通过运行。 （我们看到一些选项相互依赖或相互排斥，但这是由程序逻辑而不是参数解析器处理的，因此不在我们的范围之内。）`GrammarCoverageFuzzer`确保每个选项至少测试一次 。 （顺便说一下，数字和字母也是如此。）

```py
f = GrammarCoverageFuzzer(autopep8_grammar, max_nonterminals=5)
for i in range(20):
    invocation = "autopep8" + f.fuzz()
    print("$ " + invocation)
    args = invocation.split()
    autopep8 = ProgramRunner(args)
    result, outcome = autopep8.run()
    if result.stderr != "":
        print(result.stderr, end="")

```

```py
$ autopep8 foo.py
$ autopep8 -a --max-line-length 2 --jobs 5 --help -r foo.py
$ autopep8 --version --indent-size 0 --ignore-local-config -h foo.py
$ autopep8 --ignore z --diff -j 7 --experimental --list-fixes --verbose -i --recursive foo.py
usage: autopep8 [-h] [--version] [-v] [-d] [-i] [--global-config filename]
                [--ignore-local-config] [-r] [-j n] [-p n] [-a]
                [--experimental] [--exclude globs] [--list-fixes]
                [--ignore errors] [--select errors] [--max-line-length n]
                [--line-range line line] [--hang-closing]
                [files [files ...]]
autopep8: error: --in-place and --diff are mutually exclusive
$ autopep8 --line-range 1 6 --in-place --select _ foo.py
$ autopep8 --exclude n --pep8-passes 3 --aggressive foo.py
$ autopep8 --global-config &F -p 4 -d foo.py
$ autopep8 --hang-closing --range 8 9 -v foo.py
[file:foo.py]
--->  5 issue(s) to fix {'E251': {2}, 'E271': {3}, 'E221': {3}, 'E222': {3}}
$ autopep8 --indent-size 1 --version --hang-closing foo.py
$ autopep8 --indent-size 3 --hang-closing --aggressive foo.py
$ autopep8 --indent-size 8 -r --in-place foo.py
$ autopep8 --indent-size 9 --indent-size 7 --version foo.py
$ autopep8 -a --aggressive --help -v foo.py
$ autopep8 --indent-size 9 --indent-size 7 foo.py
$ autopep8 --indent-size 5 --indent-size 2 --verbose foo.py
[file:foo.py]
--->  Applying global fix for E265
--->  1 issue(s) to fix {'E111': {3}}
$ autopep8 --indent-size 9 --in-place --recursive foo.py
$ autopep8 --indent-size 9 --indent-size 9 foo.py
$ autopep8 --indent-size 6 --indent-size 9 -v foo.py
[file:foo.py]
--->  Applying global fix for E265
--->  1 issue(s) to fix {'E111': {3}}
$ autopep8 --indent-size 4 --indent-size -5 --list-fixes foo.py
$ autopep8 --indent-size 93 -a foo.py

```

我们的`foo.py`文件现在已被格式化多次：

```py
print(open("foo.py").read(), end="")

```

```py
def twice(x=2):
         return x + x

```

我们不再需要它，因此我们清理了一些东西：

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

```

```py
os.remove("foo.py")

```

## 模糊配置选项的类

现在让我们创建可重用的类，这些类可用于测试任意程序。 （好的，使之成为“用Python编写并使用`argparse`模块处理命令行参数的任意程序。”）

类`OptionRunner`是`ProgramRunner`的子类，它使用与上述`autopep8`相同的步骤来自动确定语法。

```py
class OptionRunner(ProgramRunner):
    def __init__(self, program, arguments=None):
        if isinstance(program, str):
            self.base_executable = program
        else:
            self.base_executable = program[0]

        self.find_contents()
        self.find_grammar()
        if arguments is not None:
            self.set_arguments(arguments)
        super().__init__(program)

```

首先，我们找到Python可执行文件的内容：

```py
class OptionRunner(OptionRunner):
    def find_contents(self):
        self._executable = find_executable(self.base_executable)
        first_line = open(self._executable).readline()
        assert first_line.find("python") >= 0
        self.contents = open(self._executable).read()

    def invoker(self):
        exec(self.contents)

    def executable(self):
        return self._executable

```

接下来，我们使用`OptionGrammarMiner`类确定语法：

```py
class OptionRunner(OptionRunner):
    def find_grammar(self):
        miner = OptionGrammarMiner(self.invoker)
        self._ebnf_grammar = miner.mine_ebnf_grammar()

    def ebnf_grammar(self):
        return self._ebnf_grammar

    def grammar(self):
        return convert_ebnf_grammar(self._ebnf_grammar)

```

两种服务方法`set_arguments()`和`set_invocation()`分别帮助我们更改参数和程序。

```py
from [Grammars](Grammars.html) import unreachable_nonterminals

```

```py
class OptionRunner(OptionRunner):
    def set_arguments(self, args):
        self._ebnf_grammar["<arguments>"] = [" " + args]
        # Delete rules for previous arguments
        for nonterminal in unreachable_nonterminals(self._ebnf_grammar):
            del self._ebnf_grammar[nonterminal]

    def set_invocation(self, program):
        self.program = program

```

我们可以在`autopep8`上实例化该类并立即获得语法：

```py
autopep8_runner = OptionRunner("autopep8", "foo.py")

```

```py
print(autopep8_runner.ebnf_grammar()["<option>"])

```

```py
[' -h', ' --help', ' --version', ' -v', ' --verbose', ' -d', ' --diff', ' -i', ' --in-place', ' --global-config <filename>', ' --ignore-local-config', ' -r', ' --recursive', ' -j <n>', ' --jobs <n>', ' -p <n>', ' --pep8-passes <n>', ' -a', ' --aggressive', ' --experimental', ' --exclude <globs>', ' --list-fixes', ' --ignore <errors>', ' --select <errors>', ' --max-line-length <n>', ' --line-range <line> <line>', ' --range <line> <line>', ' --indent-size <int>', ' --hang-closing']

```

`OptionFuzzer`与给定的`OptionRunner`交互以获得其语法，然后将其传递给其`GrammarCoverageFuzzer`超类。

```py
class OptionFuzzer(GrammarCoverageFuzzer):
    def __init__(self, runner, *args, **kwargs):
        assert issubclass(type(runner), OptionRunner)
        self.runner = runner
        grammar = runner.grammar()
        super().__init__(grammar, *args, **kwargs)

```

调用`run()`时，`OptionFuzzer`创建一个新的调用（使用其语法中的`fuzz()`），并使用该语法中的参数运行现在给定的（或先前设置的）运行器。 请注意，`run()`中指定的流道可能与初始化过程中的一组不同。 这允许从一个程序中挖掘选项并将其应用到另一上下文中。

```py
class OptionFuzzer(OptionFuzzer):
    def run(self, runner=None, inp=""):
        if runner is None:
            runner = self.runner
        assert issubclass(type(runner), OptionRunner)
        invocation = runner.executable() + " " + self.fuzz()
        runner.set_invocation(invocation.split())
        return runner.run(inp)

```

### 示例：Autopep8

让我们将新定义的类应用于`autopep8`运行程序：

```py
autopep8_fuzzer = OptionFuzzer(autopep8_runner, max_nonterminals=5)

```

```py
for i in range(3):
    print(autopep8_fuzzer.fuzz())

```

```py
 -j -8 foo.py
 --aggressive --global-config U} --version --verbose foo.py
 --help --experimental -p 01 --hang-closing -r -d --list-fixes foo.py

```

现在，我们可以使用以下这些类系统地测试`autopep8`：

```py
autopep8_fuzzer.run(autopep8_runner)

```

```py
(CompletedProcess(args=['/Users/zeller/anaconda3/bin/autopep8', '-a', '--recursive', '-v', 'foo.py'], returncode=2, stdout='', stderr='usage: autopep8 [-h] [--version] [-v] [-d] [-i] [--global-config filename]\n                [--ignore-local-config] [-r] [-j n] [-p n] [-a]\n                [--experimental] [--exclude globs] [--list-fixes]\n                [--ignore errors] [--select errors] [--max-line-length n]\n                [--line-range line line] [--hang-closing]\n                [files [files ...]]\nautopep8: error: --recursive must be used with --in-place or --diff\n'),
 'UNRESOLVED')

```

### 示例：MyPy

我们可以为Python的`mypy`静态类型检查器提取选项：

```py
assert find_executable("mypy") is not None

```

```py
mypy_runner = OptionRunner("mypy", "foo.py")
print(mypy_runner.ebnf_grammar()["<option>"])

```

```py
[' -h', ' --help', ' -v', ' --verbose', ' -V', ' --version', ' --config-file <str>', ' --warn-unused-configs', ' --no-warn-unused-configs', ' --ignore-missing-imports', ' --follow-imports <str>', ' --python-executable', ' --no-site-packages', ' --no-silence-site-packages', ' --python-version <x.y>', ' -2', ' --py2', ' --platform', ' --always-true', ' --always-false', ' --disallow-any-unimported', ' --disallow-subclassing-any', ' --allow-subclassing-any', ' --disallow-any-expr', ' --disallow-any-decorated', ' --disallow-any-explicit', ' --disallow-any-generics', ' --disallow-untyped-calls', ' --allow-untyped-calls', ' --disallow-untyped-defs', ' --allow-untyped-defs', ' --disallow-incomplete-defs', ' --allow-incomplete-defs', ' --check-untyped-defs', ' --no-check-untyped-defs', ' --warn-incomplete-stub', ' --no-warn-incomplete-stub', ' --no-implicit-optional', ' --implicit-optional', ' --strict-optional', ' --no-strict-optional', ' --strict-optional-whitelist( <GLOB>)*', ' --warn-redundant-casts', ' --no-warn-redundant-casts', ' --no-warn-no-return', ' --warn-no-return', ' --warn-return-any', ' --no-warn-return-any', ' --warn-unused-ignores', ' --no-warn-unused-ignores', ' --disallow-untyped-decorators', ' --allow-untyped-decorators', ' -i', ' --incremental', ' --no-incremental', ' --cache-dir', ' --cache-fine-grained', ' --quick-and-dirty', ' --skip-version-check', ' --pdb', ' --show-traceback', ' --tb', ' --custom-typing <MODULE>', ' --custom-typeshed-dir <DIR>', ' --shadow-file', ' --show-error-context', ' --hide-error-context', ' --show-column-numbers', ' --hide-column-numbers', ' --stats', ' --inferstats', ' --find-occurrences <CLASS.MEMBER>', ' --strict', ' --any-exprs-report <DIR>', ' --cobertura-xml-report <DIR>', ' --html-report <DIR>', ' --linecount-report <DIR>', ' --linecoverage-report <DIR>', ' --memory-xml-report <DIR>', ' --txt-report <DIR>', ' --xml-report <DIR>', ' --xslt-html-report <DIR>', ' --xslt-txt-report <DIR>', ' --junit-xml <str>', ' --scripts-are-modules', ' --debug-cache', ' --dump-deps', ' --dump-graph', ' --semantic-analysis-only', ' --local-partial-types', ' --bazel', ' --package-root', ' --cache-map( <str>)+', ' --disallow-any <str>', ' --strict-boolean', ' --no-strict-boolean', ' -f', ' --dirty-stubs', ' --use-python-path', ' -s', ' --silent-imports', ' --almost-silent', ' --fast-parser', ' --no-fast-parser', ' -m', ' --module', ' -p', ' --package', ' -c', ' --command']

```

```py
mypy_fuzzer = OptionFuzzer(mypy_runner, max_nonterminals=5)
for i in range(10):
    print(mypy_fuzzer.fuzz())

```

```py
 foo.py
 -m --no-warn-unused-configs --cache-dir --dirty-stubs --xml-report b --strict --always-false --no-warn-no-return --disallow-any-generics foo.py
 --linecoverage-report VF? --local-partial-types foo.py
 --python-executable --dump-graph --any-exprs-report j --warn-unused-ignores --bazel -2 foo.py
 --scripts-are-modules --warn-no-return --verbose -p --no-silence-site-packages --shadow-file --no-strict-optional --disallow-subclassing-any --strict-optional --almost-silent --package --help foo.py
 --check-untyped-defs --warn-incomplete-stub --no-check-untyped-defs --allow-untyped-calls --ignore-missing-imports foo.py
 --show-traceback --hide-column-numbers --disallow-any-decorated --disallow-untyped-decorators --xslt-html-report pm --warn-redundant-casts --fast-parser --package-root --html-report x --no-site-packages --hide-error-context --always-true foo.py
 --disallow-incomplete-defs --strict-optional-whitelist K -V foo.py
 --custom-typeshed-dir r^ --command -i --skip-version-check foo.py
 --config-file C --allow-incomplete-defs --no-warn-redundant-casts --find-occurrences v8 --warn-unused-configs --disallow-untyped-defs foo.py

```

### 示例：Notedown

这是`notedown`笔记本到Markdown转换器的配置选项：

```py
assert find_executable("notedown") is not None

```

```py
notedown_runner = OptionRunner("notedown")

```

```py
print(notedown_runner.ebnf_grammar()["<option>"])

```

```py
[' -h', ' --help', ' -o( <str>)?', ' --output( <str>)?', ' --from <str>', ' --to <str>', ' --run', ' --execute', ' --timeout <int>', ' --strip', ' --precode( <str>)+', ' --knit( <str>)?', ' --rmagic', ' --nomagic', ' --render', ' --template <str>', ' --match <str>', ' --examples', ' --version', ' --debug']

```

```py
notedown_fuzzer = OptionFuzzer(notedown_runner, max_nonterminals=5)
for i in range(10):
    print(notedown_fuzzer.fuzz())

```

```py
 --nomagic
 -o --examples --match 6? --timeout 93 --help --run >
 --precode Y --rmagic --version 2
 --template '* --strip s8p
 --output -h --debug ^
 --execute --render --debug v
 --knit --to q --from m --run -h --version +
 -o --rmagic --nomagic J
 --precode 4 f --version ]
 -o E --version HB

```

## 组合测试

我们的`CoverageGrammarFuzzer`可以很好地覆盖每个选项至少一次，这对于系统测试非常有用。 但是，正如我们在上面的示例中还可以看到的那样，某些选项相互要求，而另一些相互干扰。 作为优秀的测试人员，我们应该做的不仅是单独涵盖每个选项，还包括*选项的组合*。

Python `itertools`模块为我们提供了从列表创建组合的方法。 例如，我们可以采用`notedown`选项并创建所有对的列表。

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

```

```py
option_list = notedown_runner.ebnf_grammar()["<option>"]
pairs = list(combinations(option_list, 2))

```

有很多对：

```py
len(pairs)

```

```py
190

```

```py
print(pairs[:20])

```

```py
[(' -h', ' --help'), (' -h', ' -o( <str>)?'), (' -h', ' --output( <str>)?'), (' -h', ' --from <str>'), (' -h', ' --to <str>'), (' -h', ' --run'), (' -h', ' --execute'), (' -h', ' --timeout <int>'), (' -h', ' --strip'), (' -h', ' --precode( <str>)+'), (' -h', ' --knit( <str>)?'), (' -h', ' --rmagic'), (' -h', ' --nomagic'), (' -h', ' --render'), (' -h', ' --template <str>'), (' -h', ' --match <str>'), (' -h', ' --examples'), (' -h', ' --version'), (' -h', ' --debug'), (' --help', ' -o( <str>)?')]

```

测试每对这样的选项通常足以覆盖选项之间的所有干扰。 （程序很少具有涉及三个或更多配置设置的条件。）为此，我们将*的语法从*的选项列表更改为*的选项对*列表， 这些将自动覆盖所有对。

我们创建一个函数`pairwise()`，该函数接受语法中出现的选项列表，然后返回*成对选项*的列表-即我们原始的选项，但是是串联的。

```py
def pairwise(option_list):
    return [option_1 + option_2
            for (option_1, option_2) in combinations(option_list, 2)]

```

这是前20对：

```py
print(pairwise(option_list)[:20])

```

```py
[' -h --help', ' -h -o( <str>)?', ' -h --output( <str>)?', ' -h --from <str>', ' -h --to <str>', ' -h --run', ' -h --execute', ' -h --timeout <int>', ' -h --strip', ' -h --precode( <str>)+', ' -h --knit( <str>)?', ' -h --rmagic', ' -h --nomagic', ' -h --render', ' -h --template <str>', ' -h --match <str>', ' -h --examples', ' -h --version', ' -h --debug', ' --help -o( <str>)?']

```

新语法`pairwise_notedown_grammar`是`notedown`语法的副本，但选项列表已替换为上述成对选项列表。

```py
notedown_grammar = notedown_runner.grammar()
pairwise_notedown_grammar = extend_grammar(notedown_grammar)
pairwise_notedown_grammar["<option>"] = pairwise(notedown_grammar["<option>"])
assert is_valid_grammar(pairwise_notedown_grammar)

```

使用“成对”语法进行模糊测试现在可以覆盖一对对：

```py
notedown_fuzzer = GrammarCoverageFuzzer(
    pairwise_notedown_grammar, max_nonterminals=4)

```

```py
for i in range(10):
    print(notedown_fuzzer.fuzz())

```

```py
 --run --debug --help --execute
 -o --timeout 8
 --precode : --render -h --run
 --help --debug --strip --nomagic G
 --render --debug --rmagic --nomagic r
 --help --version --execute --strip ^
 -h --execute --precode t --version ip
 --nomagic --debug --version --debug -h --render K
 --examples --version --help --examples

```

我们可以实际测试所有选项组合吗？ 实际上，这并不可行，因为组合的数量会随着长度的增加而迅速增加。 随着选项数量达到最大值，它再次减少（有20个选项，只有1个组合涉及*所有*选项），但绝对数量仍然惊人：

```py
for combination_length in range(1, 20):
    tuples = list(combinations(option_list, combination_length))
    print(combination_length, len(tuples))

```

```py
1 20
2 190
3 1140
4 4845
5 15504
6 38760
7 77520
8 125970
9 167960
10 184756
11 167960
12 125970
13 77520
14 38760
15 15504
16 4845
17 1140
18 190
19 20

```

形式上，长度为$ n $的一组选项中长度为$ k $的组合数为二项式系数$$ {n \ choose k} = \ frac {n！} {k！（n-k）！} $$

$ k = 2 $（所有对）

$$ {n \ choose 2} = \ frac {n！} {2（n-2）！} = n \ times（n-1）$$

对于`autopep8`有29个选项...

```py
len(autopep8_runner.ebnf_grammar()["<option>"])

```

```py
29

```

...因此，我们需要812个测试来覆盖所有对：

```py
len(autopep8_runner.ebnf_grammar()["<option>"]) * \
    (len(autopep8_runner.ebnf_grammar()["<option>"]) - 1)

```

```py
812

```

但是，对于具有110个选项的`mypy`，我们已经进行了11,990个测试：

```py
len(mypy_runner.ebnf_grammar()["<option>"])

```

```py
110

```

```py
len(mypy_runner.ebnf_grammar()["<option>"]) * \
    (len(mypy_runner.ebnf_grammar()["<option>"]) - 1)

```

```py
11990

```

即使每对都需要一秒钟才能运行，我们仍然需要进行三个小时的测试。

如果您的程序有更多选项要让您全部组合使用，建议您进一步限制配置数量-例如，通过将组合测试限制为可能相互影响的那些组合; 并分别涵盖所有其他（可能是正交的）选项。

这种通过扩展语法创建配置的机制可以轻松地扩展到其他配置目标。 可能需要探索更多的配置，或在特定情况下进行扩展。 下面的[练习](#Exercises)为您准备了许多选项。

## 经验教训

*   除常规输入数据外，程序*组态*也是重要的测试目标。
*   对于使用标准库解析命令行选项和参数的给定程序，可以自动提取这些选项并将其转换为语法。
*   为了不仅覆盖单个选项，而且覆盖选项的组合，还可以将语法扩展到覆盖所有对，或者提出更宏大的目标。

## 后续步骤

如果您喜欢从程序中提取语法的想法，请不要错过：

*   [如何为输入数据挖掘语法](GrammarMiner.html)

本书的下一步重点是：

*   [如何解析和重组输入](Parser.html)
*   [如何为特定产品分配权重和概率](ProbabilisticGrammarFuzzer.html)
*   [如何简化导致故障的输入](Reducer.html)

## 背景

尽管配置数据和其他输入数据一样容易引起故障，但它在测试生成中受到的关注相对较少-可能是因为，与“常规”输入数据不同，配置数据并不受外部各方的控制，并且， 再次不同于常规数据，配置几乎没有差异。 创建软件配置模型并使用这些模型进行测试很普遍，而成对测试的想法也是如此。 有关概述，请参见[[Pezzè*等人*，2008。](http://ix.cs.uoregon.edu/~michal/book/)]； 有关最新技术的讨论和比较，请参见[ [J. Petke *等人*，2015年。](https://doi.org/10.1109/TSE.2015.2421279)]。

更具体地，[ [Sutton *等人*，2007。](http://www.fuzzing.org/)]还讨论了系统地覆盖命令行选项的技术。 戴等。 [ [Dai *等人*，2010。](https://doi.org/10.4018/jsse.2010070103)通过更改与配置文件关联的变量来应用配置模糊测试。

## 练习

### 练习1：#ifdef配置模糊处理

在C程序中，可以使用 *C预处理器*选择应该编译哪些代码部分，而不应该选择哪些代码部分。 例如，在C代码中

```py
#ifdef LONG_FOO
long foo() { ... }
#else
int foo() { ... }
#endif

```

如果定义了*预处理程序变量* `LONG_FOO`，则编译器将使用返回类型`long`编译函数`foo()`；否则，将使用返回类型`int`编译函数`foo()`。 此类预处理器变量可以在源文件中设置（使用`#define LONG_FOO`使用`#define`），或在C编译器命令行中使用`-DLONG_FOO`使用`-D<variable>`或`-D<variable>=<value>`设置。

这种*条件编译*用于针对其环境配置C程序。 特定于系统的代码可以包含许多条件编译。 作为示例，请看`xmlparse.c`的摘录，它是Python运行时库的一部分的XML解析器：

```py
#if defined(_WIN32) && !defined(LOAD_LIBRARY_SEARCH_SYSTEM32)
# define LOAD_LIBRARY_SEARCH_SYSTEM32  0x00000800
#endif

#if !defined(HAVE_GETRANDOM) && !defined(HAVE_SYSCALL_GETRANDOM) \
 && !defined(HAVE_ARC4RANDOM_BUF) && !defined(HAVE_ARC4RANDOM) \
 && !defined(XML_DEV_URANDOM) \
 && !defined(_WIN32) \
 && !defined(XML_POOR_ENTROPY)
# error
#endif

#if !defined(TIOCSWINSZ) || defined(__SCO__) || defined(__UNIXWARE__)
#define USE_SYSV_ENVVARS /* COLUMNS/LINES vs. TERMCAP */
#endif

#ifdef XML_UNICODE_WCHAR_T
#define XML_T(x) (const wchar_t)x
#define XML_L(x) L ## x
#else
#define XML_T(x) (const unsigned short)x
#define XML_L(x) x
#endif

int fun(int x) { return XML_T(x); }

```

上面代码中C预处理程序的典型配置可以是`cc -c -D_WIN32 -DXML_POOR_ENTROPY -DXML_UNICODE_WCHAR_T xmlparse.c`，定义给定的预处理程序变量并选择适当的代码片段。

由于编译器一次只能编译一个配置（这意味着我们一次也只能*测试*一个生成的可执行文件），因此您的任务是找出这些配置中的哪个实际上可以编译。 为此，请分三步进行。

#### 第1部分：提取预处理程序变量

编写一个*函数* `cpp_identifiers()`，给定一组行（例如，从`open(filename).readlines()`中提取），提取在`#if`或`#ifdef`预处理程序指令中引用的所有预处理程序变量。 在上面的样本C输入上应用`ifdef_identifiers()`，这样

```py
cpp_identifiers(open("xmlparse.c").readlines())

```

返回集合

```py
{'_WIN32', 'LOAD_LIBRARY_SEARCH_SYSTEM32', 'HAVE_GETRANDOM', 'HAVE_SYSCALL_GETRANDOM', 'HAVE_ARC4RANDOM_BUF', ...}

```

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

#### 第2部分：得出期权语法

在`cpp_identifiers()`的帮助下，创建一个具有C编译器调用并带有选项列表的语法，其中每个选项的形式为预处理器变量`<variable>`的`-D<variable>`形式。 使用这种语法`cpp_grammar`

```py
g = GrammarCoverageFuzzer(cpp_grammar)

```

将创建C编译器调用，例如

```py
[g.fuzz() for i in range(10)]
['cc -DHAVE_SYSCALL_GETRANDOM xmlparse.c',
 'cc -D__SCO__ -DRANDOM_BUF -DXML_UNICODE_WCHAR_T -D__UNIXWARE__ xmlparse.c',
 'cc -DXML_POOR_ENTROPY xmlparse.c',
 'cc -DRANDOM xmlparse.c',
 'cc -D_WIN xmlparse.c',
 'cc -DHAVE_ARC xmlparse.c', ...]

```

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

#### 第3部分：C预处理器配置模糊化

使用刚刚产生的语法，使用`GrammarCoverageFuzzer`

1.  分别测试每个处理器变量
2.  使用`pairwise()`测试每对处理器变量。

如果您实际运行调用会发生什么？

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

```py
os.remove("xmlparse.c")

```

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

```py
if os.path.exists("xmlparse.o"):
    os.remove("xmlparse.o")

```

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

### 练习2：.ini配置模糊化

除了命令行选项之外，配置的另一个重要来源是*配置文件*。 在本练习中，我们将考虑Python `ConfigParser`模块提供的非常简单的配置语言，该语言与Microsoft Windows *.ini* 文件中的语言非常相似。

以下`ConfigParser`输入文件示例直接来自ConfigParser文档的[：](https://docs.python.org/3/library/configparser.html)

```py
[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes

[bitbucket.org]
User = hg

[topsecret.server.com]
Port = 50022
ForwardX11 = no
```

上面的`ConfigParser`文件可以通过编程方式创建：

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

```

```py
config = configparser.ConfigParser()
config['DEFAULT'] = {'ServerAliveInterval': '45',
                     'Compression': 'yes',
                     'CompressionLevel': '9'}
config['bitbucket.org'] = {}
config['bitbucket.org']['User'] = 'hg'
config['topsecret.server.com'] = {}
topsecret = config['topsecret.server.com']
topsecret['Port'] = '50022'     # mutates the parser
topsecret['ForwardX11'] = 'no'  # same here
config['DEFAULT']['ForwardX11'] = 'yes'
with open('example.ini', 'w') as configfile:
    config.write(configfile)

with open('example.ini') as configfile:
    print(configfile.read(), end="")

```

```py
[DEFAULT]
serveraliveinterval = 45
compression = yes
compressionlevel = 9
forwardx11 = yes

[bitbucket.org]
user = hg

[topsecret.server.com]
port = 50022
forwardx11 = no

```

并再次阅读：

```py
config = configparser.ConfigParser()
config.read('example.ini')
topsecret = config['topsecret.server.com']
topsecret['Port']

```

```py
'50022'

```

#### 第1部分：读取配置

使用`configparser`，创建一个程序，读取上述配置文件并访问各个元素。

#### 第2部分：创建配置语法

设计一种语法，该语法将自动创建适合您上述程序的配置文件。 用它来模糊程序。

#### 第3部分：挖掘配置语法

通过动态跟踪对配置元素的单个访问，您可以再次从执行中提取基本语法。 为此，使用特殊方法`__getitem__`创建`ConfigParser`的子类：

```py
class TrackingConfigParser(configparser.ConfigParser):
    def __getitem__(self, key):
        print("Accessing", repr(key))
        return super().__getitem__(key)

```

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

对于`TrackingConfigParser`对象`p`，只要访问`p[key]`，就会调用`p.__getitem__(key)`：

```py
tracking_config_parser = TrackingConfigParser()
tracking_config_parser.read('example.ini')
section = tracking_config_parser['topsecret.server.com']

```

```py
Accessing 'topsecret.server.com'

```

如上所述，使用`__getitem__()`实施一种跟踪机制，该程序在程序访问读取配置时自动保存所访问的选项和读取的值。 根据这些值创建原型语法； 用它来模糊测试。

最后，别忘了清理：

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

```

```py
os.remove("example.ini")

```

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

### 练习3：提取和模糊C命令行选项

在C程序中，`getopt()`功能经常用于处理配置选项。 一个电话

```py
getopt(argc, argv, "bf:")
```

表示程序接受两个选项`-b`和`-f`，其中`-f`作为参数（如以下冒号所示）。

#### 第1部分：Getopt模糊测试

编写一个框架，对于给定的C程序，该框架将自动提取`getopt()`的参数并为其导出模糊的语法。 有多种方法可以实现此目的：

1.  扫描程序源代码以查看是否出现`getopt()`，然后返回传递的字符串。 （粗略，但应该经常工作。）
2.  将您自己的`getopt()`实现插入源代码（有效替换运行时库中的`getopt()`），该代码将输出`getopt()`参数并退出程序。 重新编译并运行。
3.  （高级。）如上所述，但不要更改源代码，而是连接到*动态链接器*，该链接器在运行时将程序与C运行时库链接在一起。 设置库加载路径（在Linux和Unix上，这是`LD_LIBRARY_PATH`环境变量），以便首先链接您自己的`getopt()`版本，然后再链接常规库。 执行程序（无需重新编译）应该会产生所需的结果。

将其应用于`grep`和`ls`上； 报告生成的语法和结果。

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

#### 第2部分：在C 中模糊长选项

与第1部分相同，但也加入了GNU变体`getopt_long()`，该变体接受带有双破折号的“长”自变量，例如`--help`。 请注意，上面的方法1在这里不起作用，因为“长”选项是在单独定义的结构中定义的。

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

### 练习4：上下文扩展

在上述选项配置中，我们有多个符号都扩展为相同的整数。 例如，`autopep8`的`--line-range`选项采用两个`<line>`参数，它们都扩展为相同的`<int>`符号：

```py
<option> ::= ... | --line-range <line> <line> | ...
<line> ::= <int>
<int> ::= (-)?<digit>+
<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
```

```py
autopep8_runner.ebnf_grammar()["<line>"]

```

```py
['<int>']

```

```py
autopep8_runner.ebnf_grammar()["<int>"]

```

```py
['(-)?<digit>+']

```

```py
autopep8_runner.ebnf_grammar()["<digit>"]

```

```py
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

```

但是，一旦`GrammarCoverageFuzzer`覆盖了*一个*选项的`<int>`的所有变体（特别是覆盖了所有数字），它将不再努力为下一个选项实现这种覆盖。 但是，可能希望分别为每个选项实现这种覆盖。

使用我们现有的`GrammarCoverageFuzzer`实现此目的的一种方法是再次更改语法。 想法是*复制*扩展名-也就是说，用新的符号$ s'$替换符号$ s $的扩展名，其定义与$ s $相同。 这样，从coverage的角度来看，$ s'$和$ s $是独立的符号，将被独立覆盖。

作为示例，请再次考虑上述`--line-range`选项。 如果我们希望我们的测试独立覆盖两个`<line>`参数的所有元素，则可以将第二个`<line>`扩展名复制到一个新符号`<line'>`中，并带有随后重复的扩展名：

```py
<option> ::= ... | --line-range <line> <line'> | ...
<line> ::= <int>
<line'> ::= <int'>
<int> ::= (-)?<digit>+
<int'> ::= (-)?<digit'>+
<digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
<digit'> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
```

设计一个函数`inline(grammar, symbol)`，该函数返回`grammar`的副本，其中，每次出现的`<symbol>`及其扩展名都将成为单独的副本。 上面的语法可能是`inline(autopep8_runner.ebnf_grammar(), "<line>")`的结果。

复制时，副本中的扩展名也应引用副本中的符号。 因此，在展开`<int>`

`<int> ::= <int><digit>`

使那个

``` <int>:: =</int>

< int'> :: = < int'> <数字'>```

（而不是`<int'> ::= <int><digit'>`或`<int'> ::= <int><digit>`）。

确保为原始语法中的每个出现精确添加一组新的符号，并且在存在递归的情况下不要进一步扩展。

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