使用 gcov/lcov/gcovr 在 Android APK 下获取代码覆盖率


前言

C/C++ 代码覆盖率常使用 gcov/lcov/gcovr 等工具生成,它们用起来非常方便,根据下面的参考文档你也能快速搭建起测试环境:

简单来说,你需要:

  1. 安装 lcov
  2. 在 c/c++ 编译选项中添加 -fprofile-arcs -ftest-coverage,编译可执行文件
  3. 运行可执行文件
  4. 使用 lcov 收集代码覆盖率信息

步骤 2 编译完成后,在 .o 目录下会生成 .gcno 文件;步骤 3 运行可执行文件后,会生成 .gcda 文件;最后配合源码文件,gcov 等工具就能生成报告了。

由于 lcov 和 gcovr 都是基于 gcov 二次开发的工具,因此最重要的是理解 gcov 工具是如何工作的。接下来将详细说明 gcov 的工作流程。


1. gcno 和 gcda 生成的位置

c/c++ 代码编译时生成 gcno ,gcno 生成的位置与 .o 位置保持一致,以 关于代码覆盖lcov的使用 代码为例,在笔者电脑中,gcno 的位置如下图,可以看到一个 .o 就有一个 .gcno 文件

在这里插入图片描述

gcda 文件在执行程序后生成,那么它的生成路径在哪呢?其实这个位置已经在被写到执行程序中,通过 strings 命令,你可以查看二进制文件中字符串内容,以上述例子中的 main 程序为例,你可以看到两行关于 .gcda 的信息:

在这里插入图片描述

啊哈,你看,gcda 的位置居然是写死的。这可不妙,因为我们想要在 Android 下拿到 gcda 文件,可 android 手机上可没有这个固定路径,会导致 gcda 文件无法生成。

幸运的是,我们可以通过设置 GCOV_PREFIX 环境变量来修改 gcda 的生成路径。例如,指定 gcda 生成在 /Users/user/Downloads/gcov_test 目录下,接着运行程序:

 export GCOV_PREFIX=/Users/user/Downloads/gcov_test
 ./main

运行结束后,在 /Users/user/Downloads/gcov_test 就生成了 main.cpp.gcda 和 a.cpp.gcda。但 gcda 仍然保持的完整路径,也就是实际路径为:

  • /Users/user/Downloads/gcov_test/Users/user/Documents/develop/lcov_test/cmake-build-debug/CMakeFiles/main.dir/main.cpp.gcda
  • /Users/user/Downloads/gcov_test/Users/user/Documents/develop/lcov_test/cmake-build-debug/CMakeFiles/main.dir/src/a.cpp.gcda

好家伙,这路径也太长了。幸好,我们可以指定 GCOV_PREFIX_STRIP 来裁剪绝对路径中的级数,例如设置成 export GCOV_PREFIX_STRIP=8 可以得到:

  • /Users/user/Downloads/gcov_test/main.cpp.gcda
  • /Users/user/Downloads/gcov_test/src/a.cpp.gcda

当然,我们还可以将 GCOV_PREFIX_STRIP 设置成例如 100 这样的非常大的数,让所有 gcda 都存放在同一个目录下。

2. 三要素

想要使用 gcvo 获取代码覆盖率信息需要三要素:

  • gcno
  • gcda
  • 源码

其中,gcno 和 gcda 应该一一对应,在同一个目录层级下,例如 a.cpp.gcno 和 a.cpp.gcda 要在同一目录下。源码的位置则在编译时就被写入到了 gcno 中,通过 strings 命令,你可以发现源码的位置:

在这里插入图片描述

很好,这样所有信息都能被串起来了,总结下:

  • gcno 在编译时生成,生成的位置与 .o 文件同级;通过 strings 命令,可以在 gcno 找到源码的文件路径
  • gcda 在执行完程序后生成,生成的位置被注入到了二进制文件中,通过 strings 命令,可以在二进制文件中找到 gcda 生成的路径;通过设置环境变量 GCOV_PREFIXGCOV_PREFIX_STRIP 我们可以较为方便的修改 gcda 生成的位置
  • gcno 和 gcda 需要一一匹配,gcov 命令通过这两个文件的信息,计算得到代码覆盖率,生成 reports

