前言不久之前,我正在开发一项新服务,该服务由一个Swift包组成,它公开了一个类似Decodable的协议,供我们的应用程序的其余部分使用。事实上,该协议是从Decodable本身继承的,看起来像这样:Fetchable.switprotocolFetchable:Decodable,Equatable{}新包将采用Fetchable兼容的类型来尝试从远程或缓存的JSON块中解码它们。由于此服务对于应用程序的正常运行至关重要,因此作为这项工作的一部分,我们希望确保它始终是故障安全的。因此,我们让应用程序附带一个备用JSON文件,如果远程和缓存数据解码失败,将使用该文件来保持应用程序运行。在任何情况下,我们都需要符合Fetchable的新类型来正确解码备用数据。但是,有一个问题,有时很难找出备用JSON文件或模型本身是否有任何错误,因为解码错误会在运行时发生,并且只有在访问某些屏幕/功能时才会发生。为了让我们对发送的代码更有信心,我们添加了一些单元测试,尝试根据我们随附的备用JSON解码符合Fetchable协议的每个模型。这些将使我们在CI上得到早期指示,表明替代数据或模型中存在错误,如果所有测试都通过,我们将确保一旦我们发布新服务,它将始终是故障安全的。我们手动编写了这些测试,但我们很快意识到这种解决方案不可扩展,因为随着越来越多符合Fetchable协议的类型被添加,我们引入了大量代码重复,最终可能有人会忘记为以下项目编写那些测试具体功能。我们考虑过自动化流程,但由于我们代码库的性质,我们遇到了一些问题,它是高度模块化的,混合了Xcode项目和Swift包。一些架构决策还意味着我们必须收集大量符号信息才能为生成的测试获取正确的类型。是什么让我再次关注它?在我忘记了一段时间之后,Xcode14的发布允许在Xcode项目中使用SwiftPackage插件,以及一些架构上的变化使提取类型信息变得更加容易,这促使我再次开始研究它。请注意,根据发行说明,Xcode项目的构建工具插件在Xcode14Beta2中尚不可用,但将在Xcode14的未来版本中提供。图像取自XcodeBeta2的发行说明在过去的几周里,我我一直在研究如何使用包插件生成单元测试,在这篇文章中,我将解释我正在尝试的方向以及它涉及的内容。实现细节我着手创建一个构建工具插件的任务,与Xcode14引入的命令插件不同,它可以任意运行并依赖于用户输入,作为Swift包构建过程的一部分运行。我知道我需要创建一个可执行文件,因为构建工具插件依赖这些来做事。这个脚本将完全用Swift编写,因为这是我最熟悉的语言,并将执行以下操作:扫描目标目录并提取所有.swift文件。将递归扫描目标以确保没有遗漏任何子目录。使用sourcekit,或者更具体地说,SourceKitten,来扫描这些.swift文件并收集类型信息。这将允许提取所有符合Fetchable协议的类型,以便可以针对它们编写测试。拥有这些类型后,使用XCTestCase生成一个.swift文件,其中包含每种类型的单元测试。让我们写一些代码。与所有Swift包一样,最简单的入门方法是在命令行上运行swiftpackageinit。这将创建两个目标,一个包含Fetchable协议定义和符合该定义的类型的实现代码,以及一个应用插件为此类类型生成单元测试的测试目标。Package.swit//swift-tools-version:5.6//swift-tools-version声明构建此package.importPackageDescriptionletpackage=Package(name:"CodeGenSample",platforms:[.macOS(.v10_11)],产品:[.library(name:"CodeGenSample",targets:["CodeGenSample"]),],dependencies:[],targets:[.target(name:"CodeGenSample",dependencies:[]),.testTarget(name:"CodeGenSampleTests",dependencies:["CodeGenSample"])])编写可执行文件如前所述,所有构建工具插件都需要一个可执行文件来执行所有必要的操作。为了帮助开发此命令行,将使用几个依赖项。第一个是SourceKitten——特别是它的SourceKitten框架库,它是一个Swift包装器,可以帮助使用Swift代码编写sourcekit请求,第二个是FastParameterParser,它是Apple提供的一个包,可以轻松创建命令行工具并以更快、更安全的方式解析执行期间传递的命令行参数。在创建executableTarget并赋予它两个依赖项之后,Package.swift看起来像这样:包=包(名称:“CodeGenSample”,平台:[.macOS(.v10_11)],产品:[.library(名称:“CodeGenSample”,目标:[“CodeGenSample”]),],依赖项:[.package(网址:“https://github.com/jpsim/SourceKitten.git”,准确:“0.32.0”),.package(网址:“https://github.com/apple/swift-argument-parser”,来自:“1.0.0”)],目标:[.target(名称:“CodeGenSample”,依赖项:[]),.testTarget(名称:“CodeGenSampleTests”,依赖项:[“CodeGenSample”]),.executableTarget(名称:“PluginExecutable”,依赖项:[.product(名称:“SourceKittenFramework”,package:"SourceKitten"),.product(name:"ArgumentParser",package:"swift-argument-parser")])])可执行目标需要一个入口点,因此,在PluginExecutable目标的源目录中,您必须创建一个名为PluginExecutable.swift的文件,其中需要创建所有可执行逻辑请注意,这个文件可以随意命名,我倾向于以与我在Package.swift中创建的目标相同的方式命名它。下面显示的脚本导入必要的依赖项并创建可执行文件的入口点(必须用@main修饰)并声明执行时传递的4个输入。所有逻辑和方法调用都在运行函数中,这是调用可执行文件时运行的方法。这是ArgumentParser语法的一部分,如果您想了解更多信息,AndyIba?ez有一篇关于该主题的精彩文章可能会很有帮助。PluginExecutable.swiftimportSourceKittenFrameworkimportArgumentParserimportFoundation@mainstructPluginExecutable:ParsableCommand{@Argument(help:"Theprotocolnametomatch")varprotocolName:String@Argument(help:"Themodule'sname")varmoduleName:String@Option(help:"包含swift文件的目录")varinput:String@Option(help:"Thepathwherethegeneratedfileswillbecreated")varoutput:Stringfuncrun()throws{//1letfiles=trydeepSearch(URL(fileURLWithPath:input,isDirectory:true))//2setenv("IN_PROCESS_SOURCEKIT","YES",1)letstructures=tryfiles.map{tryStructure(file:File(path:$0.path)!)}//3varmatchedTypes=[String]()structures.forEach{walkTree(dictionary:$0.dictionary,acc:&matchedTypes)}//4trycreateOutputFile(withContent:matchedTypes)}//...}现在让我们关注上面的运行方法了解当插件运行可执行文件时会发生什么内容:首先,扫描目标目录以查找其中的所有.swift文件。这是递归完成的,因此不会遗漏子目录。此目录的路径作为参数传递给可执行文件。对于之前调用中找到的每个文件,通过SourceKitten发出一个结构请求,以查找文件中Swift代码的类型信息。请注意,环境变量(IN_PROCESS_SOURCEKIT)也设置为true。这需要确保选择源套件的进程内版本,以便它遵守插件的沙箱规则。Xcode附带了两个版本的sourcekit可执行文件,一个用于解析进程内文件,另一个使用XPC将请求发送到解析进程外文件的守护进程。后者是mac上的默认版本,为了能够在插件进程中使用sourcekit,必须选择in-process版本。这最近作为环境变量在SourceKitten上实现,并且是运行其他使用sourcekit的可执行文件的关键,例如SwiftLint。浏览上次调用的所有响应,并扫描类型信息以提取符合Fetchable协议的任何类型。在传递给可执行文件的输出参数指定的位置创建一个输出文件,其中包含每种类型的单元测试。请注意,每个调用的具体细节并未在上面突出显示,但如果您对实现感兴趣,那么包含所有代码的存储库现已在Github上公开提供!创建插件与可执行文件相同,必须在Package.swift添加一个.plugin目标,并且必须创建一个包含插件实现的.swift文件(Plugins/SourceKitPlugin/SourceKitPlugin.swift)。Package.swift//swift-tools-version:5.6//swift-tools-version声明构建此package.importPackageDescriptionletpackage=Package(name:"CodeGenSample",platforms:[.macOS(.v10_11)],产品:[.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",来自:"1.0.0")],目标:[.target(名称:“CodeGenSample”,依赖项:[]),.testTarget(名称:“CodeGenSampleTests”,依赖项:[“CodeGenSample”],插件:[“SourceKitPlugin”],,.executableTarget(名称:“PluginExecutable”,依赖项:[.product(name:"SourceKittenFramework",包:"SourceKitten"),.product(name:"ArgumentParser",package:"swift-argument-parser")]),.plugin(name:"SourceKitPlugin",能力:.buildTool(),依赖关系:[.target(name:"PluginExecutable")])])以下代码显示了插件的初始实现,其结构符合BuildToolPlugin协议。这需要实现一个createBuildCommands方法,该方法返回一个包含单个构建命令的数组。此插件使用buildCommand而不是preBuildCommand,因为它需要作为构建过程的一部分运行,而不是在它之前,它有机会构建和使用它依赖的可执行文件。在这种情况下支持使用buildCommand的另一点是它只会在运行时运行输入文件会更改,而不是每次构建目标时都会更改。此命令必须提供要运行的可执行文件的名称和路径,可以在插件的上下文中找到:SourceKitPlugin.swiftimportPackagePlugin@mainstructSourceKitPlugin:BuildToolPlugin{funccreateBuildCommands(context:对luginContext,target:Target)asyncthrows->[Command]{return[.buildCommand(displayName:"ProtocolExtraction!",可执行文件:trycontext.tool(named:"PluginExecutable").path,arguments:["FindThis",
