当前位置:首页 >探索 >使用 Swift Package 插件生成代码 作为这项工作的插件一部分

使用 Swift Package 插件生成代码 作为这项工作的插件一部分

2024-06-28 19:33:31 [百科] 来源:避面尹邢网

使用 Swift Package 插件生成代码

作者:Swift君 移动开发 iOS 由于这项服务对应用程序的使用生成正确运行至关重要,作为这项工作的插件一部分,我们希望确保始终存在故障安全( fail-safe)。代码因此,使用生成我们让该应用程序附带了一个备用的插件JSON文件,如果远程和缓存的代码数据解码失败,将使用该文件,使用生成来保证程序的插件正常运行。

前言

不久前,代码我正在工作中开发一项新服务,使用生成该服务由 Swift Package 组成,插件该 Package 公开了一个类似于Decodable​协议,代码供我们应用程序的使用生成其余部分使用。事实上,插件该协议是代码从Decodable本身继承下来的,看起来像这样:

使用 Swift Package 插件生成代码 作为这项工作的插件一部分

Fetchable.swit

使用 Swift Package 插件生成代码 作为这项工作的插件一部分

protocol Fetchable: Decodable, Equatable { }

新的 package 将采用符合Fetchable的类型来尝试从远程或缓存的JSON数据块中解码它们。

使用 Swift Package 插件生成代码 作为这项工作的插件一部分

由于这项服务对应用程序的正确运行至关重要,作为这项工作的一部分,我们希望确保始终存在故障安全( fail-safe)。因此,我们让该应用程序附带了一个备用的JSON文件,如果远程和缓存的数据解码失败,将使用该文件,来保证程序的正常运行。

无论如何,我们需要符合Fetchable的新类型从备用数据中正确解码。然而,有一个问题,有时很难发现备用JSON文件或模型本身是否有任何错误,因为解码错误会在运行时发生,并且只有在访问某些屏幕/功能时才会发生。

为了让我们对我们要发送的代码更有信心,我们添加了一些单元测试,试图根据我们附带的备用JSON解码符合Fetchable协议的每个模型。这些将使我们在CI上有一个早期指示,表明备用数据或模型中存在错误,如果所有测试都通过,我们将确定,一旦我们发布新服务,它始终具有故障安全功能。

我们手动编写了这些测试,但我们很快就意识到这个解决方案是不可扩展的,因为随着越来越多的符合Fetchable协议的类型被添加,我们引入了大量的代码复制,并可能有人最终忘记为特定功能编写这些测试。

我们考虑过自动化该过程,但由于我们的代码库的性质,我们遇到了一些问题,代码库高度模块化,混合了Xcode项目和Swift Package。一些架构决策还意味着我们必须收集大量符号信息,才能获得生成测试的正确类型。

是什么让我再次关注到它?

在我忘记了这件事一段时间后,Xcode 14的公告允许在Xcode项目中使用 Swift Package 插件,以及一些架构更改使提取类型信息变得容易得多,这让我有动力再次开始研究这个问题。

请注意,Xcode项目的构建工具插件尚未按照发布说明在Xcode 14 Beta 2中提供,但将在Xcode 14的未来版本中提供。

图片图片取自 Xcode Beta 2 版的发布说明

在过去的几周里,我一直在研究如何使用软件包插件生成单元测试,在这篇文章中,我将解释我在向哪个方向尝试以及它涉及了什么。

实施细节

我开始了一项任务,即创建一个构建工具插件,与 Xcode 14 引入的命令插件不同,该插件可以任意运行并依赖用户输入,作为Swift软件包构建过程的一部分运行。

我知道我需要创建一个可执行文件,因为 Build Tool 插件依赖这些来执行操作。这个脚本将完全用 Swift 编写,因为这是我最熟悉的语言,并承担以下职责:

  1. 扫描目标目录并提取所有.swift文件。目标将被递归扫描,以确保不会错过子目录。
  2. 使用sourcekit,或者更具体地说,SourceKitten,扫描这些.swift​文件并收集类型信息。这将允许提取符合Fetchable协议的所有类型,以便可以针对它们编写测试。
  3. 获得这些类型后,生成一个带有XCTestCase的.swift文件,其中包含每种类型的单元测试。

让我们写一些代码吧

与所有 Swift Package 一样,最简单的入门方法是在命令行上运行swift package init。

这创建了两个目标,一个是包含Fetchable协议定义和符合该定义的类型的实现代码,另一个是应用插件为此类类型生成单元测试的测试目标。

