這個工具是根據 《iOS 覆蓋率檢測原理與增量代碼測試覆蓋率工具實現》的一次實踐(侵刪),本篇文章更注重實現細節,原理部分能夠參考原文。html
最終的效果是經過修改push腳本:python
echo '----------------' rate=$(cd $(dirname $PWD)/RCodeCoverage/ && python coverage.py $proejctName | grep "RCoverageRate:" | sed 's/RCoverageRate:\([0-9-]*\).*/\1/g') if [ $rate -eq -1 ]; then echo '沒有覆蓋率信息,跳過...' elif [ $(echo "$rate < 80.0" | bc) = 1 ];then echo '代碼覆蓋率爲'$rate',不知足需求' echo '----------------' exit 1 else echo '代碼覆蓋率爲'$rate',即將上傳代碼' fi echo '----------------' 複製代碼
在每一個commit-msg後面附帶着本次commit的代碼覆蓋率信息: ios
github連接:xuezhulian/Coveragegit
下面從增量和覆蓋率介紹這個工具的實現。github
增量的結果根據git獲得。xcode
git status
獲得當前有幾個commit須要提交。bash
aheadCommitRe = re.compile('Your branch is ahead of \'.*\' by ([0-9]*) commit') aheadCommitNum = None for line in os.popen('git status').xreadlines(): result = aheadCommitRe.findall(line) if result: aheadCommitNum = result[0] break 複製代碼
若是當前存在未提交的commit git rev-parse
能夠拿到commit的commit-id,git log
能夠獲得commit的diff。markdown
if aheadCommitNum: for i in range(0,int(aheadCommitNum)): commitid = os.popen('git rev-parse HEAD~%s'%i).read().strip() pushdiff.commitdiffs.append(CommitDiff(commitid)) stashName = 'git-diff-stash' os.system('git stash save \'%s\'; git log -%s -v -U0> "%s/diff"'%(stashName,aheadCommitNum,SCRIPT_DIR)) if string.find(os.popen('git stash list').readline(),stashName) != -1: os.system('git stash pop') else: #prevent change last commit msg without new commit print 'No new commit' exit(1) 複製代碼
根據diff匹配修改的類和行,咱們只考慮新添加的,不考慮刪除操做。數據結構
commitidRe = re.compile('commit (\w{40})') classRe = re.compile('\+\+\+ b(.*)') changedLineRe = re.compile('\+(\d+),*(\d*) \@\@') commitdiff = None classdiff = None for line in diffFile.xreadlines(): #match commit id commmidResult = commitidRe.findall(line) if commmidResult: commitid = commmidResult[0].strip() if pushdiff.contains_commitdiff(commitid): commitdiff = pushdiff.commitdiff(commitid) else: #TODO filter merge commitdiff = None if not commitdiff: continue #match class name classResult = classRe.findall(line) if classResult: classname = classResult[0].strip().split('/')[-1] classdiff = commitdiff.classdiff(classname) if not classdiff: continue #match lines lineResult = changedLineRe.findall(line) if lineResult: (startIndex,lines) = lineResult[0] # add nothing if cmp(lines,'0') == 0: pass #add startIndex line elif cmp(lines,'') == 0: classdiff.changedlines.add(int(startIndex)) #add lines from startindex else: for num in range(0,int(lines)): classdiff.changedlines.add(int(startIndex) + num) 複製代碼
至此獲得了每次push的時候,有幾個commit須要提交,每一個提交修改了哪些文件以及對應的行。拿到了增量的部分。架構
覆蓋率信息經過lcov工具分析gcno,gcda兩種格式的文件獲得。這兩種文件在原文中有詳細的描述,這裏再也不贅述。
首先咱們要作的是肯定gcno和gcda的路徑。 xcode->build phases->run script添加腳本exportenv.sh
導出環境變量。
//exportenv.sh scripts="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export | egrep '( BUILT_PRODUCTS_DIR)|(CURRENT_ARCH)|(OBJECT_FILE_DIR_normal)|(SRCROOT)|(OBJROOT)|(TARGET_DEVICE_IDENTIFIER)|(TARGET_DEVICE_MODEL)|(PRODUCT_BUNDLE_IDENTIFIER)' > "${scripts}/env.sh" 複製代碼
GCNO_DIR
的路徑就是OBJECT_FILE_DIR_normal+arch
,咱們只在模擬器收集信息,因此這裏的arch
是x86_64
。目前咱們APP總體架構採用模塊化,每一個模塊對應一個target
,經過cocoapods
管理。每一個target
的normal
路徑是不同的。若是想獲得的是pod
目錄下的gcno
文件,咱們會把本地的pod
倉庫路徑當作參數,而後根據podspec
文件修改normal
的路徑。
def handlepoddir(): global OBJECT_FILE_DIR_normal global SRCROOT #default main repo if len(sys.argv) != 2: return #filter coverage dir if sys.argv[1] == SCRIPT_DIR.split('/')[-1]: return repodir = sys.argv[1] SRCROOT = SCRIPT_DIR.replace(SCRIPT_DIR.split('/')[-1],repodir.strip()) os.environ['SRCROOT'] = SRCROOT podspec = None for podspecPath in os.popen('find %s -name \"*.podspec\" -maxdepth 1' %SRCROOT).xreadlines(): podspec = podspecPath.strip() break if podspec and os.path.exists(podspec): podspecFile = open(podspec,'r') snameRe = re.compile('s.name\s*=\s*[\"|\']([\w-]*)[\"|\']') for line in podspecFile.xreadlines(): snameResult = snameRe.findall(line) if snameResult: break sname = snameResult[0].strip() OBJECT_FILE_DIR_normal = OBJROOT + '/Pods.build/%s/%s.build/Objects-normal'%(BUILT_PRODUCTS_DIR,sname) if not os.path.exists(OBJECT_FILE_DIR_normal): print 'Error:\nOBJECT_FILE_DIR_normal:%s invalid path'%OBJECT_FILE_DIR_normal exit(1) os.environ['OBJECT_FILE_DIR_normal'] = OBJECT_FILE_DIR_normal 複製代碼
gcda
文件存儲在模擬器中。經過TARGET_DEVICE_ID
能夠確認當前模擬器的路徑。這個路徑下每一個APP對應的文件夾下面都存在一個plist文件記錄了APP的bundleid,根據這個bundleid匹配APP。而後拼出gcda文件的路徑。
def gcdadir(): GCDA_DIR = None USER_ROOT = os.environ['HOME'].strip() APPLICATIONS_DIR = '%s/Library/Developer/CoreSimulator/Devices/%s/data/Containers/Data/Application/' %(USER_ROOT,TARGET_DEVICE_ID) if not os.path.exists(APPLICATIONS_DIR): print 'Error:\nAPPLICATIONS_DIR:%s invaild file path'%APPLICATIONS_DIR exit(1) APPLICATION_ID_RE = re.compile('\w{8}-\w{4}-\w{4}-\w{4}-\w{12}') for file in os.listdir(APPLICATIONS_DIR): if not APPLICATION_ID_RE.findall(file): continue plistPath = APPLICATIONS_DIR + file.strip() + '/.com.apple.mobile_container_manager.metadata.plist' if not os.path.exists(plistPath): continue plistFile = open(plistPath,'r') plistContent = plistFile.read() plistFile.close() if string.find(plistContent,PRODUCT_BUNDLE_ID) != -1: GCDA_DIR = APPLICATIONS_DIR + file + '/Documents/gcda_files' break if not GCDA_DIR: print 'GCDA DIR invalid,please check xcode config' exit(1) if not os.path.exists(GCDA_DIR): print 'GCDA_DIR:%s path invalid'%GCDA_DIR exit(1) os.environ['GCDA_DIR'] = GCDA_DIR print("GCDA_DIR :"+GCDA_DIR) 複製代碼
肯定了gcno和gcda目錄路徑以後。結合git分析獲得的修改的文件,把這些文件對應的gcno和gcda文件拷貝到腳本目錄下的source文件夾下。
sourcespath = SCRIPT_DIR + '/sources' if os.path.isdir(sourcespath): shutil.rmtree(sourcespath) os.makedirs(sourcespath) for filename in changedfiles: gcdafile = GCDA_DIR+'/'+filename+'.gcda' if os.path.exists(gcdafile): shutil.copy(gcdafile,sourcespath) else: print 'Error:GCDA file not found for %s' %gcdafile exit(1) gcnofile = GCNO_DIR + '/'+filename + '.gcno' if not os.path.exists(gcnofile): gcnofile = gcnofile.replace(OBJECT_FILE_DIR_normal,OBJECT_FILE_DIR_main) if not os.path.exists(gcnofile): print 'Error:GCNO file not found for %s' %gcnofile exit(1) shutil.copy(gcnofile,sourcespath) 複製代碼
接下來使用了lcov
工具,這個工具可以讓咱們的代碼覆蓋率可視化,方便在覆蓋率不達標的狀況下去查看哪些文件的行沒有執行到。lcov
命令會根據gcno
和gcda
生生一箇中間文件.info
,.info
記錄了文件包含的函數、執行過的函數、包含的行、執行過的行,經過修改.info
來實現增量的結果展現。
這是咱們分析覆蓋率用到的關鍵字段。
<absolute path to the source file>
<line number of function start>,<function name>
<execution count>,<function name>
<number of functions found>
<number of function hit>
<line number>,<execution count>[,<checksum>]
<number of lines with a non-zero execution count>
<number of instrumented lines>
生成.info
過程
os.system(lcov + '-c -b %s -d %s -o \"Coverage.info\"' %(SCRIPT_DIR,sourcespath)) if not os.path.exists(SCRIPT_DIR+'/Coverage.info'): print 'Error:failed to generate Coverage.info' exit(1) if os.path.getsize(SCRIPT_DIR+'/Coverage.info') == 0: print 'Error:Coveragte.info size is 0' os.remove(SCRIPT_DIR+'/Coverage.info') exit(1) 複製代碼
接下來結合拿到的git信息修改.info
實現增量,首先刪除git沒有記錄修改的類。
for line in os.popen(lcov + ' -l Coverage.info').xreadlines(): result = headerFileRe.findall(line) if result and not result[0].strip() in changedClasses: filterClasses.add(result[0].strip()) if len(filterClasses) != 0: os.system(lcov + '--remove Coverage.info *%s* -o Coverage.info' %'* *'.join(filterClasses)) 複製代碼
刪除git沒有記錄修改的行
for line in lines: #match file name if line.startswith('SF:'): infoFilew.write('end_of_record\n') classname = line.strip().split('/')[-1].strip() changedlines = pushdiff.changedLinesForClass(classname) if len(changedlines) == 0: lcovclassinfo = None else: lcovclassinfo = lcovInfo.lcovclassinfo(classname) infoFilew.write(line) if not lcovclassinfo: continue #match lines DAResult = DARe.findall(line) if DAResult: (startIndex,count) = DAResult[0] if not int(startIndex) in changedlines: continue infoFilew.write(line) if int(count) == 0: lcovclassinfo.nohitlines.add(int(startIndex)) else: lcovclassinfo.hitlines.add(int(startIndex)) continue 複製代碼
如今.info
文件只記錄了git修改的類及對應行的覆蓋率信息,同時LcovInfo
這個數據結構保存了相關信息,後面分析每次commit的覆蓋率的時候會用到。經過·genhtml````命令生成可視化覆蓋率信息。這個結果保存在腳本目錄下的coverage路徑下,能夠打開
index.html```查看增量覆蓋率狀況。
if not os.path.getsize('Coverage.info') == 0: os.system(genhtml + 'Coverage.info -o Coverage') os.remove('Coverage.info') 複製代碼
index.html
示例,次級頁面會有更詳細信息:
最後一步經過git rebase
修改commit-msg,獲得開篇的效果。
for i in reversed(range(0,len(pushdiff.commitdiffs))): commitdiff = pushdiff.commitdiffs[i] if not commitdiff: os.system('git rebase --abort') continue coveragerate = commitdiff.coveragerate() lines = os.popen('git log -1 --pretty=%B').readlines() commitMsg = lines[0].strip() commitMsgRe = re.compile('coverage: ([0-9\.-]*)') result = commitMsgRe.findall(commitMsg) if result: if result[0].strip() == '%.2f'%coveragerate: os.system('git rebase --continue') continue commitMsg = commitMsg.replace('coverage: %s'%result[0],'coverage: %.2f'%coveragerate) else: commitMsg = commitMsg + ' coverage: %.2f%%'%coveragerate lines[0] = commitMsg+'\n' stashName = 'commit-amend-stash' os.system('git stash save \'%s\';git commit --amend -m \'%s \' --no-edit;' %(stashName,''.join(lines))) if string.find(os.popen('cd %s;git stash list'%SRCROOT).readline(),stashName) != -1: os.system('git stash pop') os.system('git rebase --continue;') 複製代碼