XCTest: Load file from disk without Bundle on all platforms (Xcode, SPM Mac/Linux) - swift

I am looking for a way to load files from disk within an XCTestCase that does not depend on Bundle.
Bundle works well when running the tests from Xcode (or with xcodebuild on the terminal), but bundles are part of the Xcode project and not available to Swift Package Manager (when running swift test), neither on the Mac nor in Linux.
Is there a way to specify the current directory where tests should be run that works in all platforms? Or maybe there is a way to determine where the tests are located that also works on all platforms?
FileManager.default.currentDirectoryPath only returns the current execution path (working directory).

This method seems to work for me, and should work with both Xcode 11 and from the command line. Copy of the answer I just wrote here.
struct Resource {
let name: String
let type: String
let url: URL
init(name: String, type: String, sourceFile: StaticString = #file) throws {
self.name = name
self.type = type
// The following assumes that your test source files are all in the same directory, and the resources are one directory down and over
// <Some folder>
// - Resources
// - <resource files>
// - <Some test source folder>
// - <test case files>
let testCaseURL = URL(fileURLWithPath: "\(sourceFile)", isDirectory: false)
let testsFolderURL = testCaseURL.deletingLastPathComponent()
let resourcesFolderURL = testsFolderURL.deletingLastPathComponent().appendingPathComponent("Resources", isDirectory: true)
self.url = resourcesFolderURL.appendingPathComponent("\(name).\(type)", isDirectory: false)
}
}
Usage:
final class SPMTestDataTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(SPMTestData().text, "Hello, World!")
let file = try Resource(name: "image", type: "png")
let image = UIImage(contentsOfFile: file.url.path)
print(image)
}
}
I found the key of using #file here

Seems like when running tests with Swift Package Manager (swift test), the working directory is the root of the project. This allows for easily loading resources from disk using a relative path (eg. ./Tests/Resources).
After evaluating different options, I wrote the following class to retrieve the path both from the test bundle, if available, or from a given path.
class Resource {
static var resourcePath = "./Tests/Resources"
let name: String
let type: String
init(name: String, type: String) {
self.name = name
self.type = type
}
var path: String {
guard let path: String = Bundle(for: Swift.type(of: self)).path(forResource: name, ofType: type) else {
let filename: String = type.isEmpty ? name : "\(name).\(type)"
return "\(Resource.resourcePath)/\(filename)"
}
return path
}
}
Resources must be added to all test targets for them to be available in the bundle. The above class could be updated to support multiple resource paths, by file extension, for instance.
Resources can be then loaded from an XCTest as needed:
let file = Resource(name: "datafile", type: "csv")
let content = try String(contentsOfFile: file)
let image = try UIImage(contentsOfFile: Resource(name: "image", type: "png"))
Extensions can be added to Resource to load the contents in different formats.
extension Resource {
var content: String? {
return try? String(contentsOfFile: path).trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
}
var base64EncodedData: Data? {
guard let string = content, let data = Data(base64Encoded: string) else {
return nil
}
return data
}
var image: Image? {
return try? UIImage(contentsOfFile: path)
}
}
And be used as:
let json = Resource(name: "datafile", type: "json").contents
let image = Resource(name: "image", type: "jpeg").image

Related

Path part of "filename.m4v" is "/private/tmp", which is unexpected

