How do I write to a local file in Swift/XCTest? - swift

My ultimate question is about saving a screenshot from an AppleTV application using XCTest and Swift4 (running on a MacBook paired to the TV device), but I'm having trouble even writing a simple text string to a local file. If I can get this simple file-save working, I'm hoping I can resolve the screenshot issue. (Apologies for making this look like two questions but they appear to be related and resulted from my troubleshooting efforts.)
First, here's what I'm trying to do with a screenshot, based on sample code I found somewhere online:
let appshot = XCUIApplication().windows.firstMatch.screenshot()
let shotpath = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0].appendingPathComponent("appshot.png")
let shotpathUrl = URL(string: "file://\(shotpath)")
print("Saving to: \(shotpath)")
do {
try appshot.pngRepresentation.write(to: shotpathUrl!)
} catch {
print("Failed saving screenshot due to \(error)")
}
This gives me the following output:
Saving to: file:///var/mobile/Containers/Data/Application/77D52C66-353B-4029-97D5-48E6BAE35C92/Downloads/appshot.png
Failed saving screenshot due to Error Domain=NSCocoaErrorDomain Code=4 "The file “appshot.png” doesn’t exist." UserInfo={NSFilePath=///var/mobile/Containers/Data/Application/77D52C66-353B-4029-97D5-48E6BAE35C92/Downloads/appshot.png, NSUnderlyingError=0x1c405bc60 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Of course, the file doesn't exist because that's the file I'm trying to create. But /var/mobile doesn't exist on my laptop either -- it looks like the path FileManager is building may exist on the AppleTV device, but I want it on my laptop where my test script is executing.
So I backed out to a much more simple case, and even this is giving me problems:
let str = "This is a test"
let path = "file:///Users/haljor/foo.txt"
let pathUrl = URL(string: path)!
print("Path: \(path)")
print("URL: \(pathUrl)")
do {
try str.write(to: pathUrl, atomically: true, encoding: .utf8)
} catch {
print("Caught error writing to \(pathUrl): \(error)")
}
And here's the output:
Path: file:///Users/haljor/foo.txt
URL: file:///Users/haljor/foo.txt
Caught error writing to file:///Users/haljor/foo.txt: Error Domain=NSCocoaErrorDomain Code=4 "The folder “foo.txt” doesn’t exist." UserInfo={NSURL=file:///Users/haljor/foo.txt, NSUserStringVariant=Folder, NSUnderlyingError=0x1c40553f0 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
Here, it looks like it's trying to write to a folder at the path I specified, not a file. Clearly there's something I'm not understanding in each of these cases.
I don't really have a preference for whether I use a fully-specified path or something using FileManager -- it just needs to land somewhere on my laptop (not the TV device). What am I missing?

You can add an attachment to the test case and save it to disk too. The problem was that Downloads folder may not exist in the container yet. The best way to handle this is via init-once property:
var downloadsFolder: URL = {
let fm = FileManager.default
let folder = fm.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
var isDirectory: ObjCBool = false
if !(fm.fileExists(atPath: folder.path, isDirectory: &isDirectory) && isDirectory.boolValue) {
try! fm.createDirectory(at: folder, withIntermediateDirectories: false, attributes: nil)
}
return folder
}()
func test() {
let appshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: appshot)
attachment.lifetime = .keepAlways
self.add(attachment)
// Save to container
let url = downloadsFolder.appendingPathComponent("appshot.png")
try! appshot.pngRepresentation.write(to: url)
}
If you want to view the attachment, right-click on the test case, select Jump to Report and expand the tree. You will see the screenshot eventually:

Related

"Interrupted system call" when reading test data in XCTestCase

