简单项目的仪表化

本节描述了如何使用单元测试来仪表化一个小型项目。该项目是一个简单的表达式解释器,除了需要一个 C++ 编译器外没有太多要求。

该项目以微缩模型复现了一个已经扩展了单元测试的现有项目,我们将现在使用 Coco 来检查测试覆盖率有多好。因此,仪表化应该是非侵入性的,并不应在很大程度上改变项目。

注意:在 UNIX® 和 macOS 下设置项目以进行仪表化的步骤与 Microsoft® Windows 下不同。因此,设置部分有两个版本,将在以下各节中说明。

UNIX 和 macOS 设置

此示例需要 GNU C++ 编译器 g++GNU make。稍后,还必须存在 diff 工具。

解析器示例的源代码可以在 Coco 的安装目录中找到。在 UNIX 下,这是目录 /opt/SquishCoco/,或者如果您本地安装了 Coco,则是您的家目录中 SquishCoco/ 的子目录。在 macOS 下,安装目录是 /Applications/SquishCoco/

注意:如果您选择了自定义目录安装 Coco,则示例将无法在没有修改的情况下运行。

我们将使用 SquishCoco/ 目录来指代安装目录,无论其位置如何。Coco 示例及其支持程序在 SquishCoco/samples/ 中,解析器在 SquishCoco/samples/parser/ 中。此目录包含程序的多个版本,在 parser_v1/parser_v5/ 的目录中。它们代表了不同开发阶段的解析器。

本例使用 CppUnit 作为其单元测试框架。在 SquishCoco/samples/ 目录中有一个 CppUnit 的版本,并且解析器示例已准备好使用它。

在执行任何其他操作之前,请将整个 samples/ 目录的内容复制到您的工位,并将 parser_v1/ 设置为您的工作目录。如果 Coco 安装在 /opt/SquishCoco/,可以按以下方式操作

$ cp -a /opt/SquishCoco/samples .

parser_v1 目录设置为工作目录

$ cd samples/parser/parser_v1

同时确保 Coco 工具在您的搜索路径中。如果它们不在,您现在可以写

$ . cocosetup.sh

别忘了开头的点。现在可以像 coveragebrowser 这样的程序从命令行调用。

解析器目录结构

我们将使用 samples/parser/parser_v1/ 作为我们的工作目录。它包含 C++ 源文件和头文件,还包括一个单元测试文件,unittests.cpp。makefile 已经为单元测试做好了准备,但不是为仪器化。仪器化是通过 bash 脚本 instrumented 来完成的。

编译和测试

运行 make 编译程序。这是一个简单的表达式解析器和计算器。

$ ./parser
Enter an expression an press Enter to calculate the result.
Enter an empty expression to quit.

> 2 + 2
        Ans = 4
> Pi
        Ans = 3.14159
> sin(Pi)
        Ans = 1.22465e-16
> sinn(90)
        Error: Unknown function sinn (col 9)
> sin(90)
        Ans = 0.893997
> cos(pi)
        Ans = -1
>
$

我们为主要的 Parser 类添加了一些单元测试。查看文件 unittests.cpp 以查看已包含的测试。用 make tests 执行它。您会看到已执行了 14 个测试,而且其中一个失败了。这是为了增加现实性,也让我们可以看到 Coco 如何处理测试失败。

仪器化

我们已经将仪器化与主项目分开。仪器化的核心是一个简短的壳脚本,instrumented。这是一个简单的包装器,调用 instrumented <command> 会用几个环境变量执行 <command>。我们现在就这样做。输入

$ make clean
$ ./instrumented make tests

第一个命令删除了所有的目标文件,因为我们需要重新编译所有内容。然后第二个命令编译包含仪器的程序并运行测试。这就完了!

我们现在来看看脚本做了什么以及它是如何做到这一点的。列出你的 parser 目录的内容

$ ls
constants.h    functions.o.csmes  parser.cpp       unittests.o
error.cpp      instrumented       parser.h         unittests.o.csmes
error.h        LICENSE            parser.o         variablelist.cpp
error.o        main.cpp           parser.o.csmes   variablelist.h
error.o.csmes  main.o             unittests        variablelist.o
functions.cpp  main.o.csmes       unittests.cpp    variablelist.o.csmes
functions.h    Makefile           unittests.csexe
functions.o    NOTICE             unittests.csmes
    

