# 雕刻单元测试

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

到目前为止，我们始终生成*系统输入*，即程序整体通过其输入通道获得的数据。 如果我们只想测试一小部分功能，则必须通过整个系统，效率很低。 本章介绍了一种称为*雕刻*的技术，该技术在进行系统测试后会自动提取一组*单元测试*，该单元测试会复制在单元测试期间看到的调用。 关键思想是*记录*这样的调用，以便我们以后可以*整体或选择性地重播*。 最重要的是，我们还探讨了如何从雕刻的单元测试中综合API语法。 这意味着我们可以*合成API测试，而根本不必编写语法。*

**前提条件**

*   雕刻使用函数调用和变量的动态跟踪，如有关配置模糊的[一章中介绍的那样。](ConfigurationFuzzer.html)
*   在API模糊测试的[一章中介绍了使用语法来测试单元。](APIFuzzer.html)

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

```

```py
import [APIFuzzer](APIFuzzer.html)

```

## 内容提要

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

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

```

然后利用以下功能。

本章提供了在系统测试期间用于*记录和重播功能调用*的方法。 由于单个函数调用比整个系统运行快得多，因此这种“雕刻”机制有可能更快地运行测试。

### 记录通话

`CallCarver`类记录了处于活动状态时发生的所有呼叫。 它与`with`子句结合使用：

```py
>>> with CallCarver() as carver:
>>>     y = my_sqrt(2)
>>>     y = my_sqrt(4)

```

执行后，`called_functions()`列出遇到的功能的名称：

```py
>>> carver.called_functions()
['my_sqrt', '__exit__']

```

`arguments()`方法列出了为函数记录的参数。 这是函数名称到参数列表的映射； 每个参数都是一对（参数名称，值）。

```py
>>> carver.arguments('my_sqrt')
[[('x', 2)], [('x', 4)]]

```

复杂的参数已正确序列化，因此可以轻松恢复它们。

### 合成呼叫

尽管这些已记录的自变量已经可以转换为自变量和调用，但是更好的替代方法是为已记录的调用创建*语法*。 这允许合成参数的任意*组合*，并且还为进一步定制调用提供了基础。

`CallGrammarMiner`类将雕刻的执行列表转换成语法。

```py
>>> my_sqrt_miner = CallGrammarMiner(carver)
>>> my_sqrt_grammar = my_sqrt_miner.mine_call_grammar()
>>> my_sqrt_grammar
{'<start>': ['<call>'],
 '<call>': ['<my_sqrt>'],
 '<my_sqrt-x>': ['4', '2'],
 '<my_sqrt>': ['my_sqrt(<my_sqrt-x>)']}

```

该语法可用于合成呼叫。

```py
>>> fuzzer = GrammarCoverageFuzzer(my_sqrt_grammar)
>>> fuzzer.fuzz()
'my_sqrt(2)'

```

这些调用可以隔离执行，从而有效地从系统测试中提取单元测试：

```py
>>> eval(fuzzer.fuzz())
2.0

```

## 系统测试与单元测试

还记得为[语法模糊](Grammars.html)引入的URL语法吗？ 有了这样的语法，我们可以一次又一次愉快地测试Web浏览器，检查它对任意页面请求的反应。

让我们定义一个非常简单的“ Web浏览器”，它可以下载URL给出的内容。

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

