Why can't code inside unit tests find bundle resources? - cocoa
Some code I am unit testing needs to load a resource file. It contains the following line:
NSString *path = [[NSBundle mainBundle] pathForResource:#"foo" ofType:#"txt"];
In the app it runs just fine, but when run by the unit testing framework pathForResource: returns nil, meaning it could not locate foo.txt.
I've made sure that foo.txt is included in the Copy Bundle Resources build phase of the unit test target, so why can't it find the file?
When the unit test harness runs your code, your unit test bundle is NOT the main bundle.
Even though you are running tests, not your application, your application bundle is still the main bundle. (Presumably, this prevents the code you are testing from searching the wrong bundle.) Thus, if you add a resource file to the unit test bundle, you won't find it if search the main bundle. If you replace the above line with:
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSString *path = [bundle pathForResource:#"foo" ofType:#"txt"];
Then your code will search the bundle that your unit test class is in, and everything will be fine.
A Swift implementation:
Swift 2
let testBundle = NSBundle(forClass: self.dynamicType)
let fileURL = testBundle.URLForResource("imageName", withExtension: "png")
XCTAssertNotNil(fileURL)
Swift 3, Swift 4
let testBundle = Bundle(for: type(of: self))
let filePath = testBundle.path(forResource: "imageName", ofType: "png")
XCTAssertNotNil(filePath)
Bundle provides ways to discover the main and test paths for your configuration:
#testable import Example
class ExampleTests: XCTestCase {
func testExample() {
let bundleMain = Bundle.main
let bundleDoingTest = Bundle(for: type(of: self ))
let bundleBeingTested = Bundle(identifier: "com.example.Example")!
print("bundleMain.bundlePath : \(bundleMain.bundlePath)")
// …/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Xcode/Agents
print("bundleDoingTest.bundlePath : \(bundleDoingTest.bundlePath)")
// …/PATH/TO/Debug/ExampleTests.xctest
print("bundleBeingTested.bundlePath : \(bundleBeingTested.bundlePath)")
// …/PATH/TO/Debug/Example.app
print("bundleMain = " + bundleMain.description) // Xcode Test Agent
print("bundleDoingTest = " + bundleDoingTest.description) // Test Case Bundle
print("bundleUnderTest = " + bundleBeingTested.description) // App Bundle
In Xcode 6|7|8|9, a unit-test bundle path will be in Developer/Xcode/DerivedData something like ...
/Users/
UserName/
Library/
Developer/
Xcode/
DerivedData/
App-qwertyuiop.../
Build/
Products/
Debug-iphonesimulator/
AppTests.xctest/
foo.txt
... which is separate from the Developer/CoreSimulator/Devices regular (non-unit-test) bundle path:
/Users/
UserName/
Library/
Developer/
CoreSimulator/
Devices/
_UUID_/
data/
Containers/
Bundle/
Application/
_UUID_/
App.app/
Also note the unit test executable is, by default, linked with the application code. However, the unit test code should only have Target Membership in just the test bundle. The application code should only have Target Membership in the application bundle. At runtime, the unit test target bundle is injected into the application bundle for execution.
Swift Package Manager (SPM) 4:
let testBundle = Bundle(for: type(of: self))
print("testBundle.bundlePath = \(testBundle.bundlePath) ")
Note: By default, the command line swift test will create a MyProjectPackageTests.xctest test bundle. And, the swift package generate-xcodeproj will create a MyProjectTests.xctest test bundle. These different test bundles have different paths. Also, the different test bundles may have some internal directory structure and content differences.
In either case, the .bundlePath and .bundleURL will return the path of test bundle currently being run on macOS. However, Bundle is not currently implemented for Ubuntu Linux.
Also, command line swift build and swift test do not currently provide a mechanism for copying resources.
However, with some effort, it is possible to set up processes for using the Swift Package Manger with resources in the macOS Xcode, macOS command line, and Ubuntu command line environments. One example can be found here: 004.4'2 SW Dev Swift Package Manager (SPM) With Resources Qref
See also: Use resources in unit tests with Swift Package Manager
Swift Package Manager (SwiftPM) 5.3
Swift 5.3 includes Package Manager Resources SE-0271 evolution proposal with "Status: Implemented (Swift 5.3)". :-)
Resources aren't always intended for use by clients of the package; one use of resources might include test fixtures that are only needed by unit tests. Such resources would not be incorporated into clients of the package along with the library code, but would only be used while running the package's tests.
Add a new resources parameter in target and testTarget APIs to allow declaring resource files explicitly.
SwiftPM uses file system conventions for determining the set of source files that belongs to each target in a package: specifically, a target's source files are those that are located underneath the designated "target directory" for the target. By default this is a directory that has the same name as the target and is located in "Sources" (for a regular target) or "Tests" (for a test target), but this location can be customized in the package manifest.
// 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)
Example
// swift-tools-version:5.3
import PackageDescription
targets: [
.target(
name: "CLIQuickstartLib",
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("Resources"),
]),
.testTarget(
name: "CLIQuickstartLibTests",
dependencies: [],
resources: [
// Copy directories as-is.
// Use to retain directory structure.
// Will be at top level in bundle.
.copy("Resources"),
]),
Current Issue
Swift 5.3 SPM Resources in tests uses wrong bundle path?
Swift Package Manager - Resources in test targets
Xcode
Bundle.module is generated by SwiftPM (see Build/BuildPlan.swift SwiftTargetBuildDescription generateResourceAccessor()) and thus not present in Foundation.Bundle when built by Xcode.
A comparable approach in Xcode would be to manually add a Resources reference folder to the module, add an Xcode build phase copy to put the Resource into some *.bundle directory, and add a #ifdef Xcode compiler directive for the Xcode build to work with the resources.
#if Xcode
extension Foundation.Bundle {
/// Returns resource bundle as a `Bundle`.
/// Requires Xcode copy phase to locate files into `*.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("Bundle.module could not load: \(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
With swift Swift 3 the syntax self.dynamicType has been deprecated, use this instead
let testBundle = Bundle(for: type(of: self))
let fooTxtPath = testBundle.path(forResource: "foo", ofType: "txt")
or
let fooTxtURL = testBundle.url(forResource: "foo", withExtension: "txt")
Confirm that the resource is added to the test target.
if you have multiple target in your project then you need to add resources between different target available in the Target Membership and you may need to switch between different Target as 3 steps shown in the figure below
I had to ensure this General Testing checkbox was set
Just for people like me, that missed this point in the original post:
Make sure that foo.md is included in the Copy Bundle Resources build phase of the unit test target
There is a code which finds the file in :
How to check if a file exists in the Documents directory in Swift?
I used it in a test as follows, which tests that my file is created and gives its location for further testing.
let fileFound = XCTestExpectation (description: "Checking for DB File Found")
let path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
if let pathComponent = url.appendingPathComponent("filename.ext") {
let filePath = pathComponent.path
let fileManager = FileManager.default
if fileManager.fileExists(atPath: filePath) {
fileFound.fulfill()
print("DB FILE AVAILABLE")
} else {
print("DB FILE NOT AVAILABLE")
}
} else {
print("DB FILE PATH NOT AVAILABLE")
}
wait(for: [fileFound], timeout: 5)
This is not testing that the file is created in the right place, only that it is created.
Related
File access permission in Swift on OSX
I'm using: let dialog = NSOpenPanel() to get a file URL. I'm then reading in the contents of the text file with: let content = try String( contentsOf: dialog.url) This works! I'm then trying to read in another text file in the same directory with a different extension: let b = dialog.url?.deletingPathExtension() // Add the new file extension let c = b?.appendingPathExtension("TSN") let content2 = try String( contentsOf: c) With this I get: "The file “FLO5.TSN” couldn’t be opened because you don’t have permission to view it." If I try and open the .tsn file with a URL from a NSOpenPanel() dialog result it works. I need to open several data files from this same directory with different extensions. Is there a way to do this?
set off sandbox!!) Xcode 9 and later enables sandboxing by default, which severely limits interactions with the system. Select your target, then Capabilities and set Downloads Folder to Read/Write:
In my case, solve it's startAccessingSecurityScopedResource Example: let selectedFile = try result.get() // get path to file do { // get access to open file if selectedFile.startAccessingSecurityScopedResource() { let path = selectedFile.path let data = NSData(contentsOfFile: path) print(data) // array bytes selectedFile.stopAccessingSecurityScopedResource() } } catch { // Couldn't read the file. print(error.localizedDescription) } Apple wiki about this feature https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso
SceneKit: cannot add SCNReferenceNode programmatically though works in Xcode Scene Editor
Adding a SCNReferenceNode works when done from the Xcode Scene Editor. When done programmatically with the code below, however, nothing appears in the scene. filePath contains a valid path as proven by the print statement as well as stepping through the code. Help? if let filePath = NSBundle.mainBundle().pathForResource("RefTest", ofType: "scn", inDirectory: "TestScene.scnassets") { let referenceURL = NSURL(fileURLWithPath: filePath) if #available(iOS 9.0, *) { let referenceNode = SCNReferenceNode(URL: referenceURL) scene!.rootNode.addChildNode(referenceNode!) print(referenceNode) } }
The class docs claim that the loading policy instructs the SCNReferenceNode to load immediately by default: When the reference node loads its content (either automatically, according to the loadingPolicy property, or when you call the load method), SceneKit loads the referenced scene file. All children of the scene file’s root node become children of the reference node. However, this doesn't seem to happen by default. You must invoke the load function like so: let referenceNode = SCNReferenceNode(URL: referenceURL referenceNode.load() Alternatively, perhaps you could play with the loadingPolicy property as well, but invoking the load function definitely works.
Full path of project
I want to open a text file in Swift and I managed to do it passing the full path as parameter: let dados = String.stringWithContentsOfFile("/Users/aczuquim/Google Drive/Swift/Verdadeiro ou Falso/Verdadeiro ou Falso/Dados.txt", encoding: NSUTF8StringEncoding, error: nil) Since, I added the file to the project, is there any way to use relative path? When I use the relative path, a nil is returned. UPDATE In the playground, the line bellow works fine: let dados = String.stringWithContentsOfFile("/Users/aczuquim/Google Drive/Swift/Verdadeiro ou Falso/Verdadeiro ou Falso/dados.txt", encoding: NSUTF8StringEncoding, error: nil) But not the following ones (path is nil): let bundle = NSBundle.mainBundle() let path = bundle.pathForResource("dados", ofType: "txt")
Bundle resource: Swift 3.0 if let bundle = Bundle.main().urlForResource("Info", withExtension: "plist") { print("Path: \(bundle)") } Swift 2.0 let bundleURL = NSBundle.mainBundle()!.URLForResource("dados", withExtension: "txt") println("\(bundleURL)") If a nil is returned then the resource is not found and an error reported (note the exclamation mark after mainBundle). Check Build Phases - Copy Bundle Resources if the resource is being included. App and Documents Folder Get the documents folder from an array of [AnyObject]! which casts the first object to the NSURL type: let docFolderURL = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask)[0] as NSURL println("DocumentFolderURL: \(docFolderURL)") Then get the App folder, by stepping up to the parent folder and delete the last path component: let appFolderURL = docFolderURL.URLByDeletingLastPathComponent println("AppFolderURL: \(appFolderURL)") Other directories may be accessed by using .URLByAppendingPathComponent(pathComponent: String?, isDirectory:Bool) etc. Temporary Directory var tempURL = NSURL.fileURLWithPath(NSTemporaryDirectory(), isDirectory: true).URLByDeletingLastPathComponent Note that Apple now prefers the use of URL´s to access folders and files and new methods use them exclusively. To use the path in older methods just call: tempURL.path
How to monitor a folder for new files in swift?
How would I monitor a folder for new files in swift, without polling (which is very inefficient)? I've heard of APIs such as kqueue and FSEvents - but I'm not sure it's possible to implement them in swift?
GCD seems to be the way to go. NSFilePresenter classes doesn't work properly. They're buggy, broken, and Apple is haven't willing to fix them for last 4 years. Likely to be deprecated. Here's a very nice posting which describes essentials of this technique. "Handling Filesystem Events with GCD", by David Hamrick. Sample code cited from the website. I translated his C code into Swift. let fildes = open("/path/to/config.plist", O_RDONLY) let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) let source = dispatch_source_create( DISPATCH_SOURCE_TYPE_VNODE, UInt(fildes), DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE, queue) dispatch_source_set_event_handler(source, { //Reload the config file }) dispatch_source_set_cancel_handler(source, { //Handle the cancel }) dispatch_resume(source); ... // sometime later dispatch_source_cancel(source); For reference, here're another QAs posted by the author: Grand Central Dispatch (GCD) dispatch source flags Monitoring a directory in Cocoa/Cocoa Touch If you're interested in watching directories, here's another posting which describes it. "Monitoring a Folder with GCD" on Cocoanetics. (unfortunately, I couldn't find the author's name. I am sorry for lacking attribution) The only noticeable difference is getting a file-descriptor. This makes event-notification-only file descriptor for a directory. _fileDescriptor = open(path.fileSystemRepresentation(), O_EVTONLY) Update Previously I claimed FSEvents API is not working, but I was wrong. The API is working very well, and if you're interested in watching on deep file tree, than it can be better then GCD by its simplicity. Anyway, FSEvents cannot be used in pure Swift programs. Because it requires passing of C callback function, and Swift does not support it currently (Xcode 6.1.1). Then I had to fallback to Objective-C and wrap it again. Also, any of this kind API is all fully asynchronous. That means actual file system state can be different at the time you are receiving the notifications. Then precise or accurate notification is not really helpful, and useful only for marking a dirty flag. Update 2 I finally ended up with writing a wrapper around FSEvents for Swift. Here's my work, and I hope this to be helpful. https://github.com/eonil/FileSystemEvents
I adapted Stanislav Smida's code to make it work with Xcode 8 and Swift 3 class DirectoryObserver { private let fileDescriptor: CInt private let source: DispatchSourceProtocol deinit { self.source.cancel() close(fileDescriptor) } init(URL: URL, block: #escaping ()->Void) { self.fileDescriptor = open(URL.path, O_EVTONLY) self.source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: self.fileDescriptor, eventMask: .all, queue: DispatchQueue.global()) self.source.setEventHandler { block() } self.source.resume() } }
The simplest solution is to use Apple's DirectoryMonitor.swift https://github.com/Lax/Learn-iOS-Swift-by-Examples/blob/master/Lister/ListerKit/DirectoryMonitor.swift var dm = DirectoryMonitor(URL: AppDelegate.applicationDocumentsDirectory) dm.delegate = self dm.startMonitoring()
Swift 5 Version for Directory Monitor, with GCD, original from Apple import Foundation /// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory. protocol DirectoryMonitorDelegate: class { func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor) } class DirectoryMonitor { // MARK: Properties /// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates. weak var delegate: DirectoryMonitorDelegate? /// A file descriptor for the monitored directory. var monitoredDirectoryFileDescriptor: CInt = -1 /// A dispatch queue used for sending file changes in the directory. let directoryMonitorQueue = DispatchQueue(label: "directorymonitor", attributes: .concurrent) /// A dispatch source to monitor a file descriptor created from the directory. var directoryMonitorSource: DispatchSource? /// URL for the directory being monitored. var url: URL // MARK: Initializers init(url: URL) { self.url = url } // MARK: Monitoring func startMonitoring() { // Listen for changes to the directory (if we are not already). if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 { // Open the directory referenced by URL for monitoring only. monitoredDirectoryFileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY) // Define a dispatch source monitoring the directory for additions, deletions, and renamings. directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: directoryMonitorQueue) as? DispatchSource // Define the block to call when a file change is detected. directoryMonitorSource?.setEventHandler{ // Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change. self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self) } // Define a cancel handler to ensure the directory is closed when the source is cancelled. directoryMonitorSource?.setCancelHandler{ close(self.monitoredDirectoryFileDescriptor) self.monitoredDirectoryFileDescriptor = -1 self.directoryMonitorSource = nil } // Start monitoring the directory via the source. directoryMonitorSource?.resume() } } func stopMonitoring() { // Stop listening for changes to the directory, if the source has been created. if directoryMonitorSource != nil { // Stop monitoring the directory via the source. directoryMonitorSource?.cancel() } } }
I've tried to go with these few lines. So far seems to work. class DirectoryObserver { deinit { dispatch_source_cancel(source) close(fileDescriptor) } init(URL: NSURL, block: dispatch_block_t) { fileDescriptor = open(URL.path!, O_EVTONLY) source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, UInt(fileDescriptor), DISPATCH_VNODE_WRITE, dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT)) dispatch_source_set_event_handler(source, { dispatch_async(dispatch_get_main_queue(), block) }) dispatch_resume(source) } // private let fileDescriptor: CInt private let source: dispatch_source_t } Be sure to not get into retain cycle. If you are going to use owner of this instance in block, do it safely. For example: self.directoryObserver = DirectoryObserver(URL: URL, block: { [weak self] in self?.doSomething() })
SKQueue is a Swift wrapper around kqueue. Here is sample code that watches a directory and notifies of write events. class SomeClass: SKQueueDelegate { func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) { print("\(notification.toStrings().map { $0.rawValue }) # \(path)") } } if let queue = SKQueue() { let delegate = SomeClass() queue.delegate = delegate queue.addPath("/some/file/or/directory") queue.addPath("/some/other/file/or/directory") }
Easiest method I've found that I'm currently using is this wonderful library: https://github.com/eonist/FileWatcher From README Installation: CocoaPods pod "FileWatcher" Carthage github "eonist/FileWatcher" "master" Manual Open FileWatcherExample.xcodeproj let filewatcher = FileWatcher([NSString(string: "~/Desktop").expandingTildeInPath]) filewatcher.callback = { event in print("Something happened here: " + event.path) } filewatcher.start() // start monitoring
I faced a problem that is not mentioned in any of the answers. As my app is using a UIDocumentBrowserViewController (i.e. Apple's own Files app) to manage its documents, I have no control over my users' habits. I was using SKQueue to monitor all files in order to keep metadata in sync, and at a certain point the app started crashing. As it turns out, there is an upper limit of 256 file descriptors that can be open by an app simultaneously, even just for monitoring. I ended up combining SKQueue and Apple's Directory Monitor (reference to which you can find in this answer of the current thread) to create a class I named SFSMonitor, which monitors a whole queue of files or directories by using Dispatch Sources. I detailed my findings and the practices I now use in this SO thread.
You could add UKKQueue to your project. See http://zathras.de/angelweb/sourcecode.htm it's easy to use. UKKQueue is written in Objective C, but you can use it from swift
Depending on your application needs, you may be able to use a simple solution. I actually used kqueue in a production product; I wasn't crazy with the performance but it worked, so I didn't think too much of it till I found a nice little trick that worked even better for my needs, plus, it used less resources which can be important for performance intensive programs. What you can do, again, if your project permits, is that every time you switch to your application, you can just check the folder as part of your logic, instead of having to periodically check the folder using kqueue. This works and uses far less resources.
Cocoa, output current App path
I am working on OS X application. In the code, I would like to output a path of the current Mac OS App to a variable for future use. So later I could read the files in the same folder. Could anyone tell me the command/method in the Xcode ? Thanks very much. UPDATE To be more clear, I am using xcode and create a cocoa-application. My application is connected with applescript to control Mac software read files on the Mac. So I have to return the files' directory and name. Actually I have no idea about what to do. So got stuck here.
To get the URL of your application, you can use: [[NSBundle mainBundle] bundleURL] See the NSBundle Class Reference for further information.
my 2 cents for swift 5,x: let path = Bundle.main.bundlePath print(path) If You need a path to REAL exacutable: (i.e. you mast pass to shell or similar..) let appName = AppName() let exePath = path + "/Contents/MacOS/" + appName print(exePath) where AppName() is: func AppName()->String{ let bund = Bundle.main //print(bund) if let displayName = bund.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String { if displayName.count>0{ return displayName } } if let name = bund.object(forInfoDictionaryKey: "CFBundleName") as? String { return name } return "no AppName" }