您会看到两种在正常编译过程中不会显示的文件。.csmes 文件包含覆盖度量所需的信息,而 .csexe 文件包含代码执行的结果。以 .o.csmes 结尾的文件是临时文件,仅在编译期间使用。

这次,实际上执行的唯一程序是 unittests,因此唯一的 .csexe 文件是 unittests.csexe。要查看覆盖率结果,您可以使用以下命令启动 CoverageBrowser

$ coveragebrowser -m unittests.csmes -e unittests.csexe

CoverageBrowser 会在打开的 加载执行报告 对话框中启动。

"Load Execution Report dialog"

点击 下一步 然后 导入 以加载数据。

如果启用了复选框 加载后删除执行报告,此按钮将称为 导入并删除。这将在加载后删除 .csexe 文件。选择 文件 > 保存 以将执行报告保存到 unittests.csexe 文件中。

有关 CoverageBrowser 的使用,请参阅 CoverageBrowser 参考手册。我们现在将描述如何进行仪器化。

项目是如何被仪器化的

instrumented 文件是一个简短的 bash 脚本

#!/bin/bash

. getcoco.sh                    # Get Coco variables

export PATH=$COCO_WRAPPER_DIR:$PATH
export COVERAGESCANNER_ARGS='--cs-on'
export COVERAGESCANNER_ARGS+=' --cs-mcdc'
export COVERAGESCANNER_ARGS+=' --cs-mcc'
# export COVERAGESCANNER_ARGS+=' --cs-function-profiler=all' # Does not work on all platforms

"$@"
    

在脚本开始时,必要的壳脚本 getcoco.sh 将设置 shell 变量 SQUISHCOCO 为 Squish Coco 的安装位置。基于这个设置,它将 COCO_WRAPPER_DIRCOCO_BIN_DIR 设置为。

(有关参数 --cs-function-profiler 以及为什么它在脚本中被禁用,请参阅关于 功能分析器 的部分。)

然后,有一些不进行翻译的export语句,最后的神秘语句执行了instrumented的命令行参数。所以如果你调用./instrumented make tests,脚本会执行make tests命令,但环境不同于通常情况。

首先,对搜索PATH进行操作,以便首先搜索到$SQUISHCOCO/wrapper/bin/目录下的程序。这个目录包含许多与Coco支持的编译器同名的文件。

$ ls /opt/SquishCoco/wrapper/bin
ar                            g++-4.9         x86_64-linux-gnu-ar
c89-gcc                       gcc             x86_64-linux-gnu-g++
c99-gcc                       gcc-4.6         x86_64-linux-gnu-g++-4.6
...

注意:还有一个ar,它不是编译器,但参与了编译过程。这是UNIX下的输出;在Mac OS X下,/Applications/SquishCoco/wrapper/目录包含更少的文件。

这些程序是编译器包装器。使用新的PATH,这些程序会代替实际的编译器执行。编译器包装器实际上是单一程序coveragescanner的符号链接(见CoverageScanner参考)。当执行编译源文件时,它们会创建源文件的 Instrumented 版本,然后运行原始编译器进行编译。

在其他四个export语句中,设置了编译器包装器的附加标志(见CoverageScanner命令行参数)。最重要的选项是第一个,--cs-on。如果没有它,编译器包装器将不活跃,只会调用其代表的编译器。以下选项开启了MC/DC多个条件覆盖率函数分析器。默认情况下它们是禁用的,因为执行它们有时可能是昂贵的。在这里它们被启用了,这样你就可以在稍后使用CoverageBrowser查看所有覆盖率模式。

生成的脚本在没有更改的情况下可以为许多简单项目工作。如果需要更多定制,通常可以通过向COVERAGESCANNER_ARGS添加更多选项来实现。

其他更改

添加make目标以便处理CoverageScanner生成的文件也是很方便的。在parser目录中,Makefile已经按照以下方式更改:

clean: testclean
        ...
        -$(DEL_FILE) *.o.csmes       # (added)

distclean: clean
        ...
        -$(DEL_FILE) *.csmes *.csexe # (added)

$code translate="no">.o.csmes文件只在编译时需要,所以可以在删除.o文件时删除它们(这是make clean所做的)。.csmes.csexe文件更有价值,应该在删除所有生成的文件时再删除。因此,我们将它们的删除语句添加到了distclean目标。