Package.swit

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "CodeGenSample",
platforms: [.macOS(.v10_11)],
products: [
.library(
name: "CodeGenSample",
targets: ["CodeGenSample"]),
],
dependencies: [
],
targets: [
.target(
name: "CodeGenSample",
dependencies: []
),
.testTarget(
name: "CodeGenSampleTests",
dependencies: ["CodeGenSample"]
)
]
)

编写可执行文件

如前所述,所有构建工具插件都需要可执行文件来执行所有必要的操作。

为了帮助开发此命令行,将使用几个依赖项。第一个是SourceKitten——特别是其SourceKitten框架库,这是一个Swift包装器,用于帮助使用Swift代码编写sourcekit请求,第二个是快速参数解析器,这是苹果提供的软件包,可以轻松创建命令行工具,并以更快、更安全的方式解析在执行过程中传递的命令行参数。

在创建executableTarget​并赋予它两个依赖项后,Package.swift就是这个样子:

Package.swift

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription

let package = Package(
name: "CodeGenSample",
platforms: [.macOS(.v10_11)],
products: [
.library(
name: "CodeGenSample",
targets: ["CodeGenSample"]),
],
dependencies: [
.package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
],
targets: [
.target(
name: "CodeGenSample",
dependencies: []
),
.testTarget(
name: "CodeGenSampleTests",
dependencies: ["CodeGenSample"]
),
.executableTarget(
name: "PluginExecutable",
dependencies: [
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
)
]
)

可执行目标需要一个入口点,因此,在PluginExecutable​目标的源目录下,必须创建一个名为PluginExecutable.swift的文件,其中所有可执行逻辑都需要创建。

请注意,这个文件可以随心所欲地命名,我倾向于以与我在Package.swift中创建的目标相同的方式命名它。

如下所示的脚本导入必要的依赖项,并创建可执行文件的入口点(必须用@main装饰),并声明在执行时传递的4个输入。

所有逻辑和方法调用都存在于run​函数中,该函数是调用可执行文件时运行的方法。这是ArgumentParser语法的一部分,如果您想了解更多信息,Andy Ibañez有一篇关于该主题的精彩文章,可能非常有帮助。

PluginExecutable.swift

import SourceKittenFramework
import ArgumentParser
import Foundation
@main
struct PluginExecutable: ParsableCommand {
@Argument(help: "The protocol name to match")
var protocolName: String
@Argument(help: "The module's name")
var moduleName: String
@Option(help: "Directory containing the swift files")
var input: String
@Option(help: "The path where the generated files will be created")
var output: String
func run() throws {
// 1
let files = try deepSearch(URL(fileURLWithPath: input, isDirectory: true))
// 2
setenv("IN_PROCESS_SOURCEKIT", "YES", 1)
let structures = try files.map { try Structure(file: File(path: $0.path)!) }
// 3
var matchedTypes = [String]()
structures.forEach { walkTree(dictionary: $0.dictionary, acc: &matchedTypes) }
// 4
try createOutputFile(withContent: matchedTypes)
}

// ...
}

现在让我们专注于上面的run方法,以了解当插件运行可执行文件时会发生什么:

  1. 首先,扫描目标目录以找到其中的所有.swift文件。这是递归完成的,这样子目录就不会错过。此目录的路径作为参数传递给可执行文件。
  2. 对于上次调用中找到的每个文件,通过SourceKitten发出Structure​请求,以查找文件中Swift代码的类型信息。请注意,环境变量(IN_PROCESS_SOURCEKIT)也被设置为true。这需要确保选择源套件的进程中版本,以便它能够遵守插件的沙盒规则。

Xcode附带两个版本的sourcekit可执行文件,一个版本解析进程中的文件,另一个使用XPC向解析进程外文件的守护进程发送请求。后者是mac上的默认版本,为了能够将sourcekit用作插件进程的一部分,必须选择进程中版本。这最近在SourceKitten上作为环境变量实现,是运行引擎盖下使用sourcekit的其他可执行文件的关键,例如SwiftLint。

  1. 浏览上次调用的所有响应,并扫描类型信息以提取符合Fetchable协议的任何类型。
  2. 在传递给可执行文件的output参数指定的位置创建一个输出文件,其中包含每种类型的单元测试。

请注意,上面没有重点介绍每个调用的具体细节,但如果你对实现感兴趣,包含所有代码的repo现在已经在Github上公开了!

创建该插件

与可执行文件一样,必须向Package.swift​添加.plugin​目标,并且必须创建包含插件实现的.swift​文件(Plugins/SourceKitPlugin/SourceKitPlugin.swift)。

Package.swift

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "CodeGenSample",
platforms: [.macOS(.v10_11)],
products: [
.library(
name: "CodeGenSample",
targets: ["CodeGenSample"]),
],
dependencies: [
.package(url: "https://github.com/jpsim/SourceKitten.git", exact: "0.32.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.0.0")
],
targets: [
.target(
name: "CodeGenSample",
dependencies: []
),
.testTarget(
name: "CodeGenSampleTests",
dependencies: [“CodeGenSample"],
plugins: [“SourceKitPlugin”],
),
.executableTarget(
name: "PluginExecutable",
dependencies: [
.product(name: "SourceKittenFramework", package: "SourceKitten"),
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
),
.plugin(
name: "SourceKitPlugin",
capability: .buildTool(),
dependencies: [.target(name: "PluginExecutable")]
)
]
)

以下代码显示了插件的初始实现,其struct​符合BuildToolPlugin​的协议。这需要实现一个返回具有单个构建命令的数组的createBuildCommands方法。

此插件使用buildCommand​而不是preBuildCommand​,因为它需要作为构建过程的一部分运行,而不是在它之前运行,因此它有机会构建和使用它所依赖的可执行文件。在这种情况下,支持使用buildCommand的另一点是,它只会在输入文件更改时运行,而不是每次构建目标时运行。

此命令必须为要运行的可执行文件提供名称和路径,这可以在插件的上下文中找到:

SourceKitPlugin.swift

import PackagePlugin
@main
struct SourceKitPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
return [
.buildCommand(
displayName: "Protocol Extraction!",
executable: try context.tool(named: "PluginExecutable").path,
arguments: [
"FindThis",
🤷,
"--input",
🤷,
"--output",
🤷
],
environment: ["IN_PROCESS_SOURCEKIT": "YES"],
outputFiles: [🤷]
)
]
}
}

