web-dev-qa-db-ja.com

Swift PackageManagerを使用した単体テストでリソースを使用する

単体テストでリソースファイルを使用し、Bundle.pathでアクセスしようとしていますが、nilが返されます。

MyProjectTests.Swiftでのこの呼び出しは、nilを返します。

Bundle(for: type(of: self)).path(forResource: "TestAudio", ofType: "m4a")

これが私のプロジェクト階層です。また、TestAudio.m4aResourcesフォルダーに移動してみました。

├── Package.Swift
├── Sources
│   └── MyProject
│       ├── ...
└── Tests
    └── MyProjectTests
        ├── MyProjectTests.Swift
        └── TestAudio.m4a

これが私のパッケージの説明です:

// Swift-tools-version:4.0

import PackageDescription

let package = Package(
    name: "MyProject",
    products: [
        .library(
            name: "MyProject",
            targets: ["MyProject"])
    ],
    targets: [
        .target(
            name: "MyProject",
            dependencies: []
        ),
        .testTarget(
            name: "MyProjectTests",
            dependencies: ["MyProject"]
        ),
    ]
)

Swift 4とSwift Package Manager DescriptionAPIバージョン4を使用しています。

15
Hugal31

現在、Swiftパッケージマネージャー(SPM)はリソースを処理しません。これは、SPMのバグ追跡システムで発生している問題です https://bugs.Swift.org/browse/SR -2866

リソースのサポートがSPMに実装されるまで、結果の実行可能ファイルが実行時にリソースを期待する場所にリソースをコピーします。 Bundle.resourcePathプロパティを出力することで、これらの場所を知ることができます。たとえばMakefileを使用して、このコピーを自動的に実行します。このようにして、MakefileはSPMの上に「ビルドオーケストレーター」になります。

このアプローチがMacOSとLinuxでどのように機能するかを示す例を作成しました- https://github.com/vadimeisenbergibm/SwiftResourceHandlingExample

ユーザーは、make buildおよびmake testの代わりにSwift buildおよびSwift testのmakeコマンドを実行します。 Makeは、リソースを予想される場所にコピーします(MacOSとLinuxでは、実行中とテスト中に異なります)。

6
Vadim Eisenberg

SwiftPM(5.1)は、リソースをネイティブにサポートしていません まだ ただし...

単体テストの実行中は、リポジトリが使用可能であることが期待できるため、#fileから派生したものをリソースにロードするだけです。これは、SwiftPMの現存するすべてのバージョンで機能します。