3. Android 下获取覆盖率

有了上面的铺垫,你已经知道了 gcov 的工作原理,想要在 android apk 下获取 gcov 的代码覆盖率,流程上大体相同:

  1. native c/c++ 编译中添加 coverage 相关的编译选项
  2. 本地编译 apk 或者库文件,在中间产物的目录中,得到 c/c++ 的 gcno 文件
  3. 指定 GCOV_PREFIXGCOV_PREFIX_STRIP 指定 gcda 在手机上生成的位置
  4. 从手机上将 gcda 文件 pull 下来到本机
  5. 使用 gcov/lcov/gcovr 等工具生成覆盖率报告

具体的 demo 你可以在 AndroidNativeCodeCoverageExample 找到。在 Android apk 上拿到代码覆盖率的具体步骤如下:

  1. app/CMakeLists.txtset(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") 设置编译选项

  2. 在 Android Studio 编译 apk 后,可以在编译目录找到 *.gcno 文件
    在这里插入图片描述
    通过 strings 命令查看源码位置,发现是一个相对路径,请注意这个相对路径,如果你执行 gcov 命令却提示找不到源码时,请确保 gcno/gcda 文件与源码的是配对的。
    在这里插入图片描述
    此外,通过 strings 命令查看生成的 .o 文件,可以查询到 *.gcda 生成的目录:
    在这里插入图片描述
    在这里插入图片描述

  3. 在 android 代码中指定 GCOV_PREFIXGCOV_PREFIX_STRIP,确保 GCOV_PREFIX 是有权限写入的。在示例中 GCOV_PREFIX 被指定为应用缓存目录;GCOV_PREFIX_STRIP=100 裁剪掉掉前 100 个目录,我们让所有 gcda 文件都在同一级目录下,方便处理。
    在这里插入图片描述

  4. 在 apk 的退出时,调用需要调用 __gcov_flush 让 gcov 生成 gcda 文件。这篇文章 解释了为什么要这么做,我这里采用了一种更简单的方式,重载 onDestroy 在退出时就去调用 __gcov_flush
    在这里插入图片描述
    如果一切顺利,你可以在手机上找到这些 *.gcda 文件:
    在这里插入图片描述

  5. 将 *.gcda 文件全部 pull 下来,存放在一个特殊位置的文件夹,在这个文件夹中,使得 gcno 纪录的源码相对位置是匹配的。在我的电脑上,这个路径为 /Users/user/Documents/develop/NativeCodeCoverageTest/app/.cxx/Debug/3g1m3d1m/gcov,在这里存放 gcno 文件,可以使得 ../../../../src/main/cpp/a.cpp 刚好是匹配的。同时,将编译生成的 gcno 也 copy 到该目录下:
    在这里插入图片描述

  6. 使用 gcov/lcov/gcorv 等工具生成 report。这里以 gcovr 为例:

gcovr -v -r /Users/user/Documents/develop/NativeCodeCoverageTest/app/src .

在这里插入图片描述
如果 gcovr 无法正确生成报告,可以加上 -v 选项看看 debug 信息;在或者先使用 gcov --dump 命令试试,毕竟 gcovr 是调用 gcov 的。


4. 总结

本文介绍了 gcov 生成代码覆盖率的基本流程和原理,重点说明了 gcno、gcda 和源码文件之间的关系;通过 strings 命令可以查询 gcno 中指定的源码位置,以及 gcda 的生成位置;通过对 GCOV_PREFIXGCOV_PREFIX_STRIP的设置,可以指定 gcda 生成的位置。最后,通过 AndroidNativeCodeCoverageExample 具体示例,详细说明了如何运行 android apk 并获取 c/c++ 的代码覆盖率。


5. 参考

猜你喜欢

转载自blog.csdn.net/weiwei9363/article/details/126002907