Skip to content

[SR-12912] Fix crash in test targets when accessing Bundle.module #2817

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 24, 2020

Conversation

Steven0351
Copy link
Contributor

@Steven0351 Steven0351 commented Jul 17, 2020

Currently, running swift test fails when a test target accesses Bundle.module during a test (https://forums.swift.org/t/swift-5-3-spm-resources-in-tests-uses-wrong-bundle-path/37051).
This patch uses the bundlePath that already exists instead of crafting the path from Bundle.main. I've tested this in both library, executable, and test targets on macOS and it seems to do the trick.

Setting the path for Bundle.module fails in the case where the bundle is accessed during tests.
This uses the already derived bundlePath instead.
@Steven0351 Steven0351 changed the title SR-12912 - Fixes crash in test targets that contain resouces when accessing Bundle.module SR-12912 - Fixes crash in test targets when accessing Bundle.module Jul 17, 2020
let stream = BufferedOutputByteStream()
stream <<< """
import class Foundation.Bundle

extension Foundation.Bundle {
static var module: Bundle = {
let bundlePath = Bundle.main.bundlePath + "/" + "\(bundleBasename)"
let bundlePath = "\(bundlePath)"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this hardcode the absolute path into each artifact (also non-tests)?

Copy link
Contributor Author

@Steven0351 Steven0351 Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with how bundlePath is derived so that is possible. I wasn't able to confirm this on an iOS device itself as Xcode won't let me install it on my test device with a custom toolchain. Is it possible to check if a given BuildPlan is compiling a test target? I saw that there is an isTestTarget member variable on BuildPlan and changed the logic to only use bundlePath when that is true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be aware that the Xcode integration of packages does not use SwiftPM from the toolchain, you will only see an effect of your changes in commandline SwiftPM.

Copy link
Contributor

@abertelrud abertelrud Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but in this case, even when just building with swift build, wouldn't this also change the path that's built into a package target that's linked into an executable, meaning that if the executable and its associated resource bundles are later moved, the bundles can't be found?

Copy link
Contributor Author

@Steven0351 Steven0351 Jul 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but in this case, even when just building with swift build, wouldn't this also change the path that's built into a package target that's linked into an executable, meaning that if the executable and its associated resource bundles are later moved, the bundles can't be found?

That seems likely, when I tested I did move the executable but not the resource bundle. Would it make sense to conditionally check if the current Bundle.main path is located in an Xcode path and fallback to the hard coded path? (see latest commit)

@neonichu I'm guessing that Xcode has its own method for computing Bundle.module? I had gone down a rabbit hole of trying to run my tests with xcodebuild on my package, but had other issues altogether there.

@Steven0351 Steven0351 requested a review from abertelrud July 17, 2020 19:41
@@ -577,14 +577,16 @@ public final class SwiftTargetBuildDescription {
private func generateResourceAccessor() throws {
// Do nothing if we're not generating a bundle.
guard let bundlePath = self.bundlePath else { return }


let pathToModule = isTestTarget ? bundlePath : "Bundle.main.bundlePath/\(bundlePath.basename)"
Copy link
Contributor Author

@Steven0351 Steven0351 Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized this is not actually going to work, I've made some changes locally and am building a new toolchain before pushing the changes, but tentatively this has been re-written to

let pathToModule = isTestTarget ? bundlePath : "Bundle.main.bundlePath + \"/\" + \(bundlePath.basename)"

…xpected, however, when the target under test accesses Bundle.module, it fails for the same reason as before
@Steven0351 Steven0351 marked this pull request as draft July 17, 2020 23:07
@Steven0351
Copy link
Contributor Author

Steven0351 commented Jul 17, 2020

I've converted this to a Draft PR because this is going to take a bit of consideration. To address the concern raised by @abertelrud, I updated the generation of Bundle.module to this:

let pathToModule = isTestTarget ? #""\#(bundlePath.pathString)""# : #"Bundle.main.bundlePath + "/" + "\#(bundlePath.basename)""#
      
 let stream = BufferedOutputByteStream()
 stream <<< """
 import class Foundation.Bundle

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

However, when a target that is being tested calls Bundle.module to access a resource during a test, the test fails with the same error:
Fatal error: could not load resource bundle: ${path to xcode developer tools}/usr/bin/Foo_Foo.bundle: file ${path to derived sources}/resource_bundle_accessor.swift, line 7

@Steven0351 Steven0351 marked this pull request as ready for review July 18, 2020 04:33
@Steven0351
Copy link
Contributor Author

Steven0351 commented Jul 18, 2020

I think I've come up with a reasonable solution: we first check if the resource bundle is accessible by appending to the bundle name to Bundle.main, if that fails we fall back to the absolute path to the resource bundle in the build directory, and if that fails we bail out with a fatal error.

@Jeehut
Copy link

Jeehut commented Aug 15, 2020

@Steven0351 Did you already find time to implement your "reasonable solution" so we can get this merged before final release of Xcode 12? Feel free to ping me if I can help here as the bug is really blocking a proper CI configuration for any open source repo using resources right now ...

@Steven0351
Copy link
Contributor Author

Steven0351 commented Aug 15, 2020

@Jeehut I had committed what I thought was a "reasonable" solution, but there has been no feedback on those commits after I marked them ready for review. Bundle.module is generated at build time, so my workaround has the original bundle, but falls back to the build path in the event that the preferred bundle is not available (which should only be the case when a target is under test). I've been using my fork to run tests for macOS Swift packages without issue, but I'm only really able to run those locally because I haven't published that for use in any of my CI anywhere.

@MaxDesiatov
Copy link
Contributor

I can trigger CI and/or request reviews from relevant people, is this ready for CI or review at all?

@Steven0351
Copy link
Contributor Author

@MaxDesiatov It's ready for review. I've run the tests locally when I originally patched and validated that my macOS Swift PM projects with bundle resources were able to both be tested and run (along with being moved around the file system without breaking non-test behavior).

@MaxDesiatov
Copy link
Contributor

Would you be able to add corresponding automated tests to the SwiftPM test suite, so that it could be tested on CI?

@Steven0351
Copy link
Contributor Author

@MaxDesiatov I could give it a go but I'll have to dig into the test suite to see how this part of the codebase is tested. I could probably write something that validates the values that are generated, but I didn't notice anything in the current test suite that would enable validating the properties at runtime. Do you have a suggested approach there?

@MaxDesiatov MaxDesiatov changed the title SR-12912 - Fixes crash in test targets when accessing Bundle.module SR-12912 Fix crash in test targets when accessing Bundle.module Aug 15, 2020
@MaxDesiatov
Copy link
Contributor

Did you check the integration test suite with test cases such as this one?

@MaxDesiatov MaxDesiatov changed the title SR-12912 Fix crash in test targets when accessing Bundle.module [SR-12912] Fix crash in test targets when accessing Bundle.module Aug 17, 2020
Added passing integration test into TestToolTests.swift
Added failing integration test into BasicTests.swift
@Steven0351
Copy link
Contributor Author

Steven0351 commented Aug 20, 2020

@MaxDesiatov I had never noticed the IntegrationsTest project since it's not visible when opening the root package in Xcode (thanks for the tip!).

I did run into an issue there: it uses the existing swift toolchain by default. This will fail if CI runs without setting the SWIFTPM_BIN_DIR environment variable, which I have no way of knowing because those configs aren't publicly available. This can be made to pass by invoking SWIFTPM_BIN_DIR=/path/to/swift-package-manager/.build/debug swift test in the IntegrationsTest directory after performing a build.

@neonichu Would you be able to review/throw in your opinion here as well?

@abertelrud
Copy link
Contributor

@swift-ci please smoke test

Copy link
Contributor

@abertelrud abertelrud left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for revising the PR (and apologies about the delay in taking another look at it). Looks good, though I'd suggest moving the lookup of the fallback bundle to a place where it will only be run if the first one isn't found. There can be a performance cost to instantiating a bundle.

let buildPath = "\(bundlePath.pathString)"

let preferredBundle = Bundle(path: mainPath)
let fallBackBundle = Bundle(path: buildPath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this could be a little expensive, it might be worth instantiating this second Bundle only if the first one can't be found, especially since at deployment time for the non-test products, the first one should always be found.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair; I've inlined the instantiation of this in the ternery expression per this feedback

@Steven0351 Steven0351 requested a review from abertelrud August 21, 2020 00:20
@abertelrud
Copy link
Contributor

Thanks for making the change!

@abertelrud
Copy link
Contributor

@swift-ci please test

@Steven0351
Copy link
Contributor Author

Looks like that Linux build failure was related to an issue compiling core-libs-foundation?

@MaxDesiatov
Copy link
Contributor

@swift-ci please smoke test

@abertelrud
Copy link
Contributor

The Linux failure on the full test does indeed look unrelated. The smoke tests did pass. I would like to get one more review on this but as far as I am concerned this looks good.

@neonichu
Copy link
Contributor

Looks like the only failure is from an accidental full test run, so this can be merged.

@neonichu neonichu merged commit cdcf0e0 into swiftlang:master Aug 24, 2020
skarred14 pushed a commit to val-verde/swift-package-manager that referenced this pull request Aug 26, 2020
…iftlang#2817)

* SR-12912
Setting the path for Bundle.module fails in the case where the bundle is accessed during tests.
This uses the already derived bundlePath instead.

* Only use bundlePath when building a testTarget

* Updating this for discussion in PR. This hardcodes the test path as expected, however, when the target under test accesses Bundle.module, it fails for the same reason as before

* This prefers Bundle.main, but if it is inaccessible it falls back to the build output directory path.

* Cleaned up changes in BuildPlan.swift
Added passing integration test into TestToolTests.swift
Added failing integration test into BasicTests.swift

* Remove integration test from TestToolTests.swift

* Only instantiate Bundle for buildPath if main bundle is not found
@ffried
Copy link
Contributor

ffried commented Sep 3, 2020

Any chance for this to make it into Swift 5.3?

MaxDesiatov pushed a commit to MaxDesiatov/swift-package-manager that referenced this pull request Sep 3, 2020
…iftlang#2817)

* SR-12912
Setting the path for Bundle.module fails in the case where the bundle is accessed during tests.
This uses the already derived bundlePath instead.

* Only use bundlePath when building a testTarget

* Updating this for discussion in PR. This hardcodes the test path as expected, however, when the target under test accesses Bundle.module, it fails for the same reason as before

* This prefers Bundle.main, but if it is inaccessible it falls back to the build output directory path.

* Cleaned up changes in BuildPlan.swift
Added passing integration test into TestToolTests.swift
Added failing integration test into BasicTests.swift

* Remove integration test from TestToolTests.swift

* Only instantiate Bundle for buildPath if main bundle is not found
@MaxDesiatov
Copy link
Contributor

I've created #2905 for that.

@ffried
Copy link
Contributor

ffried commented Sep 3, 2020

@MaxDesiatov Thank you!

neonichu pushed a commit that referenced this pull request Sep 17, 2020
) (#2905)

* SR-12912
Setting the path for Bundle.module fails in the case where the bundle is accessed during tests.
This uses the already derived bundlePath instead.

* Only use bundlePath when building a testTarget

* Updating this for discussion in PR. This hardcodes the test path as expected, however, when the target under test accesses Bundle.module, it fails for the same reason as before

* This prefers Bundle.main, but if it is inaccessible it falls back to the build output directory path.

* Cleaned up changes in BuildPlan.swift
Added passing integration test into TestToolTests.swift
Added failing integration test into BasicTests.swift

* Remove integration test from TestToolTests.swift

* Only instantiate Bundle for buildPath if main bundle is not found

Co-authored-by: Steven Sherry <[email protected]>
@odrobnik
Copy link

odrobnik commented Oct 9, 2020

Forgive my ignorance, how can one tell in which Swift version this fix will be included? Swift 5.3, as of Xcode 12 still has the original implementation as evidenced with the shorter error message.

@MaxDesiatov
Copy link
Contributor

I saw some folks mentioned it's in Xcode 12.2 Beta 2, but I haven't verified it myself

@Steven0351
Copy link
Contributor Author

I can verify it is available in Xcode 12.2 Beta 2.

@djbe
Copy link

djbe commented Oct 13, 2020

Unfortunately it's not in Xcode 12.1 though 😢 So we'll have to wait until 12.2 is released.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants