逐步实现基于源码的 Swift 代码覆盖率

简介: 最近,正在为我司的项目研究基于 Swift 的代码覆盖率检测方案的解决方案,我已经努力尝试并且找到了最佳实践。

介绍

最近,正在为我司的项目研究基于 Swift 的代码覆盖率检测方案的解决方案,我已经努力尝试并且找到了最佳实践。

在这篇短文中,我将会给你介绍:

  • 如何生成 *.profraw 文件并通过命令行测量代码覆盖率
  • 如何在 Swift App 项目里调用 C/C++ 方法
  • 如何在 Xcode 中测量完整 Swift App 项目的代码覆盖率

使用命令行练习

在我们测量完整 App 项目的代码覆盖率之前,需要创建一个简单的 Swift 源代码文件,并且用命令行生成一个 *.profraw 文件,以便我们学习生成覆盖配置文件的基本工作流程。

创建一个 Swift 文件并包含以下代码:

test()
print("hello")
func test() {
  print("test")
}
func add(_ x: Double, _ y: Double) -> Double {
  return x + y
}
test()

在终端运行以下命令:

swiftc -profile-generate -profile-coverage-mapping hello.swift

传递给编译器的选项 -profile-generate-profile-coverage-mapping 将在编译源码时启用覆盖特性。基于源码的代码覆盖功能直接对 AST 和预处理器信息进行操作。

然后运行输出的二进制文件:

./hello

运行完成之后,在当前目录下执行 ls ,我们会看到这里生成了一个名为 default.profraw 的新文件。该文件由 llvm 生成,为了衡量代码覆盖率,我们必须使用另一个工具 llvm-profdata 来组合多个原始配置文件并同时对其进行索引。

xcrun llvm-profdata merge -sparse default.profraw -o hello.profdata

在终端运行上面的命令行,我们会得到一个名为 hello.profdata 的新文件,它可以显示我们想要的覆盖率报告。我们可以使用 llvm-cov 来显示或生成 JSON 报告。

xcrun llvm-cov show ./hello -instr-profile=hello.profdata
xcrun llvm-cov export ./hello -instr-profile=hello.profdata

现在,我们已经了解了生成快速代码覆盖率报告的基本工作流程。似乎 Swift 基于源码的代码覆盖并没有那么困难。但是,Xcode 中完整的 Swift App 项目的配置与命令行有很大的不同。那我们接着往下看吧!

在 Xcode 中测量 Swift App 项目的代码覆盖率

创建 Swift 项目

选择 SwiftCovApp target -> Build Settings -> Swift Compiler — Custom Flags

在 Other Swift Flags 添加 -profile-generate-profile-coverage-mapping 选项:

如果现在尝试编译,我们将会得到以下错误报告:

为了解决这个问题,我们必须为所有目标启用代码覆盖率:

在启用代码覆盖率之后再次运行,项目将会构建成功。

我们了解到,当程序退出时,编译器会将原始配置文件写入 LLVM_PROFILE_FILE 环境变量指定的路径。所以我们应该杀掉 Application 的进程来实现 *.profraw 文件。但是,当我们结束应用程序时,它会在控制台中报错:

虽然我在 Build Settings 中设置了相同的配置,但 Xcode 中的默认环境路径为空。为了解决这个问题,我们必须新建一个头文件,并声明一些 llvm C api 函数供 Swift 调用。

在 Swift 中调用 C/C++ 方法

Swift 是一种基于 C/C++ 的强大语言,它可以直接调用 C/C++ 方法。但是,在我们调用 llvm C/C++ api 之前,我们必须将我们需要的方法导出为一个模块。

首先,创建一个头文件:

然后,将以下代码复制粘贴到该文件中:

#ifndef PROFILE_INSTRPROFILING_H_
#define PROFILE_INSTRPROFILING_H_int __llvm_profile_runtime = 0;void __llvm_profile_initialize_file(void);
const char *__llvm_profile_get_filename();
void __llvm_profile_set_filename(const char *);
int __llvm_profile_write_file();
int __llvm_profile_register_write_file_atexit(void);
const char *__llvm_profile_get_path_prefix();#endif /* PROFILE_INSTRPROFILING_H_ */

创建一个 module.modulemap 文件并将所有内容导出为一个模块。

//
//  module.modulemap
//
//  Created by yao on 2020/10/15.
//module InstrProfiling {
    header "InstrProfiling.h"
    export *
}

事实上我们不能直接创建 module.modulemap,首先创建一个 module.c 文件然后重命名为 module.modulemap,它还可以帮助我创建一个 SwiftCovApp-Bridging-Header 文件。

构建项目,然后,我们可以在 Swift 代码中调用 llvm apis。

import UIKit
import InstrProfiling
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        print("File Path Prefix: \(String(cString: __llvm_profile_get_path_prefix()) )")
        print("File Name: \(String(cString: __llvm_profile_get_filename()) )")
        let name = "test.profraw"
        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
            __llvm_profile_set_filename(filePath.utf8String)
            print("File Name: \(String(cString: __llvm_profile_get_filename()))")
            __llvm_profile_write_file()
        } catch {
            print(error)
        }
    }
}

构建并启动 App,我们将在控制台中看到原始配置文件路径。

最后,我们得到了需要的原始配置文件! 🎉

我们可以复制这个文件和 Swift App 项目中的 Mach-O(二进制文件)到 temp 目录下,这样我们就可以检查配置文件是否可以生成正确的报告。

