iOS 基于非Case的Code Coverage系統(tǒng)搭建

關(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

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)收集完成蔚润,接下來我們需要把他匯總到一起,并完成報告的生成良蛮,我們可以通過下面的簡圖了解一下它們的分布情況

image
  • 打包流程打出的包會以日期和時間的格式命名存放在服務(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

測試覆蓋率報告

測試覆蓋率報告
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市屡贺,隨后出現(xiàn)的幾起案子蠢棱,更是在濱河造成了極大的恐慌,老刑警劉巖甩栈,帶你破解...
    沈念sama閱讀 211,376評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泻仙,死亡現(xiàn)場離奇詭異,居然都是意外死亡量没,警方通過查閱死者的電腦和手機玉转,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,126評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來殴蹄,“玉大人究抓,你說我怎么就攤上這事猾担。” “怎么了刺下?”我有些...
    開封第一講書人閱讀 156,966評論 0 347
  • 文/不壞的土叔 我叫張陵绑嘹,是天一觀的道長。 經(jīng)常有香客問我橘茉,道長工腋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,432評論 1 283
  • 正文 為了忘掉前任捺癞,我火速辦了婚禮夷蚊,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘髓介。我一直安慰自己惕鼓,他們只是感情好,可當我...
    茶點故事閱讀 65,519評論 6 385
  • 文/花漫 我一把揭開白布唐础。 她就那樣靜靜地躺著箱歧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪一膨。 梳的紋絲不亂的頭發(fā)上呀邢,一...
    開封第一講書人閱讀 49,792評論 1 290
  • 那天,我揣著相機與錄音豹绪,去河邊找鬼价淌。 笑死,一個胖子當著我的面吹牛瞒津,可吹牛的內(nèi)容都是我干的蝉衣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,933評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼巷蚪,長吁一口氣:“原來是場噩夢啊……” “哼病毡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起屁柏,我...
    開封第一講書人閱讀 37,701評論 0 266
  • 序言:老撾萬榮一對情侶失蹤啦膜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后淌喻,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體僧家,經(jīng)...
    沈念sama閱讀 44,143評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,488評論 2 327
  • 正文 我和宋清朗相戀三年裸删,在試婚紗的時候發(fā)現(xiàn)自己被綠了啸臀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,626評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖乘粒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伤塌,我是刑警寧澤灯萍,帶...
    沈念sama閱讀 34,292評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站每聪,受9級特大地震影響旦棉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜药薯,卻給世界環(huán)境...
    茶點故事閱讀 39,896評論 3 313
  • 文/蒙蒙 一绑洛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧童本,春花似錦真屯、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泵额,卻和暖如春配深,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背嫁盲。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工篓叶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人羞秤。 一個月前我還...
    沈念sama閱讀 46,324評論 2 360
  • 正文 我出身青樓缸托,卻偏偏與公主長得像,于是被迫代替她去往敵國和親锥腻。 傳聞我的和親對象是個殘疾皇子嗦董,可洞房花燭夜當晚...
    茶點故事閱讀 43,494評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,761評論 25 707
  • 一、xcode使用模擬器瘦黑,gcov京革,lcov進行測試覆蓋率收集 效果: ?ios會為項目中每個類生成2個文件 gc...
    靜jingjing閱讀 4,806評論 4 11
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)幸斥,斷路器匹摇,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • 泰戈爾說:世界最遙遠的距離,是我站在你面前甲葬,你卻不知道我愛你廊勃。 媽媽對兒子說:世界上最遙遠的距離,是我站在你面前,...
    東風(fēng)東風(fēng)閱讀 255評論 8 10
  • 如果沒有光 只是一個土疙瘩 如果沒有你 夜就不閃爍 星星 只是說明你的從在
    瘋子白閱讀 226評論 0 0