最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

使用?Swift?Package?插件生成代碼的示例詳解

 更新時間:2022年08月10日 10:43:55   作者:Swift君  
這篇文章主要介紹了使用?Swift?Package?插件生成代碼,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下

前言

不久前,我正在工作中開發(fā)一項(xiàng)新服務(wù),該服務(wù)由 Swift Package 組成,該 Package 公開了一個類似于Decodable?協(xié)議,供我們應(yīng)用程序的其余部分使用。事實(shí)上,該協(xié)議是從Decodable本身繼承下來的,看起來像這樣:

Fetchable.swit

protocol Fetchable: Decodable, Equatable {}

新的 package 將采用符合Fetchable的類型來嘗試從遠(yuǎn)程或緩存的JSON數(shù)據(jù)塊中解碼它們。

由于這項(xiàng)服務(wù)對應(yīng)用程序的正確運(yùn)行至關(guān)重要,作為這項(xiàng)工作的一部分,我們希望確保始終存在故障安全( fail-safe)。因此,我們讓該應(yīng)用程序附帶了一個備用的JSON文件,如果遠(yuǎn)程和緩存的數(shù)據(jù)解碼失敗,將使用該文件,來保證程序的正常運(yùn)行。

無論如何,我們需要符合Fetchable的新類型從備用數(shù)據(jù)中正確解碼。然而,有一個問題,有時很難發(fā)現(xiàn)備用JSON文件或模型本身是否有任何錯誤,因?yàn)榻獯a錯誤會在運(yùn)行時發(fā)生,并且只有在訪問某些屏幕/功能時才會發(fā)生。

為了讓我們對我們要發(fā)送的代碼更有信心,我們添加了一些單元測試,試圖根據(jù)我們附帶的備用JSON解碼符合Fetchable協(xié)議的每個模型。這些將使我們在CI上有一個早期指示,表明備用數(shù)據(jù)或模型中存在錯誤,如果所有測試都通過,我們將確定,一旦我們發(fā)布新服務(wù),它始終具有故障安全功能。

我們手動編寫了這些測試,但我們很快就意識到這個解決方案是不可擴(kuò)展的,因?yàn)殡S著越來越多的符合Fetchable協(xié)議的類型被添加,我們引入了大量的代碼復(fù)制,并可能有人最終忘記為特定功能編寫這些測試。

我們考慮過自動化該過程,但由于我們的代碼庫的性質(zhì),我們遇到了一些問題,代碼庫高度模塊化,混合了Xcode項(xiàng)目和Swift Package。一些架構(gòu)決策還意味著我們必須收集大量符號信息,才能獲得生成測試的正確類型。

是什么讓我再次關(guān)注到它?

在我忘記了這件事一段時間后,Xcode 14的公告允許在Xcode項(xiàng)目中使用 Swift Package 插件,以及一些架構(gòu)更改使提取類型信息變得容易得多,這讓我有動力再次開始研究這個問題。

請注意,Xcode項(xiàng)目的構(gòu)建工具插件尚未按照發(fā)布說明在Xcode 14 Beta 2中提供,但將在Xcode 14的未來版本中提供。

圖片取自 Xcode Beta 2 版的發(fā)布說明

在過去的幾周里,我一直在研究如何使用軟件包插件生成單元測試,在這篇文章中,我將解釋我在向哪個方向嘗試以及它涉及了什么。

實(shí)施細(xì)節(jié)

我開始了一項(xiàng)任務(wù),即創(chuàng)建一個構(gòu)建工具插件,與 Xcode 14 引入的命令插件不同,該插件可以任意運(yùn)行并依賴用戶輸入,作為Swift軟件包構(gòu)建過程的一部分運(yùn)行。

我知道我需要創(chuàng)建一個可執(zhí)行文件,因?yàn)?Build Tool 插件依賴這些來執(zhí)行操作。這個腳本將完全用 Swift 編寫,因?yàn)檫@是我最熟悉的語言,并承擔(dān)以下職責(zé):

  • 掃描目標(biāo)目錄并提取所有.swift文件。目標(biāo)將被遞歸掃描,以確保不會錯過子目錄。
  • 使用sourcekit,或者更具體地說,SourceKitten,掃描這些.swift?文件并收集類型信息。這將允許提取符合Fetchable協(xié)議的所有類型,以便可以針對它們編寫測試。
  • 獲得這些類型后,生成一個帶有XCTestCase的.swift文件,其中包含每種類型的單元測試。

