Swift 5.3 SwiftPM Resources in tests uses wrong bundle path?

Hi,

I've downloaded 5.3-DEVELOPMENT-SNAPSHOT-2020-05-19-a to try out the new resources feature of the package manager. swift --version outputs: Apple Swift version 5.3-dev (LLVM 38f04f051e, Swift 587da7ce60) Target: x86_64-apple-darwin19.5.0. swift build --version: Swift Package Manager - Swift 5.3.0

I run into an issue where I cannot access resource files bundled in the test target. I am not sure if the root cause is that the feature is not fully finished yet, that it's a bug or I am just using it wrong.

My Package.swift
// swift-tools-version:5.3

import PackageDescription

let package = Package(
    name: "SwiftBeanCountParser",
    products: [
        .library(
            name: "SwiftBeanCountParser",
            targets: ["SwiftBeanCountParser"]),
    ],
    dependencies: [
        .package(
            url: "https://github.com/Nef10/SwiftBeanCountModel.git",
            .exact("0.1.0")
        ),
    ],
    targets: [
        .target(
            name: "SwiftBeanCountParser",
            dependencies: ["SwiftBeanCountModel"]),
        .testTarget(
            name: "SwiftBeanCountParserTests",
            dependencies: ["SwiftBeanCountParser"],
            resources: [
                .process("Resources"),
            ]),
    ]
)

When I run swift test it runs some tests till it fails with:

Fatal error: could not load resource bundle: /Applications/Xcode.app/Contents/Developer/usr/bin/SwiftBeanCountParser_SwiftBeanCountParserTests.bundle: file /Users/Steffen/Projects/SwiftBeanCountParser/.build/x86_64-apple-macosx/debug/SwiftBeanCountParserTests.build/DerivedSources/resource_bundle_accessor.swift, line 7 Exited with signal code 4

My code for loading the resource looks like this: let url = NSURL.fileURL(withPath: Bundle.module.path(forResource: "Minimal", ofType: "beancount")!), so I am using Bundle.module like introduced by the resource proposal.

The resource_bundle_accessor where the problem occurs does this does this: let bundlePath = Bundle.main.bundlePath + "/" + "SwiftBeanCountParser_SwiftBeanCountParserTests.bundle" So it seems that Bundle.main.bundlePath is set to /Applications/Xcode.app/Contents/Developer/usr/bin/. Acording to Bundle.main.path pointing to odd path this could be correct as this directory contains the xctest executable.

The folder which contains my code is /Users/Steffen/Projects/SwiftBeanCountParser. If I check in the .build folder, I can see that /Users/Steffen/Projects/SwiftBeanCountParser/.build/x86_64-apple-macosx/debug/SwiftBeanCountParser_SwiftBeanCountParserTests.bundle contains all my resource files.

