Использование ресурсов в модульных тестах с Swift Package Manager
Я пытаюсь использовать файл ресурсов в модульных тестах и получить к нему доступ Bundle.path
, но он возвращает ноль.
Этот вызов в MyProjectTests.swift возвращает ноль:
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 и API-интерфейс описания Swift Package Manager версии 4.
10 ответов
В настоящее время диспетчер пакетов 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 build
а также make test
вместо swift build
а также swift test
, Make скопирует ресурсы в ожидаемые места (разные в MacOS и Linux, во время работы и во время тестов).
Swift 5.3
Swift 5.3 включает предложение по развитию SE-0271 ресурсов диспетчера пакетов с " Статусом: реализовано (Swift 5.3) ".:-)
Ресурсы не всегда предназначены для использования клиентами пакета; одно использование ресурсов может включать в себя тестовые инструменты, которые нужны только модульным тестам. Такие ресурсы не будут включены в клиенты пакета вместе с кодом библиотеки, а будут использоваться только при выполнении тестов пакета.
- Добавить новый
resources
параметр вtarget
а такжеtestTarget
API, позволяющие явно объявлять файлы ресурсов.SwiftPM использует соглашения файловой системы для определения набора исходных файлов, принадлежащих каждому целевому объекту в пакете: в частности, исходные файлы целевого объекта - это те файлы, которые расположены под назначенным для него "целевым каталогом". По умолчанию это каталог с тем же именем, что и цель, и он находится в "Источниках" (для обычной цели) или "Тестах" (для тестовой цели), но это расположение можно настроить в манифесте пакета.
// 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 в тестах использует неправильный путь к пакету?
- Swift Package Manager - ресурсы в тестовых целях
Xcode
Bundle.module
генерируется SwiftPM (см. Build / BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) и, следовательно, не присутствует в Foundation.Bundle при сборке с помощью Xcode.
Сравнимый подход в Xcode - добавить вручную Resources
ссылочная папка в проект Xcode, добавьте этап сборки Xcode copy
поставить Resource
в некоторые *.bundle
каталог и добавьте #ifdef Xcode
директива компилятора для сборки Xcode для работы с ресурсами.
#if Xcode
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
SwiftPM (5,1) не поддерживает ресурсы изначально еще, однако...
Когда запущены модульные тесты, можно ожидать, что репозиторий будет доступен, поэтому просто загрузите ресурс чем-то, полученным из #file
. Это работает со всеми существующими версиями SwiftPM.
let thisSourceFile = URL(fileURLWithPath: #file)
let thisDirectory = thisSourceFile.deletingLastPathComponent()
let resourceURL = thisDirectory.appendingPathComponent("TestAudio.m4a")
В случаях, отличных от тестов, когда репозиторий не будет во время выполнения, ресурсы все равно могут быть включены, хотя и за счет двоичного размера. Любой произвольный файл может быть встроен в исходный код Swift, представив его как данные base 64 в строковом литерале. Workspace - это инструмент с открытым исходным кодом, который может автоматизировать этот процесс:$ workspace refresh resources
. (Отказ от ответственности: я являюсь его автором.)
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
}
Swift Package Manager (SPM) 4.2
В Swift Package Manager PackageDescription 4.2 представлена поддержка локальных зависимостей.
Локальные зависимости - это пакеты на диске, к которым можно обращаться напрямую, используя их пути. Локальные зависимости разрешены только в корневом пакете, и они переопределяют все зависимости с тем же именем в графе пакета.
Примечание: я ожидаю, но еще не проверял, что что-то вроде следующего должно быть возможно с 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 и более ранних версий
Можно использовать ресурсы в модульных тестах с помощью Swift Package Manager для MacOS и Linux с некоторыми дополнительными настройками и пользовательскими сценариями. Вот описание одного из возможных подходов:
Диспетчер пакетов Swift пока не предоставляет механизм обработки ресурсов. Ниже приведен практичный подход к использованию тестовых ресурсов. TestResources/
в пакете; и, также предусматривает последовательное TestScratch/
каталог для создания тестовых файлов, если это необходимо.
Настроить:
- Добавить каталог ресурсов тестирования
TestResources/
вPackageName/
каталог. Для использования XCode, добавьте ресурсы теста в проект "Фазы сборки" для цели пакета теста.
- Редактор проектов> TARGETS > CxSQLiteFrameworkTests > Этапы сборки> Копировать файлы: ресурсы назначения,
+
добавить файлы
- Редактор проектов> TARGETS > CxSQLiteFrameworkTests > Этапы сборки> Копировать файлы: ресурсы назначения,
Для использования в командной строке установите псевдонимы Bash, которые включают swift-copy-testresources.swift
- Поместите исполняемую версию swift-copy-testresources.swift по соответствующему пути, который включает в себя $ PATH.
- Ubuntu:
nano ~/bin/ swift-copy-testresources.swift
- Ubuntu:
Баш псевдонимы
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()
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
Расположение файлов:
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)
Я также опубликовал GitHubGist с тем же подходом в 004.4'2 SW Dev Swift Package Manager (SPM) с ресурсами Qref
Начиная с Swift 5.3, благодаря SE-0271, вы можете добавить ресурсы пакета в диспетчер пакетов Swift, добавивresources
на ваше .target
декларация.
пример:
.target(
name: "HelloWorldProgram",
dependencies: [],
resources: [.process(Images), .process("README.md")]
)
Если вы хотите узнать больше, я написал статью о среде, обсуждая эту тему. Я специально не обсуждаю.testTarget
, но, глядя на быстрое предложение, все выглядит одинаково.
Я нашел другое решение, глядя на этот файл.
Можно создать пакет с путем, например:
let currentBundle = Bundle.allBundles.filter() { $0.bundlePath.hasSuffix(".xctest") }.first!
let realBundle = Bundle(path: "\(currentBundle.bundlePath)/../../../../Tests/MyProjectTests/Resources")
Это немного уродливо, но если вы хотите избежать Makefile, это работает.
Обратите внимание, что, возможно, при использовании
.copy(…) Resources. I couldn't get it to compile - something to do with code signing.
.process(…)`действительно работает, и это прекрасно, потому что мне больше не нужно беспокоиться о структуре папок (поскольку она все сглаживает).
Я использую:
extension Bundle {
func locateFirst(forResource: String, withExtension: String) -> URL? {
for b in Bundle.allBundles {
if let u = b.url(forResource: forResource, withExtension: withExtension) {
return u
}
}
return nil
}
}
'''
And then just call locateFirst, which gives the first item.
like:
'''
let p12 = Bundle().locateFirst(forResource: "Certificates", withExtension: "p12")!
'''
Сделано простое решение, которое работает для устаревшего быстрого и будущего быстрого:
- Добавьте свои активы в корень вашего проекта
- В вашем быстром коде:
ResourceHelper.projectRootURL(projectRef: #file, fileName: "temp.bundle/payload.json").path
- Работает в Xcode и быстро строит в действиях терминала или github https://eon.codes/blog/2020/01/04/How-to-include-assets-with-swift-package-manager/