關(guān)于iOS測試的Code Coverage大致可分為兩類
- 基于Case的,Xcode 7及以后的版本已原生支持权烧,寫好Case,開啟“Gather coverage data”即可刊驴,操作方便簡單
- 基于非Case的捐寥,主要通過gcov,lcov實現(xiàn)撩嚼,相比于前者停士,操作起來就復(fù)雜一些
本文主要針對于第二種挖帘,基于非Case的Code Coverage自動化流程搭建
前言
iOS代碼打包過程中可以生成兩類文件
- gcda:包含代碼執(zhí)行情況,以及覆蓋率的信息歸納
- gcno:包含基本的塊信息恋技,以及代碼行與塊的映射關(guān)系
gcno是編譯過程中產(chǎn)生拇舀,gcda是通過gcov工具生成
通過工具lcov可以將這兩類文件生成coverage.info文件,再利用genhtml可以將coverage.info文件生成可視化的網(wǎng)頁
原理很簡單蜻底,復(fù)雜的是整個過程的自動化
收集gcno文件
修改工程配置
為了能收集到gcno文件我們需要開啟兩個編譯設(shè)置
- GCC_GENERATE_TEST_COVERAGE_FILES=YES
- GCC_INSTRUMENT_PROGRAM_FLOW_ARCS=YES
當然為了不影響到正常的Debug版本和Release版本骄崩,我們把整個Code Coverage過程搭在了AdHoc版本上,結(jié)合已有的版本發(fā)布系統(tǒng)可以保證流程的流暢運行
因為項目是通過CocoaPods的形式做了模塊化的拆分薄辅,因此需要在每一個項目中都要開啟這兩個配置
因此所有的設(shè)置都放在了Podfile文件中
# 需要收集Code Coverage的模塊
ntargets = Array['M0', '', 'M1', 'M2', 'M3']
require 'xcodeproj'
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if(config.name <=> 'AdHoc') == 0
# 設(shè)置預(yù)編譯變量CODECOVERAGE
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) CODECOVERAGE=1'
config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = 'NO'
config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = 'NO'
ntargets.each do |ntarget|
if(ntarget <=> target.name) == 0
config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = 'YES'
config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = 'YES'
break
end
end
else
config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = 'NO'
config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = 'NO'
end
end
end
# 修改主工程
project_path = './MainTarget.xcodeproj'
project = Xcodeproj::Project.open(project_path)
puts project
project.targets.each do |target|
if(target.name <=> 'MainTarget') == 0
target.build_configurations.each do |config|
if(config.name <=> 'AdHoc') == 0
# 設(shè)置預(yù)編譯變量CODECOVERAGE
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) CODECOVERAGE=1'
config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = 'YES'
config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = 'YES'
else
config.build_settings['GCC_GENERATE_TEST_COVERAGE_FILES'] = 'NO'
config.build_settings['GCC_INSTRUMENT_PROGRAM_FLOW_ARCS'] = 'NO'
end
end
end
end
project.save()
end
這里我們做了三件事情
- 修改主工程的GCC_GENERATE_TEST_COVERAGE_FILES和GCC_INSTRUMENT_PROGRAM_FLOW_ARCS
- 修改所有自工程的GCC_GENERATE_TEST_COVERAGE_FILES和GCC_INSTRUMENT_PROGRAM_FLOW_ARCS
- 在所有工程中增加預(yù)編譯變量CODECOVERAGE(方便后續(xù)的處理)
收集gcno文件
Podfile文件增加私有庫
pod 'CodeCoverage', :git => 'http://*.*.*.*/ios-team/CC.git'
這個庫里面只有一個文件“cc.sh”要拂,這個shell腳本會在工程編譯結(jié)束之后執(zhí)行,我們將它加入到主工程Build Phases的最后一步
cc.sh的文件內(nèi)容
archs=('arm64' 'armv7')
objroot=${OBJROOT}
project=${PROJECT_NAME}
configuration=${CONFIGURATION}
srcroot=${SRCROOT}
ccpath=$srcroot/Pods/CodeCoverage/gcno
iphoneos=$objroot/$project.build/$configuration-iphoneos
podsiphoneos=$objroot/Pods.build/$configuration-iphoneos
# app
apppath=$iphoneos/$project.build/Objects-normal
# 創(chuàng)建CodeCoverage文件
if [ -d $ccpath ]; then
rm -rf $ccpath
fi
mkdir -p $ccpath
podsbuilds=$(ls $podsiphoneos)
for arch in ${archs[@]};
do
mkdir $ccpath/$arch
if [ -d $apppath/$arch ];then
find $apppath/$arch -name "*.*" | grep .gcno
if [ $? -eq 0 ];then
cp $apppath/$arch/*.gcno $ccpath/$arch
fi
fi
for podsbuild in ${podsbuilds[@]};
do
podspath=$podsiphoneos/$podsbuild/Objects-normal
if [ -d $podspath/$arch ];then
find $podspath/$arch -name "*.*" | grep .gcno
if [ $? -eq 0 ];then
cp $podspath/$arch/*.gcno $ccpath/$arch
fi
fi
done
done
整個腳本的編寫略顯啰嗦站楚,其實是為了避免在這個過程中出行報錯中斷影響整個工程的編譯
這里大致可以分為以下幾個功能
- 找到Pods下面剛剛創(chuàng)建過得私有庫CodeCoverage脱惰,如果已經(jīng)存在gcno文件夾就把他清除掉
- 找到主工程編譯過程中產(chǎn)生的gcno文件并拷貝到CodeCoverage/gcno/$arch下
- 找到所有自工程編譯過程中產(chǎn)生的gcno文件并拷貝到CodeCoverage/gcno/$arch下
到這里編輯過程中產(chǎn)生的gcno文件已經(jīng)收集完成
收集gcda文件
之前的工程文件編譯設(shè)置在上一步已經(jīng)完成,這里不再贅述
在AppDelegate中添加
- (void)applicationDidEnterBackground:(UIApplication *)application {
#if !TARGET_IPHONE_SIMULATOR
#ifdef CODECOVERAGE
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *ccpath = [documentsDirectory stringByAppendingPathComponent:@"CodeCoverage"];
setenv("GCOV_PREFIX", [ccpath cStringUsingEncoding:NSUTF8StringEncoding], 1);
setenv("GCOV_PREFIX_STRIP", "13", 1);
extern void __gcov_flush(void);
__gcov_flush();
#endif
#endif
}
gcov 是 gcc附帶的代碼覆蓋率測試工具窿春,伴隨gcc發(fā)布拉一,配合gcc共同實現(xiàn)對代碼語句的覆蓋率測試,gcov可以統(tǒng)計到每一行代碼的執(zhí)行頻率
每次應(yīng)用退到后臺都會生成當前測試的代碼覆蓋率文件gcda旧乞,并以增量的方式追加到我們剛剛設(shè)置的Documents/CodeCoverage下
數(shù)據(jù)合并
gcda和gcno文件都已經(jīng)收集完成蔚润,接下來我們需要把他匯總到一起,并完成報告的生成良蛮,我們可以通過下面的簡圖了解一下它們的分布情況
- 打包流程打出的包會以日期和時間的格式命名存放在服務(wù)器
- 服務(wù)器會開啟一個socket服務(wù)用于收集來自客戶端測試報告文件gcda
- 文件以:版本/包名/設(shè)備標識/arm64(armv7)路徑存儲到服務(wù)器
- 客戶端每次上傳都會清理上次生成的報告抽碌,用最新的報告替換
- 每當收到客戶端發(fā)來的測試報告,就把對應(yīng)的包編譯過程中生成的gcno文件找到跟上傳過來的gcda文件合并生成當前客戶端的測試報告文件converage.info
- 合并當前版本所有生成的coverage[*].info文件生成總的converage.info文件
- 把converage文件導(dǎo)出為html可視化報告决瞳,通過nginx對外輸出
整個過程中需要用到LCOV
- 生成coverage.info ./lcov --capture --directory [gcno和gcda匯總的文件夾] --output-file [coverage[*].info]
- 合并coverage.info lcov -a [coverage0.info] -a [coverage1.info] -o [coverage.info]
- 生成html ./genhtml [coverage.info] --output-directory [html]
socket服務(wù)端和客戶端上傳接受文件的代碼就不貼了货徙,有點多,有多種實現(xiàn)方案皮胡,可以網(wǎng)上找找痴颊,大同小異
下面是合并gcno和gcda文件生成報告的腳本
path='/Users/***/www/gcda/Project'
gcno='/Users/***/www/gcno'
lcov='/Users/***/www/lcov'
html='/Users/***/www/html/Project'
# 獲取Project下所有的版本
versions=($(ls -t $path))
# 最新版本
version=${versions[0]}
# 獲取最新版本的所有可用包
pkgs=($(ls $path/$version))
for pkg in ${pkgs[@]}
do
# 檢測是否有打包過程中的中間件
if [ ! -d $path/$version/$pkg/.arm64 ];then
cp -rf $gcno/$pkg/armv7 $path/$version/$pkg/.armv7
cp -rf $gcno/$pkg/arm64 $path/$version/$pkg/.arm64
fi
# 獲取pkg下所有的測試機
devices=($(ls $path/$version/$pkg))
for device in ${devices[@]}
do
if [ ! -f $path/$version/$pkg/$device/coverage.info ];then
if [ -d $path/$version/$pkg/$device/arm64 ];then
cp $path/$version/$pkg/.arm64/*.gcno $path/$version/$pkg/$device/arm64
$lcov/lcov --capture --directory $path/$version/$pkg/$device/arm64 --output-file $path/$version/$pkg/$device/coverage.info
coverages=("${coverages[@]}" "$path/$version/$pkg/$device/coverage.info")
fi
if [ -d $path/$version/$pkg/$device/armv7 ];then
cp $path/$version/$pkg/.armv7/*.gcno $path/$version/$pkg/$device/armv7
$lcov/lcov --capture --directory $path/$version/$pkg/$device/armv7 --output-file $path/$version/$pkg/$device/coverage.info
coverages=("${coverages[@]}" "$path/$version/$pkg/$device/coverage.info")
fi
else
coverages=("${coverages[@]}" "$path/$version/$pkg/$device/coverage.info")
fi
done
done
cmd="$lcov/lcov"
for coverage in ${coverages[@]}
do
cmd="$cmd -a $coverage"
done
cmd="$cmd -o $path/$version/.coverage.info"
eval $cmd
# 生成html
if [ -d $html/$version ];then
rm -rf $html/$version
fi
$lcov/genhtml $path/$version/.coverage.info --output-directory $html/$version