Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions Sources/IOStreams/FileStreams.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,13 @@ public class FileSource: FileStream, Source {
///
public convenience init(path: String) throws {
guard let fileHandle = FileHandle(forReadingAtPath: path) else {
throw IOError.noSuchFile
throw CocoaError(.fileReadNoSuchFile)
}
try self.init(fileHandle: fileHandle)
}

public func read(max: Int) async throws -> Data? {
guard !closedState.closed else { throw IOError.streamClosed }
guard let dispatchIO = dispatchIO, !closedState.closed else { throw IOError.streamClosed }

let data: Data? = try await withCheckedThrowingContinuation { continuation in
withUnsafeCurrentTask { task in
Expand Down Expand Up @@ -118,13 +118,13 @@ public class FileSink: FileStream, Sink {
///
public convenience init(path: String) throws {
guard let fileHandle = FileHandle(forWritingAtPath: path) else {
throw IOError.noSuchFile
throw CocoaError(.fileNoSuchFile)
}
try self.init(fileHandle: fileHandle)
}

public func write(data: Data) async throws {
guard !closedState.closed else { throw IOError.streamClosed }
guard let dispatchIO = dispatchIO, !closedState.closed else { throw IOError.streamClosed }

try await withCheckedThrowingContinuation { continuation in

Expand Down Expand Up @@ -185,7 +185,7 @@ public class FileStream: Stream {
}

fileprivate let fileHandle: FileHandle
fileprivate var dispatchIO: DispatchIO!
fileprivate var dispatchIO: DispatchIO?
fileprivate var closedState = CloseState()

/// Initialize the stream from a file handle.
Expand All @@ -195,13 +195,13 @@ public class FileStream: Stream {
public required init(fileHandle: FileHandle) throws {

self.fileHandle = fileHandle
dispatchIO = DispatchIO(type: .stream, fileDescriptor: fileHandle.fileDescriptor, queue: .taskPriority) { error in

let dispatchIO =
DispatchIO(type: .stream, fileDescriptor: fileHandle.fileDescriptor, queue: .taskPriority) { error in
let closeError: Error?
if error != 0 {

let errorCode = POSIXError.Code(rawValue: error) ?? .EIO

closeError = IOError.map(error: POSIXError(errorCode))
closeError = POSIXError(POSIXErrorCode(rawValue: error) ?? .EIO)
}
else {
closeError = nil
Expand All @@ -214,13 +214,16 @@ public class FileStream: Stream {
dispatchIO.setLimit(lowWater: Self.progressReportLimits.lowWaterMark)
dispatchIO.setLimit(highWater: Self.progressReportLimits.highWaterMark)
dispatchIO.setInterval(interval: Self.progressReportLimits.maxInterval, flags: [])

self.dispatchIO = dispatchIO
}

fileprivate func close(error: Error?) {
guard !closedState.closed else { return }
closedState.closed = true
closedState.error = error
dispatchIO.close(flags: [.stop])
dispatchIO?.close(flags: [.stop])
dispatchIO = nil
}

public func close() throws {
Expand Down
46 changes: 6 additions & 40 deletions Sources/IOStreams/IOError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,57 +17,23 @@
import Foundation

/// I/O related errors
public enum IOError: Error {

/// An I/O operation was attempted on a file that does not exist.
case noSuchFile
public enum IOError: Error, LocalizedError {

/// End-of-stream was encountered during a read.
case endOfStream

/// The I/O operation was cancelled by its parent task.
case cancelled

/// The stream is closed.
case streamClosed

/// Filter operation failed.
/// - Parameter Error: The filter error that cause the I/O error.
case filterFailure(Error)

/// An unknown I/O operation occurred.
case unknown

/// A non-specific I/O errorr occurred that was caused by another error.
/// - Parameter Error: The original error that cause the I/O error.
case causedBy(Error)


static func map(error: Error) -> Error {
switch error {
case is CancellationError:
return error

case let posixError as POSIXError:
switch posixError.code {
case .ENOENT:
return Self.noSuchFile
case .ECANCELED:
return Self.cancelled
default:
return Self.causedBy(posixError)
}

case let cocoaError as CocoaError:
switch cocoaError.code {
case .fileNoSuchFile, .fileReadNoSuchFile:
return Self.noSuchFile
default:
return Self.causedBy(cocoaError)
}

default:
return Self.causedBy(error)
public var errorDescription: String? {
switch self {
case .endOfStream: return "End of Stream"
case .streamClosed: return "Stream Closed"
case .filterFailure(let error): return "Filter Failed: \(error.localizedDescription)"
}
}

Expand Down
17 changes: 12 additions & 5 deletions Sources/IOStreams/URLSessionStreams.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ import Foundation
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public class URLSessionSource: Source {

public enum HTTPError: Error {
public enum HTTPError: Error, LocalizedError {
case invalidResponse
case invalidStatus

public var errorDescription: String? {
switch self {
case .invalidResponse: return "Invalid Response"
case .invalidStatus: return "Invalid Status"
}
}
}

public typealias Stream = AsyncThrowingStream<Data, Error>
Expand Down Expand Up @@ -78,7 +85,7 @@ public class URLSessionSource: Source {
}

public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
continuation.finish(throwing: error.map { IOError.causedBy($0) })
continuation.finish(throwing: error)
}

public func urlSession(
Expand All @@ -89,13 +96,13 @@ public class URLSessionSource: Source {
) {

guard let httpResponse = response as? HTTPURLResponse else {
continuation.finish(throwing: IOError.causedBy(HTTPError.invalidResponse))
continuation.finish(throwing: HTTPError.invalidResponse)
completionHandler(.cancel)
return
}

if 400 ..< 600 ~= httpResponse.statusCode {
continuation.finish(throwing: IOError.causedBy(HTTPError.invalidStatus))
if 300 ..< 600 ~= httpResponse.statusCode {
continuation.finish(throwing: HTTPError.invalidStatus)
completionHandler(.cancel)
return
}
Expand Down
36 changes: 36 additions & 0 deletions Tests/IOStreamsTests/ErrorTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2022 Outfox, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@testable import IOStreams
import XCTest

final class ErrorTests: XCTestCase {

func testIOErrorDescription() throws {

XCTAssertEqual(IOError.endOfStream.errorDescription, "End of Stream")
XCTAssertEqual(IOError.streamClosed.errorDescription, "Stream Closed")
XCTAssertEqual(IOError.filterFailure(IOError.endOfStream).errorDescription, "Filter Failed: End of Stream")
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
func testHTTPErrorDescription() throws {

XCTAssertEqual(URLSessionSource.HTTPError.invalidResponse.errorDescription, "Invalid Response")
XCTAssertEqual(URLSessionSource.HTTPError.invalidStatus.errorDescription, "Invalid Status")
}

}
38 changes: 38 additions & 0 deletions Tests/IOStreamsTests/FileStreamsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,42 @@ final class FileStreamsTests: XCTestCase {
XCTAssert(source.bytesRead < fileSize, "Source should have cancelled iteration")
}

func testInvalidFileSourceThrows() async throws {

do {

_ = try FileSource(path: "/non-esixtent-file")

}
catch let error as CocoaError {

XCTAssertEqual(error.code, .fileReadNoSuchFile)

}
catch {

XCTFail("Incorrect exception caught")
}

}

func testInvalidFileSinkThrows() async throws {

do {

_ = try FileSink(path: "/non-esixtent-file")

}
catch let error as CocoaError {

XCTAssertEqual(error.code, .fileNoSuchFile)

}
catch {

XCTFail("Incorrect exception caught")
}

}

}
18 changes: 18 additions & 0 deletions Tests/IOStreamsTests/URLSessionStreamTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,22 @@ final class URLSessionStreamsTests: XCTestCase {
XCTAssert(source.bytesRead < 50 * 1024, "Source should have cancelled iteration")
}

func testSourceThrowsInvalidStatus() async throws {

do {

_ = try await URLSessionSource(url: URL(string: "http://example.com/non-existent-url")!).read(max: .max)

}
catch let error as URLSessionSource.HTTPError {

XCTAssertEqual(error, .invalidStatus)

}
catch {

XCTFail("Unexpected error thrown")
}

}
}