如上面的代码所示,还有一些空白需要填充(🤷):

  1. 提供outputPath​,用于生成单元测试文件。此文件可以在pluginWorkDirectory中生成,也可以在插件的上下文中找到。该目录提供读写权限且其中创建的任何文件都将是软件包构建过程的一部分。
  2. 提供输入路径和模块名称。这是最棘手的部分,这些需要指向正在测试的目标的来源,而不是插件正在应用于的目标——单元测试。谢天谢地,插件的目标依赖项是可访问的,我们可以从该数组中获取我们感兴趣的依赖项。此依赖项将是内部的(target​而不是product),它将为可执行文件提供其名称和目录。

SourceKitPlugin.swift

import PackagePlugin
@main
struct SourceKitPlugin: BuildToolPlugin {
func createBuildCommands(context: PluginContext, target: Target) async throws -> [Command] {
let outputPath = context.pluginWorkDirectory.appending(“GeneratedTests.swift”)

guard let dependencyTarget = target
.dependencies
.compactMap { dependency -> Target? in
switch dependency {
case .target(let target): return target
default: return nil
}
}
.filter { "\($0.name)Tests" == target.name }
.first else {
Diagnostics.error("Could not get a dependency to scan!”)

return []
}
return [
.buildCommand(
displayName: "Protocol Extraction!",
executable: try context.tool(named: "PluginExecutable").path,
arguments: [
"Fetchable",
dependencyTarget.name,
"--input",
dependencyTarget.directory,
"--output",
outputPath
],
environment: ["IN_PROCESS_SOURCEKIT": "YES"],
outputFiles: [outputPath]
)
]
}
}

注意上述可选性处理方式。如果在测试目标的依赖项中找不到合适的目标,则使用Diagnostics API将错误转发回Xcode,并告诉它完成构建过程。

让我们看下结果

插件这就完成了!现在让我们在 Xcode 中运行它!为了测试这种方法,将包含以下内容的文件添加到CodeGenSample目标中:

CodeGenSample.swift

import Foundation
protocol Fetchable: Decodable, Equatable { }
struct FeatureABlock: Fetchable {
let featureA: FeatureA

struct FeatureA: Fetchable {
let url: URL
}
}
enum Root {
struct RootBlock: Fetchable {
let url: URL
let areAllFeaturesEnabled: Bool
}
}

请注意,脚本将在结构中首次出现Fetchable​协议时停止。这意味着任何嵌套的符合Fetchable协议的类型都将被测试,只是外部模型。

给定此输入并在主目标上运行测试,生成并运行XCTestCase​,其中包含符合Fetchable协议的两种类型的测试。

GeneratedTests.swift

import XCTest
@testable import CodeGenSample
class GeneratedTests: XCTestCase {
func testFeatureABlock() {
assertCanParseFromDefaults(FeatureABlock.self)
}
func testRoot_RootBlock() {
assertCanParseFromDefaults(Root.RootBlock.self)
}
private func assertCanParseFromDefaults<T: Fetchable>(_ type: T.Type) {
// Logic goes here...
}
}

所有测试都通过了😅✅而且,尽管他们目前没有做很多事情,但可以扩展实现,以提供一些示例数据和一个JSONDecoder实例来对每个单元测试进行解析。

责任编辑:姜华 来源: Swift社区 SwiftDecodable

(责任编辑:百科)