```

```py
def webbrowser(url):
    """Download the http/https resource given by the URL"""
    import [requests](http://docs.python-requests.org/en/master/)  # Only import if needed

    r = requests.get(url)
    return r.text

```

让我们在 [fuzzingbook.org](https://www.fuzzingbook.org/) 上应用它，并使用 [Timer类](Timer.html)测量时间：

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

```

```py
with Timer() as webbrowser_timer:
    fuzzingbook_contents = webbrowser(
        "http://www.fuzzingbook.org/html/Fuzzer.html")

print("Downloaded %d bytes in %.2f seconds" %
      (len(fuzzingbook_contents), webbrowser_timer.elapsed_time()))

```

```py
Downloaded 414793 bytes in 0.21 seconds

```

```py
fuzzingbook_contents[:100]

```

```py
'\n<!-- A html document -->\n<!-- \nwith standard nbconvert css layout\nwith standard nbconvert input/out'

```

完整的Web浏览器当然也会呈现HTML内容。 我们可以使用以下命令来实现此目的（但是我们不希望这样做，因为我们不想在此处复制整个网页）：

```py
from [IPython.display](https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html) import HTML, display
HTML(fuzzingbook_contents)

```

但是，不得不一次又一次启动整个浏览器（或者让它呈现网页）意味着很多开销-特别是如果我们只想测试其一部分功能。 特别是，在代码更改之后，我们宁愿只测试受更改影响的功能子集，而不是一次又一次地运行经过良好测试的功能。

让我们假设我们更改用于解析给定URL并将其分解为各个元素的功能-方案（“ http”），网络位置（`"www.fuzzingbook.com"`）或路径（`"/html/Fuzzer.html"`）。 该函数名为`urlparse()`：

```py
from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlparse

```

```py
urlparse('https://www.fuzzingbook.com/html/Carver.html')

```

```py
ParseResult(scheme='https', netloc='www.fuzzingbook.com', path='/html/Carver.html', params='', query='', fragment='')

```

您会看到URL的各个元素– *方案*（`"http"`），*网络位置*（`"www.fuzzingbook.com"`）或路径（`"//html/Carver.html"`）都如何正确 确定。 其他元素（例如`params`，`query`或`fragment`）为空，因为它们不是我们输入的一部分。

有趣的是，仅执行`urlparse()`比运行所有`webbrowser()`快几个数量级。 让我们测量一下因素：

```py
runs = 1000
with Timer() as urlparse_timer:
    for i in range(runs):
        urlparse('https://www.fuzzingbook.com/html/Carver.html')

avg_urlparse_time = urlparse_timer.elapsed_time() / 1000
avg_urlparse_time

```

```py
2.006764000725525e-06

```

将此与网络浏览器所需的时间进行比较

```py
webbrowser_timer.elapsed_time()

```

```py
0.21377766900059214

```

时间差异巨大：

```py
webbrowser_timer.elapsed_time() / avg_urlparse_time

```

```py
106528.55488901684

```

因此，在运行一次`webbrowser()`所需的时间中，我们可以执行*数以万计的`urlparse()` –这甚至都没有考虑到浏览器呈现代码所需的时间。 下载的HTML，运行包含的脚本以及加载网页时发生的其他情况。 因此，允许我们在*单元*级别进行测试的策略非常有前途，因为它们可以节省很多开销。*

## 雕刻单元测试

单元级别的测试方法和功能需要对要测试的各个单元以及它们与其他单元之间的相互作用有很好的了解。 因此，建立适当的基础架构并手动编写单元测试是一项艰巨而又有益的工作。 但是，还有一种有趣的替代方法可以手动编写单元测试。 *自动雕刻*的技术*通过记录和重放函数调用将系统测试转换为单元测试*：

1.  在系统测试（给定或生成）中，我们*记录*对函数的所有调用，包括该函数读取的所有参数和其他变量。
2.  通过这些，我们合成了一个独立的*单元测试*，该单元测试重构了带有所有参数的函数调用。
3.  可以随时高效地执行（重放）该单元测试。

在本章的其余部分，让我们探索这些步骤。

## 记录通话

我们的第一个挑战是将函数调用及其参数记录在一起。 （为了简单起见，我们将自己限制为参数，而忽略该函数读取的任何全局变量或其他非参数。）为了记录调用和参数，我们使用为覆盖率介绍的机制  ：通过设置跟踪器函数，我们将所有调用跟踪到单个函数中，并保存其参数。 就像`Coverage`对象一样，我们希望使用`Carver`对象能够与`with`语句结合使用，以便我们可以跟踪特定的代码块：

```py
with Carver() as carver:
    function_to_be_traced()
c = carver.calls()

```

初始定义支持以下构造：

\ todo {从[动态不变量](DynamicInvariants.html)获取跟踪器}

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

```

```py
class Carver(object):
    def __init__(self, log=False):
        self._log = log
        self.reset()

    def reset(self):
        self._calls = {}

    # Start of `with` block
    def __enter__(self):
        self.original_trace_function = sys.gettrace()
        sys.settrace(self.traceit)
        return self

    # End of `with` block
    def __exit__(self, exc_type, exc_value, tb):
        sys.settrace(self.original_trace_function)

```

实际工作在`traceit()`方法中进行，该方法将所有调用记录在`_calls`属性中。 首先，我们定义两个辅助函数：

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

```

```py
def get_qualified_name(code):
    """Return the fully qualified name of the current function"""
    name = code.co_name
    module = inspect.getmodule(code)
    if module is not None:
        name = module.__name__ + "." + name
    return name

```

```py
def get_arguments(frame):
    """Return call arguments in the given frame"""
    # When called, all arguments are local variables
    arguments = [(var, frame.f_locals[var]) for var in frame.f_locals]
    arguments.reverse()  # Want same order as call
    return arguments

```

```py
class CallCarver(Carver):
    def add_call(self, function_name, arguments):
        """Add given call to list of calls"""
        if function_name not in self._calls:
            self._calls[function_name] = []
        self._calls[function_name].append(arguments)

    # Tracking function: Record all calls and all args
    def traceit(self, frame, event, arg):
        if event != "call":
            return None

        code = frame.f_code
        function_name = code.co_name
        qualified_name = get_qualified_name(code)
        arguments = get_arguments(frame)

        self.add_call(function_name, arguments)
        if qualified_name != function_name:
            self.add_call(qualified_name, arguments)

        if self._log:
            print(simple_call_string(function_name, arguments))

        return None

```

最后，我们需要一些便利功能来访问呼叫：

```py
class CallCarver(CallCarver):
    def calls(self):
        """Return a dictionary of all calls traced."""
        return self._calls

    def arguments(self, function_name):
        """Return a list of all arguments of the given function
 as (VAR, VALUE) pairs.
 Raises an exception if the function was not traced."""
        return self._calls[function_name]

    def called_functions(self, qualified=False):
        """Return all functions called."""
        if qualified:
            return [function_name for function_name in self._calls.keys()
                    if function_name.find('.') >= 0]
        else:
            return [function_name for function_name in self._calls.keys()
                    if function_name.find('.') < 0]

```

### 记录my_sqrt（）)

让我们尝试一个新的`Carver`类-首先使用一个非常简单的函数：

```py
from [Intro_Testing](Intro_Testing.html) import my_sqrt

```

```py
with CallCarver() as sqrt_carver:
    my_sqrt(2)
    my_sqrt(4)

```

我们可以检索所有看到的电话...

```py
sqrt_carver.calls()

```

```py
{'my_sqrt': [[('x', 2)], [('x', 4)]],
 '__exit__': [[('self', <__main__.CallCarver at 0x11129c9b0>),
   ('exc_type', None),
   ('exc_value', None),
   ('tb', None)]]}

```

```py
sqrt_carver.called_functions()

```

```py
['my_sqrt', '__exit__']

```

...以及特定函数的参数：

```py
sqrt_carver.arguments("my_sqrt")

```

```py
[[('x', 2)], [('x', 4)]]

```

我们定义了一个便捷功能，可以更好地打印这些列表：

```py
def simple_call_string(function_name, argument_list):
    """Return function_name(arg[0], arg[1], ...) as a string"""
    return function_name + "(" + \
        ", ".join([var + "=" + repr(value)
                   for (var, value) in argument_list]) + ")"

```

```py
for function_name in sqrt_carver.called_functions():
    for argument_list in sqrt_carver.arguments(function_name):
        print(simple_call_string(function_name, argument_list))

```

```py
my_sqrt(x=2)
my_sqrt(x=4)
__exit__(self=<__main__.CallCarver object at 0x11129c9b0>, exc_type=None, exc_value=None, tb=None)

```

这是我们可以直接用来再次调用`my_sqrt()`的语法：

```py
eval("my_sqrt(x=2)")

```

```py
1.414213562373095

```

### 雕刻urlparse（）)

如果将此应用于`webbrowser()`，会发生什么？

```py
with CallCarver() as webbrowser_carver:
    webbrowser("http://www.example.com")

```

我们看到从Web检索URL需要很多功能：

```py
function_list = webbrowser_carver.called_functions(qualified=True)
len(function_list)

```

```py
304

```

```py
print(function_list[:50])

```

```py
['requests.api.get', 'requests.api.request', 'requests.sessions.__init__', 'requests.utils.default_headers', 'requests.utils.default_user_agent', 'requests.structures.__init__', 'collections.abc.update', 'abc.__instancecheck__', '_weakrefset.__contains__', 'requests.structures.__setitem__', 'requests.hooks.default_hooks', 'requests.hooks.<dictcomp>', 'requests.cookies.cookiejar_from_dict', 'http.cookiejar.__init__', 'threading.RLock', 'http.cookiejar.__iter__', 'requests.cookies.<listcomp>', 'http.cookiejar.deepvalues', 'http.cookiejar.vals_sorted_by_key', 'requests.adapters.__init__', 'urllib3.util.retry.__init__', 'requests.adapters.init_poolmanager', 'urllib3.poolmanager.__init__', 'urllib3.request.__init__', 'urllib3._collections.__init__', 'requests.sessions.mount', 'requests.sessions.<listcomp>', 'requests.sessions.__enter__', 'requests.sessions.request', 'requests.models.__init__', 'requests.sessions.prepare_request', 'requests.cookies.merge_cookies', 'requests.cookies.update', 'requests.utils.get_netrc_auth', 'posixpath.expanduser', 'posixpath._get_sep', 'collections.abc.__contains__', 'os.__getitem__', 'os.encode', 'os.decode', 'genericpath.exists', 'urllib.parse.urlparse', 'urllib.parse._coerce_args', 'urllib.parse.urlsplit', 'urllib.parse._splitnetloc', 'urllib.parse._noop', 'netrc.__init__', '_bootlocale.getpreferredencoding', 'codecs.__init__', 'netrc._parse']

```

在其他几个函数中，我们还调用了`urlparse()`：

```py
urlparse_argument_list = webbrowser_carver.arguments("urllib.parse.urlparse")
urlparse_argument_list

```

```py
[[('url', 'http://www.example.com'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)],
 [('url', 'http://www.example.com/'),
  ('scheme', ''),
  ('allow_fragments', True)]]

```

同样，我们可以将其转换为格式正确的调用：

```py
urlparse_call = simple_call_string("urlparse", urlparse_argument_list[0])
urlparse_call

```

```py
"urlparse(url='http://www.example.com', scheme='', allow_fragments=True)"

```

同样，我们可以重新执行此调用：

```py
eval(urlparse_call)

```

```py
ParseResult(scheme='http', netloc='www.example.com', path='', params='', query='', fragment='')

```

现在，我们已经成功地从`webbrowser()`执行中提取了对`urlparse()`的调用。

## 重播呼叫

重放呼叫的全部和全部内容是棘手的，因为要解决一些挑战。 这些包括：

1.  我们需要能够*访问*各个功能。 如果我们通过名称访问函数，则该名称必须在范围内。 如果名称不可见（例如，因为它是模块内部的名称），则必须使其可见。

2.  必须记录并重建在参数之外访问的任何*资源*，以进行重播。 如果变量引用外部资源（例如文件或网络资源），则可能会很困难。

3.  *复杂对象*也必须重建。

如果要测试的功能与其环境有很大的交互作用，这些约束将使雕刻变得困难甚至无法进行。 为了说明这些问题，请考虑在`webbrowser()`中调用的`email.parser.parse()`方法：

```py
email_parse_argument_list = webbrowser_carver.arguments("email.parser.parse")

```

对该方法的调用如下所示：

```py
email_parse_call = simple_call_string(
    "email.parser.parse",
    email_parse_argument_list[0])
email_parse_call

```

```py
'email.parser.parse(self=<email.parser.Parser object at 0x11f249f60>, fp=<_io.StringIO object at 0x11f3f5828>, headersonly=False)'

```

我们看到`email.parser.parse()`是`email.parser.Parser`对象的一部分，并且它获得了`StringIO`对象。 两者都是非原始值。 我们怎么可能重建它们？

### 序列化对象

复杂对象问题的答案在于创建一个*持久性*表示形式，可以在以后的时间点将其*重构为*。 此过程称为*序列化*； 在Python中，它也称为*酸洗*。 `pickle`模块提供了创建对象的序列化表示的方法。 让我们将其应用于刚刚发现的`email.parser.Parser`对象：

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

```

```py
parser_object = email_parse_argument_list[0][0][1]
parser_object

```

```py
<email.parser.Parser at 0x11f249f60>

```

```py
pickled = pickle.dumps(parser_object)
pickled

```

```py
b'\x80\x03cemail.parser\nParser\nq\x00)\x81q\x01}q\x02(X\x06\x00\x00\x00_classq\x03chttp.client\nHTTPMessage\nq\x04X\x06\x00\x00\x00policyq\x05cemail._policybase\nCompat32\nq\x06)\x81q\x07ub.'

```

通过代表串行化`email.parser.Parser`对象的此字符串，我们可以随时重新创建Parser对象：

```py
unpickled_parser_object = pickle.loads(pickled)
unpickled_parser_object

```

```py
<email.parser.Parser at 0x11f42c828>

```

序列化机制允许我们为作为参数传递的所有对象生成表示形式（假设它们可以被腌制）。 现在，我们可以扩展`simple_call_string()`函数，使其自动腌制对象。 此外，我们将其设置为如果第一个参数名为`self`（即它是一个类方法），则将其设为`self`对象的方法。

```py
def call_value(value):
    value_as_string = repr(value)
    if value_as_string.find('<') >= 0:
        # Complex object
        value_as_string = "pickle.loads(" + repr(pickle.dumps(value)) + ")"
    return value_as_string

```

```py
def call_string(function_name, argument_list):
    """Return function_name(arg[0], arg[1], ...) as a string, pickling complex objects"""
    if len(argument_list) > 0:
        (first_var, first_value) = argument_list[0]
        if first_var == "self":
            # Make this a method call
            method_name = function_name.split(".")[-1]
            function_name = call_value(first_value) + "." + method_name
            argument_list = argument_list[1:]

    return function_name + "(" + \
        ", ".join([var + "=" + call_value(value)
                   for (var, value) in argument_list]) + ")"

```

让我们应用扩展的`call_string()`方法创建对`email.parser.parse()`的调用，包括腌制的对象：

```py
call = call_string("email.parser.parse", email_parse_argument_list[0])
print(call)

```

```py
pickle.loads(b'\x80\x03cemail.parser\nParser\nq\x00)\x81q\x01}q\x02(X\x06\x00\x00\x00_classq\x03chttp.client\nHTTPMessage\nq\x04X\x06\x00\x00\x00policyq\x05cemail._policybase\nCompat32\nq\x06)\x81q\x07ub.').parse(fp=pickle.loads(b'\x80\x03c_io\nStringIO\nq\x00)\x81q\x01(XX\x01\x00\x00Content-Encoding: gzip\r\nAccept-Ranges: bytes\r\nCache-Control: max-age=604800\r\nContent-Type: text/html; charset=UTF-8\r\nDate: Tue, 21 May 2019 18:16:28 GMT\r\nEtag: "1541025663"\r\nExpires: Tue, 28 May 2019 18:16:28 GMT\r\nLast-Modified: Fri, 09 Aug 2013 23:54:35 GMT\r\nServer: ECS (dcb/7F3B)\r\nVary: Accept-Encoding\r\nX-Cache: HIT\r\nContent-Length: 606\r\n\r\nq\x02X\x01\x00\x00\x00\nq\x03MX\x01Ntq\x04b.'), headersonly=False)

```

通过此调用涉及腌制对象，我们现在可以重新运行原始调用并获得有效结果：

```py
eval(call)

```

```py
<http.client.HTTPMessage at 0x111340c50>

```

### 所有通话

到目前为止，我们只看到一个`webbrowser()`调用。 我们实际上可以划分和重播`webbrowser()`中的多少个呼叫？ 让我们尝试一下并计算数字。

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

```

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

```

```py
all_functions = set(webbrowser_carver.called_functions(qualified=True))
call_success = set()
run_success = set()

```

```py
exceptions_seen = set()

for function_name in webbrowser_carver.called_functions(qualified=True):
    for argument_list in webbrowser_carver.arguments(function_name):
        try:
            call = call_string(function_name, argument_list)
            call_success.add(function_name)

            result = eval(call)
            run_success.add(function_name)

        except Exception as exc:
            exceptions_seen.add(repr(exc))
            # print("->", call, file=sys.stderr)
            # traceback.print_exc()
            # print("", file=sys.stderr)
            continue

```

```py
print("%d/%d calls (%.2f%%) successfully created and %d/%d calls (%.2f%%) successfully ran" % (
    len(call_success), len(all_functions), len(
        call_success) * 100 / len(all_functions),
    len(run_success), len(all_functions), len(run_success) * 100 / len(all_functions)))

```

```py
241/304 calls (79.28%) successfully created and 99/304 calls (32.57%) successfully ran

```

大约一半的呼叫成功。 让我们看一下我们得到的一些错误消息：

```py
for i in range(10):
    print(list(exceptions_seen)[i])

```

```py
NameError("name 'email' is not defined",)
SyntaxError('keyword argument repeated', ('<string>', 1, 98, None))
AttributeError("module 'urllib.request' has no attribute 'ip2num'",)
NameError("name 'OrderedDict' is not defined",)
NameError("name 'Compat32' is not defined",)
StopIteration()
AttributeError("'NoneType' object has no attribute 'close'",)
TypeError("can't pickle weakref objects",)
NameError("name 'urllib3' is not defined",)
TypeError("argument of type 'NoneType' is not iterable",)

```

我们看到：

*   **大部分呼叫可以转换为呼叫字符串。** 如果不是这种情况，则主要是由于传递了非序列化的对象。
*   **大约可以执行一半的呼叫。** 失败运行的错误消息多种多样； 最常见的是调用了某个不在范围内的内部名称。

我们的雕刻机制应该是一粒盐：我们仍然没有涵盖访问外部变量和值（例如全局变量）并且序列化机制无法重新创建外部资源的情况。 但是，如果感兴趣的功能属于*可以被雕刻和重放的功能，我们可以非常有效地以其原始参数重新运行它的调用。*

## 从中断调用中挖掘API语法

到目前为止，我们已经使用雕刻调用来重播与最初遇到的调用完全相同的调用。 但是，我们也可以*变异*雕刻的调用，以有效地模糊具有先前记录的参数的API。

总体思路如下：

1.  首先，我们记录程序执行中给定功能的所有调用。
2.  其次，我们创建一个包含所有这些调用的语法，为每个参数使用单独的规则，为找到的每个值提供替代方案； 这使我们能够产生任意调用*来重新组合*这些参数的调用。

让我们在以下各节中探索这些步骤。

### 从呼叫到语法

让我们从一个例子开始。 `power(x, y)`函数返回$ x ^ y $; 它只是对等效`math.pow()`函数的包装。 （由于`power()`是用Python定义的，因此我们可以对其进行跟踪–与`math.pow()`相比，后者是用C实现的。）

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

```

```py
def power(x, y):
    return math.pow(x, y)

```

让我们在记录参数时调用`power()`：

```py
with CallCarver() as power_carver:
    z = power(1, 2)
    z = power(3, 4)

```

```py
power_carver.arguments("power")

```

```py
[[('x', 1), ('y', 2)], [('x', 3), ('y', 4)]]

```

从记录的参数列表中，我们现在可以为`power()`调用创建语法，其中`x`和`y`扩展为所看到的值：

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

```

```py
POWER_GRAMMAR = {
    "<start>": ["power(<x>, <y>)"],
    "<x>": ["1", "3"],
    "<y>": ["2", "4"]
}

assert is_valid_grammar(POWER_GRAMMAR)

```

当用这种语法模糊时，我们可以得到`x`和`y`的任意组合； 以覆盖为目标将确保至少对所有值进行了至少一次实际测试：

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

```

```py
power_fuzzer = GrammarCoverageFuzzer(POWER_GRAMMAR)
[power_fuzzer.fuzz() for i in range(5)]

```

```py
['power(1, 2)', 'power(3, 4)', 'power(1, 2)', 'power(3, 4)', 'power(3, 4)']

```

我们需要的是一种自动将`power_carver`中看到的参数转换为`POWER_GRAMMAR`中看到的语法的方法。 这是我们在下一部分中定义的。

### 呼叫的语法挖掘器

我们引入了一个`CallGrammarMiner`类，给定一个`Carver`，它会根据所看到的调用自动生成语法。 要初始化，我们传递carver对象：

```py
class CallGrammarMiner(object):
    def __init__(self, carver, log=False):
        self.carver = carver
        self.log = log

```

#### 初始语法

初始语法产生一个调用。 可能的`<call>`扩展将在以后构建：

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

```

```py
class CallGrammarMiner(CallGrammarMiner):
    CALL_SYMBOL = "<call>"

    def initial_grammar(self):
        return extend_grammar(
            {START_SYMBOL: [self.CALL_SYMBOL],
                self.CALL_SYMBOL: []
             })

```

```py
m = CallGrammarMiner(power_carver)
initial_grammar = m.initial_grammar()
initial_grammar

```

```py
{'<start>': ['<call>'], '<call>': []}

```

#### 从参数[到](#A-Grammar-from-Arguments)的语法

让我们首先从参数列表创建语法。 方法`mine_arguments_grammar()`为在雕刻过程中看到的参数创建语法，例如：

```py
arguments = power_carver.arguments("power")
arguments

```

```py
[[('x', 1), ('y', 2)], [('x', 3), ('y', 4)]]

```

`mine_arguments_grammar()`方法迭代看到的变量，并创建变量名称的`variables`到看到的一组值的映射（作为字符串，通过`call_value()`）。 在第二步中，然后为每个变量名称创建一个带有规则的语法，并扩展为可见的值。

```py
class CallGrammarMiner(CallGrammarMiner):
    def var_symbol(self, function_name, var, grammar):
        return new_symbol(grammar, "<" + function_name + "-" + var + ">")

    def mine_arguments_grammar(self, function_name, arguments, grammar):
        var_grammar = {}

        variables = {}
        for argument_list in arguments:
            for (var, value) in argument_list:
                value_string = call_value(value)
                if self.log:
                    print(var, "=", value_string)

                if value_string.find("<") >= 0:
                    var_grammar["<langle>"] = ["<"]
                    value_string = value_string.replace("<", "<langle>")

                if var not in variables:
                    variables[var] = set()
                variables[var].add(value_string)

        var_symbols = []
        for var in variables:
            var_symbol = self.var_symbol(function_name, var, grammar)
            var_symbols.append(var_symbol)
            var_grammar[var_symbol] = list(variables[var])

        return var_grammar, var_symbols

```

```py
m = CallGrammarMiner(power_carver)
var_grammar, var_symbols = m.mine_arguments_grammar(
    "power", arguments, initial_grammar)

```

```py
var_grammar

```

```py
{'<power-x>': ['1', '3'], '<power-y>': ['4', '2']}

```

附加返回值`var_symbols`是调用中的参数符号列表：

```py
var_symbols

```

```py
['<power-x>', '<power-y>']

```

#### 来电语法

为了获得单个函数（`mine_function_grammar()`）的语法，我们向函数添加了一个调用：

```py
class CallGrammarMiner(CallGrammarMiner):
    def function_symbol(self, function_name, grammar):
        return new_symbol(grammar, "<" + function_name + ">")

    def mine_function_grammar(self, function_name, grammar):
        arguments = self.carver.arguments(function_name)

        if self.log:
            print(function_name, arguments)

        var_grammar, var_symbols = self.mine_arguments_grammar(
            function_name, arguments, grammar)

        function_grammar = var_grammar
        function_symbol = self.function_symbol(function_name, grammar)

        if len(var_symbols) > 0 and var_symbols[0].find("-self") >= 0:
            # Method call
            function_grammar[function_symbol] = [
                var_symbols[0] + "." + function_name + "(" + ", ".join(var_symbols[1:]) + ")"]
        else:
            function_grammar[function_symbol] = [
                function_name + "(" + ", ".join(var_symbols) + ")"]

        if self.log:
            print(function_symbol, "::=", function_grammar[function_symbol])

        return function_grammar, function_symbol

```

```py
m = CallGrammarMiner(power_carver)
function_grammar, function_symbol = m.mine_function_grammar(
    "power", initial_grammar)
function_grammar

```

```py
{'<power-x>': ['1', '3'],
 '<power-y>': ['4', '2'],
 '<power>': ['power(<power-x>, <power-y>)']}

```

另外返回的`function_symbol`保存刚刚添加的函数调用的名称：

```py
function_symbol

```

```py
'<power>'

```

#### 所有调用的语法

现在让我们对雕刻期间看到的所有函数调用重复上述操作。 为此，我们简单地遍历所有可见的函数调用：

```py
power_carver.called_functions()

```

```py
['power', '__exit__']

```

```py
class CallGrammarMiner(CallGrammarMiner):
    def mine_call_grammar(self, function_list=None, qualified=False):
        grammar = self.initial_grammar()
        fn_list = function_list
        if function_list is None:
            fn_list = self.carver.called_functions(qualified=qualified)

        for function_name in fn_list:
            if function_list is None and (function_name.startswith("_") or function_name.startswith("<")):
                continue  # Internal function

            # Ignore errors with mined functions
            try:
                function_grammar, function_symbol = self.mine_function_grammar(
                    function_name, grammar)
            except:
                if function_list is not None:
                    raise

            if function_symbol not in grammar[self.CALL_SYMBOL]:
                grammar[self.CALL_SYMBOL].append(function_symbol)
            grammar.update(function_grammar)

        assert is_valid_grammar(grammar)
        return grammar

```

客户可以并且应该使用的方法`mine_call_grammar()` –首先用于挖掘...

```py
m = CallGrammarMiner(power_carver)
power_grammar = m.mine_call_grammar()
power_grammar

```

```py
{'<start>': ['<call>'],
 '<call>': ['<power>'],
 '<power-x>': ['1', '3'],
 '<power-y>': ['4', '2'],
 '<power>': ['power(<power-x>, <power-y>)']}

```

...然后进行模糊测试：

```py
power_fuzzer = GrammarCoverageFuzzer(power_grammar)
[power_fuzzer.fuzz() for i in range(5)]

```

```py
['power(3, 4)', 'power(1, 2)', 'power(3, 4)', 'power(3, 4)', 'power(1, 4)']

```

这样，我们就成功地从记录的执行中提取了语法。 与“简单”的雕刻相反，我们的语法使我们可以*重组*参数，从而可以在API级别进行模糊测试。

## 模糊Web功能

现在，让我们将语法挖掘器应用到更大的API –雕刻期间已经遇到的`urlparse()`函数。

```py
with CallCarver() as webbrowser_carver:
    webbrowser("https://www.fuzzingbook.org")
    webbrowser("http://www.example.com")

```

我们可以从遇到的电话中挖掘语法：

```py
m = CallGrammarMiner(webbrowser_carver)
webbrowser_grammar = m.mine_call_grammar()

```

这是一个相当大的语法：

```py
call_list = webbrowser_grammar['<call>']
len(call_list)

```

```py
151

```

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

```

```py
['<webbrowser>', '<default_headers>', '<default_user_agent>', '<update>', '<default_hooks>', '<cookiejar_from_dict>', '<RLock>', '<deepvalues>', '<vals_sorted_by_key>', '<init_poolmanager>', '<mount>', '<prepare_request>', '<merge_cookies>', '<get_netrc_auth>', '<expanduser>', '<encode>', '<decode>', '<exists>', '<urlparse>', '<urlsplit>']

```

这是`urlsplit()`函数的规则：

```py
webbrowser_grammar["<urlsplit>"]

```

```py
['urlsplit(<urlsplit-url>, <urlsplit-scheme>, <urlsplit-allow_fragments>)']

```

这是参数。 请注意，尽管我们只传递了`http://www.fuzzingbook.org`作为参数，但我们也看到了`https:`变体。 那是因为打开`http:` URL会自动重定向到`https:` URL，然后`urlsplit()`也将对其进行处理。

```py
webbrowser_grammar["<urlsplit-url>"]

```

```py
["'https://www.fuzzingbook.org/'",
 "'https://www.fuzzingbook.org'",
 "'http://www.example.com/'",
 "'http://www.example.com'"]

```

`scheme`参数也有一些变化：

```py
webbrowser_grammar["<urlsplit-scheme>"]

```

```py
["''"]

```

如果现在在这些规则上应用模糊器，我们将系统地涵盖所看到的所有参数变体，当然包括雕刻期间未看到的组合。 再次，我们在这里在API级别进行模糊测试。

```py
urlsplit_fuzzer = GrammarCoverageFuzzer(
    webbrowser_grammar, start_symbol="<urlsplit>")
for i in range(5):
    print(urlsplit_fuzzer.fuzz())

```

```py
urlsplit('https://www.fuzzingbook.org', '', True)
urlsplit('https://www.fuzzingbook.org/', '', True)
urlsplit('http://www.example.com', '', True)
urlsplit('http://www.example.com/', '', True)
urlsplit('http://www.example.com', '', True)

```

就像雕刻一样，在API级别运行测试比执行系统测试快几个数量级。 因此，这需要在方法级别进行模糊测试的手段：

```py
from [urllib.parse](https://docs.python.org/3/library/urllib.parse.html) import urlsplit

```

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

```

```py
with Timer() as urlsplit_timer:
    urlsplit('http://www.fuzzingbook.org/', 'http', True)
urlsplit_timer.elapsed_time()

```

```py
1.5235999853757676e-05

```

```py
with Timer() as webbrowser_timer:
    webbrowser("http://www.fuzzingbook.org")
webbrowser_timer.elapsed_time()

```

```py
0.07453407400043943

```

```py
webbrowser_timer.elapsed_time() / urlsplit_timer.elapsed_time()

```

```py
4891.971299281484

```

但是再一次，雕刻时遇到的警告适用，尤其是重新创建原始功能环境的要求。 如果我们也更改或重新组合参数，则会有*违反隐式前提*的额外风险-也就是说，使用从未设计过的参数调用函数。 然后，必须识别（通常是手动）由错误的调用而不是由错误的实现导致的此类*错误警报*，并清除（例如，通过更改或限制语法）。 但是，在API级别获得的巨大速度提升可能足以证明这项额外投资的合理性。

## 经验教训

*   *雕刻*允许有效重播系统测试期间记录的功能调用。
*   函数调用比系统调用快几个数量级。
*   *序列化*允许创建复杂对象的持久表示。
*   与环境紧密交互和/或访问外部资源的功能很难实现。
*   通过刻划调用，可以生成可任意组合刻划参数的API语法。

## 后续步骤

在下一章中，我们将讨论[如何减少引起故障的输入](Reducer.html)。

## 背景

雕刻是Elbaum等人发明的。 [ [Elbaum *等人*，2006。](https://doi.org/10.1145/1181775.1181806)]最初是为Java实现的。 在本章中，我们遵循它们的几种设计选择（仅包括记录和序列化方法参数）。

在[ [Kampmann *等人*，2018\.](https://arxiv.org/abs/1812.07932) ]中描述了API级别的雕刻和模糊化的组合。

## 练习

### 练习1：渴望进行回归测试

到目前为止，在雕刻期间，我们仅研究了再现*调用*的过程，而没有研究实际检查这些调用的*结果*的过程。 这对于*回归测试*非常重要-即检查代码更改是否不妨碍现有功能。 我们不仅可以记录*调用*，还可以记录*返回值*，然后再比较相同的调用是否产生相同的值来构建此文件。 这可能并非在所有情况下都有效； 取决于时间，随机性或其他外部因素的值可能会有所不同。 尽管如此，对于从这些细节中抽象出来的功能，检查是否没有任何变化仍然是测试的重要组成部分。

我们的目标是设计一个通过记录调用和返回值来扩展`CallCarver`的类`ResultCarver`。

第一步，创建一个`traceit()`方法，该方法还通过扩展`traceit()`方法来跟踪返回值。 `traceit()`事件类型为`"return"`，`arg`参数为返回值。 这是一个仅打印出返回值的原型：

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

```py
class ResultCarver(CallCarver):
    def traceit(self, frame, event, arg):
        if event == "return":
            if self._log:
                print("Result:", arg)

        super().traceit(frame, event, arg)
        # Need to return traceit function such that it is invoked for return
        # events
        return self.traceit

```

```py
with ResultCarver(log=True) as result_carver:
    my_sqrt(2)

```

```py
my_sqrt(x=2)
Result: 1.414213562373095
__exit__(self=<__main__.ResultCarver object at 0x1a22161c18>, exc_type=None, exc_value=None, tb=None)

```

#### 第1部分：存储函数结果

扩展上面的代码，以便将结果与当前返回的函数（或方法）相关联地存储在中。 为此，您需要跟踪*当前被调用函数*的堆栈。

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

#### 第2部分：访问结果

给它一个方法`result()`，该方法返回为该特定函数名和结果记录的值：

```py
class ResultCarver(CallCarver):
    def result(self, function_name, argument):
        """Returns the result recorded for function_name(argument"""

```

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

#### 第3部分：产生断言

对于在`webbrowser()`执行期间调用的函数，创建一组*断言*，以检查返回的结果是否仍然相同。 对`urllib.parse.urlparse()`和`urllib.parse.urlsplit()`进行测试。

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

### 练习2：抽象参数

从执行中挖掘API语法时，请建立抽象方案以扩大测试期间要使用的参数范围。 如果参数的值全部符合某种`T`类型。 将其抽象为`<T>`。 例如，如果已经看到对`foo(1)`，`foo(2)`和`foo(3)`的调用，则语法应将其调用抽象为`foo(<int>)`，并适当定义`<int>`。

对许多常见类型执行此操作：整数，正数，浮点数，主机名，URL，邮件地址等。

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