1020 lines
47 KiB
Swift
1020 lines
47 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the SwiftNIO open source project
|
|
//
|
|
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
|
|
// Licensed under Apache License v2.0
|
|
//
|
|
// See LICENSE.txt for license information
|
|
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import XCTest
|
|
import NIOCore
|
|
@testable import NIOPosix
|
|
import Atomics
|
|
|
|
class NonBlockingFileIOTest: XCTestCase {
|
|
private var group: EventLoopGroup!
|
|
private var eventLoop: EventLoop!
|
|
private var allocator: ByteBufferAllocator!
|
|
private var fileIO: NonBlockingFileIO!
|
|
private var threadPool: NIOThreadPool!
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
self.allocator = ByteBufferAllocator()
|
|
self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
self.threadPool = NIOThreadPool(numberOfThreads: 6)
|
|
self.threadPool.start()
|
|
self.fileIO = NonBlockingFileIO(threadPool: threadPool)
|
|
self.eventLoop = self.group.next()
|
|
}
|
|
|
|
override func tearDown() {
|
|
XCTAssertNoThrow(try self.group?.syncShutdownGracefully())
|
|
XCTAssertNoThrow(try self.threadPool?.syncShutdownGracefully())
|
|
self.group = nil
|
|
self.eventLoop = nil
|
|
self.allocator = nil
|
|
self.threadPool = nil
|
|
self.fileIO = nil
|
|
super.tearDown()
|
|
}
|
|
|
|
func testBasicFileIOWorks() throws {
|
|
let content = "hello"
|
|
try withTemporaryFile(content: content) { (fileHandle, _) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: 5)
|
|
var buf = try self.fileIO.read(fileRegion: fr,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(content.utf8.count, buf.readableBytes)
|
|
XCTAssertEqual(content, buf.readString(length: buf.readableBytes))
|
|
}
|
|
}
|
|
|
|
func testOffsetWorks() throws {
|
|
let content = "hello"
|
|
try withTemporaryFile(content: content) { (fileHandle, _) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 3, endIndex: 5)
|
|
var buf = try self.fileIO.read(fileRegion: fr,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(2, buf.readableBytes)
|
|
XCTAssertEqual("lo", buf.readString(length: buf.readableBytes))
|
|
}
|
|
}
|
|
|
|
func testOffsetBeyondEOF() throws {
|
|
let content = "hello"
|
|
try withTemporaryFile(content: content) { (fileHandle, _) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 3000, endIndex: 3001)
|
|
var buf = try self.fileIO.read(fileRegion: fr,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(0, buf.readableBytes)
|
|
XCTAssertEqual("", buf.readString(length: buf.readableBytes))
|
|
}
|
|
}
|
|
|
|
func testEmptyReadWorks() throws {
|
|
try withTemporaryFile { (fileHandle, _) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: 0)
|
|
let buf = try self.fileIO.read(fileRegion: fr,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(0, buf.readableBytes)
|
|
}
|
|
}
|
|
|
|
func testReadingShortWorks() throws {
|
|
let content = "hello"
|
|
try withTemporaryFile(content: "hello") { (fileHandle, _) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: 10)
|
|
var buf = try self.fileIO.read(fileRegion: fr,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(content.utf8.count, buf.readableBytes)
|
|
XCTAssertEqual(content, buf.readString(length: buf.readableBytes))
|
|
}
|
|
}
|
|
|
|
func testDoesNotBlockTheThreadOrEventLoop() throws {
|
|
var innerError: Error? = nil
|
|
try withPipe { readFH, writeFH in
|
|
let bufferFuture = self.fileIO.read(fileHandle: readFH,
|
|
byteCount: 10,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop)
|
|
|
|
do {
|
|
try self.eventLoop.submit {
|
|
try writeFH.withUnsafeFileDescriptor { writeFD in
|
|
_ = try Posix.write(descriptor: writeFD, pointer: "X", size: 1)
|
|
}
|
|
try writeFH.close()
|
|
}.wait()
|
|
var buf = try bufferFuture.wait()
|
|
XCTAssertEqual(1, buf.readableBytes)
|
|
XCTAssertEqual("X", buf.readString(length: buf.readableBytes))
|
|
} catch {
|
|
innerError = error
|
|
}
|
|
return [readFH]
|
|
}
|
|
XCTAssertNil(innerError)
|
|
}
|
|
|
|
func testGettingErrorWhenEventLoopGroupIsShutdown() throws {
|
|
self.threadPool.shutdownGracefully(queue: .global()) { err in
|
|
XCTAssertNil(err)
|
|
}
|
|
|
|
try withPipe { readFH, writeFH in
|
|
XCTAssertThrowsError(try self.fileIO.read(fileHandle: readFH,
|
|
byteCount: 1,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()) { error in
|
|
XCTAssertTrue(error is NIOThreadPoolError.ThreadPoolInactive)
|
|
}
|
|
return [readFH, writeFH]
|
|
}
|
|
}
|
|
|
|
func testChunkReadingWorks() throws {
|
|
let content = "hello"
|
|
let contentBytes = Array(content.utf8)
|
|
var numCalls = 0
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: 5)
|
|
try self.fileIO.readChunked(fileRegion: fr,
|
|
chunkSize: 1,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
XCTAssertEqual(1, buf.readableBytes)
|
|
XCTAssertEqual(contentBytes[numCalls], buf.readBytes(length: 1)?.first!)
|
|
numCalls += 1
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
}
|
|
XCTAssertEqual(content.utf8.count, numCalls)
|
|
}
|
|
|
|
func testChunkReadingCanBeAborted() throws {
|
|
enum DummyError: Error { case dummy }
|
|
let content = "hello"
|
|
let contentBytes = Array(content.utf8)
|
|
var numCalls = 0
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: 5)
|
|
XCTAssertThrowsError(try self.fileIO.readChunked(fileRegion: fr,
|
|
chunkSize: 1,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
XCTAssertEqual(1, buf.readableBytes)
|
|
XCTAssertEqual(contentBytes[numCalls], buf.readBytes(length: 1)?.first!)
|
|
numCalls += 1
|
|
return self.eventLoop.makeFailedFuture(DummyError.dummy)
|
|
}.wait()) { error in
|
|
XCTAssertEqual(.dummy, error as? DummyError)
|
|
}
|
|
}
|
|
XCTAssertEqual(1, numCalls)
|
|
}
|
|
|
|
func testChunkReadingWorksForIncrediblyLongChain() throws {
|
|
let content = String(repeating: "X", count: 20*1024)
|
|
var numCalls = 0
|
|
let expectedByte = content.utf8.first!
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
try self.fileIO.readChunked(fileHandle: fileHandle,
|
|
fromOffset: 0,
|
|
byteCount: content.utf8.count,
|
|
chunkSize: 1,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
XCTAssertEqual(1, buf.readableBytes)
|
|
XCTAssertEqual(expectedByte, buf.readableBytesView.first)
|
|
numCalls += 1
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
}
|
|
XCTAssertEqual(content.utf8.count, numCalls)
|
|
}
|
|
|
|
func testReadingDifferentChunkSize() throws {
|
|
let content = "0123456789"
|
|
var numCalls = 0
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: content.utf8.count)
|
|
try self.fileIO.readChunked(fileRegion: fr,
|
|
chunkSize: 2,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
XCTAssertEqual(2, buf.readableBytes)
|
|
XCTAssertEqual(Array("\(numCalls*2)\(numCalls*2 + 1)".utf8), buf.readBytes(length: 2)!)
|
|
numCalls += 1
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
}
|
|
XCTAssertEqual(content.utf8.count/2, numCalls)
|
|
}
|
|
|
|
func testReadDoesNotReadShort() throws {
|
|
var innerError: Error? = nil
|
|
try withPipe { readFH, writeFH in
|
|
let bufferFuture = self.fileIO.read(fileHandle: readFH,
|
|
byteCount: 10,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop)
|
|
do {
|
|
for i in 0..<10 {
|
|
// this construction will cause 'read' to repeatedly return with 1 byte read
|
|
try self.eventLoop.scheduleTask(in: .milliseconds(50)) {
|
|
try writeFH.withUnsafeFileDescriptor { writeFD in
|
|
_ = try Posix.write(descriptor: writeFD, pointer: "\(i)", size: 1)
|
|
}
|
|
}.futureResult.wait()
|
|
}
|
|
try writeFH.close()
|
|
|
|
var buf = try bufferFuture.wait()
|
|
XCTAssertEqual(10, buf.readableBytes)
|
|
XCTAssertEqual("0123456789", buf.readString(length: buf.readableBytes))
|
|
} catch {
|
|
innerError = error
|
|
}
|
|
return [readFH]
|
|
}
|
|
XCTAssertNil(innerError)
|
|
}
|
|
|
|
func testChunkReadingWhereByteCountIsNotAChunkSizeMultiplier() throws {
|
|
let content = "prefix-12345-suffix"
|
|
var allBytesActual = ""
|
|
let allBytesExpected = String(content.dropFirst(7).dropLast(7))
|
|
var numCalls = 0
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 7, endIndex: 12)
|
|
try self.fileIO.readChunked(fileRegion: fr,
|
|
chunkSize: 3,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
allBytesActual += buf.readString(length: buf.readableBytes) ?? "WRONG"
|
|
numCalls += 1
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
}
|
|
XCTAssertEqual(allBytesExpected, allBytesActual)
|
|
XCTAssertEqual(2, numCalls)
|
|
}
|
|
|
|
func testReadMoreThanIntMaxBytesDoesntThrow() throws {
|
|
try XCTSkipIf(MemoryLayout<size_t>.size == MemoryLayout<UInt32>.size)
|
|
// here we try to read way more data back from the file than it contains but it serves the purpose
|
|
// even on a small file the OS will return EINVAL if you try to read > INT_MAX bytes
|
|
try withTemporaryFile(content: "some-dummy-content", { (filehandle, path) -> Void in
|
|
let content = try self.fileIO.read(fileHandle: filehandle, byteCount:Int(Int32.max)+10, allocator: .init(), eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(String(buffer: content), "some-dummy-content")
|
|
})
|
|
}
|
|
|
|
func testChunkedReadDoesNotReadShort() throws {
|
|
var innerError: Error? = nil
|
|
try withPipe { readFH, writeFH in
|
|
var allBytes = ""
|
|
let f = self.fileIO.readChunked(fileHandle: readFH,
|
|
byteCount: 10,
|
|
chunkSize: 3,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
if allBytes.utf8.count == 9 {
|
|
XCTAssertEqual(1, buf.readableBytes)
|
|
} else {
|
|
XCTAssertEqual(3, buf.readableBytes)
|
|
}
|
|
allBytes.append(buf.readString(length: buf.readableBytes) ?? "THIS IS WRONG")
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}
|
|
|
|
do {
|
|
for i in 0..<10 {
|
|
// this construction will cause 'read' to repeatedly return with 1 byte read
|
|
try self.eventLoop.scheduleTask(in: .milliseconds(50)) {
|
|
try writeFH.withUnsafeFileDescriptor { writeFD in
|
|
_ = try Posix.write(descriptor: writeFD, pointer: "\(i)", size: 1)
|
|
}
|
|
}.futureResult.wait()
|
|
}
|
|
try writeFH.close()
|
|
|
|
try f.wait()
|
|
XCTAssertEqual("0123456789", allBytes)
|
|
} catch {
|
|
innerError = error
|
|
}
|
|
return [readFH]
|
|
}
|
|
XCTAssertNil(innerError)
|
|
}
|
|
|
|
func testChunkSizeMoreThanTotal() throws {
|
|
let content = "0123456789"
|
|
var numCalls = 0
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let fr = FileRegion(fileHandle: fileHandle, readerIndex: 0, endIndex: 5)
|
|
try self.fileIO.readChunked(fileRegion: fr,
|
|
chunkSize: 10,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
XCTAssertEqual(5, buf.readableBytes)
|
|
XCTAssertEqual("01234", buf.readString(length: buf.readableBytes) ?? "bad")
|
|
numCalls += 1
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
}
|
|
XCTAssertEqual(1, numCalls)
|
|
}
|
|
|
|
func testFileRegionReadFromPipeFails() throws {
|
|
try withPipe { readFH, writeFH in
|
|
try! writeFH.withUnsafeFileDescriptor { writeFD in
|
|
_ = try! Posix.write(descriptor: writeFD, pointer: "ABC", size: 3)
|
|
}
|
|
let fr = FileRegion(fileHandle: readFH, readerIndex: 1, endIndex: 2)
|
|
XCTAssertThrowsError(try self.fileIO.readChunked(fileRegion: fr,
|
|
chunkSize: 10,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
XCTFail("this shouldn't have been called")
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()) { error in
|
|
XCTAssertEqual(ESPIPE, (error as? IOError)?.errnoCode)
|
|
}
|
|
return [readFH, writeFH]
|
|
}
|
|
}
|
|
|
|
func testReadFromNonBlockingPipeFails() throws {
|
|
try withPipe { readFH, writeFH in
|
|
do {
|
|
try readFH.withUnsafeFileDescriptor { readFD in
|
|
let flags = try Posix.fcntl(descriptor: readFD, command: F_GETFL, value: 0)
|
|
let ret = try Posix.fcntl(descriptor: readFD, command: F_SETFL, value: flags | O_NONBLOCK)
|
|
assert(ret == 0, "unexpectedly, fcntl(\(readFD), F_SETFL, O_NONBLOCK) returned \(ret)")
|
|
}
|
|
try self.fileIO.readChunked(fileHandle: readFH,
|
|
byteCount: 10,
|
|
chunkSize: 10,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
XCTFail("this shouldn't have been called")
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
XCTFail("succeeded and shouldn't have")
|
|
} catch let e as NonBlockingFileIO.Error where e == NonBlockingFileIO.Error.descriptorSetToNonBlocking {
|
|
// OK
|
|
} catch {
|
|
XCTFail("wrong error \(error) caught")
|
|
}
|
|
return [readFH, writeFH]
|
|
}
|
|
}
|
|
|
|
func testSeekPointerIsSetToFront() throws {
|
|
let content = "0123456789"
|
|
var numCalls = 0
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
try self.fileIO.readChunked(fileHandle: fileHandle,
|
|
byteCount: content.utf8.count,
|
|
chunkSize: 9,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buf in
|
|
var buf = buf
|
|
numCalls += 1
|
|
XCTAssertTrue(self.eventLoop.inEventLoop)
|
|
if numCalls == 1 {
|
|
XCTAssertEqual(9, buf.readableBytes)
|
|
XCTAssertEqual("012345678", buf.readString(length: buf.readableBytes) ?? "bad")
|
|
} else {
|
|
XCTAssertEqual(1, buf.readableBytes)
|
|
XCTAssertEqual("9", buf.readString(length: buf.readableBytes) ?? "bad")
|
|
}
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
}
|
|
XCTAssertEqual(2, numCalls)
|
|
}
|
|
|
|
func testReadingFileSize() throws {
|
|
try withTemporaryFile(content: "0123456789") { (fileHandle, _) -> Void in
|
|
let size = try self.fileIO.readFileSize(fileHandle: fileHandle,
|
|
eventLoop: eventLoop).wait()
|
|
XCTAssertEqual(size, 10)
|
|
}
|
|
}
|
|
|
|
func testChangeFileSizeShrink() throws {
|
|
try withTemporaryFile(content: "0123456789") { (fileHandle, _) -> Void in
|
|
try self.fileIO.changeFileSize(fileHandle: fileHandle,
|
|
size: 1,
|
|
eventLoop: eventLoop).wait()
|
|
let fileRegion = try FileRegion(fileHandle: fileHandle)
|
|
var buf = try self.fileIO.read(fileRegion: fileRegion,
|
|
allocator: allocator,
|
|
eventLoop: eventLoop).wait()
|
|
XCTAssertEqual("0", buf.readString(length: buf.readableBytes))
|
|
}
|
|
}
|
|
|
|
func testChangeFileSizeGrow() throws {
|
|
try withTemporaryFile(content: "0123456789") { (fileHandle, _) -> Void in
|
|
try self.fileIO.changeFileSize(fileHandle: fileHandle,
|
|
size: 100,
|
|
eventLoop: eventLoop).wait()
|
|
let fileRegion = try FileRegion(fileHandle: fileHandle)
|
|
var buf = try self.fileIO.read(fileRegion: fileRegion,
|
|
allocator: allocator,
|
|
eventLoop: eventLoop).wait()
|
|
let zeros = (1...90).map { _ in UInt8(0) }
|
|
guard let bytes = buf.readBytes(length: buf.readableBytes)?.suffix(from: 10) else {
|
|
XCTFail("readBytes(length:) should not be nil")
|
|
return
|
|
}
|
|
XCTAssertEqual(zeros, Array(bytes))
|
|
}
|
|
}
|
|
|
|
func testWriting() throws {
|
|
var buffer = allocator.buffer(capacity: 3)
|
|
buffer.writeStaticString("123")
|
|
|
|
try withTemporaryFile(content: "") { (fileHandle, path) in
|
|
try self.fileIO.write(fileHandle: fileHandle,
|
|
buffer: buffer,
|
|
eventLoop: self.eventLoop).wait()
|
|
let offset = try fileHandle.withUnsafeFileDescriptor {
|
|
try Posix.lseek(descriptor: $0, offset: 0, whence: SEEK_SET)
|
|
}
|
|
XCTAssertEqual(offset, 0)
|
|
|
|
let readBuffer = try self.fileIO.read(fileHandle: fileHandle,
|
|
byteCount: 3,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(readBuffer.getString(at: 0, length: 3), "123")
|
|
}
|
|
}
|
|
|
|
func testWriteMultipleTimes() throws {
|
|
var buffer = allocator.buffer(capacity: 3)
|
|
buffer.writeStaticString("xxx")
|
|
|
|
try withTemporaryFile(content: "AAA") { (fileHandle, path) in
|
|
for i in 0 ..< 3 {
|
|
buffer.writeString("\(i)")
|
|
try self.fileIO.write(fileHandle: fileHandle,
|
|
buffer: buffer,
|
|
eventLoop: self.eventLoop).wait()
|
|
}
|
|
let offset = try fileHandle.withUnsafeFileDescriptor {
|
|
try Posix.lseek(descriptor: $0, offset: 0, whence: SEEK_SET)
|
|
}
|
|
XCTAssertEqual(offset, 0)
|
|
|
|
let expectedOutput = "xxx0xxx01xxx012"
|
|
let readBuffer = try self.fileIO.read(fileHandle: fileHandle,
|
|
byteCount: expectedOutput.utf8.count,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(expectedOutput, String(decoding: readBuffer.readableBytesView, as: Unicode.UTF8.self))
|
|
}
|
|
}
|
|
|
|
func testWritingWithOffset() throws {
|
|
var buffer = allocator.buffer(capacity: 3)
|
|
buffer.writeStaticString("123")
|
|
|
|
try withTemporaryFile(content: "hello") { (fileHandle, _) -> Void in
|
|
try self.fileIO.write(fileHandle: fileHandle,
|
|
toOffset: 1,
|
|
buffer: buffer,
|
|
eventLoop: eventLoop).wait()
|
|
let offset = try fileHandle.withUnsafeFileDescriptor {
|
|
try Posix.lseek(descriptor: $0, offset: 0, whence: SEEK_SET)
|
|
}
|
|
XCTAssertEqual(offset, 0)
|
|
|
|
var readBuffer = try self.fileIO.read(fileHandle: fileHandle,
|
|
byteCount: 5,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(5, readBuffer.readableBytes)
|
|
XCTAssertEqual("h123o", readBuffer.readString(length: readBuffer.readableBytes))
|
|
}
|
|
}
|
|
|
|
// This is undefined behavior and may cause different
|
|
// results on other platforms. Please add #if:s according
|
|
// to platform requirements.
|
|
func testWritingBeyondEOF() throws {
|
|
var buffer = allocator.buffer(capacity: 3)
|
|
buffer.writeStaticString("123")
|
|
|
|
try withTemporaryFile(content: "hello") { (fileHandle, _) -> Void in
|
|
try self.fileIO.write(fileHandle: fileHandle,
|
|
toOffset: 6,
|
|
buffer: buffer,
|
|
eventLoop: eventLoop).wait()
|
|
|
|
let fileRegion = try FileRegion(fileHandle: fileHandle)
|
|
var buf = try self.fileIO.read(fileRegion: fileRegion,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(9, buf.readableBytes)
|
|
XCTAssertEqual("hello", buf.readString(length: 5))
|
|
XCTAssertEqual([ UInt8(0) ], buf.readBytes(length: 1))
|
|
XCTAssertEqual("123", buf.readString(length: buf.readableBytes))
|
|
}
|
|
}
|
|
|
|
func testFileOpenWorks() throws {
|
|
let content = "123"
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let (fh, fr) = try self.fileIO.openFile(path: path, eventLoop: self.eventLoop).wait()
|
|
try fh.withUnsafeFileDescriptor { fd in
|
|
XCTAssertGreaterThanOrEqual(fd, 0)
|
|
}
|
|
XCTAssertTrue(fh.isOpen)
|
|
XCTAssertEqual(0, fr.readerIndex)
|
|
XCTAssertEqual(3, fr.endIndex)
|
|
try fh.close()
|
|
}
|
|
}
|
|
|
|
func testFileOpenWorksWithEmptyFile() throws {
|
|
let content = ""
|
|
try withTemporaryFile(content: content) { (fileHandle, path) -> Void in
|
|
let (fh, fr) = try self.fileIO.openFile(path: path, eventLoop: self.eventLoop).wait()
|
|
try fh.withUnsafeFileDescriptor { fd in
|
|
XCTAssertGreaterThanOrEqual(fd, 0)
|
|
}
|
|
XCTAssertTrue(fh.isOpen)
|
|
XCTAssertEqual(0, fr.readerIndex)
|
|
XCTAssertEqual(0, fr.endIndex)
|
|
try fh.close()
|
|
}
|
|
}
|
|
|
|
func testFileOpenFails() throws {
|
|
do {
|
|
_ = try self.fileIO.openFile(path: "/dev/null/this/does/not/exist", eventLoop: self.eventLoop).wait()
|
|
XCTFail("should've thrown")
|
|
} catch let e as IOError where e.errnoCode == ENOTDIR {
|
|
// OK
|
|
} catch {
|
|
XCTFail("wrong error: \(error)")
|
|
}
|
|
}
|
|
|
|
func testOpeningFilesForWriting() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { dir in
|
|
try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: .write,
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait().close()
|
|
})
|
|
}
|
|
|
|
func testOpeningFilesForWritingFailsIfWeDontAllowItExplicitly() {
|
|
XCTAssertThrowsError(try withTemporaryDirectory { dir in
|
|
try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: .write,
|
|
flags: .default,
|
|
eventLoop: self.eventLoop).wait().close()
|
|
}) { error in
|
|
XCTAssertEqual(ENOENT, (error as? IOError)?.errnoCode)
|
|
}
|
|
}
|
|
|
|
func testOpeningFilesForWritingDoesNotAllowReading() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { dir in
|
|
let fileHandle = try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: .write,
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try! fileHandle.close()
|
|
}
|
|
XCTAssertEqual(-1 /* read must fail */,
|
|
try fileHandle.withUnsafeFileDescriptor { fd -> ssize_t in
|
|
var data: UInt8 = 0
|
|
return withUnsafeMutableBytes(of: &data) { ptr in
|
|
read(fd, ptr.baseAddress, ptr.count)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func testOpeningFilesForWritingAndReading() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { dir in
|
|
let fileHandle = try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: [.write, .read],
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try! fileHandle.close()
|
|
}
|
|
XCTAssertEqual(0 /* read should read EOF */,
|
|
try fileHandle.withUnsafeFileDescriptor { fd -> ssize_t in
|
|
var data: UInt8 = 0
|
|
return withUnsafeMutableBytes(of: &data) { ptr in
|
|
read(fd, ptr.baseAddress, ptr.count)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
func testOpeningFilesForWritingDoesNotImplyTruncation() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { dir in
|
|
// open 1 + write
|
|
try {
|
|
let fileHandle = try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: [.write, .read],
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try! fileHandle.close()
|
|
}
|
|
try fileHandle.withUnsafeFileDescriptor { fd in
|
|
var data = UInt8(ascii: "X")
|
|
XCTAssertEqual(IOResult<Int>.processed(1),
|
|
try withUnsafeBytes(of: &data) { ptr in
|
|
try Posix.write(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count)
|
|
})
|
|
}
|
|
}()
|
|
// open 2 + write again + read
|
|
try {
|
|
let fileHandle = try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: [.write, .read],
|
|
flags: .default,
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try! fileHandle.close()
|
|
}
|
|
try fileHandle.withUnsafeFileDescriptor { fd in
|
|
try Posix.lseek(descriptor: fd, offset: 0, whence: SEEK_END)
|
|
var data = UInt8(ascii: "Y")
|
|
XCTAssertEqual(IOResult<Int>.processed(1),
|
|
try withUnsafeBytes(of: &data) { ptr in
|
|
try Posix.write(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count)
|
|
})
|
|
}
|
|
XCTAssertEqual(2 /* both bytes */,
|
|
try fileHandle.withUnsafeFileDescriptor { fd -> ssize_t in
|
|
var data: UInt16 = 0
|
|
try Posix.lseek(descriptor: fd, offset: 0, whence: SEEK_SET)
|
|
let readReturn = withUnsafeMutableBytes(of: &data) { ptr in
|
|
read(fd, ptr.baseAddress, ptr.count)
|
|
}
|
|
XCTAssertEqual(UInt16(bigEndian: (UInt16(UInt8(ascii: "X")) << 8) | UInt16(UInt8(ascii: "Y"))),
|
|
data)
|
|
return readReturn
|
|
})
|
|
}()
|
|
})
|
|
}
|
|
|
|
func testOpeningFilesForWritingCanUseTruncation() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { dir in
|
|
// open 1 + write
|
|
try {
|
|
let fileHandle = try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: [.write, .read],
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try! fileHandle.close()
|
|
}
|
|
try fileHandle.withUnsafeFileDescriptor { fd in
|
|
var data = UInt8(ascii: "X")
|
|
XCTAssertEqual(IOResult<Int>.processed(1),
|
|
try withUnsafeBytes(of: &data) { ptr in
|
|
try Posix.write(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count)
|
|
})
|
|
}
|
|
}()
|
|
// open 2 (with truncation) + write again + read
|
|
try {
|
|
let fileHandle = try self.fileIO!.openFile(path: "\(dir)/file",
|
|
mode: [.write, .read],
|
|
flags: .posix(flags: O_TRUNC, mode: 0),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try! fileHandle.close()
|
|
}
|
|
try fileHandle.withUnsafeFileDescriptor { fd in
|
|
try Posix.lseek(descriptor: fd, offset: 0, whence: SEEK_END)
|
|
var data = UInt8(ascii: "Y")
|
|
XCTAssertEqual(IOResult<Int>.processed(1),
|
|
try withUnsafeBytes(of: &data) { ptr in
|
|
try Posix.write(descriptor: fd, pointer: ptr.baseAddress!, size: ptr.count)
|
|
})
|
|
}
|
|
XCTAssertEqual(1 /* read should read just one byte because we truncated the file */,
|
|
try fileHandle.withUnsafeFileDescriptor { fd -> ssize_t in
|
|
var data: UInt16 = 0
|
|
try Posix.lseek(descriptor: fd, offset: 0, whence: SEEK_SET)
|
|
let readReturn = withUnsafeMutableBytes(of: &data) { ptr in
|
|
read(fd, ptr.baseAddress, ptr.count)
|
|
}
|
|
XCTAssertEqual(UInt16(bigEndian: UInt16(UInt8(ascii: "Y")) << 8), data)
|
|
return readReturn
|
|
})
|
|
}()
|
|
})
|
|
}
|
|
|
|
func testReadFromOffset() {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello world") { (fileHandle, path) in
|
|
let buffer = self.fileIO.read(fileHandle: fileHandle,
|
|
fromOffset: 6,
|
|
byteCount: 5,
|
|
allocator: ByteBufferAllocator(),
|
|
eventLoop: self.eventLoop)
|
|
XCTAssertNoThrow(try XCTAssertEqual("world",
|
|
String(decoding: buffer.wait().readableBytesView,
|
|
as: Unicode.UTF8.self)))
|
|
})
|
|
}
|
|
|
|
func testReadChunkedFromOffset() {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello world") { (fileHandle, path) in
|
|
var numberOfCalls = 0
|
|
try self.fileIO.readChunked(fileHandle: fileHandle,
|
|
fromOffset: 6,
|
|
byteCount: 5,
|
|
chunkSize: 2,
|
|
allocator: .init(),
|
|
eventLoop: self.eventLoop) { buffer in
|
|
numberOfCalls += 1
|
|
switch numberOfCalls {
|
|
case 1:
|
|
XCTAssertEqual("wo", String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
|
|
case 2:
|
|
XCTAssertEqual("rl", String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
|
|
case 3:
|
|
XCTAssertEqual("d", String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
|
|
default:
|
|
XCTFail()
|
|
}
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
})
|
|
}
|
|
|
|
func testReadChunkedFromOffsetAfterEOFDeliversExactlyOneChunk() {
|
|
var numberOfCalls = 0
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello world") { (fileHandle, path) in
|
|
try self.fileIO.readChunked(fileHandle: fileHandle,
|
|
fromOffset: 100,
|
|
byteCount: 5,
|
|
chunkSize: 2,
|
|
allocator: .init(),
|
|
eventLoop: self.eventLoop) { buffer in
|
|
numberOfCalls += 1
|
|
XCTAssertEqual(1, numberOfCalls)
|
|
XCTAssertEqual(0, buffer.readableBytes)
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
})
|
|
}
|
|
|
|
func testReadChunkedFromEOFDeliversExactlyOneChunk() {
|
|
var numberOfCalls = 0
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "") { (fileHandle, path) in
|
|
try self.fileIO.readChunked(fileHandle: fileHandle,
|
|
byteCount: 5,
|
|
chunkSize: 2,
|
|
allocator: .init(),
|
|
eventLoop: self.eventLoop) { buffer in
|
|
numberOfCalls += 1
|
|
XCTAssertEqual(1, numberOfCalls)
|
|
XCTAssertEqual(0, buffer.readableBytes)
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
})
|
|
}
|
|
|
|
func testReadFromOffsetAfterEOFDeliversExactlyOneChunk() {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello world") { (fileHandle, path) in
|
|
XCTAssertEqual(0,
|
|
try self.fileIO.read(fileHandle: fileHandle,
|
|
fromOffset: 100,
|
|
byteCount: 5,
|
|
allocator: .init(),
|
|
eventLoop: self.eventLoop).wait().readableBytes)
|
|
})
|
|
}
|
|
|
|
func testReadFromEOFDeliversExactlyOneChunk() {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "") { (fileHandle, path) in
|
|
XCTAssertEqual(0,
|
|
try self.fileIO.read(fileHandle: fileHandle,
|
|
byteCount: 5,
|
|
allocator: .init(),
|
|
eventLoop: self.eventLoop).wait().readableBytes)
|
|
})
|
|
}
|
|
|
|
func testReadChunkedFromOffsetFileRegion() {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello world") { (fileHandle, path) in
|
|
var numberOfCalls = 0
|
|
let fileRegion = FileRegion(fileHandle: fileHandle, readerIndex: 6, endIndex: 11)
|
|
try self.fileIO.readChunked(fileRegion: fileRegion,
|
|
chunkSize: 2,
|
|
allocator: .init(),
|
|
eventLoop: self.eventLoop) { buffer in
|
|
numberOfCalls += 1
|
|
switch numberOfCalls {
|
|
case 1:
|
|
XCTAssertEqual("wo", String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
|
|
case 2:
|
|
XCTAssertEqual("rl", String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
|
|
case 3:
|
|
XCTAssertEqual("d", String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
|
|
default:
|
|
XCTFail()
|
|
}
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait()
|
|
})
|
|
}
|
|
|
|
func testReadManyChunks() {
|
|
let numberOfChunks = 2_000
|
|
XCTAssertNoThrow(try withTemporaryFile(content: String(repeating: "X",
|
|
count: numberOfChunks)) { (fileHandle, path) in
|
|
let numberOfCalls = ManagedAtomic(0)
|
|
XCTAssertNoThrow(try self.fileIO.readChunked(fileHandle: fileHandle,
|
|
fromOffset: 0,
|
|
byteCount: numberOfChunks,
|
|
chunkSize: 1,
|
|
allocator: self.allocator,
|
|
eventLoop: self.eventLoop) { buffer in
|
|
numberOfCalls.wrappingIncrement(ordering: .relaxed)
|
|
XCTAssertEqual(1, buffer.readableBytes)
|
|
XCTAssertEqual(UInt8(ascii: "X"), buffer.readableBytesView.first)
|
|
return self.eventLoop.makeSucceededFuture(())
|
|
}.wait())
|
|
XCTAssertEqual(numberOfChunks, numberOfCalls.load(ordering: .relaxed))
|
|
})
|
|
}
|
|
|
|
func testThrowsErrorOnUnstartedPool() throws {
|
|
withTemporaryFile(content: "hello, world") { fileHandle, path in
|
|
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
|
defer {
|
|
XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
|
|
}
|
|
|
|
let expectation = XCTestExpectation(description: "Opened file")
|
|
let threadPool = NIOThreadPool(numberOfThreads: 1)
|
|
let fileIO = NonBlockingFileIO(threadPool: threadPool)
|
|
fileIO.openFile(path: path, eventLoop: eventLoopGroup.next()).whenFailure { (error) in
|
|
XCTAssertTrue(error is NIOThreadPoolError.ThreadPoolInactive)
|
|
expectation.fulfill()
|
|
}
|
|
|
|
self.wait(for: [expectation], timeout: 1.0)
|
|
}
|
|
}
|
|
|
|
func testLStat() throws {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello, world") { _, path in
|
|
let stat = try self.fileIO.lstat(path: path, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(12, stat.st_size)
|
|
XCTAssertEqual(S_IFREG, S_IFMT & stat.st_mode)
|
|
})
|
|
|
|
XCTAssertNoThrow(try withTemporaryDirectory { path in
|
|
let stat = try self.fileIO.lstat(path: path, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFDIR, S_IFMT & stat.st_mode)
|
|
})
|
|
}
|
|
|
|
func testSymlink() {
|
|
XCTAssertNoThrow(try withTemporaryFile(content: "hello, world") { _, path in
|
|
let symlink = "\(path).symlink"
|
|
XCTAssertNoThrow(try self.fileIO.symlink(path: symlink, to: path, eventLoop: self.eventLoop).wait())
|
|
|
|
XCTAssertEqual(path, try self.fileIO.readlink(path: symlink, eventLoop: self.eventLoop).wait())
|
|
let stat = try self.fileIO.lstat(path: symlink, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFLNK, S_IFMT & stat.st_mode)
|
|
|
|
XCTAssertNoThrow(try self.fileIO.unlink(path: symlink, eventLoop: self.eventLoop).wait())
|
|
XCTAssertThrowsError(try self.fileIO.lstat(path: symlink, eventLoop: self.eventLoop).wait()) { error in
|
|
XCTAssertEqual(ENOENT, (error as? IOError)?.errnoCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func testCreateDirectory() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { path in
|
|
let dir = "\(path)/f1/f2///f3"
|
|
XCTAssertNoThrow(try self.fileIO.createDirectory(path: dir, withIntermediateDirectories: true, mode: S_IRWXU, eventLoop: self.eventLoop).wait())
|
|
|
|
let stat = try self.fileIO.lstat(path: dir, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFDIR, S_IFMT & stat.st_mode)
|
|
|
|
XCTAssertNoThrow(try self.fileIO.createDirectory(path: "\(dir)/f4", withIntermediateDirectories: false, mode: S_IRWXU, eventLoop: self.eventLoop).wait())
|
|
|
|
let stat2 = try self.fileIO.lstat(path: dir, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFDIR, S_IFMT & stat2.st_mode)
|
|
|
|
let dir3 = "\(path)/f4/."
|
|
XCTAssertNoThrow(try self.fileIO.createDirectory(path: dir3, withIntermediateDirectories: true, mode: S_IRWXU, eventLoop: self.eventLoop).wait())
|
|
})
|
|
}
|
|
|
|
func testListDirectory() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { path in
|
|
let file = "\(path)/file"
|
|
let handle = try self.fileIO.openFile(path: file,
|
|
mode: .write,
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try? handle.close()
|
|
}
|
|
|
|
let list = try self.fileIO.listDirectory(path: path, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual([".", "..", "file"], list.sorted(by: { $0.name < $1.name }).map(\.name))
|
|
})
|
|
}
|
|
|
|
func testRename() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { path in
|
|
let file = "\(path)/file"
|
|
let handle = try self.fileIO.openFile(path: file,
|
|
mode: .write,
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try? handle.close()
|
|
}
|
|
|
|
let stat = try self.fileIO.lstat(path: file, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFREG, S_IFMT & stat.st_mode)
|
|
|
|
let new = "\(path).new"
|
|
XCTAssertNoThrow(try self.fileIO.rename(path: file, newName: new, eventLoop: self.eventLoop).wait())
|
|
|
|
let stat2 = try self.fileIO.lstat(path: new, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFREG, S_IFMT & stat2.st_mode)
|
|
|
|
XCTAssertThrowsError(try self.fileIO.lstat(path: file, eventLoop: self.eventLoop).wait()) { error in
|
|
XCTAssertEqual(ENOENT, (error as? IOError)?.errnoCode)
|
|
}
|
|
})
|
|
}
|
|
|
|
func testRemove() {
|
|
XCTAssertNoThrow(try withTemporaryDirectory { path in
|
|
let file = "\(path)/file"
|
|
let handle = try self.fileIO.openFile(path: file,
|
|
mode: .write,
|
|
flags: .allowFileCreation(),
|
|
eventLoop: self.eventLoop).wait()
|
|
defer {
|
|
try? handle.close()
|
|
}
|
|
|
|
let stat = try self.fileIO.lstat(path: file, eventLoop: self.eventLoop).wait()
|
|
XCTAssertEqual(S_IFREG, S_IFMT & stat.st_mode)
|
|
|
|
XCTAssertNoThrow(try self.fileIO.remove(path: file, eventLoop: self.eventLoop).wait())
|
|
XCTAssertThrowsError(try self.fileIO.lstat(path: file, eventLoop: self.eventLoop).wait()) { error in
|
|
XCTAssertEqual(ENOENT, (error as? IOError)?.errnoCode)
|
|
}
|
|
})
|
|
}
|
|
}
|