讓我們寫一些代碼吧

與所有 Swift Package 一樣,最簡單的入門方法是在命令行上運(yùn)行swift package init。

這創(chuàng)建了兩個目標(biāo),一個是包含F(xiàn)etchable協(xié)議定義和符合該定義的類型的實(shí)現(xiàn)代碼,另一個是應(yīng)用插件為此類類型生成單元測試的測試目標(biāo)。

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"]
        )
     ]
)

編寫可執(zhí)行文件

如前所述,所有構(gòu)建工具插件都需要可執(zhí)行文件來執(zhí)行所有必要的操作。

為了幫助開發(fā)此命令行,將使用幾個依賴項(xiàng)。第一個是SourceKitten——特別是其SourceKitten框架庫,這是一個Swift包裝器,用于幫助使用Swift代碼編寫sourcekit請求,第二個是快速參數(shù)解析器,這是蘋果提供的軟件包,可以輕松創(chuàng)建命令行工具,并以更快、更安全的方式解析在執(zhí)行過程中傳遞的命令行參數(shù)。

在創(chuàng)建executableTarget?并賦予它兩個依賴項(xiàng)后,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")
            ]
        )
     ]
)

可執(zhí)行目標(biāo)需要一個入口點(diǎn),因此,在PluginExecutable?目標(biāo)的源目錄下,必須創(chuàng)建一個名為PluginExecutable.swift的文件,其中所有可執(zhí)行邏輯都需要創(chuàng)建。

請注意,這個文件可以隨心所欲地命名,我傾向于以與我在Package.swift中創(chuàng)建的目標(biāo)相同的方式命名它。

如下所示的腳本導(dǎo)入必要的依賴項(xiàng),并創(chuàng)建可執(zhí)行文件的入口點(diǎn)(必須用@main裝飾),并聲明在執(zhí)行時傳遞的4個輸入。

所有邏輯和方法調(diào)用都存在于run?函數(shù)中,該函數(shù)是調(diào)用可執(zhí)行文件時運(yùn)行的方法。這是ArgumentParser語法的一部分,如果您想了解更多信息,Andy Ibañez有一篇關(guān)于該主題的精彩文章,可能非常有幫助。

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)
    }

    // ...
}

現(xiàn)在讓我們專注于上面的run方法,以了解當(dāng)插件運(yùn)行可執(zhí)行文件時會發(fā)生什么:

  • 首先,掃描目標(biāo)目錄以找到其中的所有.swift文件。這是遞歸完成的,這樣子目錄就不會錯過。此目錄的路徑作為參數(shù)傳遞給可執(zhí)行文件。
  • 對于上次調(diào)用中找到的每個文件,通過SourceKitten發(fā)出Structure?請求,以查找文件中Swift代碼的類型信息。請注意,環(huán)境變量(IN_PROCESS_SOURCEKIT)也被設(shè)置為true。這需要確保選擇源套件的進(jìn)程中版本,以便它能夠遵守插件的沙盒規(guī)則。

Xcode附帶兩個版本的sourcekit可執(zhí)行文件,一個版本解析進(jìn)程中的文件,另一個使用XPC向解析進(jìn)程外文件的守護(hù)進(jìn)程發(fā)送請求。后者是mac上的默認(rèn)版本,為了能夠?qū)ourcekit用作插件進(jìn)程的一部分,必須選擇進(jìn)程中版本。這最近在SourceKitten上作為環(huán)境變量實(shí)現(xiàn),是運(yùn)行引擎蓋下使用sourcekit的其他可執(zhí)行文件的關(guān)鍵,例如SwiftLint。

  • 瀏覽上次調(diào)用的所有響應(yīng),并掃描類型信息以提取符合Fetchable協(xié)議的任何類型。
  • 在傳遞給可執(zhí)行文件的output參數(shù)指定的位置創(chuàng)建一個輸出文件,其中包含每種類型的單元測試。

