我正在尝试在单元测试中使用资源文件并使用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.
答案 0 :(得分:18)
Swift 5.3
Swift 5.3包含Package Manager Resources SE-0271演变建议,并带有“状态:已实施(Swift 5.3)”。 :-)
资源并不总是供软件包的客户使用;资源的一种使用可能包括仅单元测试所需的测试装置。这些资源不会与库代码一起合并到软件包的客户端中,而只会在运行软件包的测试时使用。
- 在
resources
和target
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 build
和make test
而不是swift build
和swift 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 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为macOS和Linux在单元测试中使用资源,并提供一些额外的设置和自定义脚本。以下是一种可能方法的描述:
Swift Package Manager尚未提供处理资源的机制。以下是在包中使用测试资源TestResources/
的可行方法;并且,如果需要,还提供用于创建测试文件的一致TestScratch/
目录。
TestResources/
目录中添加测试资源目录PackageName/
。对于Xcode使用,将测试资源添加到测试包目标的“Build Phases”项目中。
+
添加文件对于命令行使用,设置包含swift-copy-testresources.swift
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/
文件作为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:
ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path