iOS代码覆盖率(一)-全量覆盖率自动化实践

京东云开发者
• 阅读 365

作者:京东零售 邓立兵

简介

这是一个统计基于 Swift & Objective-C 工程的代码覆盖率的自动化脚本。之所以做成 Pod ,是便于更好的复用,该 Pod 只包含了收集生成代码覆盖率的脚本。整体比较简单方便。

这里只将流程,咱不讲原理。后续另外介绍

这里只将流程,咱不讲原理。后续另外介绍

使用

1、安装:

通过 CocoaPods 进行安装,在你的 Podfile 文件添加如下代码:

pod 'HDCoverage' 复制代码

然后 pod install 安装下载相关脚本文件。

2、关联脚本:

在项目的 XcodeBuild Phases 添加新的脚本(New Run Script Phase)(App在Build会执行该脚本):

"${PODS_ROOT}/HDCoverage/HDCoverage/hd_coverage_env.sh" 复制代码

iOS代码覆盖率(一)-全量覆盖率自动化实践

3、工程配置代码覆盖率参数:

这里本来是在 HDCoverage 有脚本支持的,但是基于对哪些模块(Pod作为独立模版)进行代码覆盖率,所以建议在 Podfile 自主添加如下代码灵活管理,详细说明如下:

# 实现post_install Hooks
# 需要收集Code Coverage的模块
ntargets = Array['AFNetworking']

require 'xcodeproj'
post_install do |installer|
  # 修改Pods中某一个模块的配置文件,好采集代码覆盖率,需要源码!
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if(config.name <=> 'Release') == 0
        config.build_settings['OTHER_CFLAGS'] = '$(inherited)'
        config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)'
        config.build_settings['OTHER_LDFLAGS'] = '$(inherited)'
        ntargets.each do |ntarget|
          if(ntarget <=> target.name) == 0
            config.build_settings['OTHER_CFLAGS'] = '$(inherited) -fprofile-instr-generate -fcoverage-mapping'
            config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -profile-generate -profile-coverage-mapping'
            config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -fprofile-instr-generate'
            break
          end
        end
        else
        config.build_settings['OTHER_CFLAGS'] = '$(inherited)'
        config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)'
        config.build_settings['OTHER_LDFLAGS'] = '$(inherited)'
      end
    end
  end

  # 修改主工程
  project_path = './HDCoverage.xcodeproj'
  project = Xcodeproj::Project.open(project_path)
  puts project
  project.targets.each do |target|
    if(target.name <=> 'HDCoverageDemo') == 0
      target.build_configurations.each do |config|
        if ((config.name <=> 'Release') == 0 || (config.name <=> 'Debug') == 0)
          # 设置预编译变量CODECOVERAGE
          config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] = '$(inherited) CODECOVERAGE=1'
          # OC代码覆盖率插桩配置
          config.build_settings['OTHER_CFLAGS'] = '$(inherited) -fprofile-instr-generate -fcoverage-mapping'
          # Swift代码覆盖率插桩配置
          config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited) -profile-generate -profile-coverage-mapping'
          # 采集代码覆盖率配置
          config.build_settings['OTHER_LDFLAGS'] = '$(inherited) -fprofile-instr-generate'
          # Release需要设置,不然无法解析代码覆盖率
          config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone'
          else
          config.build_settings['OTHER_CFLAGS'] = '$(inherited)'
          config.build_settings['OTHER_SWIFT_FLAGS'] = '$(inherited)'
          config.build_settings['OTHER_LDFLAGS'] = '$(inherited)'
          config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = ''
        end
      end
    end
  end
  project.save()
end

4、代码执行数据收集:

使用 GCC 无法满足 同时兼容 SwiftObjective-C ,所以这里是基于 LLVM 进行,官网文档 。也可以参考笔者翻译的 Source-based Code Coverage ,完整详细的教程可以看 Source-based Code Coverage for Swift Step by Step

4.1、首先在工程中申明 LLVM 几个关键的函数:

#ifndef PROFILE_INSTRPROFILING_H_
#define PROFILE_INSTRPROFILING_H_

// https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
int __llvm_profile_runtime = 0;
void __llvm_profile_initialize_file(void);
const char *__llvm_profile_get_filename(void);
void __llvm_profile_set_filename(const char *);
int __llvm_profile_write_file(void);
int __llvm_profile_register_write_file_atexit(void);
const char *__llvm_profile_get_path_prefix(void);
#endif /* PROFILE_INSTRPROFILING_H_ */

4.2、再次封装代码覆盖率相关API,便于上层更好使用(建议):

class HDCoverageTools: NSObject { static var shared = HDCoverageTools()

// 注意:动态库是需要单独注册,并且需要在动态库中执行\_\_llvm\_profile\_write\_file()
// 
func registerCoverage(moduleName: String) {
    let name = "\\(moduleName).profraw"
    print("registerCoverage, moduleName: \\(moduleName)")
    let fileManager = FileManager.default
    do {
        let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:false)
        let filePath: NSString = documentDirectory.appendingPathComponent(name).path as NSString
        print("HDCoverageGather filePath: \\(filePath)")
        \_\_llvm\_profile\_set\_filename(filePath.utf8String)
    } catch {
        print(error)
    }
    saveAndUpload()
}

// 合适的时机代码覆盖率上报
func saveAndUpload() {
    \_\_llvm\_profile\_write\_file()
}

4.3、启动时刻,注册代码覆盖率API:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        HDCoverageTools.shared.registerCoverage(moduleName: "HDCoverageDemo")
        return true
}