I'm writing some Swift code to parse filenames of video files and extract the show, season and episode. These are returned as key/value parts in a dictionary. As part of my unit tests, I found something odd. First the code (comments and whitespace removed):
public static func parse(_ filename: String, defaults: [String: String] = [String: String]()) -> [String: String] {
let url = URL(fileURLWithPath: filename)
let file = url.deletingPathExtension().lastPathComponent
if file.count == 0 {
return ret
}
if file.count > 0 {
ret["file"] = file
}
let ext = url.pathExtension
if ext.count > 0 {
ret["extension"] = ext
}
let path = url.deletingLastPathComponent().path
if path.count > 0 {
ret["path"] = path
}
I called this in my test case thus...
ParseVideoFilename.parse("non-empty-filename.m4v")
And this is what resulted:
["ext": "m4v", "file": "non-empty-filename", "path": "/private/tmp"]
I am a bit surprised about that path. I did not pass that in, so I assume URL is doing something here. I don't expand the path nor resolve it. Is this expected behavior, and if so, why?
Running your code in a playground just gave me file:///private/var/folders/1p/wpwdypm96_s5zfwxxzvwwp0m0000gn/T/com.apple.dt.Xcode.pg/containers/com.apple.dt.playground.stub.iOS_Simulator.Playground-C18F0418-5C1D-4772-9AE9-E3EF9AA2F07C/non-empty-filename.m4v as the output of
let url = URL(fileURLWithPath: filename)
print(url.absoluteString)
This looks to me as the current directory of the process that gets executed by Playground. I'm not at all surprised by this, since we gave URL a relative path without any base and the file URI scheme doesn't really know how to handle relative paths (see e.g. this answer)
If you absolutely don't want to see these artifacts, you can modify your parser to use an absolute path:
let url = URL(fileURLWithPath: "/" + filename)
print(url.absoluteString) // prints "file:///non-empty-filename.m4v"

How do I add an Images folder to my FileWrapper

I need a FileWrapper which contains a file and contains a folder.
The file is a single file, and the folder is used to write images to.
The folder also can contain some subfolders. I have a working code, but the issue is that when the Document gets saved, the folder gets re-written which deletes my images and subfolders.
I'm quite sure it has something to do with func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper but I need some help from somebody with more experience with FileWrappers.
This is my code:
struct MyProject: FileDocument {
var myFile: MyFile
static var readableContentTypes: [UTType] { [.myf] }
init(myFile: MyFile = MyFile() {
self.myFile = myFile
}
init(configuration: ReadConfiguration) throws {
let decoder = JSONDecoder()
guard let data = configuration.file.fileWrappers?["MYFProject"]?.regularFileContents else {
throw CocoaError(.fileReadCorruptFile)
}
do {
self.myFile = try decoder.decode(MyFile.self, from: data)
} catch {
throw error
}
}
func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
let encoder = JSONEncoder()
do {
let data = try encoder.encode(myFile)
let mainDirectory = FileWrapper(directoryWithFileWrappers: [:])
let myfWrapper = FileWrapper(regularFileWithContents: data)
let imagesWrapper = FileWrapper(directoryWithFileWrappers: [:])
let imageSubFolder = FileWrapper(directoryWithFileWrappers: [:])
for numberString in myFile.numbers {
imageSubFolder.preferredFilename = numberString
imagesWrapper.addFileWrapper(imageSubFolder)
}
myfWrapper.preferredFilename = "MYFProject"
mainDirectory.addFileWrapper(myfWrapper)
imagesWrapper.preferredFilename = "MYFImages"
mainDirectory.addFileWrapper(imagesWrapper)
return mainDirectory
} catch {
throw error
}
}
}
I use this path to write images to.
func getSubFolderImageFolder(documentPath: URL, subFolder: String) -> URL {
let sfProjectPath = documentPath.appendingPathComponent("MYFImages").appendingPathComponent(subFolder)
if !FileManager.default.fileExists(atPath: sfProjectPath.path) {
do {
try FileManager.default.createDirectory(atPath: sfProjectPath.path, withIntermediateDirectories: false, attributes: nil)
return sfProjectPath
} catch {
fatalError(error.localizedDescription)
}
}
else {
return sfProjectPath
}
}
Thanks in advance!
Your getSubFolderImageFolder function is not going to work well with file wrappers. You must use the FileWrapper methods to create the folders and files in the file wrapper.
To add a subfolder to your images folder, create a directory file wrapper the same way you created the imagesWrapper folder for the images. Add the subfolder as a child of the images folder.
let imageSubFolder = FileWrapper(directoryWithFileWrappers: [:])
imagesWrapper.addFileWrapper(imageSubFolder)
You must create a directory file wrapper for each subfolder. I notice in your updated code, you have only one subfolder file wrapper. With only one subfolder file wrapper, you have no way to store an image file in the correct subfolder.
To add the images, start by converting each image into a Data object. Create a regular file wrapper for each image, passing the image data as the argument to regularFileWithContents. Call addFileWrapper to add the image file to the appropriate folder.
let imageFile = FileWrapper(regularFileWithContents: imageData)
imageFile.preferredFilename = "ImageFilename" // Replace with your filename.
imagesWrapper.addFileWrapper(imageFile)
In your case the image subfolders will call addFileWrapper to add the images. The destination for the image file calls addFileWrapper.
You can find more detailed information about file wrappers in the following article:
Using File Wrappers in a SwiftUI App

How to read variables from txt file?

For example i've got a build archive and i want it to read some variables from txt file. So the question is there any way to upload a txt file to iPhone device and how to read that variables?
In my project i've got this constants:
baseURL: String = "someUrl"
client_id: String
client_secret: String
authorize_uri: String
token_uri: String
scope: String
redirect_uris: [String]
secret_in_body: Bool
and for example i created a txt file with this strings and somehow uploaded it to my device
i want the builded project to read this file
You could create a new plist file in your project, then parse it into a dictionary at startup. Once there you can just refer to each value by its key
If you create an extension to Bundle()
extension Bundle {
func parsePlist(ofName name: String) -> [String: String]? {
// check if plist data available
guard let plistURL = Bundle.main.url(forResource: name, withExtension: "plist"),
let data = try? Data(contentsOf: plistURL)
else {
return nil
}
// parse plist into [String: Anyobject]
guard let plistDictionary = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: String] else {
return nil
}
return plistDictionary
}
}
then you can load it into a dictionary like this
var dict = Bundle().parsePlist(ofName: "plistName")!
Xcode does a nice job of allowing you to edit the plist, and it'll be automatically included in your build

