代码覆盖率(LLVM)

1 简介

  代码覆盖率是软件测试中用来评估测试用例效果的一个重要指标,表示被测试代码中实际执行的部分与总代码的比率。它能帮助开发者了解哪些代码得到了测试,哪些部分未被覆盖,从而改进测试用例的设计和执行。通过代码覆盖率的分析,开发者可以发现潜在的缺陷和未被测试的代码,从而提高软件质量和测试效率。

  覆盖率统计的口径不同,最后统计的覆盖率的数据也不同,一般分为:

  • 语句覆盖: 计算执行的语句数与总代码行数的比率。
  • 分支覆盖: 计算执行的分支数量(如条件判断中的真和假)与总分支数量的比率。
  • 路径覆盖: 计算被测试的执行路径与所有可能路径的比率,通常较为复杂。
  • 条件组合覆盖: 计算执行的条件组合与所有可能条件组合的比率。
  • 函数覆盖: 计算执行的函数数量与总函数数量的比率。

  代码覆盖率测试虽然能够帮助开发者发现问题,但也并不意味着代码覆盖率没问题就意味着测试用例没问题。覆盖率高并不代表测试用例完全覆盖了所有可能的情况,还需要结合其他测试方法和技术来保证测试的全面性和有效性。比如:

  • 边界条件覆盖:测试输入的边界值,如最大值、最小值、0、空值等;
  • 异常处理路径:验证异常情况的处理;
  • 参数组合测试:测试不同参数组合的情况;
  • 断言有效性:确保测试用例中的断言条件得到满足。

  同时,不同场景下的代码覆盖率要求也不同。比如:

  • 单元测试:关注代码的基本执行路径,通常使用分支覆盖(Branch Coverage);
  • 集成测试:关注模块间的交互路径,使用路径覆盖(Path Coverage);
  • 系统测试:关注整个系统的功能,使用行覆盖(Line Coverage)。

2 主流工具

  市面上不论开源还是商业软件,都有很多代码覆盖率工具,下面主要介绍几种主流的代码覆盖率工具。

  • GCOV:gcov 是 Linux 下 GCC 自带的一个 C/C++ 代码覆盖率分析工具,它可以在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
  • LLVM Coverage:LLVM 是一个开源的编译器基础设施项目,它提供了一套用于构建编译器和工具链的框架。LLVM 提供了一个名为 LLVM Coverage 的功能,用于在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
  • BullseyeCoverage:BullseyeCoverage 是一个商业的代码覆盖率分析工具,它可以在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
  • OpenCppCoverage:OpenCppCoverage 是一个开源的 C++ 代码覆盖率分析工具,它可以在编译时插入计数器,在程序运行时记录代码的执行情况,并生成代码覆盖率报告;
工具名称 支持语言 插桩方式 输出格式 跨平台 测试粒度 性能损耗 调试支持
GCOV C/C++ 源码插桩 文本/HTML Linux 行/分支覆盖 一般
LLVM Coverage C/C++/Rust等 源码插桩 HTML/XML 跨平台 边缘覆盖 完善
BullseyeCoverage C/C++/C# 二进制插桩 自定义 跨平台 MC/DC覆盖 完善
OpenCppCoverage C++ 二进制插桩 HTML Windows 行覆盖 基础

字段说明

  • 测试粒度:行覆盖(基础路径)、分支覆盖(条件判断)、边缘覆盖(LLVM)、MC/DC(条件组合覆盖)
  • 性能损耗:低(<5%)、中(5-20%)、高(>20%)
  • 调试支持:基础(崩溃报告)、一般(堆栈跟踪)、完善(可视化调试)

3 代码覆盖率原理

3.1 插桩

  代码覆盖率的测量通常通过插桩技术实现,插桩技术主要分为两种类型:

  • 源码插桩:在源代码中插入额外的代码(称为插桩代码),以记录每行或每个基本块的执行情况。这种方法保持了源代码的可读性,生成的报告能够与源代码直接对应。典型的工具比如gcov;
  • 二进制插桩:在编译后的二进制代码中插入额外的代码,以记录每行或每个基本块的执行情况。这种方法通常需要对二进制代码进行修改,可能会对性能产生影响。典型的工具比如LLVM Coverage。

  需要注意的是既然是代码插装也就意味着一定对性能有影响,因此覆盖率只能用于功能测试。

  不同的工具或者编译器支持的覆盖率测试方案虽然不同,但是其基础原理基本上一致。

  比如GCC的gcov在编译时添加 -fprofile-arcs 和 -ftest-coverage 标志。这会在生成的可执行文件中插入代码,以便在运行时记录每个基本块的执行次数。程序执行后gcov 会生成 .gcda 文件,这些文件包含了每个基本块的执行次数。这些数据是后续生成覆盖率报告的基础。

  而LLVM 提供了 SanitizerCoverage 选项,用于插桩代码以收集覆盖率信息。通过编译时选项启用后,LLVM 会在生成的代码中插入额外的监控逻辑。SanitizerCoverage 使用 8 位位图来记录控制流图中的边缘覆盖情况。这种方法高效地存储了每条边是否被执行的信息,便于后续分析。覆盖率数据通常以内存映射文件的形式输出,方便在程序运行后进行读取和处理。这样可以有效管理数据并提高性能。

3.2 LLVM实现

  代码是否执行到以及执行的次数需要在运行中进行统计,为了能够统计到相关的数据,LLVM在IR中插入计数代码来统计该数据。对于编译器来说,一段程序是否执行到只需要关注入口和出口即可,如果入口执行了出口也执行了那么中间的代码一定也执行了。因此编译器统计覆盖率的基础待单元是每一个基本块(BB,Basic Block),基本块是指一段连续的代码,具有以下特点:

  • 只有一个入口点(第一个指令)。
  • 没有跳转到块内部的指令。
  • 可能有多个出口点(跳转到其他基本块的指令)。

  LLVM基本块生成的步骤为:

  1. 词法分析:将源代码分解为标记(tokens),识别语法结构。
  2. 语法分析:根据语法规则构建抽象语法树(AST),表示程序的结构。
  3. 控制流图(CFG)构建:在生成基本块之前,首先构建控制流图。CFG 显示程序的控制流路径,包括基本块;
  4. 基本块划分:遍历 AST,根据控制流结构和语句的跳转关系来划分基本块。主要步骤包括:
    • 标识入口: 每个基本块的入口是第一个指令。
    • 确定出口: 识别条件语句、循环和跳转指令,确定基本块的出口。
    • 合并块: 对于连续的无条件跳转的基本块,可以合并,以减少冗余。
  5. 生成 LLVM IR:对于每个基本块,生成相应的 LLVM IR 指令,表示该块中的计算和控制流。

  其伪代码如下:

function generateBasicBlocks(ast):
    blocks = []
    currentBlock = createNewBlock()

    for statement in ast:
        if isJump(statement):
            currentBlock.add(statement)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值