Using Xcode (11.4.1) on Catalina (10.15.4, only updated from Mojave 5 days ago), with a tiny SPM-based project opened in Xcode in "folder mode" (i.e. not an actual .xcodeproj), I have a test which should read some input data from a sample file. Following recommendations for constructing a URL to those files, Data(contentsOf:) cannot read it, although the generated URL is correct.
The code is:
func testCanReadConfigFromFile() {
let thisDirectory = URL(fileURLWithPath: #file)
.deletingLastPathComponent()
let url = thisDirectory
.appendingPathComponent("Test_data", isDirectory: true)
.appendingPathComponent("Config.json", isDirectory: false)
print(url.absoluteString)
do {
let _ = try Data(contentsOf: url)
} catch let error {
print(error)
print(error.localizedDescription)
}
}
The exception is:
Error Domain=NSCocoaErrorDomain Code=256 "The file “Config.json” couldn’t be opened."
UserInfo={NSFilePath=/full/path/to/project/Tests/ProjectTests/Test_data/Config.json,
NSUnderlyingError=0x100b0aa30
{Error Domain=NSPOSIXErrorDomain Code=4 "Interrupted system call"}}
If I take the string output by print(url.absoluteString) and in a terminal execute file <absoulteString>, it confirms that my file does exist at that path (and that it is JSON).
"Interrupted system call" makes me think Catalina's stricter sand-boxing rules are to blame, but I have not been shown a permissions dialog. This project is within my ~/Documents folder, which Xcode does have permission to read.
Edited to add:
swift test at the command line works. It's only running tests in Xcode that exhibits the problem.

How do I read a file from the filesystem in a Swift command line app?

I'm just starting learning Swift and to teach myself I'm making a simple command line app. It will eventually connect to an online data source but initially I want to load data from a file. I've seen various guides on reading the contents of a file in Swift but none of them seem to work for me. Here is my app so far:
import Foundation
// Set the file path
let path = "/Users⁩/username/workspace⁩/⁨Swift⁩/sis⁩/sis/data.json⁩"
do {
// Get the contents
let contents = try String(contentsOfFile: path, encoding: .utf8)
print(contents)
}
catch let error as NSError {
print("Ooops! Something went wrong: \(error)")
}
Running it outputs:
Ooops! Something went wrong: Error Domain=NSCocoaErrorDomain Code=260 "The file “data.json⁩” couldn’t be opened because there is no such file." UserInfo={NSFilePath=/Users⁩/username/workspace⁩/⁨Swift⁩/sis⁩/sis/data.json⁩, NSUnderlyingError=0x100e19a50 {Error Domain=NSPOSIXErrorDomain Code=2 "No such file or directory"}}
However on the terminal:
$ ls -l /Users/username/workspace/Swift/sis/sis/data.json
-rwxrwxrwx# 1 username staff 165563 16 Jan 17:14 /Users/username/workspace/Swift/sis/sis/data.json
(yeah I relaxed the permissions somewhat just in case that was the problem)
The only slightly anomalous thing I noticed (aside from the inaccurate assertion that the file doesn't exist) was that when I copy and past the path from the XCode output into iTerm2 it puts spaces between each path component:
(pasted as an image as copying it and pasting it back into this form seems to hide the spaces - this is probably irrelevant anyway)
Any help figuring this out would be really appreciated!
I copied your code, downloaded a sample json file to my desktop, and renamed it to example_ 1.json (I included a space inside the file name).
import Foundation
// Set the file path
let path = "/Users⁩/username/Desktop/example_ 1.json⁩"
do {
// Get the contents
let contents = try String(contentsOfFile: path, encoding: .utf8)
print(contents)
}
catch let error as NSError {
print("Ooops! Something went wrong: \(error)")
}
It successfully printed the file. It also worked when I defined contents as a NSString.
let contents = try NSString(contentsOfFile: path,
encoding: String.Encoding.ascii.rawValue)
I am using Swift 4.2.1
you can not read if your command line app is sandboxed. what you can do is to add this file in your project and set path of file by looking the full path of file in identity inspector.
let path = "/Users/snx/EmailReplacer/EmailReplacer/shared_domains_staging.json"
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
print(jsonResult)
}
} catch {
print(error)
}

Using a bundle to load object file for MDLAsset

I'm trying to load an .obj file to create an MDLAsset object in a macOS Swift app. Originally, I was creating the asset like so:
let myAsset = URL(fileURLWithPath: "/Users/me/Development/MyProject/MyApp/Assets.xcassets/arrow.dataset/arrow.obj")
arrowMdl = MDLAsset(url:arrow).object(at: 0)
Obviously that won't work when the app is in production - so, based off this SO answer, I tried adding the .obj file to a bundle, and then load it, like so:
let path: String = Bundle.main.path(forResource: "Arrow", ofType: "bundle")!
do {
let arrowPath = try String(contentsOfFile: path)
}
catch let error as NSError {
print(error.description)
}
However, I keep getting the following error:
Error Domain=NSCocoaErrorDomain Code=257 "The file “Arrow.bundle” couldn’t be opened because you don’t have permission to view it."
I made sure to set the permissions to read/write for everyone.
What am I doing wrong? Or, is there another way to load this asset? It looks as though MDLAsset requires a URL to for it to be initialized: https://developer.apple.com/documentation/modelio/mdlasset

Move File After Downloading

