使用Swift Package Manager在单元测试中使用资源

时间:2017-11-08 10:18:54

标签: swift swift-package-manager

我正在尝试在单元测试中使用资源文件并使用Bundle.path访问它,但它返回nil。

MyProjectTests.swift中的此调用返回nil:

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

这是我的项目层次结构。我还尝试将TestAudio.m4a移到Resources文件夹:

├── 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描述API版本4.

8 个答案:

答案 0 :(得分:18)

Swift 5.3

Swift 5.3包含Package Manager Resources SE-0271演变建议,并带有“状态:已实施(Swift 5.3)”。 :-)

资源并不总是供软件包的客户使用;资源的一种使用可能包括仅单元测试所需的测试装置。这些资源不会与库代码一起合并到软件包的客户端中,而只会在运行软件包的测试时使用。

  • resourcestarget API中添加新的testTarget参数,以允许显式声明资源文件。

SwiftPM使用文件系统约定来确定属于包中每个目标的源文件集:具体地说,目标的源文件是位于目标的指定“目标目录”下的源文件。默认情况下,此目录与目标名称相同,位于“源”(对于常规目标)或“测试”(对于测试目标)中,但是可以在软件包清单中自定义此位置。 / p>

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

示例

// swift-tools-version:5.3
import PackageDescription

  targets: [
    .target(
      name: "Example",
      dependencies: [],
      resources: [
        // Apply platform-specific rules.
        // For example, images might be optimized per specific platform rule.
        // If path is a directory, the rule is applied recursively.
        // By default, a file will be copied if no rule applies.
        // Process file in Sources/Example/Resources/*
        .process("Resources"),
      ]),
    .testTarget(
      name: "ExampleTests",
      dependencies: [Example],
      resources: [
        // Copy Tests/ExampleTests/Resources directories as-is. 
        // Use to retain directory structure.
        // Will be at top level in bundle.
        .copy("Resources"),
      ]),

报告的问题和可能的解决方法

Xcode

Bundle.module由SwiftPM生成(请参见Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()),因此在Xcode构建时,它们不会出现在Foundation.Bundle中。

在Xcode中类似的方法是将一个Resources参考文件夹手动添加到Xcode项目中,添加一个Xcode构建阶段copy来将Resource放入某些{{1} }目录,并为Xcode构建添加一些自定义*.bundle编译器指令以使用资源。

#ifdef XCODE_BUILD

答案 1 :(得分:13)

SwiftPM(5.1)本身不支持资源yet,但是...

运行单元测试时,可以期望该存储库可用,因此只需使用从#file派生的内容加载资源。这适用于所有现有的SwiftPM版本。

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

在测试以外的情况下,运行时存储库将不存在,但仍然可以包括资源,尽管以二进制大小为代价。通过将任意文件表示为字符串文字中的base 64数据,可以将其嵌入到Swift源代码中。 Workspace是一个开源工具,可以使该过程自动化:$ workspace refresh resources(免责声明:我是它的作者。)

答案 2 :(得分:6)

目前,swift包管理器(SPM)无法处理资源,这是SPM的错误跟踪系统https://bugs.swift.org/browse/SR-2866中打开的问题。

在SPM中实现对资源的支持之前,我会将资源复制到结果可执行文件在运行时期望资源的位置。您可以通过打印Bundle.resourcePath属性来了解这些位置。我会使用Makefile自动执行此复制。这样Makefile就变成了#34; build orchestrator"在SPM之上。

我写了一个例子来演示这种方法在MacOS和Linux上是如何工作的 - https://github.com/vadimeisenbergibm/SwiftResourceHandlingExample

用户将运行make命令:make buildmake test而不是swift buildswift test。 Make会将资源复制到预期的位置(在MacOS和Linux上,在运行期间和测试期间不同)。

答案 3 :(得分:2)

我找到了另一个解决this file的解决方案。

可以创建一个包含路径的包,例如:

List<object> myList = myvalues.Cast<object>().ToList();
int found = myList.IndexOf("Foo");

这有点难看,但如果你想避免使用Makefile,它就可以了。

答案 4 :(得分:2)

从Swift 5.3开始,多亏了SE-0271,您可以通过在resources声明中添加.target在swift软件包管理器上添加捆绑资源。

示例:

.target(
   name: "HelloWorldProgram",
   dependencies: [], 
   resources: [.process(Images), .process("README.md")]
)

如果您想了解更多信息,我在medium上写了一篇文章,讨论了该主题。我没有专门讨论.testTarget,但是看一下快速的建议,看起来很像。

答案 5 :(得分:1)

Swift Package Manager(SPM)4.2

Swift Package Manager PackageDescription 4.2引入了对local dependencies的支持。

  

本地依赖项是磁盘上的程序包,可以使用它们的路径直接引用。仅在根包中允许本地依赖,并且它们将覆盖包图中具有相同名称的所有依赖项。

注意:我希望,但尚未测试,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和ealier

可以使用Swift Package Manager为macOS和Linux在单元测试中使用资源,并提供一些额外的设置和自定义脚本。以下是一种可能方法的描述:

Swift Package Manager尚未提供处理资源的机制。以下是在包中使用测试资源TestResources/的可行方法;并且,如果需要,还提供用于创建测试文件的一致TestScratch/目录。

设定:

  • TestResources/目录中添加测试资源目录PackageName/
  • 对于Xcode使用,将测试资源添加到测试包目标的“Build Phases”项目中。

    • 项目编辑器&gt;目标&gt; CxSQLiteFrameworkTests&gt;构建阶段&gt;复制文件:目标资源,+添加文件
  • 对于命令行使用,设置包含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.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()

测试实用程序代码

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

文件位置:

<强>的Linux

swift buildswift test期间,流程环境变量PWD提供了包根…/PackageName的路径。 PackageName/TestResources/个文件已复制到$PWD/.buid/TestResourcesTestScratch/目录(如果在测试运行时期间使用)是在$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/文件作为Build Phases的一部分复制到测试包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)

我还在004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref

发布了同样方法的GitHubGist

答案 6 :(得分:0)

请注意,使用 .copy(…) 资源时可能存在一些错误。我无法编译它 - 与代码签名有关。不过,.process(…) 确实有效,而且它非常完美,因为我不必再担心文件夹结构(因为它可以将其全部展平)。

答案 7 :(得分:-1)

一个简单的解决方案适用于传统的swift和未来的swift:

  1. 将资产添加到项目的根目录
  2. 在您的快速代码中:ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path
  3. 在Xcode中工作并在终端或github操作中快速构建? https://eon.codes/blog/2020/01/04/How-to-include-assets-with-swift-package-manager/https://github.com/eonist/ResourceHelper/