let thisSourceFile = URL(fileURLWithPath: #file)
let thisDirectory = thisSourceFile.deletingLastPathComponent()
let resourceURL = thisDirectory.appendingPathComponent("TestAudio.m4a")

テスト以外の場合、実行時にリポジトリが存在しない場合でも、バイナリサイズは犠牲になりますが、リソースを含めることができます。任意のファイルをSwiftソースに文字列リテラルでbase 64データとして表現することで埋め込むことができます。 Workspace は、そのプロセスを自動化できるオープンソースツールです。 $ workspace refresh resources。 (免責事項:私はその作者です。)

このファイル を見て別の解決策を見つけました。

次に、パスを使用してバンドルを作成することができます。

let currentBundle = Bundle.allBundles.filter() { $0.bundlePath.hasSuffix(".xctest") }.first!
let realBundle = Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/MyProjectTests/Resources")

少し醜いですが、Makefileを避けたい場合は機能します。

2
Hugal31

Swiftパッケージマネージャー(SPM)4.2

Swift Package Manager PackageDescription4.2ローカル依存関係 のサポートを導入します。

ローカル依存関係は、パスを使用して直接参照できるディスク上のパッケージです。ローカル依存関係はルートパッケージでのみ許可され、パッケージグラフ内の同じ名前のすべての依存関係を上書きします。

注:SPM 4.2では、次のようなことが可能になると思いますが、まだテストしていません:

// Swift-tools-version:4.2
import PackageDescription

let package = Package(
    name: "MyPackageTestResources",
    dependencies: [
        .package(path: "../test-resources"),
    ],
    targets: [
        // ...
        .testTarget(
            name: "MyPackageTests",
            dependencies: ["MyPackage", "MyPackageTestResources"]
        ),
    ]
)

Swift Package Manager(SPM)4.1以降

Swift Package Manager for macOSとLinuxの両方で、いくつかの追加のセットアップとカスタムスクリプトを使用して、単体テストでリソースを使用することができます。1つの可能なアプローチの説明を次に示します。

Swift Package Managerは、リソースを処理するためのメカニズムをまだ提供していません。以下は、パッケージ内でテストリソースTestResources/を使用するための実行可能なアプローチです。 、必要に応じてテストファイルを作成するための一貫したTestScratch/ディレクトリも提供します。

セットアップ:

  • TestResources/ディレクトリにテストリソースディレクトリPackageName/を追加します。
  • Xcodeを使用するには、テストバンドルターゲットのプロジェクト「ビルドフェーズ」にテストリソースを追加します。

    • プロジェクトエディタ>ターゲット> CxSQLiteFrameworkTests>ビルドフェーズ>ファイルのコピー:宛先リソース、+ファイルの追加
  • コマンドラインで使用するには、Swift-copy-testresources.Swiftを含むBashエイリアスを設定します

  • 実行可能バージョンのSwift-copy-testresources.Swiftを、$ PATHに含まれる適切なパスに配置します。
    • Ubuntu:nano ~/bin/ Swift-copy-testresources.Swift

Bashエイリアス

macOS:nano .bash_profile

alias swiftbuild='Swift-copy-testresources.Swift $PWD; Swift build -Xswiftc "-target" -Xswiftc "x86_64-Apple-macosx10.13";'
alias swifttest='Swift-copy-testresources.Swift $PWD; Swift test -Xswiftc "-target" -Xswiftc "x86_64-Apple-macosx10.13";'
alias swiftxcode='Swift package generate-xcodeproj --xcconfig-overrides Package.xcconfig; echo "REMINDER: set Xcode build system."'

Ubuntu:nano ~/.profile。最後に追加します。/opt/Swift/currentをSwiftが特定のシステムにインストールされている場所)に変更します。

#############
### Swift ###
#############
if [ -d "/opt/Swift/current/usr/bin" ] ; then
    PATH="/opt/Swift/current/usr/bin:$PATH"
fi

alias swiftbuild='Swift-copy-testresources.Swift $PWD; Swift build;'
alias swifttest='Swift-copy-testresources.Swift $PWD; Swift test;'

スクリプト:Swift-copy-testresources.shchmod +x

#!/usr/bin/Swift

// FILE: Swift-copy-testresources.sh
// verify Swift path with "which -a Swift"
// macOS: /usr/bin/Swift 
// Ubuntu: /opt/Swift/current/usr/bin/Swift 
import Foundation

func copyTestResources() {
    let argv = ProcessInfo.processInfo.arguments
    // for i in 0..<argv.count {
    //     print("argv[\(i)] = \(argv[i])")
    // }
    let pwd = argv[argv.count-1]
    print("Executing Swift-copy-testresources")
    print("  PWD=\(pwd)")

    let fm = FileManager.default

    let pwdUrl = URL(fileURLWithPath: pwd, isDirectory: true)
    let srcUrl = pwdUrl
        .appendingPathComponent("TestResources", isDirectory: true)
    let buildUrl = pwdUrl
        .appendingPathComponent(".build", isDirectory: true)
    let dstUrl = buildUrl
        .appendingPathComponent("Contents", isDirectory: true)
        .appendingPathComponent("Resources", isDirectory: true)

    do {
        let contents = try fm.contentsOfDirectory(at: srcUrl, includingPropertiesForKeys: [])
        do { try fm.removeItem(at: dstUrl) } catch { }
        try fm.createDirectory(at: dstUrl, withIntermediateDirectories: true)
        for fromUrl in contents {
            try fm.copyItem(
                at: fromUrl, 
                to: dstUrl.appendingPathComponent(fromUrl.lastPathComponent)
            )
        }
    } catch {
        print("  SKIP TestResources not copied. ")
        return
    }

    print("  SUCCESS TestResources copy completed.\n  FROM \(srcUrl)\n  TO \(dstUrl)")
}

