blob: 58623437743602f2518b8a6f6af9bdc8cbbf30b7 [file] [log] [blame]
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2016, 2018, 2019 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
#if !DARWIN_COMPATIBILITY_TESTS // Disable until Foundation has the new FileHandle API
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
#if canImport(SwiftFoundation) && !DEPLOYMENT_RUNTIME_OBJC
@testable import SwiftFoundation
#else
@testable import Foundation
#endif
#endif
import Dispatch
#if os(Windows)
import WinSDK
#endif
class TestFileHandle : XCTestCase {
var allHandles: [FileHandle] = []
var allTemporaryFileURLs: [URL] = []
let content: Data = {
return """
CHAPTER I.
The Author gives some account of himself and family--His first
inducements to travel--He is shipwrecked, and swims for his life--Gets
safe on shore in the country of Lilliput--Is made a prisoner, and
carried up the country
CHAPTER II.
The emperor of Lilliput, attended by several of the nobility, comes to
see the Author in his confinement--The emperor's person and habits
described--Learned men appointed to teach the Author their language--He
gains favor by his mild disposition--His pockets are searched, and his
sword and pistols taken from him
CHAPTER III.
The Author diverts the emperor, and his nobility of both sexes, in a
very uncommon manner--The diversions of the court of Lilliput
described--The Author has his liberty granted him upon certain
conditions
CHAPTER IV.
Mildendo, the metropolis of Lilliput, described, together with the
emperor's palace--A conversation between the Author and a principal
secretary concerning the affairs of that empire--The Author's offers to
serve the emperor in his wars
CHAPTER V.
The Author, by an extraordinary stratagem, prevents an invasion--A high
title of honor is conferred upon him--Ambassadors arrive from the
emperor of Blefuscu, and sue for peace
""".data(using: .utf8)!
}()
func createTemporaryFile(containing data: Data = Data()) -> URL {
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString)
allTemporaryFileURLs.append(url)
expectDoesNotThrow({ try data.write(to: url) }, "Couldn't create file at \(url.path) for testing")
return url
}
func createFileHandle() -> FileHandle {
let url = createTemporaryFile(containing: content)
var fh: FileHandle?
expectDoesNotThrow({ fh = try FileHandle(forReadingFrom: url) }, "Couldn't create file handle.")
allHandles.append(fh!)
return fh!
}
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
func createFileHandleForSeekErrors() -> FileHandle {
#if os(Windows)
var hReadPipe: HANDLE? = INVALID_HANDLE_VALUE
var hWritePipe: HANDLE? = INVALID_HANDLE_VALUE
if CreatePipe(&hReadPipe, &hWritePipe, nil, 0) == FALSE {
assert(false)
}
if CloseHandle(hWritePipe) == FALSE {
assert(false)
}
return FileHandle(handle: hReadPipe!, closeOnDealloc: true)
#else
var fds: [Int32] = [-1, -1]
fds.withUnsafeMutableBufferPointer { (pointer) -> Void in
pipe(pointer.baseAddress)
}
close(fds[1])
let fh = FileHandle(fileDescriptor: fds[0], closeOnDealloc: true)
allHandles.append(fh)
return fh
#endif
}
#endif
let seekError = NSError(domain: NSCocoaErrorDomain, code: NSFileReadUnknownError, userInfo: [ NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(ESPIPE), userInfo: [:])])
func createFileHandleForReadErrors() -> FileHandle {
// Create a file handle where calling read returns -1.
// Accomplish this by creating one for a directory.
#if os(Windows)
let hDirectory: HANDLE = ".".withCString(encodedAs: UTF16.self) {
// NOTE(compnerd) we need the FILE_FLAG_BACKUP_SEMANTICS so that we
// can create the handle to the directory.
CreateFileW($0, GENERIC_READ,
DWORD(FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE),
nil, DWORD(OPEN_EXISTING),
DWORD(FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS), nil)
}
if hDirectory == INVALID_HANDLE_VALUE {
fatalError("unable to create handle to current directory")
}
let fd = _open_osfhandle(intptr_t(bitPattern: hDirectory), 0)
if fd == -1 {
fatalError("unable to associate file descriptor with handle")
}
let fh = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
#else
let fd = open(".", O_RDONLY)
expectTrue(fd > 0, "We must be able to open a fd to the current directory (.)")
let fh = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
#endif
allHandles.append(fh)
return fh
}
#if os(Windows)
let readError = NSError(domain: NSCocoaErrorDomain, code: NSFileReadUnknownError, userInfo: [ NSUnderlyingErrorKey: NSError(domain: "org.swift.Foundation.WindowsError", code: 1, userInfo: [:])])
#else
let readError = NSError(domain: NSCocoaErrorDomain, code: NSFileReadUnknownError, userInfo: [ NSUnderlyingErrorKey: NSError(domain: NSPOSIXErrorDomain, code: Int(EISDIR), userInfo: [:])])
#endif
override func tearDown() {
for handle in allHandles {
print("Closing \(handle)…")
try? handle.close()
}
for url in allTemporaryFileURLs {
print("Deleting \(url)…")
try? FileManager.default.removeItem(at: url)
}
allHandles = []
allTemporaryFileURLs = []
}
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
func testHandleCreationAndCleanup() {
_ = createFileHandle()
_ = createFileHandleForSeekErrors()
_ = createFileHandleForReadErrors()
}
#endif
func testReadUpToCount() {
let handle = createFileHandle()
// Zero:
expectDoesNotThrow({
let zeroData = try handle.read(upToCount: 0)
expectEqual(zeroData, nil, "Data should be nil")
}, "Must not throw while reading zero data")
// Max:
expectDoesNotThrow({
let maxData = try handle.read(upToCount: Int.max)
expectEqual(maxData, content, "Data should be equal to the content")
}, "Must not throw while reading Int.max data")
// EOF:
expectDoesNotThrow({
let eof = try handle.read(upToCount: Int.max)
expectEqual(eof, nil, "EOF should return nil")
}, "Must not throw while reading EOF")
// One byte at a time
let onesHandle = createFileHandle()
expectDoesNotThrow({
for index in content.indices {
let oneByteData = try onesHandle.read(upToCount: 1)
let expected = content[index ..< content.index(after: index)]
expectEqual(oneByteData, expected, "Read incorrect data at index \(index)")
}
}, "Must not throw while reading one byte at a time")
// EOF:
expectDoesNotThrow({
let eof = try handle.read(upToCount: 1)
expectEqual(eof, nil, "EOF should return nil")
}, "Must not throw while reading one-byte-at-a-time EOF")
// Errors:
expectThrows(readError, {
_ = try createFileHandleForReadErrors().read(upToCount: 1)
}, "Must throw when encountering a read error")
}
func testReadToEnd() {
let handle = createFileHandle()
// To end:
expectDoesNotThrow({
let maxData = try handle.readToEnd()
expectEqual(maxData, content, "Data to end should equal what was written out")
}, "Must not throw while reading to end")
// EOF:
expectDoesNotThrow({
let eof = try handle.readToEnd()
expectEqual(eof, nil, "EOF should return nil")
}, "Must not throw while reading EOF")
// Errors:
expectThrows(readError, {
_ = try createFileHandleForReadErrors().readToEnd()
}, "Must throw when encountering a read error")
}
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
func testOffset() {
// One byte at a time:
let handle = createFileHandle()
var offset: UInt64 = 0
for index in content.indices {
expectDoesNotThrow({ offset = try handle.offset() }, "Reading the offset must not throw")
expectEqual(offset, UInt64(index), "The offset must match")
expectDoesNotThrow({ _ = try handle.read(upToCount: 1) }, "Advancing by reading must not throw")
}
expectDoesNotThrow({ offset = try handle.offset() }, "Reading the offset at EOF must not throw")
expectEqual(offset, UInt64(content.count), "The offset at EOF must be at the end")
// Error:
expectThrows(seekError, {
_ = try createFileHandleForSeekErrors().offset()
}, "Must throw when encountering a seek error")
}
#endif
func performWriteTest<T: DataProtocol>(with data: T, expecting expectation: Data? = nil) {
let url = createTemporaryFile()
var maybeFH: FileHandle?
expectDoesNotThrow({ maybeFH = try FileHandle(forWritingTo: url) }, "Opening write handle must succeed")
guard let fh = maybeFH else { return }
allHandles.append(fh)
expectDoesNotThrow({ try fh.write(contentsOf: data) }, "Writing must succeed")
expectDoesNotThrow({ try fh.close() }, "Closing write handle must succeed")
var readData: Data?
expectDoesNotThrow({ readData = try Data(contentsOf: url) }, "Must be able to read data")
expectEqual(readData, expectation ?? content, "The content must be the same")
}
func testWritingWithData() {
performWriteTest(with: content)
}
func testWritingWithBuffer() {
content.withUnsafeBytes { (buffer) in
performWriteTest(with: buffer)
}
}
func testWritingWithMultiregionData() {
var expectation = Data()
expectation.append(content)
expectation.append(content)
expectation.append(content)
expectation.append(content)
content.withUnsafeBytes { (buffer) in
let data1 = DispatchData(bytes: buffer)
let data2 = DispatchData(bytes: buffer)
var multiregion1: DispatchData = .empty
multiregion1.append(data1)
multiregion1.append(data2)
var multiregion2: DispatchData = .empty
multiregion2.append(data1)
multiregion2.append(data2)
var longMultiregion: DispatchData = .empty
longMultiregion.append(multiregion1)
longMultiregion.append(multiregion2)
expectTrue(longMultiregion.regions.count > 0, "The multiregion data must be actually composed of multiple regions")
performWriteTest(with: longMultiregion, expecting: expectation)
}
}
func test_constants() {
XCTAssertEqual(FileHandle.readCompletionNotification.rawValue, "NSFileHandleReadCompletionNotification",
"\(FileHandle.readCompletionNotification.rawValue) is not equal to NSFileHandleReadCompletionNotification")
}
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
func test_nullDevice() {
let fh = FileHandle.nullDevice
XCTAssertFalse(fh._isPlatformHandleValid)
fh.closeFile()
fh.seek(toFileOffset: 10)
XCTAssertEqual(fh.offsetInFile, 0)
XCTAssertEqual(fh.seekToEndOfFile(), 0)
XCTAssertEqual(fh.readData(ofLength: 15).count, 0)
fh.synchronizeFile()
fh.write(Data([1,2]))
fh.seek(toFileOffset: 0)
XCTAssertEqual(fh.availableData.count, 0)
fh.write(Data([1,2]))
fh.seek(toFileOffset: 0)
XCTAssertEqual(fh.readDataToEndOfFile().count, 0)
}
#endif
func test_truncateFile() {
let url: URL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString, isDirectory: false)
_ = FileManager.default.createFile(atPath: url.path, contents: Data())
defer { _ = try? FileManager.default.removeItem(at: url) }
let fh: FileHandle = FileHandle(forUpdatingAtPath: url.path)!
fh.truncateFile(atOffset: 50)
XCTAssertEqual(fh.offsetInFile, 50)
fh.truncateFile(atOffset: 0)
XCTAssertEqual(fh.offsetInFile, 0)
fh.truncateFile(atOffset: 100)
XCTAssertEqual(fh.offsetInFile, 100)
fh.write(Data([1, 2]))
XCTAssertEqual(fh.offsetInFile, 102)
fh.seek(toFileOffset: 4)
XCTAssertEqual(fh.offsetInFile, 4)
(0..<20).forEach { fh.write(Data([$0])) }
XCTAssertEqual(fh.offsetInFile, 24)
fh.seekToEndOfFile()
XCTAssertEqual(fh.offsetInFile, 102)
fh.truncateFile(atOffset: 10)
XCTAssertEqual(fh.offsetInFile, 10)
fh.seek(toFileOffset: 0)
XCTAssertEqual(fh.offsetInFile, 0)
let data = fh.readDataToEndOfFile()
XCTAssertEqual(data.count, 10)
XCTAssertEqual(data, Data([0, 0, 0, 0, 0, 1, 2, 3, 4, 5]))
}
func test_readabilityHandlerCloseFileRace() throws {
for _ in 0..<10 {
let handle = createFileHandle()
handle.readabilityHandler = { _ = $0.offsetInFile }
handle.closeFile()
Thread.sleep(forTimeInterval: 0.001)
}
}
func test_readabilityHandlerCloseFileRaceWithError() throws {
for _ in 0..<10 {
let handle = createFileHandle()
handle.readabilityHandler = { _ = try? $0.offset() }
try handle.close()
Thread.sleep(forTimeInterval: 0.001)
}
}
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
func test_fileDescriptor() throws {
let handle = createFileHandle()
XCTAssertTrue(handle._isPlatformHandleValid, "File descriptor after opening should be valid")
try handle.close()
XCTAssertFalse(handle._isPlatformHandleValid, "File descriptor after closing should not be valid")
}
#endif
func test_availableData() {
let handle = createFileHandle()
let availableData = handle.availableData
XCTAssertEqual(availableData, content, "Available data should be the same as input")
let eofData = handle.availableData
XCTAssertTrue(eofData.isEmpty, "Should return empty data for EOF")
}
func test_readToEndOfFileInBackgroundAndNotify() {
let handle = createFileHandle()
let done = expectation(forNotification: .NSFileHandleReadToEndOfFileCompletion, object: handle, notificationCenter: .default) { (notification) -> Bool in
XCTAssertEqual(notification.userInfo as? [String: AnyHashable], [NSFileHandleNotificationDataItem: self.content], "User info was incorrect")
return true
}
handle.readToEndOfFileInBackgroundAndNotify()
wait(for: [done], timeout: 10)
}
func test_readToEndOfFileAndNotify() {
let handle = createFileHandle()
var readSomeData = false
let done = expectation(forNotification: FileHandle.readCompletionNotification, object: handle, notificationCenter: .default) { (notification) -> Bool in
guard let data = notification.userInfo?[NSFileHandleNotificationDataItem] as? Data else {
XCTFail("Couldn't find the data in the user info: \(notification)")
return true
}
if !data.isEmpty {
readSomeData = true
handle.readInBackgroundAndNotify()
return false
} else {
return true
}
}
handle.readInBackgroundAndNotify()
wait(for: [done], timeout: 10)
XCTAssertTrue(readSomeData, "At least some data must've been read")
}
func test_readToEndOfFileAndNotify_readError() {
let handle = createFileHandleForReadErrors()
let done = expectation(forNotification: FileHandle.readCompletionNotification, object: handle, notificationCenter: .default) { (notification) -> Bool in
guard let error = notification.userInfo?["NSFileHandleError"] as? NSNumber else {
XCTFail("Couldn't find the data in the user info: \(notification)")
return true
}
XCTAssertNil(notification.userInfo?[NSFileHandleNotificationDataItem])
#if os(Windows)
XCTAssertEqual(error, NSNumber(value: ERROR_DIRECTORY_NOT_SUPPORTED))
#else
XCTAssertEqual(error, NSNumber(value: EISDIR))
#endif
return true
}
handle.readInBackgroundAndNotify()
wait(for: [done], timeout: 10)
}
func test_waitForDataInBackgroundAndNotify() {
let handle = createFileHandle()
let done = expectation(forNotification: .NSFileHandleDataAvailable, object: handle, notificationCenter: .default) { (notification) in
let count = notification.userInfo?.count ?? 0
XCTAssertEqual(count, 0)
return true
}
handle.waitForDataInBackgroundAndNotify()
wait(for: [done], timeout: 10)
}
func test_readWriteHandlers() {
for _ in 0..<100 {
let pipe = Pipe()
let write = pipe.fileHandleForWriting
let read = pipe.fileHandleForReading
var notificationReceived = false
let semaphore = DispatchSemaphore(value: 0)
let count = content.count
read.readabilityHandler = { (handle) in
// Check that we can reentrantly set the handler:
handle.readabilityHandler = { (handle2) in
if let readData = try? handle2.read(upToCount: count) {
XCTAssertEqual(readData.count, count, "Should have read as much data as was sent")
semaphore.signal()
} else {
// EOF:
handle2.readabilityHandler = nil
}
}
notificationReceived = true
if let readData = try? handle.read(upToCount: count) {
XCTAssertEqual(readData.count, count, "Should have read as much data as was sent")
}
}
write.writeabilityHandler = { (handle) in
handle.writeabilityHandler = { (handle2) in
handle2.writeabilityHandler = nil
try? handle2.write(contentsOf: self.content)
}
try? handle.write(contentsOf: self.content)
}
let result = semaphore.wait(timeout: .now() + .seconds(30))
XCTAssertEqual(result, .success, "Waiting on the semaphore should not have had time to time out")
XCTAssertTrue(notificationReceived, "Notification should be sent")
}
}
static var allTests : [(String, (TestFileHandle) -> () throws -> ())] {
var tests: [(String, (TestFileHandle) -> () throws -> ())] = [
("testReadUpToCount", testReadUpToCount),
("testReadToEnd", testReadToEnd),
("testWritingWithData", testWritingWithData),
("testWritingWithBuffer", testWritingWithBuffer),
("testWritingWithMultiregionData", testWritingWithMultiregionData),
("test_constants", test_constants),
("test_truncateFile", test_truncateFile),
("test_readabilityHandlerCloseFileRace", test_readabilityHandlerCloseFileRace),
("test_readabilityHandlerCloseFileRaceWithError", test_readabilityHandlerCloseFileRaceWithError),
("test_availableData", test_availableData),
("test_readToEndOfFileInBackgroundAndNotify", test_readToEndOfFileInBackgroundAndNotify),
("test_readToEndOfFileAndNotify", test_readToEndOfFileAndNotify),
("test_readToEndOfFileAndNotify_readError", test_readToEndOfFileAndNotify_readError),
("test_waitForDataInBackgroundAndNotify", test_waitForDataInBackgroundAndNotify),
/* ⚠️ */ ("test_readWriteHandlers", testExpectedToFail(test_readWriteHandlers,
/* ⚠️ */ "<rdar://problem/50860781> sporadically times out")),
]
#if NS_FOUNDATION_ALLOWS_TESTABLE_IMPORT
tests.append(contentsOf: [
("test_fileDescriptor", test_fileDescriptor),
("test_nullDevice", test_nullDevice),
("testHandleCreationAndCleanup", testHandleCreationAndCleanup),
("testOffset", testOffset),
])
#endif
return tests
}
}
#endif