使用XcodeCoverage统计增量代码单元测试覆盖率

XcodeCoverage 是一个基于lcov的统计工具,用于计算Xcode项目的单元测试覆盖率,且能生成html格式的统计报表。
现在需要统计在一个版本周期中增量代码的覆盖率,而XcodeCoverage只能统计全量的覆盖率,因此需要借助XcodeCoverage生成的数据,手动计算版本周期中修改过的文件的覆盖率。问题可以分解为三个子问题:

  1. 获取一个版本周期内存在修改的代码文件列表
  2. 获取Xcode单元测试生成的每个代码文件的覆盖率数据
  3. 筛选并计算

获取一个版本周期内存在修改的代码文件列表

在版本库中,只要确定当某个本周期的起始结束commit,就可以利用git diff命令筛选出我们想要的文件列表。
结束commit容易确定,如果统计当前正在开发的版本,那么结束commit对应的就是版本库的HEAD
而起始commit的确定依赖于手动标记,本项目会对每个发布版本打一个tag,所以最新的一个tag对应的commit即为上个版本的发布commit,亦即当前版本的起始commit。

# get_modified_file_list.sh

#!/bin/bash

tag=`git --no-pager tag | sort -V | tail -1` #1
beginCommit=`git --no-pager show $tag --pretty=raw | head -1 | awk '{print $2}'` #2
endCommit=`git --no-pager log --max-count=1 --no-decorate | head -1 | awk '{print $2}'` #3

# echo Calculation will start from $beginCommit to $endCommit, since $tag

git --no-pager diff $beginCommit $endCommit --name-status \ #4
| awk '$2 ~ /\.m$/ {print $2}' \ #5
| awk -F '/' '{print $NF}' #6
  1. 获取最新的tag,这里需注意,tag名是符合SemVer规则描述的版本号,因此可以使用sort命令排序
  2. 根据上个版本的tag获取起始commit
  3. 获取结束commit
  4. 打印起始/结束commit之间存在修改的文件列表
  5. 提取文件路径
  6. 提取文件名

获取Xcode单元测试生成的每个代码文件的覆盖率数据

通过分析XcodeCoverage的脚本可以知道,执行Xcode单元测试之后生成的覆盖率数据文件在

~/Library/Developer/Xcode/DerivedData/Demo-axxzxbxzjokinpghkkhgihkbcrgo/Build/ProfileData/F76FA0C5-258D-4233-BE5A-C672666F0D1C/Coverage.profdata

生成的二进制包在

~/Library/Developer/Xcode/DerivedData/Demo-axxzxbxzjokinpghkkhgihkbcrgo/Build/Products/Debug-iphonesimulator/Demo.app/Demo

其中~/Library/Developer/Xcode/DerivedData/Demo-axxzxbxzjokinpghkkhgihkbcrgo/Build/这个路径,在脚本的执行过程中存储在环境变量BUILD_ROOT中,而F76FA0C5-258D-4233-BE5A-C672666F0D1C代表测试设备的UUID,存储在环境变量TARGET_DEVICE_IDENTIFIER中。因此只需要仿照XcodeCoverage导入环境变量的方式,自己实现一个exportsnv.sh,在单元测试运行时将我们需要的路径注入到env.sh,待计算覆盖率时使用source命令导入即可。

Xcode执行单元测试时提取环境变量

exportsnv.sh加到Project相应的Scheme的Run Scripts中,Xcode执行单元测试时即可将所需的环境变量导入到env.sh

# exportsnv.sh

#!/bin/bash

scripts="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
export | egrep '(TARGET_DEVICE_IDENTIFIER)|(BUILD_ROOT)|(TARGET_NAME)' > "${scripts}/env.sh"
# env.sh

declare -x BUILD_ROOT="~/Library/Developer/Xcode/DerivedData/Demo-bxynohvelscfkufcyzjsxmqxonmn/Build/Products"
declare -x TARGET_DEVICE_IDENTIFIER="6C2F1A5C-31E0-4495-9802-B870196E0399"
declare -x TARGET_NAME="Demo"

提取覆盖率数据

覆盖率数据通过xcrun llvm-cov report命令导出。

xcrun llvm-cov report -instr-profile \
    ~/Library/Developer/Xcode/DerivedData/Demo-axxzxbxzjokinpghkkhgihkbcrgo/Build/ProfileData/F76FA0C5-258D-4233-BE5A-C672666F0D1C/Coverage.profdata \
    ~/Library/Developer/Xcode/DerivedData/Demo-axxzxbxzjokinpghkkhgihkbcrgo/Build/Products/Debug-iphonesimulator/Demo.app/Demo \
    > file_level_coverage.txt

筛选并计算

# analize_coverage.sh

#!/bin/bash

ScriptsPath="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

XcodeCoveragePath="${ScriptsPath}/../Pods/XcodeCoverage"
source "${XcodeCoveragePath}/envcov.sh" #1

source "./env.sh" #2

CoverageDataName="Coverage.profdata"
CoverageDataPath="${BUILD_ROOT}/../ProfileData/${TARGET_DEVICE_IDENTIFIER}/${CoverageDataName}"
BinPackagePath="${BUILT_PRODUCTS_DIR}/${TARGET_NAME}.app/${TARGET_NAME}"

# test
xcodebuild test \
    -workspace ../Demo.xcworkspace \
    -scheme ${TARGET_NAME} \
    -destination 'platform=iOS Simulator,name=iPad Pro (12.9-inch) (2nd generation)' \
    -only-testing:DemoUnitTests \
    -enableCodeCoverage YES #3

# get modified files during current app version from repo
echo Fetching modified files...
fileList="$(./get_modified_file_list.sh | tr '\n' '|')"
fileList=${fileList%?} #4

TotalLines=11
MissLines=12

# convert coverage data to humanity-readable format
echo Calculating...
CoverageDataName="file_level_coverage.txt"
xcrun llvm-cov report -instr-profile ${CoverageDataPath} ${BinPackagePath} \ #5
| awk -v total=$TotalLines -v miss=$MissLines 'NR>=3 && $1 ~ /'"$fileList"'/ {print $1,$total,$miss}' \ #6
| awk -f cal_coverage.awk #7

echo Done.
# cal_coverage.awk

#!/bin/awk -f

BEGIN {
    totalsum = 0
    misssum = 0
}
{
    totalsum += $2
    misssum += $3
}
END {
    printf "Coverage rate: %.2f%%\n", (totalsum - misssum) / totalsum * 100
}
  1. 导入XcodeCoverage生成的环境变量

  2. 导入自己生成的环境变量

  3. -enableCodeCoverage设置为YES,才能生成Coverage.profdata文件

  4. 导入文件名列表,并修改成awk命令中正则表达式的格式

  5. 导入所有文件的单元测试覆盖率

  6. 筛选出$fileList中相应文件的覆盖率数据

  7. 计算增量代码覆盖率

    ➜  scripts git:(develop) ✗ ./analize_coverage.sh
    Feching modified files...
    Calculating...
    Coverage rate: 10.42%
    Done.
    

Tips

  • --no-pager: Do not pipe Git output into a pager
  • $( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )用于输出当前执行的脚本所在目录

参考

  • XcodeCoverage
  • lcov
  • gcov lcov 覆盖c/c++项目入门
  • Git 基础 - 打标签
  • llvm-cov
  • AWK 简明教程
  • Linux awk 命令
  • awk 系列:如何使用 awk 和正则表达式过滤文本或文件中的字符串

你可能感兴趣的:(使用XcodeCoverage统计增量代码单元测试覆盖率)