copyTestResources()

テストユーティリティコード

//////////////// //マーク:-Linux //////////////// #if os(Linux)

// /PATH_TO_PACKAGE/PackageName/.build/TestResources
func getTestResourcesUrl() -> URL? {
    guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
        else { return nil }
    let packageUrl = URL(fileURLWithPath: packagePath)
    let testResourcesUrl = packageUrl
        .appendingPathComponent(".build", isDirectory: true)
        .appendingPathComponent("TestResources", isDirectory: true)
    return testResourcesUrl
} 

// /PATH_TO_PACKAGE/PackageName/.build/TestScratch
func getTestScratchUrl() -> URL? {
    guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
        else { return nil }
    let packageUrl = URL(fileURLWithPath: packagePath)
    let testScratchUrl = packageUrl
        .appendingPathComponent(".build")
        .appendingPathComponent("TestScratch")
    return testScratchUrl
}

// /PATH_TO_PACKAGE/PackageName/.build/TestScratch
func resetTestScratch() throws {
    if let testScratchUrl = getTestScratchUrl() {
        let fm = FileManager.default
        do {_ = try fm.removeItem(at: testScratchUrl)} catch {}
        _ = try fm.createDirectory(at: testScratchUrl, withIntermediateDirectories: true)
    }
}

///////////////////
// MARK: - macOS
///////////////////
#elseif os(macOS)

func isXcodeTestEnvironment() -> Bool {
    let arg0 = ProcessInfo.processInfo.arguments[0]
    // Use arg0.hasSuffix("/usr/bin/xctest") for command line environment
    return arg0.hasSuffix("/Xcode/Agents/xctest")
}

// /PATH_TO/PackageName/TestResources
func getTestResourcesUrl() -> URL? {
    let testBundle = Bundle(for: CxSQLiteFrameworkTests.self)
    let testBundleUrl = testBundle.bundleURL

    if isXcodeTestEnvironment() { // test via Xcode 
        let testResourcesUrl = testBundleUrl
            .appendingPathComponent("Contents", isDirectory: true)
            .appendingPathComponent("Resources", isDirectory: true)
        return testResourcesUrl            
    }
    else { // test via command line
        guard let packagePath = ProcessInfo.processInfo.environment["PWD"]
            else { return nil }
        let packageUrl = URL(fileURLWithPath: packagePath)
        let testResourcesUrl = packageUrl
            .appendingPathComponent(".build", isDirectory: true)
            .appendingPathComponent("TestResources", isDirectory: true)
        return testResourcesUrl
    }
} 

func getTestScratchUrl() -> URL? {
    let testBundle = Bundle(for: CxSQLiteFrameworkTests.self)
    let testBundleUrl = testBundle.bundleURL
    if isXcodeTestEnvironment() {
        return testBundleUrl
            .deletingLastPathComponent()
            .appendingPathComponent("TestScratch")
    }
    else {
        return testBundleUrl
            .deletingLastPathComponent()
            .deletingLastPathComponent()
            .deletingLastPathComponent()
            .appendingPathComponent("TestScratch")
    }
}

func resetTestScratch() throws {
    if let testScratchUrl = getTestScratchUrl() {
        let fm = FileManager.default
        do {_ = try fm.removeItem(at: testScratchUrl)} catch {}
        _ = try fm.createDirectory(at: testScratchUrl, withIntermediateDirectories: true)
    }
}

#endif

ファイルの場所:

Linux

Swift buildおよびSwift testの間、プロセス環境変数PWDは、パッケージルート…/PackageNameのパスを提供します。 PackageName/TestResources/ファイルは$PWD/.buid/TestResourcesにコピーされます。 TestScratch/ディレクトリは、テストの実行時に使用される場合、$PWD/.buid/TestScratchに作成されます。