請注意,上面沒有重點(diǎn)介紹每個調(diào)用的具體細(xì)節(jié),但如果你對實(shí)現(xiàn)感興趣,包含所有代碼的repo現(xiàn)在已經(jīng)在Github上公開了!

創(chuàng)建該插件

與可執(zhí)行文件一樣,必須向Package.swift?添加.plugin?目標(biāo),并且必須創(chuàng)建包含插件實(shí)現(xiàn)的.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")]
        )
     ]
)

以下代碼顯示了插件的初始實(shí)現(xiàn),其struct?符合BuildToolPlugin?的協(xié)議。這需要實(shí)現(xiàn)一個返回具有單個構(gòu)建命令的數(shù)組的createBuildCommands方法。

此插件使用buildCommand?而不是preBuildCommand?,因?yàn)樗枰鳛闃?gòu)建過程的一部分運(yùn)行,而不是在它之前運(yùn)行,因此它有機(jī)會構(gòu)建和使用它所依賴的可執(zhí)行文件。在這種情況下,支持使用buildCommand的另一點(diǎn)是,它只會在輸入文件更改時運(yùn)行,而不是每次構(gòu)建目標(biāo)時運(yùn)行。

此命令必須為要運(yùn)行的可執(zhí)行文件提供名稱和路徑,這可以在插件的上下文中找到:

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: [  ]
            )
        ]
    }
}

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

  • 提供outputPath?,用于生成單元測試文件。此文件可以在pluginWorkDirectory中生成,也可以在插件的上下文中找到。該目錄提供讀寫權(quán)限且其中創(chuàng)建的任何文件都將是軟件包構(gòu)建過程的一部分。
  • 提供輸入路徑和模塊名稱。這是最棘手的部分,這些需要指向正在測試的目標(biāo)的來源,而不是插件正在應(yīng)用于的目標(biāo)——單元測試。謝天謝地,插件的目標(biāo)依賴項(xiàng)是可訪問的,我們可以從該數(shù)組中獲取我們感興趣的依賴項(xiàng)。此依賴項(xiàng)將是內(nèi)部的(target?而不是product),它將為可執(zhí)行文件提供其名稱和目錄。

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]
            )
        ]
    }
}

注意上述可選性處理方式。如果在測試目標(biāo)的依賴項(xiàng)中找不到 合適的 目標(biāo),則使用Diagnostics API將錯誤轉(zhuǎn)發(fā)回Xcode,并告訴它完成構(gòu)建過程。

讓我們看下結(jié)果

插件這就完成了!現(xiàn)在讓我們在 Xcode 中運(yùn)行它!為了測試這種方法,將包含以下內(nèi)容的文件添加到CodeGenSample目標(biāo)中:

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
    }
}

請注意,腳本將在結(jié)構(gòu)中首次出現(xiàn)Fetchable?協(xié)議時停止。這意味著任何嵌套的符合Fetchable協(xié)議的類型都將被測試,只是外部模型。

給定此輸入并在主目標(biāo)上運(yùn)行測試,生成并運(yùn)行XCTestCase?,其中包含符合Fetchable協(xié)議的兩種類型的測試。

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...
    }
}

所有測試都通過了:sweat_smile::white_check_mark:而且,盡管他們目前沒有做很多事情,但可以擴(kuò)展實(shí)現(xiàn),以提供一些示例數(shù)據(jù)和一個JSONDecoder實(shí)例來對每個單元測試進(jìn)行解析。

到此這篇關(guān)于使用 Swift Package 插件生成代碼的文章就介紹到這了,更多相關(guān)Swift Package 插件生成代碼內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論

宁化县| 肃北| 大同市| 融水| 阿拉善左旗| 通许县| 农安县| 临颍县| 客服| 丰都县| 鹰潭市| 东明县| 晋州市| 茶陵县| 武山县| 晋中市| 宁河县| 舞钢市| 柳河县| 汾西县| 秦安县| 玉溪市| 建始县| 绥棱县| 台东县| 常德市| 朝阳市| 图片| 汝城县| 诸暨市| 桃园市| 钟山县| 安吉县| 习水县| 枣阳市| 疏附县| 余江县| 辰溪县| 中卫市| 西丰县| 尉氏县|