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

Hug*_*l31 10 swift swift-package-manager

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

MyProjectTests.swift中的这个调用返回nil:

Bundle(for: type(of: self)).path(forResource: "TestAudio", ofType: "m4a")
Run Code Online (Sandbox Code Playgroud)

这是我的项目层次结构.我也尝试过移动TestAudio.m4a到一个Resources文件夹:

??? Package.swift
??? Sources
?   ??? MyProject
?       ??? ...
??? Tests
    ??? MyProjectTests
        ??? MyProjectTests.swift
        ??? TestAudio.m4a
Run Code Online (Sandbox Code Playgroud)

这是我的包装说明:

// 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"]
        ),
    ]
)
Run Code Online (Sandbox Code Playgroud)

我正在使用Swift 4和Swift Package Manager Description API版本4.

l -*_*c l 33

斯威夫特 5.3

请参阅 Apple 文档:“使用 Swift 包捆绑资源”

Swift 5.3 包含包管理器资源 SE-0271进化提案,其中包含“状态:已实施(Swift 5.3) ”。:-)

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

  • resourcestargettestTargetAPI 中添加一个新参数以允许显式声明资源文件。

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)
Run Code Online (Sandbox Code Playgroud)

例子

// 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"),
      ]),
Run Code Online (Sandbox Code Playgroud)

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

Xcode

Bundle.module由 SwiftPM 生成(请参阅Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()),因此在由 Xcode 构建时不存在于Foundation.Bundle 中

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

#if XCODE_BUILD
extension Foundation.Bundle {
    
    /// Returns resource bundle as a `Bundle`.
    /// Requires Xcode copy phase to locate files into `ExecutableName.bundle`;
    /// or `ExecutableNameTests.bundle` for test resources
    static var module: Bundle = {
        var thisModuleName = "CLIQuickstartLib"
        var url = Bundle.main.bundleURL
        
        for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
            url = bundle.bundleURL.deletingLastPathComponent()
            thisModuleName = thisModuleName.appending("Tests")
        }
        
        url = url.appendingPathComponent("\(thisModuleName).bundle")
        
        guard let bundle = Bundle(url: url) else {
            fatalError("Foundation.Bundle.module could not load resource bundle: \(url.path)")
        }
        
        return bundle
    }()
    
    /// Directory containing resource bundle
    static var moduleDir: URL = {
        var url = Bundle.main.bundleURL
        for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
            // remove 'ExecutableNameTests.xctest' path component
            url = bundle.bundleURL.deletingLastPathComponent()
        }
        return url
    }()
    
}
#endif
Run Code Online (Sandbox Code Playgroud)

  • 讽刺的是,“#if Xcode”现在是为 SwiftPM 定义的。它需要是一个自定义标志,例如“#if NOT_IN_SPM”。Apple确实需要找到一种解决方案,允许Bundle.module同时出现在常规Xcode项目和SPM中 (2认同)
  • 查找复制资源的更好方法是 `Bundle.module.url(forResource: "filename", withExtension: nil, subdirectory: "Resources")`。这涉及不同平台上生成的不同捆绑布局。 (2认同)

Ram*_*mis 32

在正确的文件结构和依赖项设置之后, Bundle.module开始为我工作。

测试目标的文件结构:

在此输入图像描述

Package.swift 中的依赖项设置:

targets: [
    // Targets are the basic building blocks of a package. A target can define a module or a test suite.
    // Targets can depend on other targets in this package, and on products in packages this package depends on.
    .target(
        name: "Parser",
        dependencies: []),
    .testTarget(
        name: "ParserTests",
        dependencies: ["Parser"],
        resources: [
            .copy("Resources/test.txt")
        ]
    ),
]
Run Code Online (Sandbox Code Playgroud)

项目中的使用:

private var testData: Data {
    let url = Bundle.module.url(forResource: "test", withExtension: "txt")!
    let data = try! Data(contentsOf: url)
    return data
}
Run Code Online (Sandbox Code Playgroud)

  • 请注意,除非您添加*可以在 Package.swift 中找到*的资源,否则“Bundle.module”不会存在。 (6认同)
  • 这是正确的答案(在 Swift 5.5 上测试)。Hugal31 你能更新“正确答案”徽章吗? (4认同)

Jer*_*cht 9

SwiftPM(5.1)不支持原生资源,但是...

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

let thisSourceFile = URL(fileURLWithPath: #file)
let thisDirectory = thisSourceFile.deletingLastPathComponent()
let resourceURL = thisDirectory.appendingPathComponent("TestAudio.m4a")
Run Code Online (Sandbox Code Playgroud)

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


Vad*_*erg 6

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

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

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

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