.build/
├── debug -> x86_64-unknown-linux/debug
...
├── TestResources
│   └── SomeTestResource.sql      <-- (copied from TestResources/)
├── TestScratch
│   └── SomeTestProduct.sqlitedb  <-- (created by running tests)
└── x86_64-unknown-linux
    └── debug
        ├── PackageName.build/
        │   └── ...
        ├── PackageNamePackageTests.build
        │   └── ...
        ├── PackageNamePackageTests.swiftdoc
        ├── PackageNamePackageTests.swiftmodule
        ├── PackageNamePackageTests.xctest  <-- executable, not Bundle
        ├── PackageName.swiftdoc
        ├── PackageName.swiftmodule
        ├── PackageNameTests.build
        │   └── ...
        ├── PackageNameTests.swiftdoc
        ├── PackageNameTests.swiftmodule
        └── ModuleCache ...

macOS CLI

.build/
|-- TestResources/
|   `-- SomeTestResource.sql      <-- (copied from TestResources/)
|-- TestScratch/
|   `-- SomeTestProduct.sqlitedb  <-- (created by running tests)
...
|-- debug -> x86_64-Apple-macosx10.10/debug
`-- x86_64-Apple-macosx10.10
    `-- debug
        |-- PackageName.build/
        |-- PackageName.swiftdoc
        |-- PackageName.swiftmodule
        |-- PackageNamePackageTests.xctest
        |   `-- Contents
        |       `-- MacOS
        |           |-- PackageNamePackageTests
        |           `-- PackageNamePackageTests.dSYM
        ...
        `-- libPackageName.a

macOS Xcode

PackageName/TestResources/ファイルは、ビルドフェーズの一部としてテストバンドルContents/Resourcesフォルダーにコピーされます。テスト中に使用する場合、TestScratch/*xctestバンドルの横に配置されます。

Build/Products/Debug/
|-- PackageNameTests.xctest/
|   `-- Contents/
|       |-- Frameworks/
|       |   |-- ...
|       |   `-- libswift*.dylib
|       |-- Info.plist
|       |-- MacOS/
|       |   `-- PackageNameTests
|       `-- Resources/               <-- (aka TestResources/)
|           |-- SomeTestResource.sql <-- (copied from TestResources/)
|           `-- libswiftRemoteMirror.dylib
`-- TestScratch/
    `-- SomeTestProduct.sqlitedb     <-- (created by running tests)

また、これと同じアプローチのGitHubGistを 4.4'2 SW Dev Swift Package Manager(SPM)With Resources Qref に投稿しました。

2
l --marc l

Swift 5.3には Package Manager Resources SE-0271 "Status:Implemented(Swift 5.3)"の進化提案が含まれています。 :-)

リソースは、パッケージのクライアントによる使用を常に意図しているわけではありません。リソースの1つの使用には、単体テストでのみ必要なテストフィクスチャが含まれる場合があります。このようなリソースは、ライブラリコードとともにパッケージのクライアントに組み込まれることはなく、パッケージのテストの実行中にのみ使用されます。

  • resourcesおよびtargetAPIに新しいtestTargetパラメータを追加して、リソースファイルを明示的に宣言できるようにします。

SwiftPMは、ファイルシステムの規則を使用して、パッケージ内の各ターゲットに属するソースファイルのセットを決定します。具体的には、ターゲットのソースファイルは、ターゲットの指定された「ターゲットディレクトリ」の下にあるファイルです。デフォルトでは、これはターゲットと同じ名前のディレクトリであり、「Sources」(通常のターゲットの場合)または「Tests」(テストターゲットの場合)にありますが、この場所はパッケージマニフェストでカスタマイズできます。

いくつかの例:

// Get path to DefaultSettings.plist file.
let path = Bundle.module.path(forResource: "DefaultSettings", ofType: "plist")

// Load an image that can be in an asset archive in a bundle.
let image = UIImage(named: "MyIcon", in: Bundle.module, compatibleWith: UITraitCollection(userInterfaceStyle: .dark))

// Find a vertex function in a compiled Metal shader library.
let shader = try mtlDevice.makeDefaultLibrary(bundle: Bundle.module).makeFunction(name: "vertexShader")

// Load a texture.
let texture = MTKTextureLoader(device: mtlDevice).newTexture(name: "Grass", scaleFactor: 1.0, bundle: Bundle.module, options: options)
0
l --marc l