    推荐文章
    • 三季度末银行业总资产增长7.7% 不良贷款余额2.8万亿元

      三季度末银行业总资产增长7.7% 不良贷款余额2.8万亿元11月16日,银保监会发布2021年三季度银行业主要监管指标数据。数据显示,三季度末,我国银行业金融机构本外币资产339.4万亿元,同比增长7.7%;不良贷款余额2.8万亿元,较上季末增加427亿元; ...[详细]
    • 周星驰“踩着七色的云彩”蹚路微短剧

      周星驰“踩着七色的云彩”蹚路微短剧21世纪经济报道记者 贺泓源 北京报道 “我的意中人是个盖世英雄,有一天他会踩着七色的云彩来娶我。”这是《大话西游》中,朱茵扮演的紫霞仙子经典台词。眼下,该片主演周星驰正在微短剧市场起着另一种承接作用 ...[详细]
    • 机器人能以人类两倍的速度阅读盲文

      机器人能以人类两倍的速度阅读盲文    科技日报北京1月30日电 记者张梦然)英国剑桥大学研究人员开发了一种机器人传感器,结合人工智能技术,它可以接近人类读者两倍的速度阅读盲文。这一机器人原本并不是作为盲人辅助技术而开发的,但其高灵 ...[详细]
    • 国内大模型与GPT

      国内大模型与GPT1月30日,上海人工智能实验室发布了大模型开源开放评测体系司南OpenCompass2.0),同时在对部分主流大模型评测诊断的基础上,揭晓了年度大模型评测榜单,提到了国内大模型的优势与短板。根据评测, ...[详细]
    • 富瀚微(300613.SZ)公布消息:就收购眸芯科技32.43%股权已完成工商变更登记

      富瀚微(300613.SZ)公布消息:就收购眸芯科技32.43%股权已完成工商变更登记富瀚微(300613.SZ)公布,之前公告披露,公司于2021年2月5日召开第三届董事会第十四次会议,于2021年2月25日召开了2021 年第二次临时股东大会审议通过了《关于收购眸芯科技(上海)有限 ...[详细]
    • 京东方、维信诺去年净利下降或亏损 OLED业务扭亏是改善关键

      京东方、维信诺去年净利下降或亏损 OLED业务扭亏是改善关键京东方000725.SZ)、维信诺002387.SZ)1月30日晚均发布了承压的2023年业绩预告,这两家中国显示面板龙头企业去年净利润下降六七成或亏损,四季度业绩环比改善。在液晶面板动态控产的同时, ...[详细]
    • 体验100小时P3R后,我找回的不只是青春

      体验100小时P3R后,我找回的不只是青春来源:游戏研究社《女神异闻录5》P5)出圈后,ATLUS曾在横滨体育馆举办了系列有史以来规模最大的一次音乐会,Persona Super Live 2017。这场演出质量在线,也有“新约”作品P3到P ...[详细]
    • 全国古树名木保护管理一张图初步建成

      全国古树名木保护管理一张图初步建成【环保时空】    科技日报讯 记者马爱平)记者1月28日从国家林业和草原局获悉,第二次全国古树名木资源普查结果显示,我国普查范围内现有古树名木共计508.19万株,包括散生122.13万株和群状38 ...[详细]
    • 清明假期火车票开售 想要去哪里游玩记得提前预订车票

      清明假期火车票开售 想要去哪里游玩记得提前预订车票需要注意啦,清明节一共放假三天的时间,而且高速也会免费通行。现在消息指出,清明假期火车票开售啦,节前一天北京前往郑州、武汉等地车票热销,想要去哪里游玩,记得提前预订车票。同时,3月20日可以购买4月3 ...[详细]
    • 山西晋中实施民办学校积分管理

      山西晋中实施民办学校积分管理    科技日报讯 记者韩荣)每发现有一项“一般负面清单”,扣2分;每发现有一项“重点负面清单”,扣4分……1月27日,记者从山西省晋中市教育部门获悉,该市对民办学校开展积分管理考核,以学年度为区间, ...[详细]
    热点阅读