注意:对于与其他构建自动化系统和IDE集成的Coco设置,请参阅设置部分中的以下章节。

Microsoft Windows设置

此示例需要Microsoft Visual C++

解析器源代码可以在<Windows Coco>目录下的\parser目录中找到。该目录包含三个版本的程序,位于子目录parser_v1parser_v3中。它们代表了不同发展阶段中的解析器。

此示例使用CppUnit作为其单元测试框架。在<Windows Coco>\squishcoco目录中有一个CppUnit版本,解析器示例已准备好使用它。

由于这些目录被写保护,您需要创建自己的工作副本。因此,将两个目录 <Windows Coco>\squishcoco\parser<Windows Coco>\squishcoco\cppunit-1.12.1 复制到您选择的目录中。然后删除目录及其所有文件上的写保护。

解析器目录结构

我们将 parser\parser_v1 作为我们的工作目录。它包含 C++ 源文件和头文件,以及一个单元测试文件 unittests.cpp。已经为单元测试准备好了 Makefile,但不是为仪表化。仪表化是由批处理文件 instrumented.bat 完成的。还有一些在 UNIX 版本中需要的文件。这里我们将忽略它们。

编译和测试

我们将在命令行上编译示例。要获取命令窗口,请执行位于 parser_v1 目录中的批处理文件 CocoCmd.bat。在这个窗口中,可以访问 Microsoft Visual Studio 命令行工具(如 nmake),也可以访问主要的 Coco 程序(如 CoverageBrowser)。

运行 nmake 编译程序。这是一个简单的表达式解析器和计算器。

$ C:\code\parser\parser_v1>parser.exe
Enter an expression an press Enter to calculate the result.
Enter an empty expression to quit.

> 2 + 2
        Ans = 4
> Pi
        Ans = 3.14159
> sin(Pi)
        Ans = 1.22465e-16
> sinn(90)
        Error: Unknown function sinn (col 9)
> sin(90)
        Ans = 0.893997
> cos(pi)
        Ans = -1
>
C:\code\parser\parser_v1>

我们为主要的类 Parser 添加了一些单元测试。查看文件 unittests.cpp 来查看已包含的测试。使用 nmake tests 执行它。您将看到已执行 14 个测试,并且其中有一个失败了。这是为了增加现实感,也允许我们以后看到 Coco 如何处理测试失败。

仪表化

我们将仪表化与主项目分开。仪表化的核心是一个简短的 Shell 脚本,instrumented.bat。这是一个简单的包装器,调用 instrumented.bat<command> 会将一些环境变量设置后执行 <command>。我们将会这样做。输入

C:\code\parser\parser_v1>nmake clean
C:\code\parser\parser_v1>instrumented.bat nmake tests

第一个命令删除了所有的目标文件,因为我们需要重新编译所有内容。然后第二个命令编译包含仪器的程序并运行测试。这就完了!

要查看脚本做了什么以及它是如何做的,列出 parser 目录的内容

C:\code\parser\parser_v1>dir /D

 Directory of C:\code\parser\parser_v1

[.]                      LICENSE                  unittests.exe
[..]                     main.cpp                 unittests.exe.csexe
constants.h              main.obj                 unittests.exe.csmes
error.cpp                main.obj.csmes           unittests.exp
error.h                  Makefile                 unittests.lib
error.obj                nmake.mak                unittests.obj
error.obj.csmes          NOTICE                   unittests.obj.csmes
functions.cpp            parser.cpp               variablelist.cpp
functions.h              parser.h                 variablelist.h
functions.obj            parser.obj               variablelist.obj
functions.obj.csmes      parser.obj.csmes         variablelist.obj.csmes
instrumented             README.squishcoco
instrumented.bat         unittests.cpp
              35 File(s)      1,079,919 bytes
               2 Dir(s)  35,159,457,792 bytes free
    

您会看到两种类型的文件,它们不会在正常编译后出现。.csmes 文件包含用于覆盖率测量的信息,而 .csexe 文件包含代码执行的结果。以 .obj.csmes 结尾的文件是临时文件,仅在编译期间使用。

当我们执行程序 unittests 时,相应的 .csexe 文件 unittests.exe.csexe 被创建或更新。要查看覆盖率结果,您可以使用以下命令启动 CoverageBrowser

C:\code\parser\parser_v1>coveragebrowser -m unittests.exe.csmes -e unittests.exe.csexe

