作者:京东零售 邓立兵
简介
这是一个统计基于 Swift & Objective-C 工程的代码覆盖率的自动化脚本。之所以做成 Pod ,是便于更好的复用,该 Pod 只包含了收集生成代码覆盖率的脚本。整体比较简单方便。
这里只将流程,咱不讲原理。后续另外介绍
这里只将流程,咱不讲原理。后续另外介绍
使用
1、安装:
通过 CocoaPods 进行安装,在你的 Podfile 文件添加如下代码:
pod 'HDCoverage' 复制代码
然后 pod install 安装下载相关脚本文件。
2、关联脚本:
在项目的 Xcode 的 Build Phases 添加新的脚本(New Run Script Phase)(App在Build会执行该脚本):
"${PODS_ROOT}/HDCoverage/HDCoverage/hd_coverage_env.sh" 复制代码
3、工程配置代码覆盖率参数:
这里本来是在 HDCoverage 有脚本支持的,但是基于对哪些模块(Pod作为独立模版)进行代码覆盖率,所以建议在 Podfile 自主添加如下代码灵活管理,详细说明如下:
1# 实现post_install Hooks 2# 需要收集Code Coverage的模块 3ntargets = Array['AFNetworking'] 4 5require 'xcodeproj' 6post_install do |installer| 7 # 修改Pods中某一个模块的配置文件,好采集代码覆盖率,需要源码! 8 installer.pods_project.targets.each do |target| 9 target.build_configurations.each do |config| 10 if(config.name <=> 'Release') == 0 11 config.build_settings['OTHER_CFLAGS'] = '$(inherited)' 12 config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)' 13 config.build_settings['OTHER_LDFLAGS'] = '$(inherited)' 14 ntargets.each do |ntarget| 15 if(ntarget <=> target.name) == 0 16 config.build_settings['OTHER_CFLAGS'] = '$(inherited) -fprofile-instr-generate -fcoverage-mapping' 17 config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -profile-generate -profile-coverage-mapping' 18 config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -fprofile-instr-generate' 19 break 20 end 21 end 22 else 23 config.build_settings['OTHER_CFLAGS'] = '$(inherited)' 24 config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)' 25 config.build_settings['OTHER_LDFLAGS'] = '$(inherited)' 26 end 27 end 28 end 29 30 # 修改主工程 31 project_path = './HDCoverage.xcodeproj' 32 project = Xcodeproj::Project.open(project_path) 33 puts project 34 project.targets.each do |target| 35 if(target.name <=> 'HDCoverageDemo') == 0 36 target.build_configurations.each do |config| 37 if ((config.name <=> 'Release') == 0 || (config.name <=> 'Debug') == 0) 38 # 设置预编译变量CODECOVERAGE 39 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) CODECOVERAGE=1' 40 # OC代码覆盖率插桩配置 41 config.build_settings['OTHER_CFLAGS'] = '$(inherited) -fprofile-instr-generate -fcoverage-mapping' 42 # Swift代码覆盖率插桩配置 43 config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -profile-generate -profile-coverage-mapping' 44 # 采集代码覆盖率配置 45 config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -fprofile-instr-generate' 46 # Release需要设置,不然无法解析代码覆盖率 47 config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone' 48 else 49 config.build_settings['OTHER_CFLAGS'] = '$(inherited)' 50 config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)' 51 config.build_settings['OTHER_LDFLAGS'] = '$(inherited)' 52 config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '' 53 end 54 end 55 end 56 end 57 project.save() 58end
4、代码执行数据收集:
使用 GCC 无法满足 同时兼容 Swift和 Objective-C ,所以这里是基于 LLVM 进行,官网文档 。也可以参考笔者翻译的 Source-based Code Coverage ,完整详细的教程可以看 Source-based Code Coverage for Swift Step by Step。
4.1、首先在工程中申明 LLVM 几个关键的函数:
1#ifndef PROFILE_INSTRPROFILING_H_ 2#define PROFILE_INSTRPROFILING_H_ 3 4// https://clang.llvm.org/docs/SourceBasedCodeCoverage.html 5int __llvm_profile_runtime = 0; 6void __llvm_profile_initialize_file(void); 7const char *__llvm_profile_get_filename(void); 8void __llvm_profile_set_filename(const char *); 9int __llvm_profile_write_file(void); 10int __llvm_profile_register_write_file_atexit(void); 11const char *__llvm_profile_get_path_prefix(void); 12#endif /* PROFILE_INSTRPROFILING_H_ */
4.2、再次封装代码覆盖率相关API,便于上层更好使用(建议):
class HDCoverageTools: NSObject { static var shared = HDCoverageTools()
1// 注意:动态库是需要单独注册,并且需要在动态库中执行\_\_llvm\_profile\_write\_file() 2// 3func registerCoverage(moduleName: String) { 4 let name = "\\(moduleName).profraw" 5 print("registerCoverage, moduleName: \\(moduleName)") 6 let fileManager = FileManager.default 7 do { 8 let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false) 9 let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString 10 print("HDCoverageGather filePath: \\(filePath)") 11 \_\_llvm\_profile\_set\_filename(filePath.utf8String) 12 } catch { 13 print(error) 14 } 15 saveAndUpload() 16} 17 18// 合适的时机代码覆盖率上报 19func saveAndUpload() { 20 \_\_llvm\_profile\_write\_file() 21} 22
4.3、启动时刻,注册代码覆盖率API:
1func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 2 HDCoverageTools.shared.registerCoverage(moduleName: "HDCoverageDemo") 3 return true 4}
4.4、在合适的时刻(按照大家的业务场景)将覆盖率数据写入制定的路径:
1func sceneDidEnterBackground(_ scene: UIScene) { 2 // 笔者这里测试,是在App进入后台后写入 3 DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { [self] in 4 HDCoverageTools.shared.saveAndUpload() 5 } 6 }
5、测试以便生成覆盖率数据:
以本工程的demo为例说明
5.1、在运行成功后,Finder 会自动弹出如下目录:
这里是在项目主工程生成 CoverageResult 目录,并且将生成代码覆盖率可视化的脚本 hd_parse_profraw.sh 拷贝到这里,将 项目 HDCoverageDemo.app 拷贝过来,主要是获取其 Mach-O :
1$ tree -L 2 2. 3├── MachOFiles 4│ └── HDCoverageDemo.app 5├── Profraw 6└── hd_parse_profraw.sh
5.2、执行测试用例,这里我分别点击了:"主工程(OC)-Case1/Case2"、"主工程(Swift)-Case2/Case3"、"Framework(OC)-Case1/Case2"、"FrameworkSwift)-Case2/Case3" 后,App退到后台
5.3、查看控制台,可以看到 profraw 文件:
1registerCoverage, moduleName: HDCoverageDemo 2HDCoverageGather filePath: /Users/denglibing/Library/Developer/CoreSimulator/Devices/5D01D4AA-40AE-4FC6-845C-391A94828EE3/data/Containers/Data/Application/283906A5-1681-44A5-8522-126D29D2F148/Documents/HDCoverageDemo.profraw 3
将 HDCoverageDemo.profraw 拷贝到 CoverageResult/Profraw 目录中;
5.4、执行 hd_parse_profraw.sh 脚本:
1$ tree -L 2 2. 3├── MachOFiles 4│ └── HDCoverageDemo.app 5├── Profraw 6│ └── HDCoverageDemo.profraw 7└── hd_parse_profraw.sh 8 9$ sh hd_parse_profraw.sh 10CoverageResult: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/CoverageResult 11machOFiles: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/MachOFiles 12/Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/CoverageResult 不存在,已经创建 13disposeProfrawFiles profraws: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/Profraw 14disposeProfrawFiles profraw file: HDCoverageDemo.profraw 15=================================== 16 17findMachOFileName: HDCoverageDemo 18findMachOFilePath: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/MachOFiles/HDCoverageDemo.app/HDCoverageDemo 19=================================== 20 21disposeProfrawToHtml, machoFileName: HDCoverageDemo machOFilePath: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/MachOFiles/HDCoverageDemo.app/HDCoverageDemo
执行成功后将自动将可视化的代码覆盖率目录打开:
1$ tree -L 3 2. 3├── CoverageResult 4│ └── HDCoverageDemo 5│ ├── coverage 6│ ├── index.html 7│ └── style.css 8├── MachOFiles 9│ └── HDCoverageDemo.app 10│ ├── Base.lproj 11│ ├── Frameworks 12│ ├── HDCoverageDemo 13│ ├── Info.plist 14│ ├── PkgInfo 15│ └── _CodeSignature 16├── Profraw 17│ ├── HDCoverageDemo.profdata 18│ └── HDCoverageDemo.profraw 19└── hd_parse_profraw.sh 20 219 directories, 8 files 22复制代码
5.5、查看:打开 CoverageResult/HDCoverageDemo/index.html 即可得到本次测试的代码覆盖率情况:
点击某一个 Filename区域 可以查看详情,例如点击 HDOCFramework.m:
可以看出,tag == 3 的代码行数并没有执行到,这正和上面测试的 "Framework(OC)-Case1/Case2" 符合。
小结
全量代码覆盖率可以帮助开发者聚焦变动代码的逻辑缺陷,从而更好地避免线上问题。这里更多的是讲述基于 Swift & Objective-C 工程的 全量代码覆盖率 的方案,没有原理,只有简单的流程。中途尝试过多个方案,最终依靠 Cocoapods 能力将自动化脚本赋能出去。
但是实际开发过程,不可能每次都去关注 全量代码覆盖率,下一篇继续介绍:iOS代码覆盖率(二)-增量覆盖率自动化实践
参考
Source-based Code Coverage for Swift Step by Step : 非常详细的Swift代码覆盖率教程,受益匪浅。
iOS 基于非Case的Code Coverage系统搭建 : 基于对OC项目的代码覆盖率介绍,提供了脚本化思路,收益匪浅。
llvm-profdata - Profile data tool: 用于处理生成profdata命令
Source-based Code Coverage :llvm官网基于源码对Swift和OC进行代码覆盖率
Source-based Code Coverage 中文版 :llvm官网基于源码对Swift和OC进行代码覆盖率-笔者翻译(轻喷)
