So I am working on a swift project managed by Swift Package Manager. For some of my tests I would like to load files from the directory ${PACKAGE_ROOT}/testData (note: PACKAGE_ROOT is not an environment variable which exists, this is just for illustrative purposes)
The thing is, if I run the tests via the command line (i.e. $ swift test then I can get the root using the current directory path: FileManager.default.currentDirectoryPath. However, I would like to use the XCode test runner to take advantage of the debugger, and when run from the test runner, the currentDirectoryPath is in DerivedData.
Is there a good way to get a path relative to the test project directory at runtime? A few other critical things here:
I want the tests to pass regardless of the project location. I.e. if someone clones the project to an arbitrary location, the tests should run identically. Therefore explicitly setting the complete path to the project is out.
I want to avoid something which requires modification of the generated XCode project, because these changes will not be stable if the project needs to be re-generated from the package. I.e. if a new dependency or target is added.
I think the problem is foremost that the binary executable has no knowledge whatsoever that there even exists a source or project directory. If it knows, then at most it can have a hardcoded path because how else would it know?
When the executable is launched, the source directory might not even exist anymore after all.
So the only hope here is that the resources are included in the executable or copied to some global location that can be relied upon I guess.
You could use URL and #file along with the name of the directory that holds the tests (in most SwiftPM packages this is Tests).
The following will give you the absolute path to the root directory per file:
import Foundation // Can be omitted when importing XCTest.
fileprivate let packageRootPath = URL(fileURLWithPath: #file).pathComponents
.prefix(while: { $0 != "Tests" }).joined(separator: "/").dropFirst()
fileprivate let testDataPath = packageRootPath + "/testData"
@dennisvennink is basically right about using #file, though his search algorithm is a little risky, since you cannot control where the repository gets cloned to and you don’t know where else “Tests” might occur in the absolute path. Since you do know the relative path of the file in which you are coding, you can just string together calls to deletingLastPathComponent() to back out to the repository root.
I find it useful to specify the directory once and then use it all over the place, such as here.
Yes I think I will go with soething similar to @dennisvennink's solution. But since I know the internal structure of the project directory I can do it a bit more simply:
let packageRoot = URL(fileURLWithPath: #file.replacingOccurrences(of: "relatve/path/to/file", with: ""))
As you said, this is slightly more reliable than using "Tests" which is nor guaranteed to be missing from the parent context.
After looking into this a bit more, I think it would also be viable to write to the user temp directory (FileManager().temporaryDirectory) in the test setup function. This might be slightly more "correct" since it would mean the tests are completely self-contained.
Definitely better if it can start out empty for each test session. I originally understood you wanted to load something which was checked into your repository in a specific location.
I originally understood you wanted to load something which was checked into your repository in a specific location.
Yeah that was the original plan, but I'm working with text, so it's not a huge deal to purge and rewrite a folder within the temp directory every time the tests run. The downside is that I would have to keep long string literals in the test code, and it does slightly increase the code complexity of the test target to ensure the test data is initialized correctly. It could also be done with non-string-encoded data: i.e. hex-encoded arbitrary data, but again this means more non-trivial complexity in the test target.