CoverageBrowser 会在打开的 加载执行报告 对话框中启动。

"Load Execution Report dialog"

点击 下一步 然后 导入 以加载数据。

如果启用了复选框 加载后删除执行报告,此按钮将称为 导入并删除。这将在加载后删除 .csexe 文件。选择 文件 > 保存 以将执行报告保存到 unittests.csexe 文件中。

有关 CoverageBrowser 的使用,请参阅 CoverageBrowser 参考。我们现在将描述仪表化的过程。

如何对项目进行仪表化

文件 instrumented.bat 是一个简短的批处理文件

@echo off
setlocal

set PATH=%SQUISHCOCO%\visualstudio;%PATH%
set COVERAGESCANNER_ARGS=--cs-on --cs-mcdc --cs-mcc --cs-function-profiler=all

call %*

endlocal

变量 SQUISHCOCO 包含 Coco 安装目录的名称。它是在 Coco 安装期间设置的。

一开始,使用setlocal命令确保下面的命令仅临时改变环境变量。然后有两个set语句,最后有一个call语句将instrumented的命令行参数执行。所以如果你调用instrumented nmake tests,由批处理文件执行nmake tests命令,但是在不同的环境中。最后,使用endlocal撤销环境变量的更改。

脚本中的重要部分是两个set语句。在第一个中,搜索路径被操作,以便优先搜索<Windows Coco>\squishcoco\visualstudio中的程序。这个目录包含与编译器和链接器同名文件。

注意:这是32位编译器的版本。还有一个目录<Windows Coco>\visualstudio_x64用于64位编译。

C:\code\parser\parser_v1>dir /d "\Program Files\squishcoco\visualstudio"

 Directory of C:\Program Files\squishcoco\visualstudio

[.]            cl.exe         link.cspro     msvcr100.dll
[..]           lib.cspro      link.exe
cl.cspro       lib.exe        msvcp100.dll
               8 File(s)      5,662,299 bytes
               2 Dir(s)  35,053,502,464 bytes free

该目录中的.exe文件是编译器包装器。使用新的PATH,它们代替真实编译器执行。每个编译器包装器实际上是一个程序coveragescanner.exe的副本(见CoverageScanner参考)。当用于编译源文件时,它会创建仪器的源版本,然后运行原始编译器进行编译。

在第二个set语句中,为编译器包装器(见CoverageScanner命令行参数)设置了额外的标志。最重要的选项是第一个,--cs-on。如果它不存在,编译器包装器将不活跃,仅调用它们所代表的编译器。以下选项启用MC/DC多条件覆盖功能内省。它们默认是禁用的,因为执行它们有时可能会花费较多时间。在此启用它们,以便你可以在后续的CoverageBrowser中看到所有覆盖模式。

生成的脚本在没有更改的情况下可以为许多简单项目工作。如果需要更多定制,通常可以通过向COVERAGESCANNER_ARGS添加更多选项来实现。

其他更改

make目标添加到处理CoverageScanner生成的文件也是方便的。在parser_v1目录中,Makefile已经按以下方式更改

clean: testclean
        ...
        -$(DEL_FILE) *.obj.csmes     # (added)

distclean: clean
        ...
        -$(DEL_FILE) *.csmes *.csexe # (added)

因为仅需要用于编译的.obj.csmes文件,所以可以在删除.obj文件时通过make clean删除。而.csmes.csexe文件更为珍贵,应该只在删除所有生成的文件时删除。因此,我们将它们的删除语句添加到distclean目标。

注意:对于与其他构建自动化系统和IDE集成的Coco设置,请参阅设置部分中的以下章节。

除了基本仪器使用之外

在以下章节中,我们将说明Coco的附加能力。它们需要更改项目中的一些代码。

从仪器中排除代码

迄今为止生成的覆盖率信息存在问题:它覆盖了太多的文件。问题文件属于测试框架,而不属于受测程序。包含它们会人为地降低覆盖率。

使用Coco,可以通过额外的命令行选项从覆盖率中排除文件。在parser_v2中已经这样做。查看parser_v2/instrumented(或在Microsoft Windows下的parser_v2\instrumented.bat)。在它里面,设置了三个额外的命令行选项:

  • 选项 --cs-exclude-path=../../cppunit-1.12.1 排除了一个目录及其所有子目录的源文件。这里我们用它来排除 CppUnit 框架的所有文件。

    您可以使用斜杠或反斜杠与该选项一起使用——Coco 会内部将其标准化。

  • 选项 –cs-exclude-file-abs-wildcard=*/unittests.cpp–cs-exclude-file-abs-wildcard=*/CppUnitListener.cpp 拒绝特定文件。我们使用它们来拒绝文件 unittests.cppCppUnitListener.cpp。 (下文将描述第二个文件。)

使测试名称可见

对于下一个修改,我们希望修改项目,以便我们不仅知道代码行是否被测试覆盖,而且还知道是被哪些测试覆盖。为此,我们将向代码中添加 CoverageScanner 的调用(C 和 C++ 库),以告诉 Coco 测试的名称以及它们的起始和结束位置。

更新后的项目版本可在 parser_v2 目录中找到。与 parser_v1 版本相比,最大的不同是添加了文件 CppUnitListener.cpp。该文件几乎与CppUnit 相同。文件包含一个名为 CppUnitListener 的类和一个新的 main() 函数。在 unittests.cpp 中的 main() 函数已被删除,但该文件除此之外没有变化。

CppUnitListener.cpp 提供了一个单元测试监听器,允许在执行每个测试之前和之后钩入框架。因此,可以在不修改测试代码本身的情况下,将额外的测试信息(如测试名称和结果)记录在代码覆盖率数据中。有关 CppUnitListener.cpp 的清单及其工作原理的解释,请参阅 CppUnit

现在,您可以像以前版本一样执行此程序。在 CoverageBrowser 中查看结果。在 Executions 子窗口中,您现在可以看到每个单个测试的代码覆盖率。您可以特别看到是测试 testInvalidNumber 失败了。

将鼠标悬停于源窗口中的一行之上,并选择被标记为深绿色的行。这意味着它已被测试执行。您将看到一个带有执行此行测试列表的工具提示。为了使工具提示保持简短,这里有一个最大测试数量显示。要查看完整列表,请单击该行。然后,行的全部信息将在 Explanation 窗口中显示。

注意:明亮的绿色线条也被执行了,但未由 Coco 仪器化。

补丁文件分析

考虑以下场景:在一个大型项目中,需要评估一个最后的时刻补丁。没有足够的时间运行完整的测试套件,但需要进行风险评估。对于类似这种情况,Coco 提供了 补丁分析 功能。使用它,可以特别显示已更改代码行的代码覆盖率,并找到覆盖它们的大套件中的测试。现在可以了解更改的风险程度。

我们将在我们的示例中模拟这种情况。在解析器的新版本中,代码中的字符分类函数,例如 isWhiteSpace()isAlpha() 等,已经更改并改用标准 C 分类函数,如 isspace() 而不是 strchr()。解析器的新版本可找到在 parser_v3 目录中。

现在我们将将其与在parser_v2中的版本进行比较,但 neither 运行测试 nor 甚至编译它。相反,我们需要以下两个信息

  1. parser_v2的覆盖率数据,如前节所述。
  2. 显示两个目录之间差异的补丁文件。在parser目录中已经有一个可以使用的Diff文件,即parser.diff

    Diff文件必须以统一差异格式。这是许多版本控制系统的diff功能的标准输出格式,例如git diff(见Diff文件的生成)。在类UNIX系统中,Patch文件也可以通过diff实用程序生成。可以通过以下方式从parser目录调用

    $ diff -u parser_v2 parser_v3 > parser.diff

    parser目录中还有一个GNU diff的Microsoft Windows版本,因此相同的命令也可以在Windows命令提示符中工作。

启动。然后通过菜单项文件 > 打开加载仪器数据库parser_v2/unittests.csmes,并通过菜单项文件 > 加载执行报告加载测量文件parser_v2/unittests.csexe

现在选择菜单项工具 > 补丁文件分析。当补丁文件分析对话框出现时

  • 标题框中输入标题。
  • 补丁文件框中输入补丁文件的路径。
  • 类型框中输入报告的路径(包括文件名)。
  • 设置工具栏最大大小为5(或任何大于零的值)。
  • 作出任何其他选项调整。

然后单击打开在浏览器中查看报告。

补丁分析报告

报告包含两个表,一个总结了补丁对代码覆盖率的影响列表,最后是补丁文件的注释版本。

本节概述中的两个表格包含受补丁影响的行数和种类统计信息。

第一个表按照执行它们测试的结果对代码中的修复行进行分组。在这里可以看到补丁对测试通过(现在可能会失败)或失败(现在可能会成功)的影响有多大。还有手动检查的测试及其状态未知的项目。在我们的例子中,我们没有注册测试结果,并且所有测试都被计算为Unknown

第二个表格显示了应用补丁后测试覆盖率预期的变化类型。它包含三列,包含移除和插入的行以及它们的总和。从前两列可以看到是否对补丁代码的测试覆盖率增加或减少。(在解析器示例中,保持不变。)表中的最后一行也很重要:它显示了Coco无法将其归类为插入或删除的行的数量。毕竟,补丁分析是一种启发式方法。

受修改影响的测试列表是每个执行了补丁代码的测试用例列表,包括其结果。对补丁的定性分析很有帮助。

受补丁行影响的测试覆盖率

更多详细信息可以在报告中的补丁文件部分找到。这是一个带注释的原始补丁文件版本,旧版本的文本用红色标出,新版本的文本用绿色标出。未更改的行以灰色显示。最重要的列是测试列,它显示每个代码行执行该行的测试次数(如果移除它)或应用补丁后可能执行它。工具提示显示这些测试的名称。

反向补丁

假设已经检查入了一个补丁。它改变了应用程序的行为,你想编写更多测试以确保所有新代码都已覆盖。在这种情况下,反向补丁分析很有帮助。

要使用解析器示例模拟反向补丁分析,切换到parser_v3目录,并像之前parser_v2那样编译代码。然后启动CoverageBrowser并加载这一次的仪器数据库parser_v3/unittests.csmes和测量文件parser_v3/unittests.csexe。这也可以在命令行中完成,例如,形式为

$ cd parser_v3
$ coveragebrowser -m unittests.csmes -e unittests.csexe

像以前一样生成补丁分析报告。Coco会自动识别patch.diff是针对程序当前版本的补丁,并生成相应的报告。这个补丁报告包含与正向补丁报告相同的数据,但是标题已更改为反映新的解释。

错误位置

通过比较运行在程序上所有测试的覆盖率数据,CoverageBrowser可以计算源代码中错误可能的位置。

该算法期望至少有一个测试已失败。在我们这个例子中,解析器没有检测到浮点数的指数不合法。可以编写以下代码

$ cd parser_v2
$ ./parser
Enter an expression an press Enter to calculate the result.
Enter an empty expression to quit.

> 1E+
        Ans = 1

但是1E+不是一个有效的数字,因为它在+之后没有数字。

要找出导致这种情况的源代码中的哪一行,我们需要一些额外的测试。以下测试已经包含在文件unittests.cpp中测试套件中

测试输入预期结果执行状态
1.11.1已通过
1.1e111已通过
1.1e+111已通过
1.1e-10.11已通过
1.1e+错误信息未通过

包含这些测试已经在测试套件中,我们可以使用CoverageBrowser找到导致错误的行的一个候选者。在CoverageBrowser中有一个错误位置窗口,但默认情况下它是隐藏的。您可以通过检查菜单中的错误位置字段来显示它。

窗口显示后,在执行窗口中选择所有执行,然后单击计算按钮。然后窗口将填充一个可能的错误位置列表,其中最可能的是(根据Coco)第一个。在我们这个例子中只有一个候选:文件parser.cpp的第263行。这是解析指数符号+的位置。

此时可以实现一个修复,但由于这只是演示,我们在这里不会这样做。

解析器样本的错误位置

函数分析器

使用函数分析器,Coco测量程序中所有函数执行时所需的时间。

函数分析器默认禁用。要打开它,需要在编译器和链接器命令行参数中设置CoverageScanner选项--cs-function-profiler

注意:函数分析器不能在所有Unix系统上工作,因此在instrumented脚本是禁用的。要启用它,必须删除设置标志--cs-function-profiler那一行的注释符号。

正如使用代码覆盖率一样,可以使用CoverageBrowser来测量一组选定执行的时间消耗。也可以用它来比较两个程序版本或两次测试执行的时间测量。

基本使用

在解析器示例中,函数分析器已启用。作为一个简单的示例,运行parser_v3目录中的单元测试并将仪器数据库加载到CoverageBrowser中。然后会显示一个分析器停靠窗口。如果没有显示,请选择菜单项视图 > 函数分析器。当没有选择测试时,它只包含记录执行时间的函数列表

未选择测试时的分析器窗口

选择一个测试后,函数的时间信息就会出现

  • 总时长:函数的累积执行时间。
  • 次数:调用的次数。
  • 平均时长:单一调用的平均执行时间。

所有时间信息均针对所选执行。单击任意列标题以排序并查找最高值。

选择测试时的分析器窗口

比较两组执行

CoverageBrowser还可以比较两套测试的分析信息。其原则很简单:选择一组参考函数并将其与另一组进行比较。

通过选择菜单项工具 > 执行比较分析来激活执行比较模式。然后将在执行窗口中显示参考列。将测试CppUnit/ParserTest/testFloatExp选为参考,将CppUnit/ParserTest/testFloat选为选定执行。

测试比较模式下的执行窗口

使用上下文菜单显示总时长(比率)总时长(差异)列。单击总时长(比率)列进行排序。你应该得到一个与以下屏幕截图等效的屏幕。时间信息可能与图像上的不同,因为执行时间取决于运行测试的机器。

测试比较模式下的分析器窗口

表中的条目形式为<比率> (<参考>-><选定>)<差异> (<参考>-><选定>)。其中,<参考>是参考执行中函数的运行时间,<选定>是所选执行的总体运行时间。《比率》是它们的商,《差异》是它们的差。

我们可以看到,函数Error::Error的比率无限,因为它在参考测试中没有执行。我们还可以看到,函数Parser::getToken比参考测试慢20倍。但是,差异列中的条目实际上并没有真正的意义,因为差异只有78微秒。

比较程序的两个版本

还可以比较同一程序的两个不同版本的执行时间。

注意: 这要求 .csexe 数据已经合并到其 .csmes 文件中。如果尚未这样做,请打开 parser_v2/unittests.csmesparser_v2/unittests.csexeCoverageBrowser 中,选择 文件 > 保存,然后对 parser_v3 也进行相同操作。

为此,使用 工具 > 与...比较 选择引用执行数据库。在生成的文件选择对话框中,选择文件 parser_v2/unittests.csmes

由于源文件的绝对名称不相同,因此需要首先重命名它们。在 窗口中,通过右键单击选择上下文菜单,并选择 重命名源。作为当前名称输入 parser_v2,作为新名称输入 parser_v3,然后按 确定

现在在 执行 窗口中选择所有执行项。然后 CoverageBrowser 以与上一节中两个程序执行相同的方式比较这两个程序版本。

对比两个软件版本时的分析器窗口

使用函数分析器提高性能

本节说明了Coco如何用于找到和解决性能问题。它涵盖了缩小性能问题、识别有问题的函数、重写源代码以及将重构代码的性能与原始版本进行比较。

应用程序构建

为了启用分析器,CoverageScanner 需要一个标志,--cs-function-profiler

有两个版本可用

  • --cs-function-profiler=all:分析所有函数。
  • --cs-function-profiler=skip-trivial:跳过只有一条语句的函数。

    这些函数大多是获取器或设置器函数。分析它们将只减慢程序而不会提供更多信息。

使用这些选项,Coco将收集分析数据与代码覆盖率。无需使用 CoverageBrowser 对结果进行分析。

对于我们的解析器示例,设置 --cs-function-profiler=all 已经是仪器脚本的一部分。因此,只需使用如以前一样的 parser/parser_v3 目录的仪器化版本即可。

分析性能问题

识别测试用例

现在将文件 unittests.csmesunittests.csexe 加载到 CoverageBrowser 中。在 执行 窗口中,我们可以看到某些测试执行需要大量时间

识别存在性能问题的测试。

在这些测试中,定义了新的解析器变量,从 testAddVariable10 中的10个到 testAddVariable40000 中的40 000个新变量。我们可以看到,这些测试的时间随着变量数量的增加而呈指数增长。

为了分析这种情况,我们使用 CoverageBrowser 中的执行比较模式。要激活它,我们使用菜单项 文件 > 执行比较分析。我们将具有1000个变量定义的测试用例选为参考测试,并将其与具有10 000个定义的测试用例进行比较。

比较具有1000个变量定义的测试用例与具有10 000个变量定义的测试用例。

识别有问题的函数

函数分析器 窗口可以实现更详细的分析

使用函数分析器窗口进行比较。

此窗口显示具有10 000个变量的测试用例和具有1 000个变量的参考测试用例的执行时间和调用次数。

此外,还有两列用于表示当前测试和参考之间比例的比值。列 总持续时间(比值) 的内容由以下公式给出:

比值 = current_test持续时间 / reference_test持续时间

计数(比值) 也有类似的公式。

通过这些信息,我们可以很容易地看出,如果一个测试中的变量定义数量增长了10倍,那么 toupper()Variablelist::add()Variablelist::get_id() 的执行时间将增长超过100倍。此外,似乎有必要减少 toupper() 的调用次数,因为它的执行次数增加了99倍,而其他函数的执行次数增长的比例小于10。

接下来,我们来看看这些函数的代码。(它在 parser_v3/variablelist.cpp 文件中。)我们看到一个最初用 C 语言编写的源代码的典型示例:该代码使用 char* 作为字符串数据类型,并将变量及其值存储为结构体数组。使用了 STL 容器,但只是为了替代经典的 C 语言数组。函数 get_id() 通过遍历完整的变量表来查找某个变量的条目位置。因此,搜索步骤的数量与表中的变量数量成正比,但如果替换为线性搜索,则可能是对数级别。在每次迭代中还有一个对 toupper() 的调用,这进一步降低了算法的速度。

    /*
     * Add a name and value to the variable list
     */
    bool Variablelist::add(const char* name, double value)
    {
        VAR new_var;
        strncpy(new_var.name, name, 30);
        new_var.value = value;

        int id = get_id(name);
        if (id == -1) {
            // variable does not yet exist
            var.push_back(new_var);
        }
        else {
            // variable already exists. overwrite it
            var[id] = new_var;
        }
        return true;
    }

    /*
     * Returns the id of the given name in the variable list. Returns -1 if name
     * is not present in the list. Name is case insensitive
     */
    int Variablelist::get_id(const char* name)
    {
        // first make the name uppercase
        char nameU[NAME_LEN_MAX+1];
        char varU[NAME_LEN_MAX+1];
        toupper(nameU, name);

        for (unsigned int i = 0; i < var.size(); i++) {
            toupper(varU, var[i].name);
            if (strcmp(nameU, varU) == 0) {
                return i;
            }
        }
        return -1;
    }

    /*
     * str is copied to upper and made uppercase
     * upper is the returned string
     * str should be null-terminated
     */
    void toupper(char upper[], const char str[])
    {
        int i = -1;
        do {
            i++;
            upper[i] = std::toupper(str[i]);
        } while (str[i] != '\0');
    }

重写源代码

我们将结构体数组替换为 std::map。更新的代码(在 parser_v4/variablelist.cpp 中)如下所示:

std::map<std::string, double> var;

bool Variablelist::add(const char* name, double value)
{
    var[ toUpper( name ) ] = value ;
    return true;
}

bool Variablelist::set_value(const char* name, const double value)
{
    return add(name, value);
}

std::string toUpper(const char str[])
{
    std::string upper;
    upper.reserve(NAME_LEN_MAX);
    int i = -1;
    do {
        i++;
        upper += std::toupper(str[i]);
    } while (str[i] != '\0');
    return upper;
}

这里 toupper() 已经被重写为用于 std::string,并且函数 add() 只是对 std::map 的简单赋值,它具有对数复杂度。

比较结果

现在我们可以在 parser_v4 版本中重新执行测试,并比较结果。我们打开 CoverageBrowser,加载 parser_v4/unittests.csmes 的最新版本,并将其与之前的版本进行比较。比较的方法与上一节中的方法相同(参见 比较程序的两个版本),只是这次是比较 parser_v3parser_v4

CoverageBrowserExecutions 窗口中,我们看到速度问题得到了解决。测试的执行时间不再以指数级别增长。

同时执行两个软件版本中的测试套件。

函数分析器 窗口证实了这一发现。Coco 以粗体突出显示已修改的函数,用下划线标出新函数,并划掉已删除的函数。现在,通过测量执行时间差(1分32秒),我们可以看到性能提升几乎与之前的执行时间相同。

在函数分析器窗口中比较分析器数据。

Coco v7.2.0©2024 The Qt Company Ltd.
Qt 及其相关标志是芬兰及/或全球其他国家的 The Qt Company Ltd. 的商标。所有其他商标均为其各自所有者的财产。