Im using parse to download some files to the device and all is good but i want to then move the file from the default cache directory which is where parse stores them (/Library/Caches/PFFileCache/) to somewhere more stable say the users documents directory.
The localised error i am getting is:
Error: “7b54d8a0f1a64b710058d4408ca4d696_The%20Name%20of%20the%20Wind%2029-92.mp3” couldn’t be moved to “Documents” because either the former doesn't exist, or the folder containing the latter doesn't exist.
But I'm sure both exist. It may be something to do with the name as when i ge the name from the PFFile it doesn't have the encoded %20 in the file name.
This is my code block:
let cachedPFFile = object.object(forKey: "partAudio") as! PFFile
print(cachedPFFile)
let getCachedFilePath = cachedPFFile.getPathInBackground()
getCachedFilePath.waitUntilFinished()
let cachedFilePath = getCachedFilePath.result as! String
print(cachedFilePath)
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = String(describing: paths[0])
let saveDirectory = documentsDirectory.appending(cachedPFFile.name)
print(documentsDirectory)
print(saveDirectory)
let fileManager = FileManager.default
if fileManager.fileExists(atPath: saveDirectory) == false {
do {
try fileManager.moveItem(atPath: cachedFilePath, toPath: saveDirectory)
print("Move successful")
} catch let error {
print("Error: \(error.localizedDescription)")
}
}
This is the entire log:
<PFFile: 0x608000458f00>
2017-02-20 18:42:35.430 ParseStarterProject-Swift[2260:55934] Warning: A long-running operation is being executed on the main thread.
Break on warnBlockingOperationOnMainThread() to debug.
/Users/Genie/Library/Developer/CoreSimulator/Devices/A2FB00CE-B018-4FDF-9635-35FD6678DF8D/data/Containers/Data/Application/BA7C112C-BECB-4734-8C67-A9CB84F0E1F3/Library/Caches/Parse/PFFileCache/7b54d8a0f1a64b710058d4408ca4d696_The%20Name%20of%20the%20Wind%2029-92.mp3
file:///Users/Genie/Library/Developer/CoreSimulator/Devices/A2FB00CE-B018-4FDF-9635-35FD6678DF8D/data/Containers/Data/Application/BA7C112C-BECB-4734-8C67-A9CB84F0E1F3/Documents/
file:///Users/Genie/Library/Developer/CoreSimulator/Devices/A2FB00CE-B018-4FDF-9635-35FD6678DF8D/data/Containers/Data/Application/BA7C112C-BECB-4734-8C67-A9CB84F0E1F3/Documents/7b54d8a0f1a64b710058d4408ca4d696_The Name of the Wind 29-92.mp3
Error: “7b54d8a0f1a64b710058d4408ca4d696_The%20Name%20of%20the%20Wind%2029-92.mp3” couldn’t be moved to “Documents” because either the former doesn't exist, or the folder containing the latter doesn't exist.
I am confused by this because both the file and the new directory exist??
though I'm not sure about the "file:///Users" as this doesn't work in finder, have to use the true path "/Users" how can i remove "file://" from the start of the string??
Also I've been searching SO without luck on how to add the %20 in the spaces of the file name none of the obvious options seem to work.
You have threading issues. Your entire idea of "waitUntilFinished" is wrong. You cannot block the main thread — and you are getting a warning telling you so. Listen to that warning!
Do not call getFilePathInBackground, and do not wait for the download to finish. Instead, call getFileInBackgroundWithBlock:, and inside the block proceed with the rest of your code, i.e. move the file.

Copy TPK file from AppGroup Container to Documents

I have a file that exists within the AppGroup Shared Container and I was wondering if it was possible to copy the file from the Shared Container into the application bundle.
I am getting the file path as follows :
let filePath = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.com.sharedBasemap")!.URLByAppendingPathComponent("localLayer.tpk")!.path
The reason I am trying to do this is it seems that the ArcGIS SDK will not recognize the TPK file from within the App Group so I am wondering if it will recognize it if I copy it into the app bundle.
EDIT: Based on Leo's comment it appears that you can not copy to the bundle, so I am trying to copy to the App Support folder.Here is my code now, I see the "file exists" message but then it is displaying the Oops message indicating it can not move the file :
let filePath = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.com.sharedBasemap")!.URLByAppendingPathComponent("localLayer.tpk")!.path!
let appSupportFolder = String(NSFileManager.defaultManager().URLsForDirectory(.ApplicationSupportDirectory, inDomains: .UserDomainMask)[0]) + "localLayer.tpk"
let fileManager = NSFileManager.defaultManager()
if NSFileManager.defaultManager().fileExistsAtPath(filePath){
print("File exists at \(filePath)")
do {
try fileManager.copyItemAtPath(filePath, toPath: appSupportFolder)
}
catch let error as NSError {
print("Ooops! Something went wrong: \(error)")
}
} else {
print("File does not exist")
}
EDIT 2: I have modified the code again to just move the TPK file into the documents directory.I believe that piece is working but I receive an error message when trying to load the TPK file into ArcGIS.At this point in time, I am thinking that the issue is related to the ArcGIS SDK and that it does not support loading a TPK file from anywhere except the application bundle.
let destPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true).first!
let fullDestPath = NSURL(fileURLWithPath: destPath).URLByAppendingPathComponent("localLayer.tpk")
let fullDestPathString = fullDestPath!.path!
im pretty sure the appSupportFolder doesn't exist by default -- nobody creates it unless needed -- try to verify that first and create it if needed
pseudocode if(!fileExists(supportFolder)) { createDirectory(supportFolder) }