4.4、在合适的时刻(按照大家的业务场景)将覆盖率数据写入制定的路径:

func sceneDidEnterBackground(_ scene: UIScene) {
        // 笔者这里测试,是在App进入后台后写入
        DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { [self] in
            HDCoverageTools.shared.saveAndUpload()
        }
    }

5、测试以便生成覆盖率数据:

以本工程的demo为例说明

5.1、在运行成功后,Finder 会自动弹出如下目录:

iOS代码覆盖率(一)-全量覆盖率自动化实践

这里是在项目主工程生成 CoverageResult 目录,并且将生成代码覆盖率可视化的脚本 hd_parse_profraw.sh 拷贝到这里,将 项目 HDCoverageDemo.app 拷贝过来,主要是获取其 Mach-O :

$ tree -L 2
.
├── MachOFiles
│   └── HDCoverageDemo.app
├── Profraw
└── hd_parse_profraw.sh

5.2、执行测试用例,这里我分别点击了:"主工程(OC)-Case1/Case2"、"主工程(Swift)-Case2/Case3"、"Framework(OC)-Case1/Case2"、"FrameworkSwift)-Case2/Case3" 后,App退到后台

iOS代码覆盖率(一)-全量覆盖率自动化实践

5.3、查看控制台,可以看到 profraw 文件:

registerCoverage, moduleName: HDCoverageDemo
HDCoverageGather filePath: /Users/denglibing/Library/Developer/CoreSimulator/Devices/5D01D4AA-40AE-4FC6-845C-391A94828EE3/data/Containers/Data/Application/283906A5-1681-44A5-8522-126D29D2F148/Documents/HDCoverageDemo.profraw

HDCoverageDemo.profraw 拷贝到 CoverageResult/Profraw 目录中;

5.4、执行 hd_parse_profraw.sh 脚本:

$ tree -L 2
.
├── MachOFiles
│   └── HDCoverageDemo.app
├── Profraw
│   └── HDCoverageDemo.profraw
└── hd_parse_profraw.sh

$ sh hd_parse_profraw.sh
CoverageResult: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/CoverageResult 
machOFiles: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/MachOFiles
/Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/CoverageResult 不存在,已经创建
disposeProfrawFiles profraws: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/Profraw
disposeProfrawFiles profraw file: HDCoverageDemo.profraw
===================================

findMachOFileName: HDCoverageDemo
findMachOFilePath: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/MachOFiles/HDCoverageDemo.app/HDCoverageDemo
===================================

disposeProfrawToHtml, machoFileName: HDCoverageDemo machOFilePath: /Users/denglibing/HDProject/iOSProject/SProject/hdcoverage/Example/CoverageResult/MachOFiles/HDCoverageDemo.app/HDCoverageDemo

执行成功后将自动将可视化的代码覆盖率目录打开:

$ tree -L 3
.
├── CoverageResult
│   └── HDCoverageDemo
│       ├── coverage
│       ├── index.html
│       └── style.css
├── MachOFiles
│   └── HDCoverageDemo.app
│       ├── Base.lproj
│       ├── Frameworks
│       ├── HDCoverageDemo
│       ├── Info.plist
│       ├── PkgInfo
│       └── _CodeSignature
├── Profraw
│   ├── HDCoverageDemo.profdata
│   └── HDCoverageDemo.profraw
└── hd_parse_profraw.sh

9 directories, 8 files
复制代码

5.5、查看:打开 CoverageResult/HDCoverageDemo/index.html 即可得到本次测试的代码覆盖率情况:

iOS代码覆盖率(一)-全量覆盖率自动化实践

点击某一个 Filename区域 可以查看详情,例如点击 HDOCFramework.m:

iOS代码覆盖率(一)-全量覆盖率自动化实践

可以看出,tag == 3 的代码行数并没有执行到,这正和上面测试的 "Framework(OC)-Case1/Case2" 符合。

小结

全量代码覆盖率可以帮助开发者聚焦变动代码的逻辑缺陷,从而更好地避免线上问题。这里更多的是讲述基于 Swift & Objective-C 工程的 全量代码覆盖率 的方案,没有原理,只有简单的流程。中途尝试过多个方案,最终依靠 Cocoapods 能力将自动化脚本赋能出去。

但是实际开发过程,不可能每次都去关注 全量代码覆盖率,下一篇继续介绍:iOS代码覆盖率(二)-增量覆盖率自动化实践

Demo及脚本源码地址,欢迎指导+Star

参考

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进行代码覆盖率-笔者翻译(轻喷)

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
3年前
PPDB:今晚老齐直播
【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。
Wesley13 Wesley13
3年前
VBox 启动虚拟机失败
在Vbox(5.0.8版本)启动Ubuntu的虚拟机时,遇到错误信息:NtCreateFile(\\Device\\VBoxDrvStub)failed:0xc000000034STATUS\_OBJECT\_NAME\_NOT\_FOUND(0retries) (rc101)Makesurethekern
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
MBR笔记
<bochs:100000000000e\WGUI\Simclientsize(0,0)!stretchedsize(640,480)!<bochs:2b0x7c00<bochs:3c00000003740i\BIOS\$Revision:1.166$$Date:2006/08/1117
Stella981 Stella981
3年前
Jenkins 插件开发之旅:两天内从 idea 到发布(上篇)
本文首发于:Jenkins中文社区(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fjenkinszh.cn)!huashan(https://oscimg.oschina.net/oscnet/f499d5b4f76f20cf0bce2a00af236d10265.jpg)
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_