這個工具是根據 《iOS 覆蓋率檢測原理與增量代碼測試覆蓋率工具實現》的一次實踐(侵刪),本篇文章更注重實現細節,原理部分能夠參考原文。css
最終的效果是經過修改push腳本:html
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的代碼覆蓋率信息: vue
github連接:xuezhulian/Coveragepython
下面從增量和覆蓋率介紹這個工具的實現。ios
增量的結果根據git獲得。c++
git status
獲得當前有幾個commit須要提交。git
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。es6
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匹配修改的類和行,咱們只考慮新添加的,不考慮刪除操做。github
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須要提交,每一個提交修改了哪些文件以及對應的行。拿到了增量的部分。sql
覆蓋率信息經過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;')
複製代碼