fileExistsAtPath check for filename?

How to check whether there is a file in a directory with only the name without extension? Now the files are written in my directory, their name will be generated from the id file. Accordingly, when I'm looking for a file, let file = "\ (fileId) .pdf", in the directory it is, but if no extension, it will not be found. Either return as easier extension from the server?
public var isDownloaded: Bool {
let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let url = NSURL(fileURLWithPath: path)
let filePath = url.URLByAppendingPathComponent("\(fileMessageModel.attachment.id)")!.path!
let fileManager = NSFileManager.defaultManager()
return fileManager.fileExistsAtPath(filePath)
}
enumeratorAtPath creates a deep enumerator -- i.e. it will scan contents of subfolders and their subfolders too. For a shallow search, user contentOfDirectortAtPath:
func file(fileName: String, existsAt path: String) -> Bool {
var isFound = false
if let pathContents = try? NSFileManager.defaultManager().contentsOfDirectoryAtPath(path) {
pathContents.forEach { file in
if (file as NSString).stringByDeletingPathExtension == fileName {
isFound = true
return
}
}
}
return isFound
}
if let path = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first {
if file("something", existsAt: path) {
// The file exists, do something about it
}
}
What about iterating over the files in the directory and testing the name with extension excluded?
let filemanager:FileManager = FileManager()
let files = filemanager.enumeratorAtPath(/* your directory path */)
while let file = files?.nextObject() {
// Remove file name extension
// Do file name comparison here
}
In terms of time complexity is will be O(n), however, as long as there are not too many files, you are good to go. On the other hand, if there are many files, you will need to consider a more efficient way to traverse, may be a trie data structure consisted of all file names in that directory.

I cant read my text files from my application's Bundle

I used to read the text files from my application's bundle by using the following code. However, no matter what my application can't find them anymore. I am 100% sure that all my files are in the Assets.xcassets, I can see them, edit them, transform them from a directory to another. But my application doesn't want to read them, please tell me what I missed!!
this is the procedure I am using...
func readBundle(file:String) -> String
{
var res: String = ""
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
{
let fm = NSFileManager()
let exists = fm.fileExistsAtPath(path)
if(exists)
{
let c = fm.contentsAtPath(path)
res = NSString(data: c!, encoding: NSUTF8StringEncoding) as! String
}
}
return res
}
I am using it like this:
let res = readBundle("test")
print(res)
when storing non image files in XCAssets, you should use NSDataAsset to acccess their content
https://developer.apple.com/library/ios/documentation/UIKit/Reference/NSDataAsset_Class/
func readBundle(file:String) -> String
{
var res = ""
if let asset = NSDataAsset(name: file) ,
string = String(data:asset.data, encoding: NSUTF8StringEncoding){
res = string
}
return res
}
In the another option then 'XCAssets' you can create a separate folder/group of your resources other than images in the project structure, check if they exist in the Copy Bundle Resource in the Build phases section of your project's main target
If you add resource like this your current code should work as it is
func readBundle(file:String) -> String
{
var res: String = ""
if let path = NSBundle.mainBundle().pathForResource(file, ofType: "txt")
{
//you should be able to get the path
//other code as you has written in the question
}
return res
}