欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

前言

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

Fetchable.swit

protocol Fetchable: Decodable, Equatable {}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

讓我們寫(xiě)一些代碼吧

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

這創(chuàng)建了兩個(gè)目標(biāo),一個(gè)是包含F(xiàn)etchable協(xié)議定義和符合該定義的類(lèi)型的實(shí)現(xiàn)代碼,另一個(gè)是應(yīng)用插件為此類(lèi)類(lèi)型生成單元測(cè)試的測(cè)試目標(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"]
        )
     ]
)

編寫(xiě)可執(zhí)行文件

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

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

在創(chuàng)建executableTarget?并賦予它兩個(gè)依賴(lài)項(xiàng)后,Package.swift就是這個(gè)樣子:

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

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

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

所有邏輯和方法調(diào)用都存在于run?函數(shù)中,該函數(shù)是調(diào)用可執(zhí)行文件時(shí)運(yùn)行的方法。這是ArgumentParser語(yǔ)法的一部分,如果您想了解更多信息,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)在讓我們專(zhuān)注于上面的run方法,以了解當(dāng)插件運(yùn)行可執(zhí)行文件時(shí)會(huì)發(fā)生什么:

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

Xcode附帶兩個(gè)版本的sourcekit可執(zhí)行文件,一個(gè)版本解析進(jìn)程中的文件,另一個(gè)使用XPC向解析進(jìn)程外文件的守護(hù)進(jìn)程發(fā)送請(qǐng)求。后者是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),并掃描類(lèi)型信息以提取符合Fetchable協(xié)議的任何類(lèi)型。
  • 在傳遞給可執(zhí)行文件的output參數(shù)指定的位置創(chuàng)建一個(gè)輸出文件,其中包含每種類(lèi)型的單元測(cè)試。

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

創(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è)返回具有單個(gè)構(gòu)建命令的數(shù)組的createBuildCommands方法。

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

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

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

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

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

讓我們看下結(jié)果

插件這就完成了!現(xiàn)在讓我們?cè)?Xcode 中運(yùn)行它!為了測(cè)試這種方法,將包含以下內(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
    }
}

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

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

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

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

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

相關(guān)文章

  • RxSwift使用技巧之過(guò)濾操作詳解

    RxSwift使用技巧之過(guò)濾操作詳解

    RxSwift的目的是讓讓數(shù)據(jù)/事件流和異步任務(wù)能夠更方便的序列化處理,能夠使用Swift進(jìn)行響應(yīng)式編程,下面這篇文章主要給大家介紹了關(guān)于RxSwift使用技巧之過(guò)濾操作的相關(guān)資料,需要的朋友可以參考下。
    2017-09-09
  • swift3.0指紋解鎖的實(shí)現(xiàn)方法

    swift3.0指紋解鎖的實(shí)現(xiàn)方法

    這篇文章主要為大家詳細(xì)介紹了swift3.0指紋解鎖的實(shí)現(xiàn)方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2017-03-03
  • Swift3.0 GCD定時(shí)器的使用DEMO

    Swift3.0 GCD定時(shí)器的使用DEMO

    這篇文章主要介紹了Swift3.0 GCD定時(shí)器的使用,實(shí)現(xiàn)倒計(jì)時(shí),UIDatePicker的使用, 仿寫(xiě)一個(gè)活動(dòng)倒計(jì)時(shí)的DEMO,需要的朋友可以參考下
    2017-02-02
  • Swift中的高階函數(shù)功能作用示例詳解

    Swift中的高階函數(shù)功能作用示例詳解

    這篇文章主要為大家介紹了Swift中的高階函數(shù)功能作用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-03-03
  • Swift版使用ThPullRefresh實(shí)現(xiàn)下拉上拉刷新數(shù)據(jù)

    Swift版使用ThPullRefresh實(shí)現(xiàn)下拉上拉刷新數(shù)據(jù)

    這篇文章主要介紹了Swift版使用ThPullRefresh實(shí)現(xiàn)下拉上拉刷新數(shù)據(jù),需要的朋友可以參考下
    2016-01-01
  • Swift 5.1 之類(lèi)型轉(zhuǎn)換與模式匹配的教程詳解

    Swift 5.1 之類(lèi)型轉(zhuǎn)換與模式匹配的教程詳解

    這篇文章主要介紹了Swift 5.1 之類(lèi)型轉(zhuǎn)換與模式匹配的相關(guān)知識(shí),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-05-05
  • Swift算法實(shí)現(xiàn)字符串轉(zhuǎn)數(shù)字的方法示例

    Swift算法實(shí)現(xiàn)字符串轉(zhuǎn)數(shù)字的方法示例

    最近學(xué)完了swift想著實(shí)踐下,就通過(guò)一些簡(jiǎn)單的算法進(jìn)行學(xué)習(xí)研究,下面這篇文章主要介紹了Swift算法實(shí)現(xiàn)字符串轉(zhuǎn)數(shù)字的方法,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。
    2017-03-03
  • 利用Swift實(shí)現(xiàn)一個(gè)響應(yīng)式編程庫(kù)

    利用Swift實(shí)現(xiàn)一個(gè)響應(yīng)式編程庫(kù)

    最近在學(xué)習(xí)swift,最近有空所以總結(jié)一下最近學(xué)習(xí)的內(nèi)容,下面這篇文章主要給大家介紹了關(guān)于利用Swift實(shí)現(xiàn)一個(gè)響應(yīng)式編程庫(kù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。
    2017-12-12
  • 程序遷移到swift 3.0的一些實(shí)用技巧

    程序遷移到swift 3.0的一些實(shí)用技巧

    Swift項(xiàng)目為適配iOS10,無(wú)奈只能更新Xcode 8 ,可是發(fā)現(xiàn)一入3.0深似海,從此幸福是路人。于是邊摸索邊修改,終于完成了代碼遷移。節(jié)后在完成手頭工作后,整理思路把Swift3.0遷移的一些實(shí)用技巧分享大家,需要的朋友可以參考下。
    2017-03-03
  • SwiftUI智能家居開(kāi)關(guān)燈頁(yè)面搭建示例

    SwiftUI智能家居開(kāi)關(guān)燈頁(yè)面搭建示例

    這篇文章主要為大家介紹了SwiftUI智能家居開(kāi)關(guān)燈頁(yè)面搭建示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-08-08

最新評(píng)論