Am I doing anything wrong? Is this feature not fully finished yet? Or is this a bug I should report (I couldn't find any similar bug or forums post)

Thanks,
Steffen

2 Likes

Sounds like it to me.

Thanks.

I created a minimal reproducible sample project at GitHub - Nef10/SPMResourcesInTest: Example to reproduce a bug in the SPM and filed the bug: [SR-12912] SPM: Resources do not work in test targets · Issue #4541 · apple/swift-package-manager · GitHub

1 Like

Does any one found a solution to this problem?

If you are looking for a workaround until the bug is fixed, you can do one of two things:

  • Put the resources in an otherwise empty normal target, along with enough Swift code to pass them on in a public interface. Since no product uses the target, but rather only tests, it won’t be included when your package is used as a dependency.
  • Instead of the new resource API, use #file/#filePath along and add/remove path components to point at the resource in the source directory. Your tests will work as long as the source is still available at the location from which it was compiled.

Having the same issue right now. Thank you for the workarounds @SDGGiesbrecht, but both of these require too much work to revert back once the issue is fixes IMHO. Isn't there a solution where I just need to add some extension to Bundle or so and reference that instead of Bundle.module and can just search & replace once it's fixed and delete the extension?

I will try to work something out myself based on the code in the related PR.
I'll report if I find an easier workaround ...

UPDATE:
OK, while trying to find a better workaround, I noticed that the code in the target or tests isn't even run, the failing happens in a step before while loading Bundle.module, so it actually can't be fixed within the target with an extension or such. So I hope PR#2817 will get merged and released before the final Xcode 12 release!

You can look at SwiftPM’s output directory and reverse engineer a way of manually pointing at the resulting location of resource files. I guess we are talking about tests here anyway, so it might not be such a bad idea after all. The main issue is that the output layout is explicitly undefined and may vary across platforms, toolchain versions, build tools (SwiftPM vs Xcode) or other differences in the build. The only thing guaranteed is that the provided Bundle.module accessor will [is supposed to] work. So I recommend against relying on such a manual extension for anything included in a versioned stable release. But if its just for tests, I guess you may as well go for it.

I'm running into the same problem and came up with the same workaround @SDGGiesbrecht mentioned first. However, I'm unable to make it work. I've put all the resources into a separate target and added a simple Swift file to the same target with an extension on Bundle:

import class Foundation.Bundle

extension Bundle {
    public static var testResources: Bundle { module }
}

However, with the current Beta (Xcode 12 Beta 6) this still crashes when I try to access Bundle.testResources in a test target:

Fatal error: could not load resource bundle: /Applications/Xcode-beta.app/Contents/Developer/usr/bin/my_package_MyTargetTestResources.bundle: file /path/to/my_package/.build/x86_64-apple-macosx/debug/MyTargetTestResources.build/DerivedSources/resource_bundle_accessor.swift, line 7 

Any ideas on why this doesn't work either? I assume because resource_bundle_accessor.swift contains the following code:

import class Foundation.Bundle

extension Foundation.Bundle {
    static var module: Bundle = {
        let bundlePath = Bundle.main.bundlePath + "/" + "my_package_MyTargetTestResources.bundle"
        guard let bundle = Bundle(path: bundlePath) else {
            fatalError("could not load resource bundle: \(bundlePath)")
        }
        return bundle
    }()
}

And I assume Bundle.main is XCTest's bundle, which is the reason why /Applications/Xcode-beta.app/Contents/Developer/usr/bin/ shows up as path in the error message.

The PR changes this AFAIK, but this makes SPM resources unusable for any current SPM package that has unit tests. Or am I missing something?

Oof! That sounds like a real problem. I should have read the error message more closely. I thought we were just talking about resources of tests, not resources of anything accessed during tests.

The PR to cherrypick the fix into 5.3 appears to have come too late. You’ll find it and the discussion here. And yes, that seems to indicate that in 5.3.0, resources will not be directly usable in any code that undergoes testing... :frowning:

The best workaround I can think of is this:

#if DEBUG
  // #file strategy (source is still likely around?)
#else
  // resource bundle strategy (not testing release build anyway?)
#endif

1 Like

I played around with this a bit yesterday, but have only found a workaround for the case where resources are needed in a test target:

  1. Create a new executable target (one with a main.swift file) e.g. ResourcesPathProvider containing the resources.
  2. Put the following code in the main.swift file:
import class Foundation.Bundle
print(Bundle.module.bundlePath, terminator: "")
  1. Add an extension on Bundle in the target where you need the resources:
import Foundation
@testable import ResourcesPathProvider

extension Bundle {
    private static var productsDirectory: URL {
        #if os(macOS)
        if let bundle = Bundle.allBundles.first(where: { $0.bundlePath.hasSuffix(".xctest") }) {
            return bundle.bundleURL.deletingLastPathComponent()
        }
        fatalError("Couldn't find the products directory")
        #else
        return Bundle.main.bundleURL
        #endif
    }

    static var module: Bundle = {
        do {
            let process = Process()
            process.executableURL = productsDirectory.appendingPathComponent("ResourcesPathProvider")
            let pipe = Pipe()
            process.standardOutput = pipe

            try process.run()
            process.waitUntilExit()

            let data = pipe.fileHandleForReading.readDataToEndOfFile()
            let output = String(decoding: data, as: UTF8.self)
            guard let bundle = Bundle(path: output) else {
                fatalError("Couldn't load module bundle from provided path: \(output)")
            }
            return bundle
        } catch {
            fatalError("Couldn't load module bundle: \(error)")
        }
    }()
}
  1. Use Bundle.module :slight_smile:
    Note that I intentionally used module as the name for the accessor, so that once Swift fixes this, I can simply move the resources to the test target, remove the executable target and the extension.

The code in the extension is a slightly adapted version of the one SPM automatically generates for tests of new executable packages. It basically just searches the executable and executes it. Given that the executable simply prints the path to its resources bundle, we can use the output and load the bundle. The upside is, that this seems to be rather safe and does not rely on many build system intrinsics, except for the part where we're looking for the executable. However, given that SPM ships with that code, I think it's rather safe to rely on it for now. At least until some version of Swift finally fixes this issue.

However, this only works for cases where the resources are needed in tests. For testing targets that themselves have resources, I have yet to find a workaround.

Things get worse for multi platform projects. If you e.g. support iOS, tvOS or watchOS in your package, the workaround I described above won't work, since you can't run an executable. Xcode fails to respect the target conditionals and still tries to build the CLT when targeting e.g. an iOS simulator.

The bright side is: On these platforms you don't need a workaround at all, since the resource bundle is perfectly loadable from Bundle.main ¯\_(ツ)_/¯

This looks like its still an issue.

I created a ticket on the swift Jira (not sure if that's the correct place).

But essentially the swift command line test function fails whenever I include bundled resources:

1 Like

looks like availability for this fix is Xcode 12.2: https://github.com/apple/swift-package-manager/pull/2817


I don’t know if it’s same issue, I have tried on Xcode 12.2 beta2, the unable to find bound issue still exist. Let me describe my problem:

Refer to the picture. ViekaSupport reference to MetalCoreKit, MetalCoreKit contains the resource, when run the TestMask.playground, it’s failed to find the bundle in MetalCoreKit. This was worked on Xcode 12.0 beta 6.

I think that’s a separate issue, but it might possibly be related.

The issue that PR addresses is where test targets with resources look in your bin folder for package resources instead of the build folder (or whatever folder you have set). And this only happens when you run the tests via the command line. When running in Xcode the tests run fine.

So the impact is mostly on CI/CD pipelines/Linux.

I tried on xcode 12.2 version but still facing the issue while running the test target.
Fatal error: unable to find bundle named: file Common/resource_bundle_accessor.swift, line 27

not sure if something else needs to be done

2 Likes

Any news on this issue? I just ran into it for the first time. Xcode 12.4 (12D4e).

2 Likes

It works fine when in development from Xcode to a device, but it crashes and shows Fatal error: unable to find bundle named: file Common/resource_bundle_accessor.swift, line 27 when exporting to App Store or to an .ipa install file. I used Xcode 12.4 and 12.5 beta.

Still the same on the Version 12.4 (12D4e)

I had a similar issue in my project as well. I have a Swift Package with Resources folder that contains fonts and asset catalogue,

Style/
  - Source/
    - Color.swift
    - Resources/
      - Processed/
        - Colors.xcassets
        - Fonts/
          - MyFont.ttf

And this module is embedded in my Main.app. One of my unit test target CalendarTests.xctest was accessing the apis of Style module. So when I ran the the CalendarTests.xctest it was crashing here,

fatalError("unable to find bundle named Style_Style")

However when I run the Main app everything works fine, but only during the test run, I see this crash.

After digging through various build directories inside DerivedData, I noticed that my CalendarTests.xctest contains a copy of Style_Style.bundle, but the auto generated resource_bundle_accessor.swift is not searching inside the "currently executing" test bundle, in this case CalendarTests.xctest/Style_Style.bundle. The auto generated code looks for Bundle.main and Style module's bundle(i.e PackageFrameworks/Style.framework/), but not the bundle that is "currently executing".

In order to circumvent this problem, I made my own "bundle search" logic instead of relying on Bundle.module property, which seems to fixed this crash.

This is my custom bundle search code,

extension Foundation.Bundle {
    /// Returns the resource bundle associated with the current Swift module.
    static var styleModule: Bundle = {
        let bundleName = "Style_Style"

        let candidates = [
            // Bundle should be present here when the package is linked into an App.
            Bundle.main.resourceURL,

            // Bundle should be present here when the package is linked into a framework.
            Bundle(for: BundleFinder.self).resourceURL,

            // For command-line tools.
            Bundle.main.bundleURL,
        ] + Bundle.allBundles.map { $0.bundleURL }

        for candidate in candidates {
            let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
            if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
                return bundle
            }
        }
        fatalError("unable to find bundle named Style_Style")
    }()
}

The code looks identical to the auto generated code, except that I have additionally included Bundle.allBundles.map { $0.bundleURL } to candidates array.

All the classes in my Style module now uses the new Bundle.styleModule property, instead of Bundle.module.

The next thing Im gonna do is to create a macro as @SDGGiesbrecht suggested.

var candidates = [ ... ]

#if TESTING
    candidates += Bundle.allBundles.map { $0.bundleURL }
#endif

...

Im not sure if this is a clean solution, but at least it gives a workaround solution while waiting for SR-13714.

1 Like