Use resources in unit tests with Swift Package Manager

本小妞迷上赌 提交于 2019-12-03 06:45:23
Vadim Eisenberg

Currently, swift package manager (SPM) doesn't handle resources, here is an issue open in SPM's bug tracking system https://bugs.swift.org/browse/SR-2866.

Until support for resources is implemented in SPM, I would copy resources to the locations where the result executable expects resources to be during runtime. You can know these locations by printing Bundle.resourcePath property. I would perform this copying automatically, using a Makefile for example. This way Makefile becomes a "build orchestrator" on top of SPM.

I wrote an example to demonstrate how this approach works on MacOS and Linux -https://github.com/vadimeisenbergibm/SwiftResourceHandlingExample.

The user would run make commands: make build and make test instead of swift build and swift test. Make will copy the resources to the expected locations (different on MacOS and Linux, during run and during tests).

SwiftPM (5.1) does not support resources natively yet, however...

When unit tests are running, the repository can be expected to be available, so simply load the resource with something derived from #file. This works with all extant versions of SwiftPM.

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

In cases other than tests, where the repository will not be around at runtime, resources can still be included, albeit at the expense of the binary size. Any arbitrary file can be embedded into Swift source by expressing it as base 64 data in a string literal. Workspace is an open‐source tool that can automate that process: $ workspace refresh resources. (Disclaimer: I am its author.)

I found another solution looking at this file.

It's possible to create a bundle with a path, for example:

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

It's a bit ugly, but if you want to avoid a Makefile, it works.

Swift Package Manager (SPM) 4.2

Swift Package Manager PackageDescription 4.2 introduces support of local dependencies.

Local dependencies are packages on disk that can be referred directly using their paths. Local dependencies are only allowed in the root package and they override all dependencies with same name in the package graph.

Note: I expect, but have not yet tested, that something like the following should be possible with 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 and ealier

It is possible to use resources in unit tests with Swift Package Manager for both macOS and Linux with some additional setup and custom scripts. Here is a description of one possible approach:

The Swift Package Manager does not yet provide a mechanism for handling resources. The following is a workable approach for using test resources TestResources/ within a package; and, also provides for a consistent TestScratch/ directory for creating test files if needed.

Setup:

  • Add test resources directory TestResources/ in the PackageName/ directory.
  • For Xcode use, add test resources to project "Build Phases" for the test bundle target.

    • Project Editor > TARGETS > CxSQLiteFrameworkTests > Build Phases > Copy Files: Destination Resources, + add files
  • For command line use, set up Bash aliases which include swift-copy-testresources.swift

  • Place an executable version of swift-copy-testresources.swift on an appropriate path which is included $PATH.
    • Ubuntu: nano ~/bin/ swift-copy-testresources.swift

Bash Aliases

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. Apppend to end. Change /opt/swift/current to where Swift is installed for a given system.

#############
### 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;'

Script: swift-copy-testresources.sh chmod +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()

Test Utility Code

//////////////// // MARK: - 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

File Locations:

Linux

During the swift build and swift test the process environment variable PWD provides a path the package root …/PackageName. The PackageName/TestResources/ files are copied to $PWD/.buid/TestResources. The TestScratch/ directory, if used during test runtime, is created in $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/ files are copied into the test bundle Contents/Resources folder as part of the Build Phases. If used during tests, TestScratch/ is placed alongside the *xctest bundle.

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)

I also posted a GitHubGist of this same approach at 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!