使用 Swift 包管理器在单元测试中使用资源
Use resources in unit tests with Swift Package Manager
我试图在单元测试中使用资源文件并使用 Bundle.path
访问它,但它 returns 没有。
这个调用在 MyProjectTests.swift returns 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 包管理器描述 API 版本 4。
我在 this file 中找到了另一个解决方案。
可以创建带有路径的包,例如:
let currentBundle = Bundle.allBundles.filter() { [=10=].bundlePath.hasSuffix(".xctest") }.first!
let realBundle = Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/MyProjectTests/Resources")
它有点难看,但如果你想避免 Makefile,它是可行的。
用于 Swift 5.2 及更早版本的 Swift 脚本方法...
Swift 包管理器 (SwiftPM)
通过一些额外的设置和自定义脚本,可以在 macOS 和 Linux 的 SwiftPM 单元测试中使用资源。以下是对一种可能方法的描述:
SwiftPM 还没有提供处理资源的机制。以下是在包中使用测试资源 TestResources/
的可行方法;并且,还提供了一个一致的 TestScratch/
目录,用于在需要时创建测试文件。
设置:
在PackageName/
目录下添加测试资源目录TestResources/
为了 Xcode 使用,将测试资源添加到测试包目标的项目“Build Phases”。
- 项目编辑器 > 目标 > CxSQLiteFrameworkTests > 构建阶段 > 复制文件:目标资源,
+
添加文件
对于命令行使用,设置 Bash 别名,其中包括 swift-copy-testresources.swift
将 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 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)
我还在 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref
上发布了同样方法的 GitHubGist
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
。 (免责声明:我是它的作者。)
Swift 5.3
查看 Apple 文档:"Bundling Resources with a Swift Package"
Swift 5.3 包括 Package Manager Resources SE-0271 进化提案,“状态:已实施(Swift 5.3)”。
Resources aren't always intended for use by clients of the package; one use of resources might include test fixtures that are only needed by unit tests. Such resources would not be incorporated into clients of the package along with the library code, but would only be used while running the package's tests.
- Add a new
resources
parameter in target
and testTarget
APIs to allow declaring resource files explicitly.
SwiftPM uses file system conventions for determining the set of source files that belongs to each target in a package: specifically, a target's source files are those that are located underneath the designated "target directory" for the target. By default this is a directory that has the same name as the target and is located in "Sources" (for a regular target) or "Tests" (for a test target), but this location can be customized in the package manifest.
// 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"),
]),
报告的问题和可能的解决方法
- Swift 5.3 SPM Resources in tests uses wrong bundle path?
- Swift Package Manager - Resources in test targets
Xcode
Bundle.module
由 SwiftPM 生成(见 Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) and thus not present in Foundation.Bundle 由 Xcode 生成。
Xcode 中的一种类似方法是手动将 Resources
参考文件夹添加到 Xcode 项目,添加 Xcode 构建阶段 copy
将 Resource
放入某个 *.bundle
目录,并为 Xcode 构建添加一些自定义 #ifdef XCODE_BUILD
编译器指令以使用资源。
#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
从 Swift 5.3 开始,感谢 SE-0271,您可以通过在 .target
声明中添加 resources
来在 swift 包管理器上添加包资源.
示例:
.target(
name: "HelloWorldProgram",
dependencies: [],
resources: [.process(Images), .process("README.md")]
)
如果你想了解更多,我在medium上写了一篇文章,讨论这个话题。我不专门讨论 .testTarget
,但看一下 swift 提案,它看起来很像。
A 提出了一个适用于旧版 swift 和未来 swift 的简单解决方案:
- 将资产添加到项目的根目录中
- 在您的 swift 代码中:
ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path
- 适用于 Xcode 和 swift 内置终端或 github 操作
https://eon.codes/blog/2020/01/04/How-to-include-assets-with-swift-package-manager/ and https://github.com/eonist/ResourceHelper/
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")
]
),
]
在项目中的使用:
private var testData: Data {
let url = Bundle.module.url(forResource: "test", withExtension: "txt")!
let data = try! Data(contentsOf: url)
return data
}
我试图在单元测试中使用资源文件并使用 Bundle.path
访问它,但它 returns 没有。
这个调用在 MyProjectTests.swift returns 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 包管理器描述 API 版本 4。
我在 this file 中找到了另一个解决方案。
可以创建带有路径的包,例如:
let currentBundle = Bundle.allBundles.filter() { [=10=].bundlePath.hasSuffix(".xctest") }.first!
let realBundle = Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/MyProjectTests/Resources")
它有点难看,但如果你想避免 Makefile,它是可行的。
用于 Swift 5.2 及更早版本的 Swift 脚本方法...
Swift 包管理器 (SwiftPM)
通过一些额外的设置和自定义脚本,可以在 macOS 和 Linux 的 SwiftPM 单元测试中使用资源。以下是对一种可能方法的描述:
SwiftPM 还没有提供处理资源的机制。以下是在包中使用测试资源 TestResources/
的可行方法;并且,还提供了一个一致的 TestScratch/
目录,用于在需要时创建测试文件。
设置:
在
PackageName/
目录下添加测试资源目录TestResources/
为了 Xcode 使用,将测试资源添加到测试包目标的项目“Build Phases”。
- 项目编辑器 > 目标 > CxSQLiteFrameworkTests > 构建阶段 > 复制文件:目标资源,
+
添加文件
- 项目编辑器 > 目标 > CxSQLiteFrameworkTests > 构建阶段 > 复制文件:目标资源,
对于命令行使用,设置 Bash 别名,其中包括 swift-copy-testresources.swift
将 swift-copy-testresources.swift 的可执行版本放在包含 $PATH.
的适当路径上- Ubuntu:
nano ~/bin/ swift-copy-testresources.swift
- Ubuntu:
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 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)
我还在 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref
上发布了同样方法的 GitHubGistSwiftPM (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
。 (免责声明:我是它的作者。)
Swift 5.3
查看 Apple 文档:"Bundling Resources with a Swift Package"
Swift 5.3 包括 Package Manager Resources SE-0271 进化提案,“状态:已实施(Swift 5.3)”。
Resources aren't always intended for use by clients of the package; one use of resources might include test fixtures that are only needed by unit tests. Such resources would not be incorporated into clients of the package along with the library code, but would only be used while running the package's tests.
- Add a new
resources
parameter intarget
andtestTarget
APIs to allow declaring resource files explicitly.SwiftPM uses file system conventions for determining the set of source files that belongs to each target in a package: specifically, a target's source files are those that are located underneath the designated "target directory" for the target. By default this is a directory that has the same name as the target and is located in "Sources" (for a regular target) or "Tests" (for a test target), but this location can be customized in the package manifest.
// 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"),
]),
报告的问题和可能的解决方法
- Swift 5.3 SPM Resources in tests uses wrong bundle path?
- Swift Package Manager - Resources in test targets
Xcode
Bundle.module
由 SwiftPM 生成(见 Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) and thus not present in Foundation.Bundle 由 Xcode 生成。
Xcode 中的一种类似方法是手动将 Resources
参考文件夹添加到 Xcode 项目,添加 Xcode 构建阶段 copy
将 Resource
放入某个 *.bundle
目录,并为 Xcode 构建添加一些自定义 #ifdef XCODE_BUILD
编译器指令以使用资源。
#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
从 Swift 5.3 开始,感谢 SE-0271,您可以通过在 .target
声明中添加 resources
来在 swift 包管理器上添加包资源.
示例:
.target(
name: "HelloWorldProgram",
dependencies: [],
resources: [.process(Images), .process("README.md")]
)
如果你想了解更多,我在medium上写了一篇文章,讨论这个话题。我不专门讨论 .testTarget
,但看一下 swift 提案,它看起来很像。
A 提出了一个适用于旧版 swift 和未来 swift 的简单解决方案:
- 将资产添加到项目的根目录中
- 在您的 swift 代码中:
ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path
- 适用于 Xcode 和 swift 内置终端或 github 操作 https://eon.codes/blog/2020/01/04/How-to-include-assets-with-swift-package-manager/ and https://github.com/eonist/ResourceHelper/
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")
]
),
]
在项目中的使用:
private var testData: Data {
let url = Bundle.module.url(forResource: "test", withExtension: "txt")!
let data = try! Data(contentsOf: url)
return data
}