创建一个新的 Swift 文件:

import Foundation
struct BasicMath {
    static func add(_ a: Double, _ b: Double) -> Double {
        return a + b
    }
    
    var x: Double = 0
    var y: Double = 0
    
    func sqrt(_ x: Double, _ min: Double, _ max: Double) -> Double {
        let q = 1e-10
        let mid = (max + min) / 2.0
        
        if fabs(mid * mid - x) > q {
            if mid * mid < x {
                return sqrt(x, mid, max)
            } else if mid * mid > x {
                return sqrt(x, min, mid)
            } else {
                return mid
            }
        }
        
        return mid
    }
    
    func sqrt(_ x: Double) -> Double {
        sqrt(x, 0, x)
    }
}

在 ViewController.swift 中调用 __llvm_profile_write_file 之前调用 sqrt。然后,构建并运行。

print("√2=\(BasicMath().sqrt(2))")
__llvm_profile_write_file()

在命令行中运行以下命令:

mkdir TestCoverage
cd TestCoverage
cp /Users/yao/Library/Developer/CoreSimulator/Devices/4545834C-8D1F-4D2C-B243-F9E617F6C52D/data/Containers/Data/Application/6AEFAB1B-DA52-4FAF-9B27-3D47A898E55C/Documents/test.profraw .
cp /Users/yao/Library/Developer/Xcode/DerivedData/SwiftCovApp-bohvioqnvkjxnnesyhlznzvmmgcg/Build/Products/Debug-iphonesimulator/SwiftCovApp.app/SwiftCovApp .
ls
xcrun llvm-profdata merge -sparse test.profraw -o test.profdata
xcrun llvm-cov show ./SwiftCovApp -instr-profile=test.profdata

我们就能看到最后的报告啦~👏🎉

参考

关于我们

我们是由 Swift 爱好者共同维护,我们会分享以 Swift 实战、SwiftUI、Swift 基础为核心的技术内容,也整理收集优秀的学习资料。

目录
相关文章
|
4月前
|
测试技术 Swift iOS开发
探索iOS自动化测试:使用Swift编写UI测试
【8月更文挑战第31天】在软件开发的海洋中,自动化测试是保证船只不偏离航线的灯塔。本文将带领读者启航,深入探索iOS应用的自动化UI测试。我们将通过Swift语言,点亮代码的灯塔,照亮测试的道路。文章不仅会展示如何搭建测试环境,还会提供实用的代码示例,让理论知识在实践中生根发芽。无论你是新手还是有经验的开发者,这篇文章都将是你技能提升之旅的宝贵指南。
|
5月前
|
Dart Swift iOS开发
用pigeon kotlin swift写一个自己的插件
用pigeon kotlin swift写一个自己的插件
|
7月前
|
设计模式 安全 测试技术
【Swift 开发专栏】Swift 的代码审查与最佳实践
【4月更文挑战第30天】本文探讨了Swift代码审查的关键点和最佳实践,强调一致性、变量使用、函数设计、错误处理、性能、安全和注释。建议遵循单一职责原则,使用清晰命名,避免魔法数字,合理利用数据结构,封装与抽象,处理异常,优化内存,应用设计模式,进行单元测试,并持续学习。通过实际案例分析,展示如何提升代码质量和开发效率。
95 2
|
Swift
Swift实用小册16:ErrorHandling异常处理的使用
错误处理(Error handling),是响应错误以及从错误中恢复的过程。 在Swift开发过程中,我们常常会遇到由于一些方法无法执行或者参数丢失等原因导致的系统报错问题,严重一点可能会导致系统奔溃。而错误处理(Error handling),正是当这样那样的问题发生时,系统能够检测到错误并告知我们。
391 0
Swift实用小册16:ErrorHandling异常处理的使用
|
Swift iOS开发 MacOS
Swift-进阶 01:Swift源码编译
Swift-进阶 01:Swift源码编译
526 0
Swift-进阶 01:Swift源码编译
|
Swift 机器学习/深度学习 C语言
swift4.1 系统学习八
// // main.swift // swift08 // // Created by iOS on 2018/9/28. // Copyright © 2018年 weiman. All rights reserved. // import Foundation //swift学习笔记8 /* 1. for-in 循环 swift中使用for-in循环还是很频繁的。
898 0
|
Swift
swift4.1 系统学习七
import Foundation //swift学习笔记07 /* 控制流语句 学过任何一种语言的小伙伴们都对控制流语句不陌生,几乎每一天都在用。 控制流分类: 1.分支 2.循环 */ /* do语句块 这个语句块在我之前的学习中一直在使用,是一种在学习中很方便的写代码的方式。
1021 0
|
索引 Swift 自然语言处理
swift4.1 系统学习六
swift学习笔记6 字符和字符串 在swift中,String类型也是结构体,属于值类型,而不是引用类型。这一点,与OC是不一样的。 // // main.
849 0
|
存储 Swift iOS开发
swift4.1 系统学习四
// // main.swift // swift04 // // Created by iOS on 2018/9/21. // Copyright © 2018年 weiman.
1066 0
|
索引 机器学习/深度学习 Swift
swift4.1 系统学习三
// // main.swift // swift03 // // Created by iOS on 2018/9/21. // Copyright © 2018年 weiman. All rights reserved. // import Foundation /* swift学习笔记4 */ // 1.元组 /* 元组是swift中一种复合类型。
849 0