使用XcodeCoverage统计增量代码单元测试覆盖率
XcodeCoverage
是一个基于lcov
的统计工具,用于计算Xcode项目的单元测试覆盖率,且能生成html格式的统计报表。
现在需要统计在一个版本周期中增量代码的覆盖率,而XcodeCoverage只能统计全量的覆盖率,因此需要借助XcodeCoverage生成的数据,手动计算版本周期中修改过的文件的覆盖率。问题可以分解为三个子问题:
- 获取一个版本周期内存在修改的代码文件列表
- 获取Xcode单元测试生成的每个代码文件的覆盖率数据
- 筛选并计算
获取一个版本周期内存在修改的代码文件列表
在版本库中,只要确定当某个本周期的起始和结束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
- 获取最新的tag,这里需注意,tag名是符合
SemVer
规则描述的版本号,因此可以使用sort
命令排序 - 根据上个版本的tag获取起始commit
- 获取结束commit
- 打印起始/结束commit之间存在修改的文件列表
- 提取文件路径
- 提取文件名
获取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
}
-
导入XcodeCoverage生成的环境变量
-
导入自己生成的环境变量
-
-enableCodeCoverage
设置为YES
,才能生成Coverage.profdata
文件 -
导入文件名列表,并修改成awk命令中正则表达式的格式
-
导入所有文件的单元测试覆盖率
-
筛选出
$fileList
中相应文件的覆盖率数据 -
计算增量代码覆盖率
➜ 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 )
用于输出当前执行的脚本所在目录