diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf0b8717..d1d6f077 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -81,34 +81,6 @@ jobs: - name: Test run: swift test --skip-build - xcode_15_4: - runs-on: macos-14 - env: - DEVELOPER_DIR: /Applications/Xcode_15.4.app/Contents/Developer - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Version - run: swift --version - - name: Build - run: swift build --build-tests - - name: Test - run: swift test --skip-build - - linux_swift_5_10: - runs-on: ubuntu-latest - container: swift:5.10 - timeout-minutes: 5 - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Version - run: swift --version - - name: Build - run: swift build --build-tests - - name: Test - run: swift test --skip-build - linux_swift_6_0: runs-on: ubuntu-latest container: swift:6.0 diff --git a/FlyingFox.podspec.json b/FlyingFox.podspec.json index d6903263..6ab1c11f 100644 --- a/FlyingFox.podspec.json +++ b/FlyingFox.podspec.json @@ -25,5 +25,5 @@ "pod_target_xcconfig": { "OTHER_SWIFT_FLAGS": "-package-name FlyingFox" }, - "swift_version": "5.10" + "swift_version": "6.0" } diff --git a/FlyingFox/Sources/URLSession+Async.swift b/FlyingFox/Sources/URLSession+Async.swift deleted file mode 100644 index b74ac73f..00000000 --- a/FlyingFox/Sources/URLSession+Async.swift +++ /dev/null @@ -1,72 +0,0 @@ -// -// URLSession+Async.swift -// FlyingFox -// -// Created by Simon Whitty on 13/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import Foundation -import FlyingSocks - -#if compiler(<6) && canImport(FoundationNetworking) -import FoundationNetworking - -extension URLSession { - - // Ports macOS Foundation method to earlier Linux versions - func data(for request: URLRequest) async throws -> (Data, URLResponse) { - let state = Mutex((isCancelled: false, task: URLSessionDataTask?.none)) - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation { continuation in - let task = dataTask(with: request) { data, response, error in - guard let data = data, let response = response else { - continuation.resume(throwing: error!) - return - } - continuation.resume(returning: (data, response)) - } - let shouldCancel = state.withLock { - $0.task = task - return $0.isCancelled - } - task.resume() - if shouldCancel { - task.cancel() - } - } - } onCancel: { - let taskToCancel = state.withLock { - $0.isCancelled = true - return $0.task - } - if let taskToCancel { - taskToCancel.cancel() - } - } - } -} -#endif diff --git a/FlyingFox/XCTests/AsyncSequence+ExtensionsTests.swift b/FlyingFox/XCTests/AsyncSequence+ExtensionsTests.swift deleted file mode 100644 index e89e2381..00000000 --- a/FlyingFox/XCTests/AsyncSequence+ExtensionsTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// AsyncSequence+ExtensionsTests.swift -// FlyingFox -// -// Created by Simon Whitty on 13/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import XCTest - -final class AsyncSequenceExtensionTests: XCTestCase { - - func testCollectStrings() async throws { - var iterator = ConsumingAsyncSequence("fish,chips".data(using: .utf8)!) - .collectStrings(separatedBy: ",") - .makeAsyncIterator() -// - await AsyncAssertEqual(try await iterator.next(), "fish") - await AsyncAssertEqual(try await iterator.next(), "chips") - await AsyncAssertEqual(try await iterator.next(), nil) - } - - func testCollectStringsWithTrailingSeperator() async throws { - var iterator = ConsumingAsyncSequence("fish,chips,".data(using: .utf8)!) - .collectStrings(separatedBy: ",") - .makeAsyncIterator() - - await AsyncAssertEqual(try await iterator.next(), "fish") - await AsyncAssertEqual(try await iterator.next(), "chips") - await AsyncAssertEqual(try await iterator.next(), nil) - } - - func testCollectStringsWithTrailingSeperatorA() async throws { - var iterator = ConsumingAsyncSequence([0x61, 0x2c, 0x62, 0x2c, 0xff]) - .collectStrings(separatedBy: ",") - .makeAsyncIterator() - - await AsyncAssertEqual(try await iterator.next(), "a") - await AsyncAssertEqual(try await iterator.next(), "b") - await AsyncAssertThrowsError(try await iterator.next(), of: AsyncSequenceError.self) - } - - func testTakeNextThrowsError_WhenSequenceEnds() async { - let sequence = ConsumingAsyncSequence(bytes: []) - - await AsyncAssertThrowsError(try await sequence.takeNext(), of: SequenceTerminationError.self) - } -} - - diff --git a/FlyingFox/XCTests/AsyncSocketTests.swift b/FlyingFox/XCTests/AsyncSocketTests.swift deleted file mode 100644 index 3de1df59..00000000 --- a/FlyingFox/XCTests/AsyncSocketTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// AsyncSocketTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -@testable import FlyingSocks -import Foundation - -extension AsyncSocket { - - static func make() async throws -> AsyncSocket { - try await make(pool: .client) - } - - static func make(pool: some AsyncSocketPool) throws -> AsyncSocket { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - return try AsyncSocket(socket: socket, pool: pool) - } - - static func makePair() async throws -> (AsyncSocket, AsyncSocket) { - try await makePair(pool: .client) - } - - func writeString(_ string: String) async throws { - try await write(string.data(using: .utf8)!) - } - - func readString(length: Int) async throws -> String { - let bytes = try await read(bytes: length) - guard let string = String(data: Data(bytes), encoding: .utf8) else { - throw SocketError.makeFailed("Read") - } - return string - } -} diff --git a/FlyingFox/XCTests/HTTPBodyPatternTests.swift b/FlyingFox/XCTests/HTTPBodyPatternTests.swift deleted file mode 100644 index a2a835cf..00000000 --- a/FlyingFox/XCTests/HTTPBodyPatternTests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// HTTPBodyPatternTests.swift -// FlyingFox -// -// Created by Simon Whitty on 13/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class HTTPBodyPatternTests: XCTestCase { - -#if canImport(Darwin) - func testBodyArray_MatchesRoute() { - let pattern = JSONPredicatePattern.json(where: "animals[1].name == 'fish'") - - XCTAssertTrue( - pattern.evaluate( - #""" - { - "animals": [ - {"name": "dog"}, - {"name": "fish"} - ] - } - """#.data(using: .utf8)! - ) - ) - - XCTAssertFalse( - pattern.evaluate( - #""" - { - "animals": [ - {"name": "fish"}, - {"name": "dog"} - ] - } - """#.data(using: .utf8)! - ) - ) - - XCTAssertFalse( - pattern.evaluate( - Data([0x01, 0x02]) - ) - ) - } -#endif -} diff --git a/FlyingFox/XCTests/HTTPBodySequenceTests.swift b/FlyingFox/XCTests/HTTPBodySequenceTests.swift deleted file mode 100644 index 628012f4..00000000 --- a/FlyingFox/XCTests/HTTPBodySequenceTests.swift +++ /dev/null @@ -1,243 +0,0 @@ -// -// HTTPBodySequenceTests.swift -// FlyingFox -// -// Created by Simon Whitty on 02/04/2023. -// Copyright © 2023 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import XCTest - -final class HTTPBodySequenceTests: XCTestCase { - - func testEmptyPayload_ReturnsEmptyData() async { - let sequence = HTTPBodySequence() - - await AsyncAssertEqual( - try await sequence.get(), - Data() - ) - } - - func testEmptyPayload_ReturnNilBuffer() async { - let sequence = HTTPBodySequence() - - var iterator = sequence.makeAsyncIterator() - - await AsyncAssertNil( - try await iterator.next() - ) - } - - func testDataPayload_IsReturnedInOneIteration() async { - let sequence = HTTPBodySequence(bytes: [ - 0x00, 0x01, 0x02, 0x03, 0x04 - ]) - - var iterator = sequence.makeAsyncIterator() - - await AsyncAssertEqual( - try await iterator.next(), - Data([0x00, 0x01, 0x02, 0x03, 0x04]) - ) - - await AsyncAssertNil( - try await iterator.next() - ) - } - - func testDataPayload_CanBeIteratedMultipleTimes() async { - let data = Data([ - 0x00, 0x01, 0x02 - ]) - - let sequence = HTTPBodySequence(data: data) - - await AsyncAssertEqual( - try await sequence.get(), - data - ) - - await AsyncAssertEqual( - try await sequence.get(), - data - ) - - await AsyncAssertEqual( - try await sequence.get(), - data - ) - } - - func testDataPayload_ReturnsCount() { - XCTAssertEqual( - HTTPBodySequence().count, - 0 - ) - XCTAssertEqual( - HTTPBodySequence(bytes: [0x0]).count, - 1 - ) - XCTAssertEqual( - HTTPBodySequence(bytes: [0x00, 0x01, 0x02, 0x03, 0x04]).count, - 5 - ) - } - - func testSequencePayload_ReturnsCount() { - XCTAssertEqual( - HTTPBodySequence.make(from: []).count, - 0 - ) - XCTAssertEqual( - HTTPBodySequence.make(from: [0x0]).count, - 1 - ) - XCTAssertEqual( - HTTPBodySequence.make(from: [0x00, 0x01, 0x02, 0x03, 0x04]).count, - 5 - ) - } - - func testSequencePayload_CanBeReturned() async { - let sequence = HTTPBodySequence.make( - from: [0x00, 0x01, 0x02, 0x03, 0x04], - chunkSize: 2 - ) - - await AsyncAssertEqual( - try await sequence.get(), - Data([0x00, 0x01, 0x02, 0x03, 0x04]) - ) - } - - func testSequencePayload_CanBeIterated() async { - let sequence = HTTPBodySequence.make( - from: [0x00, 0x01, 0x02, 0x03, 0x04], - chunkSize: 2 - ) - - var iterator = sequence.makeAsyncIterator() - - await AsyncAssertEqual( - try await iterator.next(), - Data([0x00, 0x01]) - ) - - await AsyncAssertEqual( - try await iterator.next(), - Data([0x02, 0x03]) - ) - - await AsyncAssertEqual( - try await iterator.next(), - Data([0x04]) - ) - - await AsyncAssertNil( - try await iterator.next() - ) - } - - func testSequencePayloadA_IsFlushed() async throws { - // given - let body = HTTPBodySequence.make( - from: [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9], - bufferSize: 3 - ) - - // when then - await AsyncAssertEqual( - try await body.collectAll(), - [ - Data([0x0, 0x1, 0x2]), - Data([0x3, 0x4, 0x5]), - Data([0x6, 0x7, 0x8]), - Data([0x9]) - ] - ) - } - - func testSequencePayload_IsFlushed() async { - // given - let buffer = ConsumingAsyncSequence( - bytes: [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9] - ) - - let sequence = HTTPBodySequence(shared: buffer, count: 10) - - // then - XCTAssertEqual(buffer.index, 0) - - // when - await AsyncAssertNoThrow( - try await sequence.flushIfNeeded() - ) - - // then - XCTAssertEqual(buffer.index, 10) - } - - func testFilePayloadCanReplay() async throws { - let sequence = try HTTPBodySequence(file: .fishJSON, suggestedBufferSize: 1) - - XCTAssertTrue(sequence.canReplay) - await AsyncAssertEqual( - try await sequence.get(), - try Data(contentsOf: .fishJSON) - ) - } -} - -private extension URL { - static var fishJSON: URL { - Bundle.module.url(forResource: "Stubs/fish.json", withExtension: nil)! - } -} - -private extension HTTPBodySequence { - - init(bytes: [UInt8]) { - self.init(data: Data(bytes)) - } - - static func make(from bytes: [UInt8], count: Int? = nil, chunkSize: Int = 2) -> Self { - HTTPBodySequence( - from: ConsumingAsyncSequence(bytes), - count: count ?? bytes.count, - suggestedBufferSize: chunkSize - ) - } - - static func make(from bytes: [UInt8], bufferSize: Int) -> Self { - HTTPBodySequence( - data: Data(bytes), - suggestedBufferSize: bufferSize - ) - } -} diff --git a/FlyingFox/XCTests/HTTPClientTests.swift b/FlyingFox/XCTests/HTTPClientTests.swift deleted file mode 100644 index 7eb7fd07..00000000 --- a/FlyingFox/XCTests/HTTPClientTests.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// HTTPClientTests.swift -// FlyingFox -// -// Created by Simon Whitty on 8/06/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@_spi(Private) import struct FlyingFox._HTTPClient -@testable import FlyingFox -@testable import FlyingSocks -import XCTest -import Foundation - -final class HTTPClientTests: XCTestCase { - -#if canImport(Darwin) - func testClient() async throws { - // given - let server = HTTPServer(address: .loopback(port: 0)) - let task = Task { try await server.run() } - defer { task.cancel() } - let client = _HTTPClient() - - // when - let port = try await server.waitForListeningPort() - let response = try await client.sendHTTPRequest(HTTPRequest.make(), to: .loopback(port: port)) - - // then - XCTAssertEqual(response.statusCode, .notFound) - } -#endif - -} diff --git a/FlyingFox/XCTests/HTTPConnectionTests.swift b/FlyingFox/XCTests/HTTPConnectionTests.swift deleted file mode 100644 index e8b95c05..00000000 --- a/FlyingFox/XCTests/HTTPConnectionTests.swift +++ /dev/null @@ -1,177 +0,0 @@ -// -// HTTPConnectionTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import XCTest -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -final class HTTPConnectionTests: XCTestCase { - - func testConnection_ReceivesRequest() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - - let connection = HTTPConnection(socket: s1) - try await s2.writeString( - """ - GET /hello/world HTTP/1.1\r - Content-Length: 5 - \r - Hello - - """ - ) - - let request = try await connection.requests.first() - await AsyncAssertAsyncEqual( - request, - .make(method: .GET, - version: .http11, - path: "/hello/world", - headers: [.contentLength: "5"], - body: "Hello".data(using: .utf8)!) - ) - - try s1.close() - try s2.close() - } - - func testConnectionRequestsAreReceived_WhileConnectionIsKeptAlive() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - - let connection = HTTPConnection(socket: s1) - try await s2.writeString( - """ - GET /hello HTTP/1.1\r - Connection: Keep-Alive\r - \r - GET /hello HTTP/1.1\r - Connection: Keep-Alive\r - \r - GET /hello HTTP/1.1\r - \r - - """ - ) - - let count = try await connection.requests.reduce(0, { count, _ in count + 1 }) - XCTAssertEqual(count, 3) - - try s1.close() - try s2.close() - } - - func testConnectionResponse_IsSent() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - - let connection = HTTPConnection(socket: s1) - - try await connection.sendResponse( - .make(version: .http11, - statusCode: .gone, - body: "Hello World!".data(using: .utf8)!) - ) - - let response = try await s2.readString(length: 53) - XCTAssertEqual( - response, - """ - HTTP/1.1 410 Gone\r - Content-Length: 12\r - \r - Hello World! - """ - ) - } - - func testConnectionDisconnects_WhenErrorIsReceived() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - - try s2.close() - let connection = HTTPConnection(socket: s1) - - let count = try await connection.requests.reduce(0, { count, _ in count + 1 }) - XCTAssertEqual(count, 0) - - try connection.close() - } - - func testConnectionHostName() { - XCTAssertEqual( - HTTPConnection.makeIdentifer(from: .ip4("8.8.8.8", port: 8080)), - "8.8.8.8" - ) - XCTAssertEqual( - HTTPConnection.makeIdentifer(from: .ip6("::1", port: 8080)), - "::1" - ) - XCTAssertEqual( - HTTPConnection.makeIdentifer(from: .unix("/var/sock/fox")), - "/var/sock/fox" - ) - } -} - -private extension HTTPConnection { - init(socket: AsyncSocket) { - self.init( - socket: socket, - decoder: HTTPDecoder.make(sharedRequestReplaySize: 1024), - logger: .disabled - ) - } -} - -extension AsyncSequence { - func first() async throws -> Element { - guard let next = try await first(where: { _ in true }) else { - throw AsyncSequenceError("Premature termination") - } - return next - } -} - -extension HTTPRequest: AsyncEquatable { - static func ==(lhs: HTTPRequest, rhs: HTTPRequest) async -> Bool { - let lhsData = try? await lhs.bodyData - let rhsData = try? await rhs.bodyData - guard let lhsData, let rhsData else { return false } - return lhs.method == rhs.method && - lhs.version == rhs.version && - lhs.path == rhs.path && - lhs.query == rhs.query && - lhs.headers == rhs.headers && - lhsData == rhsData - } -} diff --git a/FlyingFox/XCTests/HTTPDecoderTests.swift b/FlyingFox/XCTests/HTTPDecoderTests.swift deleted file mode 100644 index 20557fee..00000000 --- a/FlyingFox/XCTests/HTTPDecoderTests.swift +++ /dev/null @@ -1,315 +0,0 @@ -// -// HTTPDecoderTests.swift -// FlyingFox -// -// Created by Simon Whitty on 17/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import Foundation -import XCTest - -final class HTTPDecoderTests: XCTestCase { - - func testGETMethod_IsParsed() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET /hello HTTP/1.1\r - \r - """ - ) - - XCTAssertEqual( - request.method, - .GET - ) - } - - func testPOSTMethod_IsParsed() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - POST /hello HTTP/1.1\r - \r - """ - ) - - XCTAssertEqual( - request.method, - .POST - ) - } - - func testCUSTOMMethod_IsParsed() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - FISH /hello HTTP/1.1\r - \r - """ - ) - - XCTAssertEqual( - request.method, - HTTPMethod("FISH") - ) - } - - func testPath_IsParsed() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET /hello/world?fish=Chips&with=Mushy%20Peas HTTP/1.1\r - \r - """ - ) - - XCTAssertEqual( - request.path, - "/hello/world" - ) - - XCTAssertEqual( - request.query, - [.init(name: "fish", value: "Chips"), - .init(name: "with", value: "Mushy Peas")] - ) - } - - func testNaughtyPath_IsParsed() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET /../a/b/../c/./d.html?fish=Chips&with=Mushy%20Peas HTTP/1.1\r - \r - """ - ) - - XCTAssertEqual( - request.path, - "/a/c/d.html" - ) - - XCTAssertEqual( - request.query, - [.init(name: "fish", value: "Chips"), - .init(name: "with", value: "Mushy Peas")] - ) - } - - func testHeaders_AreParsed() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET /hello HTTP/1.1\r - Fish: Chips\r - Connection: Keep-Alive\r - content-type: none\r - \r - """ - ) - - XCTAssertEqual( - request.headers, - [HTTPHeader("Fish"): "Chips", - HTTPHeader("Connection"): "Keep-Alive", - HTTPHeader("Content-Type"): "none"] - ) - } - - func testBody_IsNotParsed_WhenContentLength_IsNotProvided() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET /hello HTTP/1.1\r - \r - Hello - """ - ) - - await AsyncAssertEqual( - try await request.bodyData, - Data() - ) - } - - func testBody_IsParsed_WhenContentLength_IsProvided() async throws { - let request = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET /hello HTTP/1.1\r - Content-Length: 5\r - \r - Hello - """ - ) - - await AsyncAssertEqual( - try await request.bodyData, - "Hello".data(using: .utf8) - ) - } - - func testInvalidStatusLine_ThrowsErrorM() async throws { - do { - _ = try await HTTPDecoder.make().decodeRequestFromString( - """ - GET/hello HTTP/1.1\r - \r - """ - ) - XCTFail("Expected Error") - } catch { - XCTAssertTrue(error is HTTPDecoder.Error) - } - } - - func testBody_ThrowsError_WhenSequenceEnds() async throws { - await AsyncAssertThrowsError( - _ = try await HTTPDecoder.make(sharedRequestReplaySize: 1024).readBody(from: AsyncBufferedEmptySequence(completeImmediately: true), length: "100").get(), - of: SocketError.self - ) - await AsyncAssertThrowsError( - _ = try await HTTPDecoder.make(sharedRequestReplaySize: 1024).readBody(from: AsyncBufferedEmptySequence(completeImmediately: true), length: "100").get(), - of: SocketError.self - ) - } - - func testBodySequence_CanReplay_WhenSizeIsLessThanMax() async throws { - let decoder = HTTPDecoder.make(sharedRequestBufferSize: 1, sharedRequestReplaySize: 100) - let sequence = try await decoder.readBodyFromString("Fish & Chips") - XCTAssertEqual(sequence.count, 12) - XCTAssertTrue(sequence.canReplay) - } - - func testBodySequence_CanNotReplay_WhenSizeIsGreaterThanMax() async throws { - let decoder = HTTPDecoder.make(sharedRequestBufferSize: 1, sharedRequestReplaySize: 2) - let sequence = try await decoder.readBodyFromString("Fish & Chips") - XCTAssertEqual(sequence.count, 12) - XCTAssertFalse(sequence.canReplay) - } - - func testInvalidPathDecodes() { - let comps = HTTPDecoder.make().makeComponents(from: nil) - XCTAssertEqual( - comps.path, "" - ) - XCTAssertEqual( - comps.query, [] - ) - } - - func testPercentEncodedPathDecodes() { - XCTAssertEqual( - HTTPDecoder.make().readComponents(from: "/fish%20chips").path, - "/fish chips" - ) - XCTAssertEqual( - HTTPDecoder.make().readComponents(from: "/ocean/fish%20and%20chips").path, - "/ocean/fish and chips" - ) - } - - func testPercentQueryStringDecodes() { - XCTAssertEqual( - HTTPDecoder.make().readComponents(from: "/?fish=%F0%9F%90%9F").query, - [.init(name: "fish", value: "🐟")] - ) - XCTAssertEqual( - HTTPDecoder.make().readComponents(from: "?%F0%9F%90%A1=chips").query, - [.init(name: "🐡", value: "chips")] - ) - } - - func testEmptyQueryItem_Decodes() { - var urlComps = URLComponents() - urlComps.queryItems = [.init(name: "name", value: nil)] - - XCTAssertEqual( - HTTPDecoder.make().makeComponents(from: urlComps).query, - [.init(name: "name", value: "")] - ) - } - - func testResponseInvalidStatusLine_ThrowsErrorM() async throws { - do { - _ = try await HTTPDecoder.make().decodeResponseFromString( - """ - HTTP/1.1\r - \r - """ - ) - XCTFail("Expected Error") - } catch { - XCTAssertTrue(error is HTTPDecoder.Error) - } - } - - func testResponseBody_IsNotParsed_WhenContentLength_IsNotProvided() async throws { - let response = try await HTTPDecoder.make().decodeResponseFromString( - """ - HTTP/1.1 202 OK \r - \r - Hello - """ - ) - - await AsyncAssertEqual( - try await response.bodyData, - Data() - ) - } - - func testResponseBody_IsParsed_WhenContentLength_IsProvided() async throws { - let response = try await HTTPDecoder.make().decodeResponseFromString( - """ - HTTP/1.1 202 OK \r - Content-Length: 5\r - \r - Hello - """ - ) - - await AsyncAssertEqual( - try await response.bodyData, - "Hello".data(using: .utf8) - ) - } -} - -private extension HTTPDecoder { - - func decodeRequestFromString(_ string: String) async throws -> HTTPRequest { - try await decodeRequest(from: ConsumingAsyncSequence(string.data(using: .utf8)!)) - } - - func decodeResponseFromString(_ string: String) async throws -> HTTPResponse { - try await decodeResponse(from: ConsumingAsyncSequence(string.data(using: .utf8)!)) - } - - func readBodyFromString(_ string: String) async throws -> HTTPBodySequence { - let data = string.data(using: .utf8)! - return try await readBody( - from: ConsumingAsyncSequence(data), - length: "\(data.count)" - ) - } -} diff --git a/FlyingFox/XCTests/HTTPEncoderTests.swift b/FlyingFox/XCTests/HTTPEncoderTests.swift deleted file mode 100644 index 533d6054..00000000 --- a/FlyingFox/XCTests/HTTPEncoderTests.swift +++ /dev/null @@ -1,210 +0,0 @@ -// -// HTTPEncoderTests.swift -// FlyingFox -// -// Created by Simon Whitty on 17/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation -import XCTest - -final class HTTPEncoderTests: XCTestCase { - - func testHeaderLines_BeginWithStatusCode() { - XCTAssertEqual( - HTTPEncoder - .makeHeaderLines(from: .make(version: .http2, statusCode: .ok)) - .first, - "HTTP/2 200 OK" - ) - - XCTAssertEqual( - HTTPEncoder - .makeHeaderLines(from: .make(version: HTTPVersion("Fish"), - statusCode: HTTPStatusCode(999, phrase: "Chips"))) - .first, - "Fish 999 Chips" - ) - } - - func testHeaderLines_IncludesContentSize() { - XCTAssertTrue( - HTTPEncoder.makeHeaderLines(from: .make(body: Data())) - .contains("Content-Length: 0") - ) - - XCTAssertTrue( - HTTPEncoder.makeHeaderLines(from: .make(body: Data([0x01]))) - .contains("Content-Length: 1") - ) - XCTAssertTrue( - HTTPEncoder.makeHeaderLines(from: .make(body: Data([0x01, 0x02, 0x03]))) - .contains("Content-Length: 3") - ) - } - - func testHeaderLines_IncludeSuppliedHeaders() { - let lines = HTTPEncoder - .makeHeaderLines(from: .make(headers: [ - .connection: "keep", - .location: "Swan Hill", - .init(rawValue: "Flying"): "Fox" - ])) - - XCTAssertTrue( - lines.contains("Connection: keep") - ) - - XCTAssertTrue( - lines.contains("Location: Swan Hill") - ) - - XCTAssertTrue( - lines.contains("Flying: Fox") - ) - } - - func testHeaderLines_EndWithCarriageReturnLineFeed() { - XCTAssertEqual( - HTTPEncoder - .makeHeaderLines(from: .make()) - .last, - "\r\n" - ) - - XCTAssertEqual( - HTTPEncoder - .makeHeaderLines(from: .make(headers: [.connection: "keep", .contentType: "none"])) - .last, - "\r\n" - ) - } - - func testEncodesResponseHeader() throws { - XCTAssertEqual( - HTTPEncoder.encodeResponseHeader( - .make(version: .http11, - statusCode: .ok, - headers: [:], - body: "Hello World!".data(using: .utf8)!) - ), - """ - HTTP/1.1 200 OK\r - Content-Length: 12\r - \r - - """.data(using: .utf8) - ) - } - - func testEncodesChunkedResponse() async throws { - let data = try await HTTPEncoder.encodeResponse( - .makeChunked( - version: .http11, - statusCode: .ok, - headers: [.transferEncoding: "Fish"], - body: "Hello World!".data(using: .utf8)!, - chunkSize: 10 - ) - ) - - XCTAssertEqual( - data, - """ - HTTP/1.1 200 OK\r - Transfer-Encoding: Fish, chunked\r - \r - 0A\r - Hello Worl\r - 02\r - d!\r - 0\r - \r - - """.data(using: .utf8) - ) - } - - func testEncodesRequest() async throws { - await AsyncAssertEqual( - try await HTTPEncoder.encodeRequest( - .make(method: .GET, - version: .http11, - path: "greeting/hello world", - query: [], - headers: [:], - body: "Hello World!".data(using: .utf8)!) - ), - """ - GET greeting/hello%20world HTTP/1.1\r - Content-Length: 12\r - \r - Hello World! - """.data(using: .utf8) - ) - } - - func testEncodesRequest_WithQuery() async throws { - await AsyncAssertEqual( - try await HTTPEncoder.encodeRequest( - .make(method: .GET, - version: .http11, - path: "greeting/hello world", - query: [.init(name: "fish", value: "chips")], - headers: [:], - body: "Hello World!".data(using: .utf8)!) - ), - """ - GET greeting/hello%20world?fish=chips HTTP/1.1\r - Content-Length: 12\r - \r - Hello World! - """.data(using: .utf8) - ) - } -} - -private extension HTTPVersion { - static let http2 = HTTPVersion("HTTP/2") -} - -private extension HTTPEncoder { - - static func encodeResponse(_ response: HTTPResponse) async throws -> Data { - var data = encodeResponseHeader(response) - switch response.payload { - case .httpBody(let sequence): - for try await chunk in sequence { - data.append(chunk) - } - case .webSocket: - () - } - return data - } -} diff --git a/FlyingFox/XCTests/HTTPHeaderTests.swift b/FlyingFox/XCTests/HTTPHeaderTests.swift deleted file mode 100644 index 7ab1c69a..00000000 --- a/FlyingFox/XCTests/HTTPHeaderTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// HTTPHeaderTests.swift -// FlyingFox -// -// Created by Simon Whitty on 11/07/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation -import XCTest - -final class HTTPHeaderTests: XCTestCase { - - func testStringValue() { - // given - var headers = HTTPHeaders() - headers[.transferEncoding] = "Identity" - - XCTAssertEqual( - headers[.transferEncoding], - "Identity" - ) - - XCTAssertEqual( - headers.values(for: .transferEncoding), - ["Identity"] - ) - - XCTAssertEqual( - headers.values(for: .contentType), - [] - ) - - headers.addValue("chunked", for: .transferEncoding) - - XCTAssertEqual( - headers[.transferEncoding], - "Identity, chunked" - ) - - XCTAssertEqual( - headers.values(for: .transferEncoding), - ["Identity", "chunked"] - ) - } -} diff --git a/FlyingFox/XCTests/HTTPLoggingTests.swift b/FlyingFox/XCTests/HTTPLoggingTests.swift deleted file mode 100644 index 6ba3ee0b..00000000 --- a/FlyingFox/XCTests/HTTPLoggingTests.swift +++ /dev/null @@ -1,154 +0,0 @@ -// -// HTTPLoggingTests.swift -// FlyingFox -// -// Created by Simon Whitty on 23/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -@testable import FlyingSocks -import Foundation -import XCTest - -final class HTTPLoggingTests: XCTestCase { - - func testPrintLogger_DefaultCategory() { - let logger = PrintLogger.print() - - XCTAssertEqual( - logger.category, - "FlyingFox" - ) - } - - func testListeningLog_INETPort() { - XCTAssertEqual( - PrintLogger.makeListening(on: .ip4("0.0.0.0", port: 1234)), - "starting server port: 1234" - ) - } - - func testListeningLog_INET() throws { - XCTAssertEqual( - PrintLogger.makeListening(on: .ip4("8.8.8.8", port: 1234)), - "starting server 8.8.8.8:1234" - ) - } - - func testListeningLog_INET6Port() { - XCTAssertEqual( - PrintLogger.makeListening(on: .ip6("::", port: 5678)), - "starting server port: 5678" - ) - } - - func testListeningLog_INET6() throws { - XCTAssertEqual( - PrintLogger.makeListening(on: .ip6("::1", port: 1234)), - "starting server ::1:1234" - ) - } - - func testListeningLog_UnixPath() { - XCTAssertEqual( - PrintLogger.makeListening(on: .unix("/var/fox/xyz")), - "starting server path: /var/fox/xyz" - ) - } - - func testListeningLog_Invalid() { - XCTAssertEqual( - PrintLogger.makeListening(on: nil), - "starting server" - ) - } - - func testDisabledLogger_DoesNotExecuteDebugClosure() { - let logger = DisabledLogger() - let didNotReturnString = expectation(description: "debug closure") - didNotReturnString.isInverted = true - - logger.logDebug({ - didNotReturnString.fulfill() - return "disabled" - }()) - - wait(for: [didNotReturnString], timeout: 0.1) - } - - func testDisabledLogger_DoesNotExecuteInfoClosure() { - let logger = DisabledLogger() - let didNotReturnString = expectation(description: "info closure") - didNotReturnString.isInverted = true - - logger.logInfo({ - didNotReturnString.fulfill() - return "disabled" - }()) - - wait(for: [didNotReturnString], timeout: 0.1) - } - - func testDisabledLogger_DoesNotExecuteWarningClosure() { - let logger = DisabledLogger() - let didNotReturnString = expectation(description: "warning closure") - didNotReturnString.isInverted = true - - logger.logWarning({ - didNotReturnString.fulfill() - return "disabled" - }()) - - wait(for: [didNotReturnString], timeout: 0.1) - } - - func testDisabledLogger_DoesNotExecuteErrorClosure() { - let logger = DisabledLogger() - let didNotReturnString = expectation(description: "error closure") - didNotReturnString.isInverted = true - - logger.logError({ - didNotReturnString.fulfill() - return "disabled" - }()) - - wait(for: [didNotReturnString], timeout: 0.1) - } - - func testDisabledLogger_DoesNotExecuteCriticalClosure() { - let logger = DisabledLogger() - let didNotReturnString = expectation(description: "critical closure") - didNotReturnString.isInverted = true - - logger.logCritical({ - didNotReturnString.fulfill() - return "disabled" - }()) - - wait(for: [didNotReturnString], timeout: 0.1) - } -} diff --git a/FlyingFox/XCTests/HTTPMethodTests.swift b/FlyingFox/XCTests/HTTPMethodTests.swift deleted file mode 100644 index b4f06803..00000000 --- a/FlyingFox/XCTests/HTTPMethodTests.swift +++ /dev/null @@ -1,44 +0,0 @@ -// -// HTTPMethodTests.swift -// FlyingFox -// -// Created by Simon Whitty on 11/07/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation -import XCTest - -final class HTTPMethodTests: XCTestCase { - - func testStringValue() { - XCTAssertEqual( - Set([HTTPMethod("fish"), .DELETE, .POST]).stringValue, - "POST,DELETE,FISH" - ) - } -} diff --git a/FlyingFox/XCTests/HTTPRequest+AddressTests.swift b/FlyingFox/XCTests/HTTPRequest+AddressTests.swift deleted file mode 100644 index e741bf29..00000000 --- a/FlyingFox/XCTests/HTTPRequest+AddressTests.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// HTTPRequest+AddressTests.swift -// FlyingFox -// -// Created by Simon Whitty on 03/08/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import FlyingFox -import XCTest - -final class HTTPRequestAddressTests: XCTestCase { - - typealias Address = HTTPRequest.Address - - func testRemoteAddress_IP4() { - let request = HTTPRequest.make(remoteAddress: .ip4("fish", port: 80)) - XCTAssertEqual(request.remoteAddress, .ip4("fish", port: 80)) - XCTAssertEqual(request.remoteIPAddress, "fish") - } - - func testRemoteAddress_IP6() { - let request = HTTPRequest.make(remoteAddress: .ip6("chips", port: 8080)) - XCTAssertEqual(request.remoteAddress, .ip6("chips", port: 8080)) - XCTAssertEqual(request.remoteIPAddress, "chips") - } - - func testRemoteAddress_Unix() { - let request = HTTPRequest.make(remoteAddress: .unix("shrimp")) - XCTAssertEqual(request.remoteAddress, .unix("shrimp")) - XCTAssertNil(request.remoteIPAddress) - } - - func testRemoteAddress_XForwardedFor() { - let request = HTTPRequest.make( - headers: [.xForwardedFor: "fish, chips"], - remoteAddress: .ip4("shrimp", port: 80) - ) - XCTAssertEqual(request.remoteAddress, .ip4("shrimp", port: 80)) - XCTAssertEqual(request.remoteIPAddress, "fish") - } -} diff --git a/FlyingFox/XCTests/HTTPRequest+Mock.swift b/FlyingFox/XCTests/HTTPRequest+Mock.swift deleted file mode 100644 index 40c68fc0..00000000 --- a/FlyingFox/XCTests/HTTPRequest+Mock.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// HTTPRequest+Mock.swift -// FlyingFox -// -// Created by Simon Whitty on 18/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation - -extension HTTPRequest { - static func make(method: HTTPMethod = .GET, - version: HTTPVersion = .http11, - path: String = "/", - query: [QueryItem] = [], - headers: HTTPHeaders = [:], - body: Data = Data(), - remoteAddress: Address? = nil) -> Self { - HTTPRequest(method: method, - version: version, - path: path, - query: query, - headers: headers, - body: HTTPBodySequence(data: body), - remoteAddress: remoteAddress) - } - - static func make(method: HTTPMethod = .GET, _ url: String, headers: HTTPHeaders = [:]) -> Self { - let (path, query) = HTTPDecoder.make().readComponents(from: url.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!) - return HTTPRequest.make( - method: method, - path: path, - query: query, - headers: headers - ) - } - - var bodyString: String { - get async throws { - try await String(decoding: bodyData, as: UTF8.self) - } - } -} - -extension HTTPDecoder { - static func make(sharedRequestBufferSize: Int = 128, sharedRequestReplaySize: Int = 1024) -> HTTPDecoder { - HTTPDecoder( - sharedRequestBufferSize: sharedRequestBufferSize, - sharedRequestReplaySize: sharedRequestReplaySize - ) - } -} diff --git a/FlyingFox/XCTests/HTTPRequest+QueryItemTests.swift b/FlyingFox/XCTests/HTTPRequest+QueryItemTests.swift deleted file mode 100644 index f1aa627c..00000000 --- a/FlyingFox/XCTests/HTTPRequest+QueryItemTests.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// HTTPRequest+QueryItemTests.swift -// FlyingFox -// -// Created by Simon Whitty on 6/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import FlyingFox -import XCTest - -final class HTTPRequestQueryItemTests: XCTestCase { - - typealias QueryItem = HTTPRequest.QueryItem - - func testSubscript_ReturnsFirstItemWithName() { - let items = [ - QueryItem(name: "meal", value: "fish"), - QueryItem(name: "side", value: "chips"), - QueryItem(name: "side", value: "peas") - ] - - XCTAssertEqual(items["meal"], "fish") - XCTAssertEqual(items["side"], "chips") - XCTAssertNil(items["other"]) - } - - func testSubscript_UpdatesFirstItemWithName() { - var items = [ - QueryItem(name: "meal", value: "fish"), - QueryItem(name: "side", value: "chips"), - QueryItem(name: "side", value: "peas") - ] - - items["side"] = "salad" - - XCTAssertEqual( - items, - [ - QueryItem(name: "meal", value: "fish"), - QueryItem(name: "side", value: "salad"), - QueryItem(name: "side", value: "peas") - ] - ) - } - - func testSubscript_RemovesFirstItemWithName() { - var items = [ - QueryItem(name: "meal", value: "fish"), - QueryItem(name: "side", value: "chips"), - QueryItem(name: "side", value: "peas") - ] - - items["side"] = nil - - XCTAssertEqual( - items, - [ - QueryItem(name: "meal", value: "fish"), - QueryItem(name: "side", value: "peas") - ] - ) - } - - func testSubscript_AddsItemIfDoesNotExist() { - var items = [ - QueryItem(name: "meal", value: "fish") - ] - - items["side"] = "chips" - - XCTAssertEqual( - items, - [ - QueryItem(name: "meal", value: "fish"), - QueryItem(name: "side", value: "chips") - ] - ) - } -} diff --git a/FlyingFox/XCTests/HTTPRequestTests.swift b/FlyingFox/XCTests/HTTPRequestTests.swift deleted file mode 100644 index 35f17156..00000000 --- a/FlyingFox/XCTests/HTTPRequestTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// HTTPRequestTests.swift -// FlyingFox -// -// Created by Simon Whitty on 02/04/2023. -// Copyright © 2023 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import XCTest - -final class HTTPResponseTests: XCTestCase { - - func testCompleteBodyData() async { - // given - let response = HTTPResponse.make(body: Data([0x01, 0x02])) - - // then - await AsyncAssertEqual( - try await response.bodyData, - Data([0x01, 0x02]) - ) - } - - func testSequenceBodyData() async { - // given - let buffer = ConsumingAsyncSequence( - bytes: [0x5, 0x6] - ) - let sequence = HTTPBodySequence(from: buffer, count: 2, suggestedBufferSize: 2) - let response = HTTPResponse.make(body: sequence) - - // then - await AsyncAssertEqual( - try await response.bodyData, - Data([0x5, 0x6]) - ) - } - - func testWebSocketBodyData() async { - // given - let response = HTTPResponse.make(webSocket: MessageFrameWSHandler.make()) - - // then - await AsyncAssertEqual( - try await response.bodyData, - Data() - ) - } - - func testUnknownRouteParameter() async { - XCTAssertNil(HTTPRequest.make().routeParameters["unknown"]) - } -} diff --git a/FlyingFox/XCTests/HTTPResponse+Mock.swift b/FlyingFox/XCTests/HTTPResponse+Mock.swift deleted file mode 100644 index bef33f73..00000000 --- a/FlyingFox/XCTests/HTTPResponse+Mock.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// HTTPResponse+Mock.swift -// FlyingFox -// -// Created by Simon Whitty on 17/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import Foundation - -extension HTTPResponse { - - static func make(version: HTTPVersion = .http11, - statusCode: HTTPStatusCode = .ok, - headers: [HTTPHeader: String] = [:], - body: Data = Data()) -> Self { - HTTPResponse(version: version, - statusCode: statusCode, - headers: headers, - body: body) - } - - static func makeChunked(version: HTTPVersion = .http11, - statusCode: HTTPStatusCode = .ok, - headers: [HTTPHeader: String] = [:], - body: Data = Data(), - chunkSize: Int = 5) -> Self { - let consuming = ConsumingAsyncSequence(body) - return HTTPResponse( - version: version, - statusCode: statusCode, - headers: headers, - body: HTTPBodySequence(from: consuming, suggestedBufferSize: chunkSize) - ) - } - - static func make(version: HTTPVersion = .http11, - statusCode: HTTPStatusCode = .ok, - headers: [HTTPHeader: String] = [:], - body: HTTPBodySequence) -> Self { - HTTPResponse(version: version, - statusCode: statusCode, - headers: headers, - body: body) - } - - static func make(headers: [HTTPHeader: String] = [:], - webSocket handler: some WSHandler) -> Self { - HTTPResponse(headers: headers, - webSocket: handler) - } - - var bodyString: String? { - get async throws { - try await String(data: bodyData, encoding: .utf8) - } - } -} diff --git a/FlyingFox/XCTests/HTTPResponseTests.swift b/FlyingFox/XCTests/HTTPResponseTests.swift deleted file mode 100644 index 0d0e3aed..00000000 --- a/FlyingFox/XCTests/HTTPResponseTests.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// HTTPRequestTests.swift -// FlyingFox -// -// Created by Simon Whitty on 02/04/2023. -// Copyright © 2023 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class HTTPRequestTests: XCTestCase { - - func testRequestBodyData_CanBeChanged() async { - // when - var request = HTTPRequest.make(body: Data([0x01, 0x02])) - - // then - await AsyncAssertEqual( - try await request.bodyData, - Data([0x01, 0x02]) - ) - - // when - request.setBodyData(Data([0x05, 0x06])) - - // then - await AsyncAssertEqual( - try await request.bodyData, - Data([0x05, 0x06]) - ) - } -} diff --git a/FlyingFox/XCTests/HTTPRouteParameterValueTests.swift b/FlyingFox/XCTests/HTTPRouteParameterValueTests.swift deleted file mode 100644 index 99288dbb..00000000 --- a/FlyingFox/XCTests/HTTPRouteParameterValueTests.swift +++ /dev/null @@ -1,236 +0,0 @@ -// -// HTTPRouteParameterValueTests.swift -// FlyingFox -// -// Created by Simon Whitty on 13/07/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import FlyingFox -import XCTest - -final class HTTPRouteParameterValueTests: XCTestCase { - - func testBoolConversion() { - XCTAssertTrue( - try Bool(parameter: "true") - ) - XCTAssertTrue( - try Bool(parameter: "TRUE") - ) - XCTAssertFalse( - try Bool(parameter: "false") - ) - XCTAssertFalse( - try Bool(parameter: "FALSE") - ) - - XCTAssertThrowsError( - try Bool(parameter: "fish") - ) - } - - func testStringConversion() { - XCTAssertEqual( - String(parameter: "fish"), - "fish" - ) - } - - func testIntConversion() { - XCTAssertEqual( - try Int8(parameter: "100"), - 100 - ) - XCTAssertEqual( - try Int8(parameter: "-100"), - -100 - ) - XCTAssertEqual( - try Int16(parameter: "10000"), - 10000 - ) - XCTAssertEqual( - try Int16(parameter: "-10000"), - -10000 - ) - XCTAssertEqual( - try Int32(parameter: "1000000000"), - 1000000000 - ) - XCTAssertEqual( - try Int32(parameter: "-1000000000"), - -1000000000 - ) - XCTAssertEqual( - try Int64(parameter: "1000000000000"), - 1000000000000 - ) - XCTAssertEqual( - try Int64(parameter: "-1000000000000"), - -1000000000000 - ) - - XCTAssertThrowsError( - try Int8(parameter: "1000") - ) - XCTAssertThrowsError( - try Int8(parameter: "fish") - ) - XCTAssertThrowsError( - try Int16(parameter: "1000000000") - ) - XCTAssertThrowsError( - try Int16(parameter: "fish") - ) - XCTAssertThrowsError( - try Int32(parameter: "1000000000000") - ) - XCTAssertThrowsError( - try Int32(parameter: "fish") - ) - XCTAssertThrowsError( - try Int64(parameter: "1000000000000000000000") - ) - XCTAssertThrowsError( - try Int64(parameter: "fish") - ) - } - - func testUIntConversion() { - XCTAssertEqual( - try UInt8(parameter: "100"), - 100 - ) - XCTAssertEqual( - try UInt16(parameter: "10000"), - 10000 - ) - XCTAssertEqual( - try UInt32(parameter: "1000000000"), - 1000000000 - ) - XCTAssertEqual( - try UInt64(parameter: "1000000000000"), - 1000000000000 - ) - - XCTAssertThrowsError( - try UInt8(parameter: "1000") - ) - XCTAssertThrowsError( - try UInt8(parameter: "fish") - ) - XCTAssertThrowsError( - try UInt16(parameter: "1000000000") - ) - XCTAssertThrowsError( - try UInt16(parameter: "fish") - ) - XCTAssertThrowsError( - try UInt32(parameter: "1000000000000") - ) - XCTAssertThrowsError( - try UInt32(parameter: "fish") - ) - XCTAssertThrowsError( - try UInt64(parameter: "1000000000000000000000") - ) - XCTAssertThrowsError( - try UInt64(parameter: "fish") - ) - } - - func testDoubleConversion() { - XCTAssertEqual( - try Double(parameter: "10.5"), - 10.5 - ) - XCTAssertEqual( - try Double(parameter: "-10.5"), - -10.5 - ) - - XCTAssertThrowsError( - try Double(parameter: "fish") - ) - } - - func testFloatConversion() { - XCTAssertEqual( - try Float(parameter: "10.5"), - 10.5 - ) - XCTAssertEqual( - try Float(parameter: "-10.5"), - -10.5 - ) - - XCTAssertThrowsError( - try Float(parameter: "fish") - ) - } - - func testFloat32Conversion() { - XCTAssertEqual( - try Float32(parameter: "10.5"), - 10.5 - ) - XCTAssertEqual( - try Float32(parameter: "-10.5"), - -10.5 - ) - - XCTAssertThrowsError( - try Float32(parameter: "fish") - ) - } - - func testEnumConversion() { - enum Food: String, HTTPRouteParameterValue { - case fish - case chips - } - - XCTAssertEqual( - try Food(parameter: "fish"), - .fish - ) - XCTAssertEqual( - try Food(parameter: "chips"), - .chips - ) - - XCTAssertThrowsError( - try Food(parameter: "10") - ) - } -} - -private enum Food: String, HTTPRouteParameterValue { - case fish - case chips -} diff --git a/FlyingFox/XCTests/HTTPRouteTests.swift b/FlyingFox/XCTests/HTTPRouteTests.swift deleted file mode 100644 index 7167987f..00000000 --- a/FlyingFox/XCTests/HTTPRouteTests.swift +++ /dev/null @@ -1,559 +0,0 @@ -// -// HTTPRouteTests.swift -// FlyingFox -// -// Created by Simon Whitty on 13/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class HTTPRouteTests: XCTestCase { - - func testMethodAndPathWithQuery() { - let route = HTTPRoute(method: .PUT, path: "/quick/brown/fox?eats=chips") - - XCTAssertEqual( - route.methods, [.PUT] - ) - XCTAssertEqual( - route.path, - [.caseInsensitive("quick"), .caseInsensitive("brown"), .caseInsensitive("fox")] - ) - XCTAssertEqual( - route.query, - [.init(name: "eats", value: .caseInsensitive("chips"))] - ) - } - - func testPathComponents() { - XCTAssertEqual( - HTTPRoute("hello/world").path, - [.caseInsensitive("hello"), .caseInsensitive("world")] - ) - - XCTAssertEqual( - HTTPRoute("hello/*").path, - [.caseInsensitive("hello"), .wildcard] - ) - } - - func testPercentEncodedPathComponents() { - XCTAssertEqual( - HTTPRoute("GET /hello world").path, - [.caseInsensitive("hello world")] - ) - - XCTAssertEqual( - HTTPRoute("/hello%20world").path, - [.caseInsensitive("hello world")] - ) - - XCTAssertEqual( - HTTPRoute("🐡/*").path, - [.caseInsensitive("🐡"), .wildcard] - ) - - XCTAssertEqual( - HTTPRoute("%F0%9F%90%A1/*").path, - [.caseInsensitive("🐡"), .wildcard] - ) - } - - func testPercentEncodedQueryItems() { - XCTAssertEqual( - HTTPRoute("/?fish=%F0%9F%90%9F").query, - [.init(name: "fish", value: .caseInsensitive("🐟"))] - ) - XCTAssertEqual( - HTTPRoute("/?%F0%9F%90%A1=chips").query, - [.init(name: "🐡", value: .caseInsensitive("chips"))] - ) - } - - func testMethod() { - XCTAssertEqual( - HTTPRoute("hello/world").methods, - HTTPMethod.allMethods - ) - - XCTAssertTrue( - HTTPRoute("GET hello").methods.contains(.GET) - ) - - XCTAssertEqual( - HTTPRoute("GET,POST hello").methods, - [.GET, .POST] - ) - - XCTAssertFalse( - HTTPRoute("GET,POST hello").methods.contains(.PUT) - ) - } - - func testWildcard_MatchesPath() async { - let route = HTTPRoute("/fish/*") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(path: "/fish/chips") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(path: "/fish/chips/mushy/peas") - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(path: "/chips") - ) - } - - func testMethod_Matches() async { - let route = HTTPRoute("POST /fish/chips") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .POST, path: "/fish/chips") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .init(rawValue: "post"), path: "/fish/chips/") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: "post", path: "/fish/chips/") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: "POST", path: "/fish/chips/") - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, path: "/fish/chips") - ) - } - - func testWildcardMethod_Matches() async { - let route = HTTPRoute("/fish/chips") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .POST, path: "/fish/chips") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, path: "/fish/chips/") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .init("GET"), path: "/fish/chips") - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, path: "/chips/") - ) - } - - func testWildcardMethod_MatchesRoute() async { - let route = HTTPRoute("GET /mock") - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: HTTPMethod("GET"), - path: "/") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: HTTPMethod("GET"), - path: "/mock") - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: HTTPMethod("GET"), - path: "/mock/fish") - ) - } - - func testEmptyWildcard_MatchesAllRoutes() async { - let route = HTTPRoute("*") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: HTTPMethod("GET"), - path: "/") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: HTTPMethod("GET"), - path: "/mock") - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: HTTPMethod("GET"), - path: "/mock/fish") - ) - } - - func testQueryItem_MatchesRoute() async { - let route = HTTPRoute("GET /mock?fish=chips") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "fish", value: "chips")]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cat", value: "dog"), - .init(name: "fish", value: "squid"), - .init(name: "fish", value: "chips")]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cat", value: "dog"), - .init(name: "fish", value: "squid")]) - ) - } - - func testMultipleQueryItems_MatchesRoute() async { - let route = HTTPRoute("GET /mock?fish=chips&cats=dogs") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "fish", value: "chips"), - .init(name: "cats", value: "dogs")]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cats", value: "dogs"), - .init(name: "fish", value: "chips")]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "fish", value: "chips")]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cats", value: "dogs")]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cat", value: "dog"), - .init(name: "fish", value: "squid")]) - ) - } - - func testQueryItemWildcard_MatchesRoute() async { - let route = HTTPRoute("GET /mock?fish=*") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "fish", value: "chips")]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cat", value: "dog"), - .init(name: "fish", value: "squid"), - .init(name: "fish", value: "chips")]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cat", value: "dog"), - .init(name: "fish", value: "squid")]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - query: [.init(name: "cat", value: "dog")]) - ) - } - - func testWildcardPathWithQueryItem_MatchesRoute() async { - let route = HTTPRoute("/mock/*?fish=*") - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock/anemone", - query: [.init(name: "fish", value: "chips")]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .POST, - path: "/mock/crabs", - query: [.init(name: "fish", value: "shrimp")]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock/anemone") - ) - } - - func testHeader_MatchesRoute() async { - let route = HTTPRoute("GET /mock", headers: [.contentEncoding: "json"]) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentEncoding: "json"]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentEncoding: "xml, json"]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentEncoding: "json", - .contentType: "xml"]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentEncoding: "xml"]) - ) - } - - func testMultipleHeaders_MatchesRoute() async { - let route = HTTPRoute("GET /mock", headers: [.host: "fish", - .contentType: "json"]) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.host: "fish", - .contentType: "json"]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentType: "json", - .host: "fish"]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.host: "fish"]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentType: "json"]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentType: "xml", - .host: "fish"]) - ) - } - - func testHeaderWildcard_MatchesRoute() async { - let route = HTTPRoute("GET /mock", headers: [.authorization: "*"]) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.authorization: "Bearer abc"]) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.authorization: "Bearer xyz"]) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(method: .GET, - path: "/mock", - headers: [.contentType: "xml"]) - ) - } - -#if canImport(Darwin) - func testBody_MatchesRoute() async { - let route = HTTPRoute("GET /mock", body: .json(where: "food == 'fish'")) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(path: "/mock", - body: #"{"age": 45, "food": "fish"}"#.data(using: .utf8)!) - ) - - await AsyncAssertTrue( - await route ~= HTTPRequest.make(path: "/mock", - body: #"{"food": "fish"}"#.data(using: .utf8)!) - ) - - await AsyncAssertFalse( - await route ~= HTTPRequest.make(path: "/mock", - body: #"{"age": 45}"#.data(using: .utf8)!) - ) - } -#endif - - func testDeprecated_Method() { - XCTAssertEqual(HTTPRoute("/fish/*").method, .wildcard) - XCTAssertEqual(HTTPRoute("GET /fish/*").method, .caseInsensitive("GET")) - XCTAssertEqual(HTTPRoute("PUT /fish/*").method, .caseInsensitive("PUT")) - XCTAssertEqual(HTTPRoute("GET,PUT /fish/*").method, .caseInsensitive("GET")) - } - - func testRouteParameters() { - let route = HTTPRoute("GET /mock/:id") - let parameters = route.parameters - XCTAssertEqual(parameters.count, 1) - XCTAssertEqual(parameters["id"], .path(name: "id", index: 1)) // Position 1 in the components array - - let route2 = HTTPRoute("GET /mock/:id/:bloop/hello/guys/:zonk") - let parameters2 = route2.parameters - XCTAssertEqual(parameters2.count, 3) - XCTAssertEqual(parameters2["id"], .path(name: "id", index: 1)) - XCTAssertEqual(parameters2["bloop"], .path(name: "bloop", index: 2)) - XCTAssertEqual(parameters2["zonk"], .path(name: "zonk", index: 5)) - - let route3 = HTTPRoute("GET /mock/:id/not:bloop/hello/guys/:zonk") - let parameters3 = route3.parameters - XCTAssertEqual(parameters3.count, 2) - XCTAssertEqual(parameters3["id"], .path(name: "id", index: 1)) - XCTAssertNil(parameters3["bloop"]) - XCTAssertEqual(parameters2["zonk"], .path(name: "zonk", index: 5)) - - let route4 = HTTPRoute("GET /mock/:id?food=:fish") - let parameters4 = route4.parameters - XCTAssertEqual(parameters4.count, 2) - XCTAssertEqual(parameters4["id"], .path(name: "id", index: 1)) - XCTAssertEqual(parameters4["fish"], .query(name: "fish", index: "food")) - } - - func testRouteParameterValues() { - let route = HTTPRoute("GET /mock/:id?foo=:foo&bar=:bar") - - XCTAssertEqual( - route.extractParameters(from: .make("/mock/15?foo=🐟&bar=🍤")), - [ - .init(name: "id", value: "15"), - .init(name: "foo", value: "🐟"), - .init(name: "bar", value: "🍤") - ] - ) - - XCTAssertEqual( - route.extractParameters(from: .make("/mock/99?bar=🐠&foo=🍟")), - [ - .init(name: "id", value: "99"), - .init(name: "foo", value: "🍟"), - .init(name: "bar", value: "🐠") - ] - ) - - XCTAssertEqual( - route.extractParameters(from: .make("/mock?bar=🐠")), - [ - .init(name: "bar", value: "🐠") - ] - ) - } - - func testRouteParameterValuesA() { - let route = HTTPRoute("GET /:foo/:bar") - enum Beast: String, HTTPRouteParameterValue { - case fish - } - - XCTAssertEqual( - route.extractParameters(from: .make("/10/fish"))["foo"], - 10 - ) - XCTAssertEqual( - route.extractParameters(from: .make("/20/fish"))["bar"], - "fish" - ) - XCTAssertEqual( - route.extractParameters(from: .make("/20/fish"))["bar"], - Beast.fish - ) - } - - func testPathParameters() { - // given - let route = HTTPRoute("GET /mock/:id/hello/:zonk") - let request = HTTPRequest.make(path: "/mock/12/hello/fish") - - XCTAssertTrue( - try route.extractParameterValues(from: request) == (12, "fish") - ) - - XCTAssertTrue( - try route.extractParameterValues(from: request) == (12) - ) - - XCTAssertThrowsError( - try route.extractParameterValues(of: (Int, Int).self, from: request) - ) - - XCTAssertThrowsError( - try route.extractParameterValues(of: (Int, String, String).self, from: request) - ) - } - - func testDescription() { - XCTAssertEqual( - HTTPRoute("GET /mock/:id/hello/:zonk").description, - "GET /mock/:id/hello/:zonk" - ) - XCTAssertEqual( - HTTPRoute("/mock/*").description, - "/mock/*" - ) - XCTAssertEqual( - HTTPRoute("FUZZ,TRACE,GET /mock?hello=*").description, - "GET,TRACE,FUZZ /mock?hello=*" - ) - } -} diff --git a/FlyingFox/XCTests/HTTPServerTests.swift b/FlyingFox/XCTests/HTTPServerTests.swift deleted file mode 100644 index 9bde24b8..00000000 --- a/FlyingFox/XCTests/HTTPServerTests.swift +++ /dev/null @@ -1,559 +0,0 @@ -// -// HTTPServerTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -@testable import FlyingSocks -import XCTest -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -final class HTTPServerTests: XCTestCase { - - private var stopServer: HTTPServer? - - func startServerWithPort(_ server: HTTPServer, preferConnectionsDiscarding: Bool = true) async throws -> UInt16 { - self.stopServer = server - Task { - try await HTTPServer.$preferConnectionsDiscarding.withValue(preferConnectionsDiscarding) { - try await server.run() - } - } - return try await server.waitForListeningPort() - } - - @discardableResult - func startServer(_ server: HTTPServer) async throws -> Task { - self.stopServer = server - let task = Task { try await server.run() } - try await server.waitUntilListening() - return task - } - - override func tearDown() async throws { - await stopServer?.stop(timeout: 0) - } - - func testThrowsError_WhenAlreadyStarted() async throws { - let server = HTTPServer.make() - try await startServer(server) - - await AsyncAssertThrowsError(try await server.run(), of: SocketError.self) { - XCTAssertEqual($0, .unsupportedAddress) - } - } - - func testWaitsUntilListening() async throws { - let server = HTTPServer.make() - let task = Task { try await server.waitUntilListening() } - try await Task.sleep(seconds: 0.1) - - try await startServer(server) - - await AsyncAssertNoThrow( - try await task.result.get() - ) - } - - func testThrowsError_WhenSocketAlreadyListening() async throws { - let server = HTTPServer.make(port: 42185) - let socket = try await server.makeSocketAndListen() - defer { try! socket.close() } - - await AsyncAssertThrowsError(try await server.run(), of: SocketError.self) { - XCTAssertTrue( - $0.errorDescription?.contains("Address already in use") == true - ) - } - } - - func testRestarts_AfterStopped() async throws { - let server = HTTPServer.make() - try await startServer(server) - await server.stop() - - await AsyncAssertNoThrow( - try await startServer(server) - ) - } - - func testTaskCanBeCancelled() async throws { - let server = HTTPServer.make() - let task = try await startServer(server) - - task.cancel() - - await AsyncAssertThrowsError(try await task.value) - } - - func testTaskCanBeCancelled_AfterServerIsStopped() async throws { - let server = HTTPServer.make() - let task = try await startServer(server) - - await server.stop() - task.cancel() - - await AsyncAssertThrowsError(try await task.value) - } - - func testRequests_AreMatchedToHandlers_ViaRoute() async throws { - let server = HTTPServer.make() - - await server.appendRoute("/accepted") { _ in - HTTPResponse.make(statusCode: .accepted) - } - await server.appendRoute("/gone") { _ in - HTTPResponse.make(statusCode: .gone) - } - - var response = await server.handleRequest(.make(method: .GET, path: "/accepted")) - XCTAssertEqual( - response.statusCode, - .accepted - ) - - response = await server.handleRequest(.make(method: .GET, path: "/gone")) - XCTAssertEqual( - response.statusCode, - .gone - ) - } - - func testUnmatchedRequests_Return404() async throws { - let server = HTTPServer.make() - - let response = await server.handleRequest(.make(method: .GET, path: "/accepted")) - XCTAssertEqual( - response.statusCode, - .notFound - ) - } - - func testConnections_AreHandled_DiscardingTaskGroup() async throws { - let server = HTTPServer.make() - let port = try await startServerWithPort(server, preferConnectionsDiscarding: true) - - let request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)")!) - let (_, response) = try await URLSession.shared.data(for: request) - - XCTAssertEqual( - (response as? HTTPURLResponse)?.statusCode, - 404 - ) - } - - func testConnections_AreHandled_FallbackTaskGroup() async throws { - let server = HTTPServer.make() - let port = try await startServerWithPort(server, preferConnectionsDiscarding: false) - - let request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)")!) - let (_, response) = try await URLSession.shared.data(for: request) - - XCTAssertEqual( - (response as? HTTPURLResponse)?.statusCode, - 404 - ) - } - - func testHandlerErrors_Return500() async throws { - let server = HTTPServer.make() { _ in - throw SocketError.disconnected - } - - let response = await server.handleRequest(.make(method: .GET, path: "/accepted")) - XCTAssertEqual( - response.statusCode, - .internalServerError - ) - } - - func testHandlerTimeout_Returns500() async throws { - let server = HTTPServer.make(timeout: 0.1) { _ in - try await Task.sleep(seconds: 1) - return HTTPResponse.make(statusCode: .accepted) - } - - let response = await server.handleRequest(.make(method: .GET, path: "/accepted")) - XCTAssertEqual( - response.statusCode, - .internalServerError - ) - } - - func testKeepAlive_IsAddedToResponses() async throws { - let server = HTTPServer.make() - - var response = await server.handleRequest( - .make(method: .GET, path: "/accepted", headers: [.connection: "keep-alive"]) - ) - XCTAssertTrue( - response.shouldKeepAlive - ) - - response = await server.handleRequest( - .make(method: .GET, path: "/accepted") - ) - XCTAssertFalse( - response.shouldKeepAlive - ) - } - -#if canImport(Darwin) - func testServer_ReturnsWebSocketFramesToURLSession() async throws { - try await Task.sleep(seconds: 0.3) - let server = HTTPServer.make(address: .loopback(port: 0)) - await server.appendRoute("GET /socket", to: .webSocket(EchoWSMessageHandler())) - let port = try await startServerWithPort(server) - - let wsTask = URLSession.shared.webSocketTask(with: URL(string: "ws://localhost:\(port)/socket")!) - wsTask.resume() - try await wsTask.send(.string("Hello")) - await AsyncAssertEqual(try await wsTask.receive(), .string("Hello")) - } -#endif - - func testServer_ReturnsWebSocketFrames() async throws { - let address = Socket.makeAddressUnix(path: "foxing") - try? Socket.unlink(address) - let server = HTTPServer.make(address: address) - await server.appendRoute("GET /socket", to: .webSocket(EchoWSMessageHandler())) - try await startServer(server) - - let socket = try await AsyncSocket.connected(to: address) - defer { try? socket.close() } - - var request = HTTPRequest.make(path: "/socket") - request.headers[.host] = "localhost" - request.headers[.upgrade] = "websocket" - request.headers[.connection] = "Keep-Alive, Upgrade" - request.headers[.webSocketVersion] = "13" - request.headers[.webSocketKey] = "ABCDEFGHIJKLMNOP".data(using: .utf8)!.base64EncodedString() - try await socket.writeRequest(request) - - let response = try await socket.readResponse() - XCTAssertEqual(response.headers[.webSocketAccept], "9twnCz4Oi2Q3EuDqLAETCuip07c=") - XCTAssertEqual(response.headers[.connection], "upgrade") - XCTAssertEqual(response.headers[.upgrade], "websocket") - - let frame = WSFrame.make(fin: true, opcode: .text, mask: .mock, payload: "FlyingFox".data(using: .utf8)!) - try await socket.writeFrame(frame) - - await AsyncAssertEqual( - try await socket.readFrame(), - WSFrame(fin: true, opcode: .text, mask: nil, payload: "FlyingFox".data(using: .utf8)!) - ) - } - -#if canImport(Darwin) - func testDefaultLogger_IsOSLog() async throws { - if #available(iOS 14.0, tvOS 14.0, *) { - XCTAssertTrue(HTTPServer.defaultLogger() is OSLogHTTPLogging) - } - } -#endif - - func testServer_StartsOnUnixSocket() async throws { - let address = sockaddr_un.unix(path: #function) - try? Socket.unlink(address) - let server = HTTPServer.make(address: address) - await server.appendRoute("*") { _ in - return HTTPResponse.make(statusCode: .accepted) - } - try await startServer(server) - - let socket = try await AsyncSocket.connected(to: address) - defer { try? socket.close() } - try await socket.writeRequest(.make()) - - await AsyncAssertEqual( - try await socket.readResponse().statusCode, - .accepted - ) - } - - func testServer_StartsOnIP4Socket() async throws { - let server = HTTPServer.make(address: .inet(port: 0)) - await server.appendRoute("*") { _ in - return HTTPResponse.make(statusCode: .accepted) - } - let port = try await startServerWithPort(server) - - let socket = try await AsyncSocket.connected(to: .inet(ip4: "127.0.0.1", port: port)) - defer { try? socket.close() } - - try await socket.writeRequest(.make()) - - await AsyncAssertEqual( - try await socket.readResponse().statusCode, - .accepted - ) - } - - func testServer_AllowsExistingConnectionsToDisconnect_WhenStopped() async throws { - let server = HTTPServer.make() - await server.appendRoute("*") { _ in - try await Task.sleep(seconds: 0.5) - return .make(statusCode: .ok) - } - let port = try await startServerWithPort(server) - let socket = try await AsyncSocket.connected(to: .inet(ip4: "127.0.0.1", port: port)) - defer { try? socket.close() } - - try await socket.writeRequest(.make()) - try await Task.sleep(seconds: 0.1) - let taskStop = Task { await server.stop(timeout: 1) } - - await AsyncAssertEqual( - try await socket.readResponse().statusCode, - .ok - ) - await taskStop.value - } - - func testServer_DisconnectsWaitingRequests_WhenStopped() async throws { - let server = HTTPServer.make() - - let port = try await startServerWithPort(server) - let socket = try await AsyncSocket.connected(to: .inet(ip4: "127.0.0.1", port: port)) - defer { try? socket.close() } - - try await Task.sleep(seconds: 0.1) - await AsyncAssertEqual(await server.connections.count, 1) - - let taskStop = Task { await server.stop(timeout: 1) } - try await Task.sleep(seconds: 0.5) - - await AsyncAssertEqual(await server.connections.count, 0) - await taskStop.value - } - - func testServer_Returns500_WhenHandlerTimesout() async throws { - let server = HTTPServer.make(timeout: 0.5) - await server.appendRoute("*") { _ in - try await Task.sleep(seconds: 5) - return .make(statusCode: .ok) - } - let port = try await startServerWithPort(server) - - let request = URLRequest(url: URL(string: "http://127.0.0.1:\(port)")!) - let (_, response) = try await URLSession.shared.data(for: request) - - XCTAssertEqual( - (response as? HTTPURLResponse)?.statusCode, - 500 - ) - } - - func testServer_ListeningAddress_IP6() async throws { - let server = HTTPServer.make(address: .inet6(port: 5191)) - try await startServer(server) - await AsyncAssertEqual( - await server.listeningAddress, - .ip6("::", port: 5191) - ) - } - -#if canImport(Darwin) - // docker containers don't like loopback - func testServer_ListeningAddress_Loopback() async throws { - let server = HTTPServer.make(address: .loopback(port: 0)) - try await startServer(server) - await AsyncAssertNotNil( - await server.listeningAddress - ) - } -#endif - - func testDefaultLoggerFallback_IsPrintLogger() async throws { - XCTAssertTrue(HTTPServer.defaultLogger(forceFallback: true) is PrintLogger) - } - - func testWaitUntilListing_WaitsUntil_SocketIsListening() async { - let server = HTTPServer.make() - - let waiting = Task { - try await server.waitUntilListening() - return true - } - - Task { try await server.run() } - self.stopServer = server - - await AsyncAssertEqual(try await waiting.value, true) - } - - func testWaitUntilListing_ThrowsWhen_TaskIsCancelled() async { - let server = HTTPServer.make() - - let waiting = Task { - try await server.waitUntilListening() - return true - } - - waiting.cancel() - await AsyncAssertThrowsError(try await waiting.value, of: CancellationError.self) - } - - func testWaitUntilListing_ThrowsWhen_TimeoutExpires() async throws { - let server = HTTPServer.make() - - let waiting = Task { - try await server.waitUntilListening(timeout: 0.1) - return true - } - - await AsyncAssertThrowsError(try await waiting.value, of: (any Error).self) - } - - func testRoutes_To_ParamaterPackWithRequest() async throws { - let server = HTTPServer.make() - await server.appendRoute("/fish/:id") { (request: HTTPRequest, id: String) in - HTTPResponse.make(statusCode: .ok, body: "Hello \(id)".data(using: .utf8)!) - } - await server.appendRoute("/chips/:id") { (id: String) in - HTTPResponse.make(statusCode: .ok, body: "Hello \(id)".data(using: .utf8)!) - } - let port = try await startServerWithPort(server) - - let socket = try await AsyncSocket.connected(to: .inet(ip4: "127.0.0.1", port: port)) - defer { try? socket.close() } - - try await socket.writeRequest(.make("/fish/🐟", headers: [.connection: "keep-alive"])) - - await AsyncAssertEqual( - try await socket.readResponse().bodyString, - "Hello 🐟" - ) - - try await socket.writeRequest(.make("/chips/🍟")) - - await AsyncAssertEqual( - try await socket.readResponse().bodyString, - "Hello 🍟" - ) - } - - func testRequests_IncludeRemoteAddress() async throws { - let server = HTTPServer.make() - - await server.appendRoute("/echo") { req in - HTTPResponse.make(statusCode: .ok, body: (req.remoteIPAddress ?? "nil").data(using: .utf8)!) - } - - let port = try await startServerWithPort(server) - - let socket = try await AsyncSocket.connected(to: .inet(ip4: "127.0.0.1", port: port)) - defer { try? socket.close() } - - try await socket.writeRequest(.make("/echo")) - await AsyncAssertFalse( - try await socket.readResponse().bodyString == "nil" - ) - } -} - -extension HTTPServer { - - static func make(address: some SocketAddress, - timeout: TimeInterval = 15, - logger: any Logging = .disabled, - handler: (any HTTPHandler)? = nil) -> HTTPServer { - HTTPServer(address: address, - timeout: timeout, - logger: logger, - handler: handler) - } - - static func make(port: UInt16 = 0, - timeout: TimeInterval = 15, - logger: some Logging = .disabled, - handler: (any HTTPHandler)? = nil) -> HTTPServer { - HTTPServer(port: port, - timeout: timeout, - logger: logger, - handler: handler) - } - - static func make(port: UInt16 = 0, - timeout: TimeInterval = 15, - logger: some Logging = .disabled, - handler: @Sendable @escaping (HTTPRequest) async throws -> HTTPResponse) -> HTTPServer { - HTTPServer(port: port, - timeout: timeout, - logger: logger, - handler: handler) - } - - func waitForListeningPort(timeout: TimeInterval = 3) async throws -> UInt16 { - try await waitUntilListening(timeout: timeout) - switch listeningAddress { - case let .ip4(_, port: port), - let .ip6(_, port: port): - return port - default: - throw Error.noPort - } - } - - private enum Error: Swift.Error { - case noPort - } -} - - - -#if canImport(Darwin) -extension URLSessionWebSocketTask.Message { - public static func == (lhs: URLSessionWebSocketTask.Message, rhs: URLSessionWebSocketTask.Message) -> Bool { - switch (lhs, rhs) { - case (.string(let lval), .string(let rval)): - return lval == rval - case (.data(let lval), .data(let rval)): - return lval == rval - default: - return false - } - } -} -#if compiler(>=6) -extension URLSessionWebSocketTask.Message: @retroactive Equatable { } -#else -extension URLSessionWebSocketTask.Message: Equatable { } -#endif -#endif - -extension Task where Success == Never, Failure == Never { - static func sleep(seconds: TimeInterval) async throws { - try await sleep(nanoseconds: UInt64(1_000_000_000 * seconds)) - } -} - diff --git a/FlyingFox/XCTests/HTTPStatusCodeTests.swift b/FlyingFox/XCTests/HTTPStatusCodeTests.swift deleted file mode 100644 index 097a6732..00000000 --- a/FlyingFox/XCTests/HTTPStatusCodeTests.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// HTTPStatusCodeTests.swift -// FlyingFox -// -// Created by Andre Jacobs on 17/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import XCTest -import FlyingFox - -final class HTTPStatusCodeTests: XCTestCase { - - func test1xxStatusCodes() throws { - XCTAssertEqual(HTTPStatusCode.continue, HTTPStatusCode(100, phrase: "Continue")) - XCTAssertEqual(HTTPStatusCode.switchingProtocols, HTTPStatusCode(101, phrase: "Switching Protocols")) - XCTAssertEqual(HTTPStatusCode.earlyHints, HTTPStatusCode(103, phrase: "Early Hints")) - } - - func test2xxStatusCodes() throws { - XCTAssertEqual(HTTPStatusCode.ok, HTTPStatusCode(200, phrase: "OK")) - XCTAssertEqual(HTTPStatusCode.created, HTTPStatusCode(201, phrase: "Created")) - XCTAssertEqual(HTTPStatusCode.accepted, HTTPStatusCode(202, phrase: "Accepted")) - XCTAssertEqual(HTTPStatusCode.nonAuthoritativeInformation, HTTPStatusCode(203, phrase: "Non-Authoritative Information")) - XCTAssertEqual(HTTPStatusCode.noContent, HTTPStatusCode(204, phrase: "No Content")) - XCTAssertEqual(HTTPStatusCode.resetContent, HTTPStatusCode(205, phrase: "Reset Content")) - XCTAssertEqual(HTTPStatusCode.partialContent, HTTPStatusCode(206, phrase: "Partial Content")) - } - - func test3xxStatusCodes() throws { - XCTAssertEqual(HTTPStatusCode.multipleChoice, HTTPStatusCode(300, phrase: "Multiple Choice")) - XCTAssertEqual(HTTPStatusCode.movedPermanently, HTTPStatusCode(301, phrase: "Moved Permanently")) - XCTAssertEqual(HTTPStatusCode.found, HTTPStatusCode(302, phrase: "Found")) - XCTAssertEqual(HTTPStatusCode.seeOther, HTTPStatusCode(303, phrase: "See Other")) - XCTAssertEqual(HTTPStatusCode.notModified, HTTPStatusCode(304, phrase: "Not Modified")) - XCTAssertEqual(HTTPStatusCode.useProxy, HTTPStatusCode(305, phrase: "Use Proxy")) - XCTAssertEqual(HTTPStatusCode.unused, HTTPStatusCode(306, phrase: "unused")) - XCTAssertEqual(HTTPStatusCode.temporaryRedirect, HTTPStatusCode(307, phrase: "Temporary Redirect")) - XCTAssertEqual(HTTPStatusCode.permanentRedirect, HTTPStatusCode(308, phrase: "Permanent Redirect")) - } - - func test4xxStatusCodes() throws { - XCTAssertEqual(HTTPStatusCode.badRequest, HTTPStatusCode(400, phrase: "Bad Request")) - XCTAssertEqual(HTTPStatusCode.unauthorized, HTTPStatusCode(401, phrase: "Unauthorized")) - XCTAssertEqual(HTTPStatusCode.paymentRequired, HTTPStatusCode(402, phrase: "Payment Required")) - XCTAssertEqual(HTTPStatusCode.forbidden, HTTPStatusCode(403, phrase: "Forbidden")) - XCTAssertEqual(HTTPStatusCode.notFound, HTTPStatusCode(404, phrase: "Not Found")) - XCTAssertEqual(HTTPStatusCode.methodNotAllowed, HTTPStatusCode(405, phrase: "Method Not Allowed")) - XCTAssertEqual(HTTPStatusCode.notAcceptable, HTTPStatusCode(406, phrase: "Not Acceptable")) - XCTAssertEqual(HTTPStatusCode.proxyAuthenticationRequired, HTTPStatusCode(407, phrase: "Proxy Authentication Required")) - XCTAssertEqual(HTTPStatusCode.requestTimeout, HTTPStatusCode(408, phrase: "Request Timeout")) - XCTAssertEqual(HTTPStatusCode.conflict, HTTPStatusCode(409, phrase: "Conflict")) - XCTAssertEqual(HTTPStatusCode.gone, HTTPStatusCode(410, phrase: "Gone")) - XCTAssertEqual(HTTPStatusCode.lengthRequired, HTTPStatusCode(411, phrase: "Length Required")) - XCTAssertEqual(HTTPStatusCode.preconditionFailed, HTTPStatusCode(412, phrase: "Precondition Failed")) - XCTAssertEqual(HTTPStatusCode.payloadTooLarge, HTTPStatusCode(413, phrase: "Payload Too Large")) - XCTAssertEqual(HTTPStatusCode.uriTooLong, HTTPStatusCode(414, phrase: "URI Too Long")) - XCTAssertEqual(HTTPStatusCode.unsupportedMediaType, HTTPStatusCode(415, phrase: "Unsupported Media Type")) - XCTAssertEqual(HTTPStatusCode.rangeNotSatisfiable, HTTPStatusCode(416, phrase: "Range Not Satisfiable")) - XCTAssertEqual(HTTPStatusCode.expectationFailed, HTTPStatusCode(417, phrase: "Expectation Failed")) - XCTAssertEqual(HTTPStatusCode.teapot, HTTPStatusCode(418, phrase: "I'm a teapot")) - XCTAssertEqual(HTTPStatusCode.misdirectedRequest, HTTPStatusCode(421, phrase: "Misdirected Request")) - XCTAssertEqual(HTTPStatusCode.unprocessableContent, HTTPStatusCode(422, phrase: "Unprocessable Content")) - XCTAssertEqual(HTTPStatusCode.locked, HTTPStatusCode(423, phrase: "Locked")) - XCTAssertEqual(HTTPStatusCode.failedDependency, HTTPStatusCode(424, phrase: "Failed Dependency")) - XCTAssertEqual(HTTPStatusCode.tooEarly, HTTPStatusCode(425, phrase: "Too Early")) - XCTAssertEqual(HTTPStatusCode.upgradeRequired, HTTPStatusCode(426, phrase: "Upgrade Required")) - XCTAssertEqual(HTTPStatusCode.preconditionRequired, HTTPStatusCode(428, phrase: "Precondition Required")) - XCTAssertEqual(HTTPStatusCode.tooManyRequests, HTTPStatusCode(429, phrase: "Too Many Requests")) - XCTAssertEqual(HTTPStatusCode.requestHeaderFieldsTooLarge, HTTPStatusCode(431, phrase: "Request Header Fields Too Large")) - XCTAssertEqual(HTTPStatusCode.unavailableForLegalReasons, HTTPStatusCode(451, phrase: "Unavailable For Legal Reasons")) - } - - func test5xxStatusCodes() throws { - XCTAssertEqual(HTTPStatusCode.internalServerError, HTTPStatusCode(500, phrase: "Internal Server Error")) - XCTAssertEqual(HTTPStatusCode.notImplemented, HTTPStatusCode(501, phrase: "Not Implemented")) - XCTAssertEqual(HTTPStatusCode.badGateway, HTTPStatusCode(502, phrase: "Bad Gateway")) - XCTAssertEqual(HTTPStatusCode.serviceUnavailable, HTTPStatusCode(503, phrase: "Service Unavailable")) - XCTAssertEqual(HTTPStatusCode.gatewayTimeout, HTTPStatusCode(504, phrase: "Gateway Timeout")) - XCTAssertEqual(HTTPStatusCode.httpVersionNotSupported, HTTPStatusCode(505, phrase: "HTTP Version Not Supported")) - XCTAssertEqual(HTTPStatusCode.variantAlsoNegotiates, HTTPStatusCode(506, phrase: "Variant Also Negotiates")) - XCTAssertEqual(HTTPStatusCode.notExtended, HTTPStatusCode(510, phrase: "Not Extended")) - XCTAssertEqual(HTTPStatusCode.networkAuthenticationRequired, HTTPStatusCode(511, phrase: "Network Authentication Required")) - } - -} diff --git a/FlyingFox/XCTests/Handlers/DirectoryHTTPHandlerTests.swift b/FlyingFox/XCTests/Handlers/DirectoryHTTPHandlerTests.swift deleted file mode 100644 index b7d83e5c..00000000 --- a/FlyingFox/XCTests/Handlers/DirectoryHTTPHandlerTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// DirectoryHTTPHandlerTests.swift -// FlyingFox -// -// Created by Simon Whitty on 5/03/2023. -// Copyright © 2023 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class DirectoryHTTPHandlerTests: XCTestCase { - - func testDirectoryHandler_ReturnsFile() async throws { - let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") - - let response = try await handler.handleRequest(.make(path: "server/path/fish.json")) - XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.headers[.contentType], "application/json") - await AsyncAssertEqual( - try await response.bodyData, - #"{"fish": "cakes"}"#.data(using: .utf8) - ) - } - - func testDirectoryHandler_PlainInitialiser_ReturnsFile() async throws { - let root = try XCTUnwrap(Bundle.module.url(forResource: "Stubs", withExtension: nil)) - let handler = DirectoryHTTPHandler(root: root, serverPath: "server/path") - - let response = try await handler.handleRequest(.make(path: "server/path/fish.json")) - XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.headers[.contentType], "application/json") - await AsyncAssertEqual( - try await response.bodyData, - #"{"fish": "cakes"}"#.data(using: .utf8) - ) - } - - func testDirectoryHandler_ReturnsSubDirectoryFile() async throws { - let handler: some HTTPHandler = .directory(for: .module, subPath: "Stubs", serverPath: "server/path") - - let response = try await handler.handleRequest(.make(path: "server/path/subdir/vinegar.json")) - XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.headers[.contentType], "application/json") - await AsyncAssertEqual( - try await response.bodyData, - #"{"type": "malt"}"#.data(using: .utf8) - ) - } - - func testDirectoryHandler_Returns404WhenFileDoesNotExist() async throws { - let handler: some HTTPHandler = .directory(for: .module, subPath: "Stubs", serverPath: "server/path") - - let response = try await handler.handleRequest(.make()) - XCTAssertEqual(response.statusCode, .notFound) - } - - func testDirectoryHandler_Returns404WhenRequestHasPathButFileDoesNotExist() async throws { - let handler = DirectoryHTTPHandler(bundle: .module, subPath: "Stubs", serverPath: "server/path") - - let response = try await handler.handleRequest(.make(path: "server/path/subdir/guitars.json")) - XCTAssertEqual(response.statusCode, .notFound) - } - - func testFileURL_isRelative_toServerPath() { - let handler = DirectoryHTTPHandler(root: URL(fileURLWithPath: "/temp"), - serverPath: "/sub/folder") - - XCTAssertEqual( - handler.makeFileURL(for: "/sub/folder/fish/chips"), - URL(fileURLWithPath: "/temp/fish/chips") - ) - - XCTAssertNil( - handler.makeFileURL(for: "/sub/file/fish/chips") - ) - } - - func testServerPath_doesNotRequier_leadingSlash() { - let handler = DirectoryHTTPHandler(root: URL(fileURLWithPath: "/temp"), - serverPath: "sub/folder") - - XCTAssertEqual( - handler.makeFileURL(for: "/sub/folder/fish/chips"), - URL(fileURLWithPath: "/temp/fish/chips") - ) - } - - func testServerPath_doesNotRequire_trailingSlash() { - let handler = DirectoryHTTPHandler(root: URL(fileURLWithPath: "/temp/file"), - serverPath: "sub") - - XCTAssertEqual( - handler.makeFileURL(for: "sub/a"), - URL(fileURLWithPath: "/temp/file/a") - ) - } - - func testServerPath_IsNotRequired() { - let handler = DirectoryHTTPHandler(root: URL(fileURLWithPath: "/temp/file")) - - XCTAssertEqual( - handler.makeFileURL(for: "sub/a"), - URL(fileURLWithPath: "/temp/file/sub/a") - ) - } - - func testServerPath_doesNotRequire_canBeEmpty() { - let handler = DirectoryHTTPHandler(root: URL(fileURLWithPath: "/temp/file"), - serverPath: "") - - XCTAssertEqual( - handler.makeFileURL(for: "sub/a"), - URL(fileURLWithPath: "/temp/file/sub/a") - ) - } -} diff --git a/FlyingFox/XCTests/Handlers/HTTPHandlerTests.swift b/FlyingFox/XCTests/Handlers/HTTPHandlerTests.swift deleted file mode 100644 index fbccc79f..00000000 --- a/FlyingFox/XCTests/Handlers/HTTPHandlerTests.swift +++ /dev/null @@ -1,274 +0,0 @@ -// -// HTTPHandlerTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class HTTPHandlerTests: XCTestCase { - - //MARK: - HTTPHandler - - func testUnhandledHandler_ThrowsError() async { - let handler: some HTTPHandler = .unhandled() - - await AsyncAssertThrowsError(try await handler.handleRequest(.make()), of: HTTPUnhandledError.self) - } - - //MARK: - RedirectHTTPHandler - - func testRedirectHandler_Returns301WithSuppliedLocation() async throws { - let handler = RedirectHTTPHandler.redirect(to: "http://fish.com/cakes") - - let response = try await handler.handleRequest(.make()) - XCTAssertEqual(response.statusCode, .movedPermanently) - XCTAssertEqual(response.headers[.location], "http://fish.com/cakes") - } - - func testRedirectHandler_ThrowsErrorWhenSuppliedLocationIsInvalid() async { - let handler = RedirectHTTPHandler(location: "http:// fish cakes") - await AsyncAssertThrowsError(try await handler.handleRequest(.make()), of: URLError.self) - } - - //MARK: - FileHTTPHandler - - func testFileHandler_init_path() { - let expectedURL = URL(string: "file://var/tmp/flyingfox.json")! - let expectedContentType = "application/json" - let handler = FileHTTPHandler(path: expectedURL, contentType: expectedContentType) - XCTAssertEqual(handler.path, expectedURL) - XCTAssertEqual(handler.contentType, expectedContentType) - } - - func testFileHandler_init_namedInBundle() throws { - let handler = FileHTTPHandler(named: "Stubs/fish.json", in: .module) - let path = try XCTUnwrap(handler.path?.absoluteString) - XCTAssertTrue(path.hasSuffix("fish.json")) - XCTAssertEqual(handler.contentType, "application/json") - } - - func testFileHandler_Returns200WithData() async throws { - let handler: some HTTPHandler = .file(named: "Stubs/fish.json", in: .module) - - let response = try await handler.handleRequest(.make()) - XCTAssertEqual(response.statusCode, .ok) - XCTAssertEqual(response.headers[.contentType], "application/json") - await AsyncAssertEqual( - try await response.bodyData, - #"{"fish": "cakes"}"#.data(using: .utf8) - ) - } - - func testFileHandler_ReturnsSuppliedContentType() async throws { - let handler = FileHTTPHandler(named: "Stubs/fish.json", in: .module, contentType: "chips") - - let response = try await handler.handleRequest(.make()) - XCTAssertEqual(response.headers[.contentType], "chips") - } - - func testFileHandler_Returns404WhenFileDoesNotExist() async throws { - let handler = FileHTTPHandler(named: "chips.json", in: .module) - - let response = try await handler.handleRequest(.make()) - XCTAssertEqual(response.statusCode, .notFound) - } - - func testFileHandler_Returns404WhenPathDoesNotExist() async throws { - let handler = FileHTTPHandler(path: URL(fileURLWithPath: "unknown"), contentType: "chips") - - let response = try await handler.handleRequest(.make()) - XCTAssertEqual(response.statusCode, .notFound) - } - - func testFileHandler_DetectsCorrectContentType() { - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.json"), - "application/json" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.html"), - "text/html" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.htm"), - "text/html" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.css"), - "text/css" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.js"), - "application/javascript" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.javascript"), - "application/javascript" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.png"), - "image/png" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.jpeg"), - "image/jpeg" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.jpg"), - "image/jpeg" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.pdf"), - "application/pdf" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.svg"), - "image/svg+xml" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.ico"), - "image/vnd.microsoft.icon" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.wasm"), - "application/wasm" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.webp"), - "image/webp" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.jp2"), - "image/jp2" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.m4v"), - "video/x-m4v" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.properties"), - "text/plain" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.xml"), - "application/xml" - ) - XCTAssertEqual( - FileHTTPHandler.makeContentType(for: "fish.somefile"), - "application/octet-stream" - ) - } - - //MARK: - ProxyHTTPHandler - - func disabled_testProxyHandler_ReturnsResponse() async throws { - let handler = ProxyHTTPHandler(base: "https://pie.dev", timeout: 2) - var response = try await handler.handleRequest(.make(method: .GET, - path: "/status/202", - query: [.init(name: "hello", value: "world")])) - XCTAssertEqual(response.statusCode.code, 202) - - response = try await handler.handleRequest(.make(method: .GET, path: "/status/200")) - XCTAssertEqual(response.statusCode.code, 200) - } - - func testProxyHandler_ThrowsErrorWhenBaseIsInvalid() async throws { - let handler = ProxyHTTPHandler(base: "http:// fish cakes") - await AsyncAssertThrowsError( - try await handler.handleRequest(.make()), - of: URLError.self - ) - } - - func testProxyHandler_MakesRequestWithQuery() async throws { - let handler = ProxyHTTPHandler(base: "fish.com") - - let request = try await handler.makeURLRequest( - for: .make(path: "/chips/squid", - query: [.init(name: "mushy", value: "peas")]) - ) - - XCTAssertEqual( - request.url, - URL(string: "fish.com/chips/squid?mushy=peas") - ) - } - - func testProxyHandler_MakesRequestWithHeaders() async throws { - let handler = ProxyHTTPHandler(base: "fish.com") - - let request = try await handler.makeURLRequest( - for: .make(headers: [.contentType: "json", - HTTPHeader("Fish"): "chips"]) - ) - - XCTAssertEqual( - request.allHTTPHeaderFields, - ["Content-Type": "json", - "Fish": "chips"] - ) - } - - func testProxyHandler_DoesNotFowardSomeHeaders() async throws { - let handler = ProxyHTTPHandler.proxy(via: "fish.com") - - let request = try await handler.makeURLRequest( - for: .make(headers: [.connection: "json", - .host: "fish.com", - .contentLength: "20"]) - ) - - XCTAssertNil( - request.allHTTPHeaderFields?["Host"] - ) - - XCTAssertNil( - request.allHTTPHeaderFields?["Connetion"] - ) - - XCTAssertNil( - request.allHTTPHeaderFields?["Content-Length"] - ) - } - - //MARK: - RoutedHTTPHandler - - func testRoutedHandler_CatchesUnhandledError() async throws { - var handler = RoutedHTTPHandler() - - handler.appendRoute("/hello", to: .unhandled()) - handler.appendRoute("/hello") { _ in - HTTPResponse(statusCode: .ok) - } - - let response = try await handler.handleRequest(.make(path: "/hello")) - XCTAssertEqual(response.statusCode, .ok) - } -} diff --git a/FlyingFox/XCTests/Handlers/RoutedHTTPHandlerTests.swift b/FlyingFox/XCTests/Handlers/RoutedHTTPHandlerTests.swift deleted file mode 100644 index 57d35724..00000000 --- a/FlyingFox/XCTests/Handlers/RoutedHTTPHandlerTests.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// RoutedHTTPHandler.swift -// FlyingFox -// -// Created by Simon Whitty on 12/04/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class RoutedHTTPHandlerTests: XCTestCase { - - func testRoutes_CanBeReplaced() async throws { - // given - var handler = RoutedHTTPHandler() - - // when - handler.insertRoute("GET /fish", at: 0, to: MockHandler()) - handler.insertRoute("GET /chips", at: 0) { _ in throw HTTPUnhandledError() } - - // then - XCTAssertEqual( - handler.map(\.route.stringValue), - ["GET /chips", "GET /fish"] - ) - - // when - handler[1] = ("POST /shrimp", MockHandler()) - - // then - XCTAssertEqual( - handler.map(\.route.stringValue), - ["GET /chips", "POST /shrimp"] - ) - - // when - handler.replaceSubrange(0..., with: [(HTTPRoute("POST /fish"), MockHandler())]) - - // then - XCTAssertEqual( - handler.map(\.route.stringValue), - ["POST /fish"] - ) - - // when - handler.removeAll() - - // then - XCTAssertTrue( - handler.isEmpty - ) - } - - func testPathParameters() async throws { - // given - var handler = RoutedHTTPHandler() - - handler.appendRoute("GET /:id/hello?food=:food&qty=:qty") { request in - let body = [ - request.routeParameters["id"], - request.routeParameters["food"], - request.routeParameters["qty"] - ] - .compactMap { $0 } - .joined(separator: " ") - return HTTPResponse( - statusCode: .ok, - body: body.data(using: .utf8)! - ) - } - - // when then - await AsyncAssertEqual( - try await handler.handleRequest(.make("/10/hello?food=fish&qty=🐟")).bodyString, - "10 fish 🐟" - ) - - await AsyncAssertEqual( - try await handler.handleRequest(.make("/450/hello?qty=🍟&food=chips")).bodyString, - "450 chips 🍟" - ) - } - - func testParameterPackRoute() async throws { - // given - var handler = RoutedHTTPHandler() - - handler.appendRoute("GET /:id/hello?food=:food&qty=:qty") { (id: Int, food: String, qty: String) -> HTTPResponse in - HTTPResponse( - statusCode: .ok, - body: "\(id * 2) \(food) \(qty)".data(using: .utf8)! - ) - } - - // when then - await AsyncAssertEqual( - try await handler.handleRequest(.make("/10/hello?qty=🐟&food=fish")).bodyString, - "20 fish 🐟" - ) - - await AsyncAssertEqual( - try await handler.handleRequest(.make("/450/hello?food=shrimp&qty=🍤")).bodyString, - "900 shrimp 🍤" - ) - } -} - -private struct MockHandler: HTTPHandler { - func handleRequest(_ request: HTTPRequest) async throws -> HTTPResponse { - throw HTTPUnhandledError() - } -} - -private extension HTTPRoute { - - var stringValue: String { - let methods = methods.map(\.rawValue).sorted().joined(separator: ",") - let path = path.map(\.description).joined(separator: "/") - return methods + " /" + path - } -} diff --git a/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift b/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift deleted file mode 100644 index 714d1cbf..00000000 --- a/FlyingFox/XCTests/Handlers/WebSocketHTTPHandlerTests.swift +++ /dev/null @@ -1,227 +0,0 @@ -// -// WebSocketHTTPHandlerTests.swift -// FlyingFox -// -// Created by Simon Whitty on 19/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest - -final class WebSocketHTTPHandlerTests: XCTestCase { - - func testResponseIncludesExpectedHeaders() async throws { - let handler = WebSocketHTTPHandler.make() - - let response = try await handler.handleRequest(.make( - headers: [ - .host: "localhost", - .connection: "uPgRaDe,Keep-Alive", // case-insensitive and can be a list of values - .upgrade: "WeBsOcKeT", // case-insensitive - .webSocketKey: "ABCDEFGHIJKLMNOP".data(using: .utf8)!.base64EncodedString(), - .webSocketVersion: "13" - ] - )) - - XCTAssertEqual( - response.statusCode, - .switchingProtocols - ) - XCTAssertEqual( - response.headers[.webSocketAccept], - "9twnCz4Oi2Q3EuDqLAETCuip07c=" - ) - XCTAssertEqual( - response.headers[.connection], - "upgrade" - ) - XCTAssertEqual( - response.headers[.upgrade], - "websocket" - ) - } - - func testHandlerVerifiesHeaders() async throws { - // Checks for conformance to RFC 6455 section 4.2.1 (https://datatracker.ietf.org/doc/html/rfc6455#section-4.2.1) - - let handler = WebSocketHTTPHandler.make() - - let headers: HTTPHeaders = [ - .host: "localhost", - .connection: "Upgrade", - .upgrade: "websocket", - .webSocketKey: "ABCDEFGHIJKLMNOP".data(using: .utf8)!.base64EncodedString(), - .webSocketVersion: "13" - ] - - var withoutHostHeaders = headers - withoutHostHeaders[.host] = nil - - var incorrectConnectionHeaders = headers - incorrectConnectionHeaders[.connection] = "Downgrade" - - var incorrectUpgradeHeaders = headers - incorrectUpgradeHeaders[.upgrade] = "webplugs" - - var incorrectSocketKeyHeaders = headers - incorrectSocketKeyHeaders[.webSocketKey] = "ABC" - - var incorrectSocketVersionHeaders = headers - incorrectSocketVersionHeaders[.webSocketVersion] = "-1" - - let withoutHostResponse = try await handler.handleRequest(.make(headers: withoutHostHeaders)) - XCTAssertEqual( - withoutHostResponse.statusCode, - .badRequest - ) - - let incorrectConnectionResponse = try await handler.handleRequest(.make(headers: incorrectConnectionHeaders)) - XCTAssertEqual( - incorrectConnectionResponse.statusCode, - .badRequest - ) - - let incorrectUpgradeResponse = try await handler.handleRequest(.make(headers: incorrectUpgradeHeaders)) - XCTAssertEqual( - incorrectUpgradeResponse.statusCode, - .badRequest - ) - - let incorrectSocketKeyResponse = try await handler.handleRequest(.make(headers: incorrectSocketKeyHeaders)) - XCTAssertEqual( - incorrectSocketKeyResponse.statusCode, - .badRequest - ) - - let incorrectSocketVersionResponse = try await handler.handleRequest(.make(headers: incorrectSocketVersionHeaders)) - XCTAssertEqual( - incorrectSocketVersionResponse.statusCode, - .badRequest - ) - } - - func testHandlerVerifiesRequestMethod() async throws { - let handler = WebSocketHTTPHandler.make(accepts: [.GET]) - - let incorrectMethodResponse = try await handler.handleRequest(.make(method: .POST)) - XCTAssertEqual( - incorrectMethodResponse.statusCode, - .badRequest - ) - } - - func testWebSocketKey_IsCreatedFromUUID() async throws { - XCTAssertEqual( - WebSocketHTTPHandler.makeSecWebSocketKeyValue(for: UUID(uuidString: "123e4567-e89b-12d3-a456-426614174000")!), - "Ej5FZ+ibEtOkVkJmFBdAAA==" - ) - - XCTAssertNotEqual( - WebSocketHTTPHandler.makeSecWebSocketKeyValue(), - "Ej5FZ+ibEtOkVkJmFBdAAA==" - ) - } - - func testHeaderVerification() { - XCTAssertNoThrow( - try WebSocketHTTPHandler.verifyHandshakeRequestHeaders( - .makeWSHeaders() - ) - ) - XCTAssertThrowsError( - try WebSocketHTTPHandler.verifyHandshakeRequestHeaders( - .makeWSHeaders(host: nil) - ) - ) - XCTAssertThrowsError( - try WebSocketHTTPHandler.verifyHandshakeRequestHeaders( - .makeWSHeaders(upgrade: nil) - ) - ) - XCTAssertThrowsError( - try WebSocketHTTPHandler.verifyHandshakeRequestHeaders( - .makeWSHeaders(upgrade: "other") - ) - ) - XCTAssertThrowsError( - try WebSocketHTTPHandler.verifyHandshakeRequestHeaders( - .makeWSHeaders(connection: nil) - ) - ) - XCTAssertThrowsError( - try WebSocketHTTPHandler.verifyHandshakeRequestHeaders( - .makeWSHeaders(webSocketKey: nil) - ) - ) - } -} - -private extension HTTPHeaders { - - static func makeWSHeaders(host: String? = "localhost", - connection: String? = "Upgrade", - upgrade: String? = "websocket", - webSocketKey: String? = "ABCDEFGHIJKLMNOP", - webSocketVersion: String? = "13") -> Self { - var headers = HTTPHeaders() - headers[.host] = host - headers[.connection] = connection - headers[.upgrade] = upgrade - headers[.webSocketKey] = webSocketKey?.data(using: .utf8)!.base64EncodedString() - headers[.webSocketVersion] = webSocketVersion - return headers - } -} - -private extension WebSocketHTTPHandler { - static func make(handler: some WSHandler = MockHandler(), accepts methods: Set = [.GET]) -> WebSocketHTTPHandler { - WebSocketHTTPHandler(handler: MockHandler(), accepts: methods) - } -} - -private struct MockHandler: WSHandler { - func makeFrames(for client: AsyncThrowingStream) async throws -> AsyncStream { - UnsafeFrames(source: client).makeStream() - } -} - -private final class UnsafeFrames: @unchecked Sendable { - - private var iterator: AsyncThrowingStream.Iterator - - init(source: AsyncThrowingStream) { - self.iterator = source.makeAsyncIterator() - } - - func makeStream() -> AsyncStream { - AsyncStream { await self.nextFrame() } - } - - func nextFrame() async -> WSFrame? { - try? await iterator.next() - } -} diff --git a/FlyingFox/XCTests/SocketAddress+Glibc.swift b/FlyingFox/XCTests/SocketAddress+Glibc.swift deleted file mode 100644 index 215c9393..00000000 --- a/FlyingFox/XCTests/SocketAddress+Glibc.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SocketAddress+Glibc.swift -// FlyingFox -// -// Created by Simon Whitty on 15/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -#if canImport(Glibc) -// swift 5 on linux fails to import comformance for these Glibc types 🤷🏻‍♂️: -import Glibc -import FlyingSocks - -extension sockaddr_in: SocketAddress { - public static let family = sa_family_t(AF_INET) -} - -extension sockaddr_in6: SocketAddress { - public static let family = sa_family_t(AF_INET6) -} - -extension sockaddr_un: SocketAddress { - public static let family = sa_family_t(AF_UNIX) -} - -#endif diff --git a/FlyingFox/XCTests/Stubs/fish.json b/FlyingFox/XCTests/Stubs/fish.json deleted file mode 100644 index af915f8a..00000000 --- a/FlyingFox/XCTests/Stubs/fish.json +++ /dev/null @@ -1 +0,0 @@ -{"fish": "cakes"} \ No newline at end of file diff --git a/FlyingFox/XCTests/Stubs/subdir/vinegar.json b/FlyingFox/XCTests/Stubs/subdir/vinegar.json deleted file mode 100644 index 7f7b37e4..00000000 --- a/FlyingFox/XCTests/Stubs/subdir/vinegar.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "malt"} \ No newline at end of file diff --git a/FlyingFox/XCTests/URLSession+AsyncTests.swift b/FlyingFox/XCTests/URLSession+AsyncTests.swift deleted file mode 100644 index 1e00c137..00000000 --- a/FlyingFox/XCTests/URLSession+AsyncTests.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// URLSession+AsyncTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import XCTest -import Foundation -#if canImport(FoundationNetworking) -import FoundationNetworking -#endif - -final class URLSessionAsyncTests: XCTestCase { - - func disabled_testURLSession_MakesRequest() async throws { - var request = URLRequest(url: URL(string: "https://pie.dev/status/208")!) - request.timeoutInterval = 2 - let (_, response) = try await URLSession.shared.data(for: request) - - XCTAssertEqual( - (response as! HTTPURLResponse).statusCode, - 208 - ) - } - - func testURLSession_ReturnsError() async throws { - let request = URLRequest(url: URL(string: "https://flying.fox.invalid/")!) - await AsyncAssertThrowsError(try await URLSession.shared.data(for: request), of: URLError.self) - } - - func testURLSession_CancelsRequest() async throws { - let request = URLRequest(url: URL(string: "https://httpstat.us/200?sleep=10000")!) - - let task = Task { - _ = try await URLSession.shared.data(for: request) - } - - task.cancel() - - await AsyncAssertThrowsError(try await task.value, of: URLError.self) { - XCTAssertEqual($0.code, .cancelled) - } - } -} diff --git a/FlyingFox/XCTests/WebSocket/AsyncStream+WSFrameTests.swift b/FlyingFox/XCTests/WebSocket/AsyncStream+WSFrameTests.swift deleted file mode 100644 index 28de1a86..00000000 --- a/FlyingFox/XCTests/WebSocket/AsyncStream+WSFrameTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// AsyncStream+WSFrameTests.swift -// FlyingFox -// -// Created by Simon Whitty on 18/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import FlyingSocks -import Foundation -import XCTest - -final class WSFrameSequenceTests: XCTestCase { - - func testSequenceDecodesFramesFromBytes() async throws { - await AsyncAssertEqual( - try await AsyncThrowingStream.make([.fish, .chips, .close]).collectAll(), - [.fish, .chips, .close] - ) - await AsyncAssertEqual( - try await AsyncThrowingStream.make([.close]).collectAll(), - [.close] - ) - await AsyncAssertEqual( - try await AsyncThrowingStream.make([]).collectAll(), - [] - ) - } - - func testProtocolFrames_CatchErrors_AndCloseStream() async throws { - var continuation: AsyncThrowingStream.Continuation! - let stream = AsyncThrowingStream { - continuation = $0 - } - - continuation.yield(.fish) - continuation.yield(.chips) - continuation.finish(throwing: SocketError.unsupportedAddress) - - await AsyncAssertEqual( - try await AsyncStream.protocolFrames(from: stream).collectAll(), - [.fish, .chips, .close(message: "Protocol Error")] - ) - } -} - -extension WSFrame { - static let fish = WSFrame.make(payload: "Fish".data(using: .utf8)!) - static let chips = WSFrame.make(payload: "Chips".data(using: .utf8)!) - static let close = WSFrame.close(message: "Bye") - static let ping = WSFrame.make(opcode: .ping) - static let pong = WSFrame.make(opcode: .pong) -} - -extension AsyncThrowingStream where Element == WSFrame, Failure == any Error { - static func make(_ frames: [WSFrame]) -> Self { - let bytes = ConsumingAsyncSequence(frames.flatMap(WSFrameEncoder.encodeFrame)) - return AsyncThrowingStream.decodingFrames(from: bytes) - } -} - -extension AsyncSequence { - func collectAll() async throws -> [Element] { - let collect = collectUntil { _ in false } - var iterator = collect.makeAsyncIterator() - return try await iterator.next() ?? [] - } -} diff --git a/FlyingFox/XCTests/WebSocket/WSFrameEncoderTests.swift b/FlyingFox/XCTests/WebSocket/WSFrameEncoderTests.swift deleted file mode 100644 index 0620a1ee..00000000 --- a/FlyingFox/XCTests/WebSocket/WSFrameEncoderTests.swift +++ /dev/null @@ -1,380 +0,0 @@ -// -// WSFrameEncoderTests.swift -// FlyingFox -// -// Created by Simon Whitty on 16/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -@testable import FlyingSocks -import Foundation -import XCTest - -final class WSFrameEncoderTests: XCTestCase { - - func testEncodeFrame0() { - var frame = WSFrame.make(fin: false, - rsv1: false, - rsv2: false, - rsv3: false, - opcode: .continuation, - mask: nil) - - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b00000000 - ) - - frame.fin = true - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b10000000 - ) - frame.rsv1 = true - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b11000000 - ) - - frame.rsv2 = true - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b11100000 - ) - - frame.rsv3 = true - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b11110000 - ) - frame.opcode = .text - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b11110001 - ) - frame.opcode = .pong - XCTAssertEqual( - WSFrameEncoder.encodeFrame0(frame), - 0b11111010 - ) - } - - func testEncodeFrame1() { - XCTAssertEqual( - WSFrameEncoder.encodeLength(0b00011001, hasMask: false), - [0b00011001] - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(0b00011001, hasMask: true), - [0b10011001] - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(Int(UInt16.max), hasMask: false).first, - 0b01111110 - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(Int(UInt16.max), hasMask: true).first, - 0b11111110 - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(Int(UInt16.max) + 1, hasMask: false).first, - 0b01111111 - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(Int(UInt16.max) + 1, hasMask: true).first, - 0b11111111 - ) - } - - func testEncodeLength() { - XCTAssertEqual( - WSFrameEncoder.encodeLength(125, hasMask: false).count, - 1 - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(126, hasMask: false).count, - 3 - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(Int(UInt16.max), hasMask: false).count, - 3 - ) - XCTAssertEqual( - WSFrameEncoder.encodeLength(Int(UInt16.max) + 1, hasMask: false).count, - 9 - ) - } - - func testEncodePayload() { - XCTAssertEqual( - WSFrameEncoder.encodePayload(Data([0x01, 0x02]), mask: nil), - Data([0x01, 0x02]) - ) - XCTAssertEqual( - WSFrameEncoder.encodePayload(Data([0x01, 0x02, 0x03, 0x04]), - mask: .init(m1: 0xFF, m2: 0xFF, m3: 0xFF, m4: 0xFF)), - Data([0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFD, 0xFC, 0xFB]) - ) - } - - func testEncodeFrame() { - let frame = WSFrame.make(fin: true, - opcode: .text, - payload: "Abc".data(using: .utf8)!) - - XCTAssertEqual( - WSFrameEncoder.encodeFrame(frame), - Data([ - 0b10000001, 3, .ascii("A"), .ascii("b"), .ascii("c") - ]) - ) - } - - func testDecodeFrame() async { - await AsyncAssertEqual( - try await WSFrameEncoder.decodeFrame(0b10000001, 3, .ascii("A"), .ascii("b"), .ascii("c")), - .make(fin: true, - opcode: .text, - payload: "Abc".data(using: .utf8)!) - ) - } - - func testDecodeFrame0() { - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b10000000), - .make(fin: true, rsv1: false, rsv2: false, rsv3: false, opcode: .continuation) - ) - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b01000000), - .make(fin: false, rsv1: true, rsv2: false, rsv3: false, opcode: .continuation) - ) - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b10100000), - .make(fin: true, rsv1: false, rsv2: true, rsv3: false, opcode: .continuation) - ) - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b11110001), - .make(fin: true, rsv1: true, rsv2: true, rsv3: true, opcode: .text) - ) - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b11110010), - .make(fin: true, rsv1: true, rsv2: true, rsv3: true, opcode: .binary) - ) - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b11111000), - .make(fin: true, rsv1: true, rsv2: true, rsv3: true, opcode: .close) - ) - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b11111001), - .make(fin: true, rsv1: true, rsv2: true, rsv3: true, opcode: .ping) - ) - - XCTAssertEqual( - WSFrameEncoder.decodeFrame(from: 0b11111010), - .make(fin: true, rsv1: true, rsv2: true, rsv3: true, opcode: .pong) - ) - } - - func testDecodeLength() async { - await AsyncAssertEqual( - try await WSFrameEncoder.decodeLength(0x01), - 1 - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeLength(0x7D), - 125 - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeLength(0x7E, 0xFF, 0x00), - 0xFF00 - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeLength(0x7E, 0x00, 0xFF), - 0x00FF - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeLength(0x7E, 0xFF, 0xFF), - 0xFFFF - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeLength(0x7F, 0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA, 0x99, 0x00), - 0x0099AABBCCDDEEFF - ) - } - - func testDecodeInvalidLength_ThrowsError() async { - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodeLength(0x7E), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodeLength(0x7F, 0xFF, 0xFF, 0xFF), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodeLength(0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF), - of: WSFrameEncoder.Error.self - ) - } - - func testDecodeMask() async { - await AsyncAssertEqual( - try await WSFrameEncoder.decodeMask(0xF0, 0x01, 0x02, 0x03, 0x04), - .init(m1: 0x01, m2: 0x02, m3: 0x03, m4: 0x04) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeMask(0x70), - nil - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeMask(0xFE, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04), - .init(m1: 0x01, m2: 0x02, m3: 0x03, m4: 0x04) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeMask(0x7E, 0x00, 0x00), - nil - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeMask(0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04), - .init(m1: 0x01, m2: 0x02, m3: 0x03, m4: 0x04) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodeMask(0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00), - nil - ) - } - - func testDecodeInvalidMask_ThrowsError() async { - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodeMask(0xFD, 0x01, 0x02), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodeMask(0xFE, 0x00, 0x00, 0x01, 0x02), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodeMask(0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - } - - func testDecodePayload() async { - await AsyncAssertEqual( - try await WSFrameEncoder.decodePayload(length: 2, mask: nil, 0x01, 0x02, 0x03, 0x04), - Data([0x01, 0x02]) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodePayload(length: 4, mask: nil, 0x01, 0x02, 0x03, 0x04), - Data([0x01, 0x02, 0x03, 0x04]) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodePayload(length: 2, mask: (0xFF, 0xFF, 0xFF, 0xFF), 0x00, 0x01, 0x02, 0x03), - Data([0xFF, 0xFE]) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodePayload(length: 2, mask: nil, 0x01, 0x02, 0x03, 0x04), - Data([0x01, 0x02]) - ) - await AsyncAssertEqual( - try await WSFrameEncoder.decodePayload(length: 4, mask: (0xFF, 0xFF, 0xFF, 0xFF), 0x00, 0x01, 0x02, 0x03), - Data([0xFF, 0xFE, 0xFD, 0xFC]) - ) - } - - func testDecodeInvalidPayload_ThrowsError() async { - await AsyncAssertThrowsError( - try await WSFrameEncoder.decodePayload(length: 10, mask: nil, 0x01, 0x02, 0x03, 0x04), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - } - - func testWebSocketConnectionToVI() async throws { - let addr = try Socket.makeAddressINET(fromIP4: "192.236.209.31", port: 80) - let socket = try await AsyncSocket.connected(to: addr) - defer { try? socket.close() } - - let key = WebSocketHTTPHandler.makeSecWebSocketKeyValue() - - var request = HTTPRequest.make(path: "/mirror") - request.headers[.host] = "ws.vi-server.org" - request.headers[.upgrade] = "websocket" - request.headers[.connection] = "Upgrade" - request.headers[.webSocketVersion] = "13" - request.headers[.webSocketKey] = key - - try await socket.writeRequest(request) - let response = try await socket.readResponse() - - XCTAssertEqual( - response.headers[.webSocketAccept], - WebSocketHTTPHandler.makeSecWebSocketAcceptValue(for: key) - ) - - var frame = WSFrame.make(fin: true, opcode: .text, mask: .mock, payload: "FlyingFox".data(using: .utf8)!) - try await socket.writeFrame(frame) - - frame = WSFrame.make(fin: true, opcode: .text, mask: .mock, payload: "FlyingSox".data(using: .utf8)!) - try await socket.writeFrame(frame) - - frame = WSFrame.close(mask: .mock) - try await socket.writeFrame(frame) - } - - func testRoundtripLength() async throws { - for length in 0..<256 { - let encoded = WSFrameEncoder.encodeLength(length, hasMask: false) - let decoded = try await WSFrameEncoder.decodeLengthMask(encoded) - XCTAssertEqual(length, decoded.length) - } - } -} - -private extension WSFrameEncoder { - - static func decodeFrame(_ bytes: UInt8...) async throws -> WSFrame { - try await decodeFrame(from: ConsumingAsyncSequence(bytes)) - } - - static func decodeLength(_ bytes: UInt8...) async throws -> Int { - try await decodeLengthMask(bytes).length - } - - static func decodeMask(_ bytes: UInt8...) async throws -> WSFrame.Mask? { - try await decodeLengthMask(bytes).mask - } - - static func decodePayload(length: Int, mask: (UInt8, UInt8, UInt8, UInt8)?, _ bytes: UInt8...) async throws -> Data { - try await decodePayload(from: ConsumingAsyncSequence(bytes), length: length, mask: mask.map(WSFrame.Mask.init)) - } - - static func decodeLengthMask(_ bytes: [UInt8]) async throws -> (length: Int, mask: WSFrame.Mask?) { - try await decodeLengthMask(from: ConsumingAsyncSequence(bytes)) - } -} diff --git a/FlyingFox/XCTests/WebSocket/WSFrameTests.swift b/FlyingFox/XCTests/WebSocket/WSFrameTests.swift deleted file mode 100644 index 074681fd..00000000 --- a/FlyingFox/XCTests/WebSocket/WSFrameTests.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// WSFrameTests.swift -// FlyingFox -// -// Created by Simon Whitty on 17/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation -import XCTest - -final class WSFrameTests: XCTestCase { - - func testCloseFrame() { - XCTAssertEqual( - WSFrame.close(), - .make(fin: true, - opcode: .close, - mask: nil, - payload: Data([0x03, 0xE8])) - ) - XCTAssertEqual( - WSFrame.close(mask: .mock), - .make(fin: true, - opcode: .close, - mask: .mock, - payload: Data([0x03, 0xE8])) - ) - XCTAssertEqual( - WSFrame.close(message: "Err"), - .make(fin: true, - opcode: .close, - mask: nil, - payload: Data([0x03, 0xEA, .ascii("E"), .ascii("r"), .ascii("r")])) - ) - XCTAssertEqual( - WSFrame.close(message: "Err", mask: .mock), - .make(fin: true, - opcode: .close, - mask: .mock, - payload: Data([0x03, 0xEA, .ascii("E"), .ascii("r"), .ascii("r")])) - ) - XCTAssertEqual( - WSFrame.close(code: WSCloseCode(4999, reason: "Err")), - .make( - fin: true, - opcode: .close, - mask: nil, - payload: Data([0x13, 0x87, .ascii("E"), .ascii("r"), .ascii("r")]) - ) - ) - XCTAssertEqual( - WSFrame.close(code: WSCloseCode(4999, reason: "Err"), mask: .mock), - .make( - fin: true, - opcode: .close, - mask: .mock, - payload: Data([0x13, 0x87, .ascii("E"), .ascii("r"), .ascii("r")]) - ) - ) - } -} - -extension UInt8 { - static func ascii(_ char: Character) -> Self { - char.asciiValue! - } -} - -extension WSFrame.Mask { - static let mock = WSFrame.Mask(m1: 0x1, m2: 0x2, m3: 0x3, m4: 0x4) -} - -extension WSFrame { - - static func make(fin: Bool = true, - rsv1: Bool = false, - rsv2: Bool = false, - rsv3: Bool = false, - opcode: Opcode = .text, - mask: Mask? = nil, - payload: Data = Data()) -> Self { - WSFrame(fin: fin, - rsv1: rsv1, - rsv2: rsv2, - rsv3: rsv3, - opcode: opcode, - mask: mask, - payload: payload) - } - - static func make(fin: Bool = true, - isContinuation: Bool = false, - text: String) -> Self { - WSFrame(fin: fin, - rsv1: false, - rsv2: false, - rsv3: false, - opcode: isContinuation ? .continuation : .text, - mask: nil, - payload: text.data(using: .utf8)!) - } - - static func makeTextFrames(_ payload: String, maxCharacters: Int) -> [WSFrame] { - var messages = payload.chunked(size: maxCharacters).enumerated().map { idx, substring in - WSFrame.make(fin: false, isContinuation: idx != 0, text: String(substring)) - } - - if let last = messages.indices.last { - messages[last].fin = true - } - return messages - } -} diff --git a/FlyingFox/XCTests/WebSocket/WSFrameValidatorTests.swift b/FlyingFox/XCTests/WebSocket/WSFrameValidatorTests.swift deleted file mode 100644 index bc065d13..00000000 --- a/FlyingFox/XCTests/WebSocket/WSFrameValidatorTests.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// WSFrameValidatorTests.swift -// FlyingFox -// -// Created by Simon Whitty on 19/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation -import XCTest - -final class WSFrameValidatorTests: XCTestCase { - - func testContinuationFrames_AreAppended() async { - let frames = WSFrame.makeTextFrames("Fish & Chips🍟", maxCharacters: 2) - XCTAssertEqual(frames.count, 7) - - await AsyncAssertEqual( - try await WSFrameValidator.validate(frames).collectAll(), - [WSFrame(fin: true, opcode: .text, mask: nil, payload: "Fish & Chips🍟".data(using: .utf8)!)] - ) - } - - func testControlFrames_BetweenContinuations_AreHandled() async { - await AsyncAssertEqual( - try await WSFrameValidator.validate([ - .make(fin: false, isContinuation: false, text: "Hello"), - .ping, - .make(fin: true, isContinuation: true, text: " World!") - ]).collectAll(), - [.ping, - .make(fin: true, isContinuation: false, text: "Hello World!")] - ) - } - - func testSingleFrames() async { - await AsyncAssertEqual( - try await WSFrameValidator.validate([.fish, .chips, .ping, .fish, .close]) - .collectAll(), - [.fish, .chips, .ping, .fish, .close] - ) - } - - func testValidation() async { - await AsyncAssertThrowsError( - try await WSFrameValidator.validate([.make(fin: false), .make(fin: false)]).collectAll(), - of: WSFrameValidator.Error.self - ) - - await AsyncAssertThrowsError( - try await WSFrameValidator.validate([.make(fin: true, opcode: .continuation)]).collectAll(), - of: WSFrameValidator.Error.self - ) - } - - func testControlFrames_ThrowError_WhenNotFin() async { - await AsyncAssertThrowsError( - try await WSFrameValidator.validate([.make(fin: false, opcode: .ping)]).collectAll(), - of: WSFrameValidator.Error.self - ) - await AsyncAssertThrowsError( - try await WSFrameValidator.validate([.make(fin: false, opcode: .pong)]).collectAll(), - of: WSFrameValidator.Error.self - ) - await AsyncAssertThrowsError( - try await WSFrameValidator.validate([.make(fin: false, opcode: .close)]).collectAll(), - of: WSFrameValidator.Error.self - ) - } -} - -private extension WSFrameValidator { - - static func validate(_ frames: [WSFrame]) async throws -> AsyncThrowingStream { - UnsafeFrames(frames: frames).makeStream() - } -} - -private final class UnsafeFrames: @unchecked Sendable { - - var iterator: AsyncThrowingCompactMapSequence, WSFrame>.Iterator - - init(frames: [WSFrame]) { - self.iterator = WSFrameValidator.validateFrames(from: AsyncThrowingStream.make(frames)).makeAsyncIterator() - } - - func makeStream() -> AsyncThrowingStream { - AsyncThrowingStream { try await self.nextFrame() } - } - - func nextFrame() async throws -> WSFrame? { - try await iterator.next() - } -} - diff --git a/FlyingFox/XCTests/WebSocket/WSHandlerTests.swift b/FlyingFox/XCTests/WebSocket/WSHandlerTests.swift deleted file mode 100644 index c754edc7..00000000 --- a/FlyingFox/XCTests/WebSocket/WSHandlerTests.swift +++ /dev/null @@ -1,165 +0,0 @@ -// -// WSHandlerTests.swift -// FlyingFox -// -// Created by Simon Whitty on 20/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingFox -import Foundation -import XCTest - -final class WSHandlerTests: XCTestCase { - - func testFrames_CreateExpectedMessages() { - let handler = MessageFrameWSHandler.make() - - XCTAssertEqual( - try handler.makeMessage(for: .make(fin: true, opcode: .text, payload: "Hello".data(using: .utf8)!)), - .text("Hello") - ) - XCTAssertThrowsError( - try handler.makeMessage(for: .make(fin: true, opcode: .text, payload: Data([0x03, 0xE8]))) - ) - - XCTAssertEqual( - try handler.makeMessage(for: .make(fin: true, opcode: .binary, payload: Data([0x01, 0x02]))), - .data(Data([0x01, 0x02])) - ) - - XCTAssertNil( - try handler.makeMessage(for: .make(fin: true, opcode: .ping)) - ) - XCTAssertNil( - try handler.makeMessage(for: .make(fin: true, opcode: .pong)) - ) - } - - func testFrames_CreatesCloseMessage() throws { - let handler = MessageFrameWSHandler.make() - let payload = Data([0x13, 0x87, .ascii("f"), .ascii("i"), .ascii("s"), .ascii("h")]) - - XCTAssertEqual( - try handler.makeMessage(for: .make(fin: true, opcode: .close, payload: payload)), - .close(WSCloseCode(4999, reason: "fish")) - ) - XCTAssertEqual( - try handler.makeMessage(for: .make(fin: true, opcode: .close)), - .close(.noStatusReceived) - ) - } - - func testMessages_CreateExpectedFrames() { - let handler = MessageFrameWSHandler.make() - XCTAssertEqual( - handler.makeFrames(for: .text("Jack of Hearts")), - [.make(fin: true, opcode: .text, payload: "Jack of Hearts".data(using: .utf8)!)] - ) - XCTAssertEqual( - handler.makeFrames(for: .data(Data([0x01, 0x02]))), - [.make(fin: true, opcode: .binary, payload: Data([0x01, 0x02]))] - ) - } - - func testMessages_AreSplitIntoMultipleFrames() { - let handler = MessageFrameWSHandler.make(frameSize: 4) - - XCTAssertEqual( - handler.makeFrames(for: .text("Jack of Hearts")), - [.make(fin: false, opcode: .text, payload: "Jack".data(using: .utf8)!), - .make(fin: false, opcode: .continuation, payload: " of ".data(using: .utf8)!), - .make(fin: false, opcode: .continuation, payload: "Hear".data(using: .utf8)!), - .make(fin: true, opcode: .continuation, payload: "ts".data(using: .utf8)!)] - ) - } - - func testMessages_ThrowError_WhenAttemptedToBeConvertedToResponseFrames() { - let handler = MessageFrameWSHandler.make() - XCTAssertThrowsError( - try handler.makeResponseFrames(for: .make(fin: true, opcode: .text, payload: "Lily".data(using: .utf8)!)), - of: MessageFrameWSHandler.FrameError.self - ) - } - - func testResponseFrames() async throws { - let messages = Messages() - let handler = MessageFrameWSHandler.make(handler: messages) - - let frames = try await handler.makeFrames(for: [.fish, .ping, .pong, .chips, .close]) - - await AsyncAssertEqual( - try await messages.input.takeNext(), - .text("Fish") - ) - - await AsyncAssertEqual( - try await messages.input.takeNext(), - .text("Chips") - ) - - await AsyncAssertEqual( - try await frames.collectAll(), - [.pong, .close] - ) - } - - func testResponseFramesEnds() async throws { - let handler = MessageFrameWSHandler.make() - let frames = try await handler.makeFrames(for: [.ping]) - - await AsyncAssertEqual( - try await frames.collectAll(), - [.pong] - ) - } -} - -extension MessageFrameWSHandler { - - static func make(handler: some WSMessageHandler = Messages(), - frameSize: Int = 1024) -> Self { - MessageFrameWSHandler(handler: handler, - frameSize: frameSize) - } - - func makeFrames(for frames: [WSFrame]) async throws -> AsyncStream { - try await makeFrames(for: .make(frames)) - } -} - -final class Messages: WSMessageHandler, @unchecked Sendable { - - var input: AsyncStream! - var output: AsyncStream.Continuation! - - func makeMessages(for request: AsyncStream) async throws -> AsyncStream { - self.input = request - return AsyncStream { - self.output = $0 - } - } -} diff --git a/FlyingFox/XCTests/XCTest+Extension.swift b/FlyingFox/XCTests/XCTest+Extension.swift deleted file mode 120000 index ed0f373e..00000000 --- a/FlyingFox/XCTests/XCTest+Extension.swift +++ /dev/null @@ -1 +0,0 @@ -../../FlyingSocks/XCTests/XCTest+Extension.swift \ No newline at end of file diff --git a/FlyingSocks.podspec.json b/FlyingSocks.podspec.json index bbb2847b..235255f3 100644 --- a/FlyingSocks.podspec.json +++ b/FlyingSocks.podspec.json @@ -22,5 +22,5 @@ "pod_target_xcconfig": { "OTHER_SWIFT_FLAGS": "-package-name FlyingFox" }, - "swift_version": "5.10" + "swift_version": "6.0" } diff --git a/FlyingSocks/Sources/IdentifiableContinuation.swift b/FlyingSocks/Sources/IdentifiableContinuation.swift index 5e64f414..6050bfac 100644 --- a/FlyingSocks/Sources/IdentifiableContinuation.swift +++ b/FlyingSocks/Sources/IdentifiableContinuation.swift @@ -29,7 +29,6 @@ // SOFTWARE. // -#if compiler(>=6.0) /// Invokes the passed in closure with an `IdentifableContinuation` for the current task. /// /// The body of the closure executes synchronously on the calling actor. Once it returns the calling task is suspended. @@ -123,105 +122,6 @@ package func withIdentifiableThrowingContinuation( } } } -#else -/// Invokes the passed in closure with an `IdentifableContinuation` for the current task. -/// -/// The body of the closure executes synchronously on the calling actor. Once it returns the calling task is suspended. -/// It is possible to immediately resume the task, or escape the continuation in order to complete it afterwards, which -/// will then resume the suspended task. -/// -/// You must invoke the continuation's `resume` method exactly once. -/// - Parameters: -/// - isolation: Actor isolation used when executing the body closure. -/// - function: A string identifying the declaration that is the notional -/// source for the continuation, used to identify the continuation in -/// runtime diagnostics related to misuse of this continuation. -/// - body: A closure that takes a `IdentifiableContinuation` parameter. -/// - handler: Cancellation closure executed when the current Task is cancelled. Handler is always called _after_ the body closure is compeled. -/// - Returns: The value continuation is resumed with. -@_unsafeInheritExecutor -@inlinable -package func withIdentifiableContinuation( - isolation: isolated some Actor, - function: String = #function, - body: (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void -) async -> T { - let id = IdentifiableContinuation.ID() - let state = Mutex((isStarted: false, isCancelled: false)) - return await withTaskCancellationHandler { - await withCheckedContinuation(function: function) { - let continuation = IdentifiableContinuation(id: id, continuation: $0) - body(continuation) - let sendCancel = state.withLock { - $0.isStarted = true - return $0.isCancelled - } - if sendCancel { - handler(id) - } - _ = isolation - } - } onCancel: { - let sendCancel = state.withLock { - $0.isCancelled = true - return $0.isStarted - } - if sendCancel { - handler(id) - } - } -} - -/// Invokes the passed in closure with an `IdentifableContinuation` for the current task. -/// -/// The body of the closure executes synchronously on the calling actor. Once it returns the calling task is suspended. -/// It is possible to immediately resume the task, or escape the continuation in order to complete it afterwards, which -/// will then resume the suspended task. -/// -/// You must invoke the continuation's `resume` method exactly once. -/// - Parameters: -/// - isolation: Actor isolation used when executing the body closure. -/// - function: A string identifying the declaration that is the notional -/// source for the continuation, used to identify the continuation in -/// runtime diagnostics related to misuse of this continuation. -/// - body: A closure that takes a `IdentifiableContinuation` parameter. -/// - handler: Cancellation closure executed when the current Task is cancelled. Handler is always called _after_ the body closure is compeled. -/// - Returns: The value continuation is resumed with. -@_unsafeInheritExecutor -@inlinable -package func withIdentifiableThrowingContinuation( - isolation: isolated some Actor, - function: String = #function, - body: (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void -) async throws -> T { - let id = IdentifiableContinuation.ID() - let state = Mutex((isStarted: false, isCancelled: false)) - return try await withTaskCancellationHandler { - try await withCheckedThrowingContinuation(function: function) { - let continuation = IdentifiableContinuation(id: id, continuation: $0) - body(continuation) - let sendCancel = state.withLock { - $0.isStarted = true - return $0.isCancelled - } - if sendCancel { - handler(id) - } - _ = isolation - } - } onCancel: { - let sendCancel = state.withLock { - $0.isCancelled = true - return $0.isStarted - } - if sendCancel { - handler(id) - } - } -} -#endif @usableFromInline package struct IdentifiableContinuation: Sendable, Identifiable where E: Error { @@ -254,7 +154,6 @@ package struct IdentifiableContinuation: Sendable, Identifiable where E: E private let continuation: CheckedContinuation -#if compiler(>=6.0) package func resume(returning value: sending T) { continuation.resume(returning: value) } @@ -262,15 +161,6 @@ package struct IdentifiableContinuation: Sendable, Identifiable where E: E package func resume(with result: sending Result) { continuation.resume(with: result) } -#else - package func resume(returning value: T) { - continuation.resume(returning: value) - } - - package func resume(with result: Result) { - continuation.resume(with: result) - } -#endif package func resume(throwing error: E) { continuation.resume(throwing: error) diff --git a/FlyingSocks/Sources/MutexSwift5.swift b/FlyingSocks/Sources/MutexSwift5.swift deleted file mode 100644 index 70b0b306..00000000 --- a/FlyingSocks/Sources/MutexSwift5.swift +++ /dev/null @@ -1,179 +0,0 @@ -// -// MutexSwift5.swift -// swift-mutex -// -// Created by Simon Whitty on 03/06/2025. -// Copyright 2025 Simon Whitty -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/swift-mutex -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -#if compiler(<6.0) - -// Backports the Swift 6 type Mutex to Swift 5 - -@usableFromInline -package struct Mutex: @unchecked Sendable { - let storage: Storage - - @usableFromInline - package init(_ initialValue: Value) { - self.storage = Storage(initialValue) - } - - @usableFromInline - package borrowing func withLock( - _ body: (inout Value) throws -> Result - ) rethrows -> Result { - storage.lock() - defer { storage.unlock() } - return try body(&storage.value) - } - - @usableFromInline - package borrowing func withLockIfAvailable( - _ body: (inout Value) throws -> Result - ) rethrows -> Result? { - guard storage.tryLock() else { return nil } - defer { storage.unlock() } - return try body(&storage.value) - } -} - -#if canImport(Darwin) - -import struct os.os_unfair_lock_t -import struct os.os_unfair_lock -import func os.os_unfair_lock_lock -import func os.os_unfair_lock_unlock -import func os.os_unfair_lock_trylock - -final class Storage { - private let _lock: os_unfair_lock_t - var value: Value - - init(_ initialValue: consuming Value) { - self._lock = .allocate(capacity: 1) - self._lock.initialize(to: os_unfair_lock()) - self.value = initialValue - } - - func lock() { - os_unfair_lock_lock(_lock) - } - - func unlock() { - os_unfair_lock_unlock(_lock) - } - - func tryLock() -> Bool { - os_unfair_lock_trylock(_lock) - } - - deinit { - self._lock.deinitialize(count: 1) - self._lock.deallocate() - } -} -#elseif canImport(Glibc) || canImport(Musl) || canImport(Bionic) - -#if canImport(Musl) -import Musl -#elseif canImport(Bionic) -import Android -#else -import Glibc -#endif - -final class Storage { - private let _lock: UnsafeMutablePointer - var value: Value - - init(_ initialValue: consuming Value) { - var attr = pthread_mutexattr_t() - pthread_mutexattr_init(&attr) - self._lock = .allocate(capacity: 1) - let err = pthread_mutex_init(self._lock, &attr) - precondition(err == 0, "pthread_mutex_init error: \(err)") - self.value = initialValue - } - - func lock() { - let err = pthread_mutex_lock(_lock) - precondition(err == 0, "pthread_mutex_lock error: \(err)") - } - - func unlock() { - let err = pthread_mutex_unlock(_lock) - precondition(err == 0, "pthread_mutex_unlock error: \(err)") - } - - func tryLock() -> Bool { - pthread_mutex_trylock(_lock) == 0 - } - - deinit { - let err = pthread_mutex_destroy(self._lock) - precondition(err == 0, "pthread_mutex_destroy error: \(err)") - self._lock.deallocate() - } -} -#elseif canImport(WinSDK) - -import ucrt -import WinSDK - -final class Storage { - private let _lock: UnsafeMutablePointer - - var value: Value - - init(_ initialValue: Value) { - self._lock = .allocate(capacity: 1) - InitializeSRWLock(self._lock) - self.value = initialValue - } - - func lock() { - AcquireSRWLockExclusive(_lock) - } - - func unlock() { - ReleaseSRWLockExclusive(_lock) - } - - func tryLock() -> Bool { - TryAcquireSRWLockExclusive(_lock) != 0 - } -} - -#endif - -package extension Mutex where Value: Sendable { - func copy() -> Value { - withLock { $0 } - } -} - -#endif diff --git a/FlyingSocks/Sources/SocketAddress.swift b/FlyingSocks/Sources/SocketAddress.swift index a5acce40..f645ac6a 100644 --- a/FlyingSocks/Sources/SocketAddress.swift +++ b/FlyingSocks/Sources/SocketAddress.swift @@ -43,11 +43,7 @@ import CSystemLinux public protocol SocketAddress: Sendable { static var family: sa_family_t { get } -#if compiler(>=6.0) func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R -#else - func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R -#endif } extension SocketAddress { @@ -110,10 +106,9 @@ public extension SocketAddress where Self == sockaddr_un { #endif } -extension sockaddr_storage: SocketAddress, @unchecked Swift.Sendable { +extension sockaddr_storage: SocketAddress, @retroactive @unchecked Sendable { public static let family = sa_family_t(AF_UNSPEC) -#if compiler(>=6.0) public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { try withUnsafeBytes(of: self) { (p) throws(E) -> R in try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) @@ -125,76 +120,36 @@ extension sockaddr_storage: SocketAddress, @unchecked Swift.Sendable { try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self)) } } -#else - public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { - try withUnsafeBytes(of: self) { p in - try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) - } - } - - public mutating func withMutableSockAddr(_ body: (UnsafeMutablePointer) throws -> R) rethrows -> R { - try withUnsafeMutableBytes(of: &self) { p in - try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self)) - } - } -#endif } -extension sockaddr_in: SocketAddress, @unchecked Swift.Sendable { +extension sockaddr_in: SocketAddress, @retroactive @unchecked Sendable { public static let family = sa_family_t(AF_INET) -#if compiler(>=6.0) public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { try withUnsafeBytes(of: self) { (p) throws(E) -> R in try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) } } -#else - public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { - try withUnsafeBytes(of: self) { p in - try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) - } - } -#endif - } -extension sockaddr_in6: SocketAddress, @unchecked Swift.Sendable { +extension sockaddr_in6: SocketAddress, @retroactive @unchecked Sendable { public static let family = sa_family_t(AF_INET6) -#if compiler(>=6.0) public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { try withUnsafeBytes(of: self) { (p) throws(E) -> R in try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) } } -#else - public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { - try withUnsafeBytes(of: self) { p in - try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) - } - } -#endif - } -extension sockaddr_un: SocketAddress, @unchecked Swift.Sendable { +extension sockaddr_un: SocketAddress, @retroactive @unchecked Sendable { public static let family = sa_family_t(AF_UNIX) -#if compiler(>=6.0) public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { try withUnsafeBytes(of: self) { (p) throws(E) -> R in try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) } } -#else - public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { - try withUnsafeBytes(of: self) { p in - try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) - } - } -#endif - } public extension SocketAddress { diff --git a/FlyingSocks/Sources/SocketAddress.swift.orig b/FlyingSocks/Sources/SocketAddress.swift.orig new file mode 100644 index 00000000..e1bc2b67 --- /dev/null +++ b/FlyingSocks/Sources/SocketAddress.swift.orig @@ -0,0 +1,295 @@ +// +// Socket+Address.swift +// FlyingFox +// +// Created by Simon Whitty on 15/03/2022. +// Copyright © 2022 Simon Whitty. All rights reserved. +// +// Distributed under the permissive MIT license +// Get the latest version from here: +// +// https://github.com/swhitty/FlyingFox +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +#if canImport(WinSDK) +import WinSDK.WinSock2 +#elseif canImport(Android) +import Android +#endif + +#if canImport(CSystemLinux) +import CSystemLinux +#endif + +public protocol SocketAddress: Sendable { + static var family: sa_family_t { get } + +#if compiler(>=6.0) + func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R +#else + func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R +#endif +} + +extension SocketAddress { + public var family: sa_family_t { + withSockAddr { addr, _ in addr.pointee.sa_family } + } + + public func makeStorage() -> sockaddr_storage { + var storage = sockaddr_storage() + + withUnsafeMutablePointer(to: &storage) { + $0.withMemoryRebound(to: Self.self, capacity: 1) { + $0.pointee = self + } + } + + return storage + } +} + +public extension SocketAddress where Self == sockaddr_in { + + static func inet(port: UInt16) -> Self { + Socket.makeAddressINET(port: port) + } + + static func inet(ip4: String, port: UInt16) throws -> Self { + var addr = Socket.makeAddressINET(port: port) + addr.sin_addr = try Socket.makeInAddr(fromIP4: ip4) + return addr + } +} + +public extension SocketAddress where Self == sockaddr_in6 { + + static func inet6(port: UInt16) -> Self { + Socket.makeAddressINET6(port: port) + } + + static func inet6(ip6: String, port: UInt16) throws -> Self { + var addr = Socket.makeAddressINET6(port: port) + addr.sin6_addr = try Socket.makeInAddr(fromIP6: ip6) + return addr + } + + static func loopback(port: UInt16) -> Self { + Socket.makeAddressLoopback(port: port) + } +} + +public extension SocketAddress where Self == sockaddr_un { + static func unix(path: String) -> Self { + Socket.makeAddressUnix(path: path) + } + + #if canImport(Glibc) || canImport(Musl) || canImport(Android) + static func unix(abstractNamespace: String) -> Self { + Socket.makeAbstractNamespaceUnix(name: abstractNamespace) + } + #endif +} + +<<<<<<< HEAD +extension sockaddr_storage: SocketAddress, @unchecked Swift.Sendable { + public static let family = sa_family_t(AF_UNSPEC) + +#if compiler(>=6.0) + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { + try withUnsafeBytes(of: self) { (p) throws(E) -> R in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } + + public mutating func withMutableSockAddr(_ body: (UnsafeMutablePointer) throws(E) -> R) throws(E) -> R { + try withUnsafeMutableBytes(of: &self) { (p) throws(E) -> R in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self)) + } + } +#else + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { + try withUnsafeBytes(of: self) { p in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } + + public mutating func withMutableSockAddr(_ body: (UnsafeMutablePointer) throws -> R) rethrows -> R { + try withUnsafeMutableBytes(of: &self) { p in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self)) + } + } +#endif +} + +extension sockaddr_in: SocketAddress, @unchecked Swift.Sendable { + public static let family = sa_family_t(AF_INET) + +#if compiler(>=6.0) + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { + try withUnsafeBytes(of: self) { (p) throws(E) -> R in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } +#else + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { + try withUnsafeBytes(of: self) { p in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } +#endif + +} + +extension sockaddr_in6: SocketAddress, @unchecked Swift.Sendable { + public static let family = sa_family_t(AF_INET6) + +#if compiler(>=6.0) + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { + try withUnsafeBytes(of: self) { (p) throws(E) -> R in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } +#else + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { + try withUnsafeBytes(of: self) { p in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } +#endif + +} + +extension sockaddr_un: SocketAddress, @unchecked Swift.Sendable { + public static let family = sa_family_t(AF_UNIX) + +#if compiler(>=6.0) + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws(E) -> R) throws(E) -> R { + try withUnsafeBytes(of: self) { (p) throws(E) -> R in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } +#else + public func withSockAddr(_ body: (UnsafePointer, socklen_t) throws -> R) rethrows -> R { + try withUnsafeBytes(of: self) { p in + try body(p.baseAddress!.assumingMemoryBound(to: sockaddr.self), socklen_t(p.count)) + } + } +#endif + +======= +extension sockaddr_storage: SocketAddress, @retroactive @unchecked Sendable { + public static let family = sa_family_t(AF_UNSPEC) +} + +extension sockaddr_in: SocketAddress, @retroactive @unchecked Sendable { + public static let family = sa_family_t(AF_INET) +} + +extension sockaddr_in6: SocketAddress, @retroactive @unchecked Sendable { + public static let family = sa_family_t(AF_INET6) +} + +extension sockaddr_un: SocketAddress, @retroactive @unchecked Sendable { + public static let family = sa_family_t(AF_UNIX) +>>>>>>> 9053e23 (remove Swift 5) +} + +public extension SocketAddress { + static func make(from storage: sockaddr_storage) throws -> Self { + guard self is sockaddr_storage.Type || storage.ss_family == family else { + throw SocketError.unsupportedAddress + } + var storage = storage + return withUnsafePointer(to: &storage) { + $0.withMemoryRebound(to: Self.self, capacity: 1) { + $0.pointee + } + } + } +} + +extension Socket { + + public enum Address: Sendable, Hashable { + case ip4(String, port: UInt16) + case ip6(String, port: UInt16) + case unix(String) + } + + public static func makeAddress(from addr: sockaddr_storage) throws -> Address { + switch Int32(addr.ss_family) { + case AF_INET: + var addr_in = try sockaddr_in.make(from: addr) + let maxLength = socklen_t(INET_ADDRSTRLEN) + let buffer = UnsafeMutablePointer.allocate(capacity: Int(maxLength)) + defer { buffer.deallocate() } + try Socket.inet_ntop(AF_INET, &addr_in.sin_addr, buffer, maxLength) + return .ip4(String(cString: buffer), port: UInt16(addr_in.sin_port).byteSwapped) + + case AF_INET6: + var addr_in6 = try sockaddr_in6.make(from: addr) + let maxLength = socklen_t(INET6_ADDRSTRLEN) + let buffer = UnsafeMutablePointer.allocate(capacity: Int(maxLength)) + defer { buffer.deallocate() } + try Socket.inet_ntop(AF_INET6, &addr_in6.sin6_addr, buffer, maxLength) + return .ip6(String(cString: buffer), port: UInt16(addr_in6.sin6_port).byteSwapped) + + case AF_UNIX: + var sockaddr_un = try sockaddr_un.make(from: addr) + return withUnsafePointer(to: &sockaddr_un.sun_path.0) { + return .unix(String(cString: $0)) + } + default: + throw SocketError.unsupportedAddress + } + } + + public static func unlink(_ address: sockaddr_un) throws { + var address = address + guard Socket.unlink(&address.sun_path.0) == 0 else { + throw SocketError.makeFailed("unlink") + } + } + + static func makeAddressINET(fromIP4 ip: String, port: UInt16) throws -> sockaddr_in { + var address = Socket.makeAddressINET(port: port) + address.sin_addr = try Socket.makeInAddr(fromIP4: ip) + return address + } + + static func makeInAddr(fromIP4 address: String) throws -> in_addr { + var addr = in_addr() + guard address.withCString({ Socket.inet_pton(AF_INET, $0, &addr) }) == 1 else { + throw SocketError.makeFailed("inet_pton AF_INET") + } + return addr + } + + static func makeInAddr(fromIP6 address: String) throws -> in6_addr { + var addr = in6_addr() + guard address.withCString({ Socket.inet_pton(AF_INET6, $0, &addr) }) == 1 else { + throw SocketError.makeFailed("inet_pton AF_INET6") + } + return addr + } +} diff --git a/FlyingSocks/Sources/Task+Timeout.swift b/FlyingSocks/Sources/Task+Timeout.swift index 7b0044c2..1f7920c1 100644 --- a/FlyingSocks/Sources/Task+Timeout.swift +++ b/FlyingSocks/Sources/Task+Timeout.swift @@ -34,7 +34,6 @@ import Foundation @available(*, unavailable, renamed: "SocketError.timeout") public typealias TimeoutError = SocketError -#if compiler(>=6.0) package func withThrowingTimeout( isolation: isolated (any Actor)? = #isolation, seconds: TimeInterval, @@ -65,40 +64,6 @@ package func withThrowingTimeout( } }.value } -#else -package func withThrowingTimeout( - seconds: TimeInterval, - body: () async throws -> T -) async throws -> T { - let transferringBody = { try await Transferring(body()) } - typealias NonSendableClosure = () async throws -> Transferring - typealias SendableClosure = @Sendable () async throws -> Transferring - return try await withoutActuallyEscaping(transferringBody) { - (_ fn: @escaping NonSendableClosure) async throws -> Transferring in - let sendableFn = unsafeBitCast(fn, to: SendableClosure.self) - return try await _withThrowingTimeout(seconds: seconds, body: sendableFn) - }.value -} - -// Sendable -private func _withThrowingTimeout( - seconds: TimeInterval, - body: @Sendable @escaping () async throws -> T -) async throws -> T { - try await withThrowingTaskGroup(of: T.self) { group in - group.addTask { - try await body() - } - group.addTask { - try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000)) - throw SocketError.makeTaskTimeout(seconds: seconds) - } - let success = try await group.next()! - group.cancelAll() - return success - } -} -#endif package extension Task { diff --git a/FlyingSocks/Tests/IdentifiableContinuationTests.swift b/FlyingSocks/Tests/IdentifiableContinuationTests.swift index aa746773..9e746e62 100644 --- a/FlyingSocks/Tests/IdentifiableContinuationTests.swift +++ b/FlyingSocks/Tests/IdentifiableContinuationTests.swift @@ -183,38 +183,22 @@ private actor Waiter { func makeTask(delay: TimeInterval = 0, onCancel: T) -> Task where E == Never { Task { try? await Task.sleep(seconds: delay) -#if compiler(>=6.0) return await withIdentifiableContinuation { addContinuation($0) } onCancel: { id in Task { await self.resumeID(id, returning: onCancel) } } -#else - return await withIdentifiableContinuation(isolation: self) { - addContinuation($0) - } onCancel: { id in - Task { await self.resumeID(id, returning: onCancel) } - } -#endif } } func makeTask(delay: TimeInterval = 0, onCancel: Result) -> Task where E == any Error { Task { try? await Task.sleep(seconds: delay) -#if compiler(>=6.0) return try await withIdentifiableThrowingContinuation { addContinuation($0) } onCancel: { id in Task { await self.resumeID(id, with: onCancel) } } -#else - return try await withIdentifiableThrowingContinuation(isolation: self) { - addContinuation($0) - } onCancel: { id in - Task { await self.resumeID(id, with: onCancel) } - } -#endif } } @@ -251,22 +235,14 @@ private extension Actor { body: @Sendable (IdentifiableContinuation) -> Void, onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } ) async -> T { -#if compiler(>=6.0) await withIdentifiableContinuation(body: body, onCancel: handler) -#else - await withIdentifiableContinuation(isolation: self, body: body, onCancel: handler) -#endif } func throwingIdentifiableContinuation( body: @Sendable (IdentifiableContinuation) -> Void, onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } ) async throws -> T { -#if compiler(>=6.0) try await withIdentifiableThrowingContinuation(body: body, onCancel: handler) -#else - try await withIdentifiableThrowingContinuation(isolation: self, body: body, onCancel: handler) -#endif } } diff --git a/FlyingSocks/XCTests/AsyncBufferedDataSequenceTests.swift b/FlyingSocks/XCTests/AsyncBufferedDataSequenceTests.swift deleted file mode 100644 index d2153823..00000000 --- a/FlyingSocks/XCTests/AsyncBufferedDataSequenceTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AsyncBufferedDataSequenceTests.swift -// FlyingFox -// -// Created by Simon Whitty on 10/07/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import FlyingSocks -import Foundation -import XCTest - -final class AsyncBufferedDataSequenceTests: XCTestCase { - - func testSequence() async { - let buffer = AsyncBufferedCollection(bytes: [ - 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, - 0x8, 0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF - ]) - - await AsyncAssertEqual( - await buffer.collectBuffers(ofLength: 5), - [ - Data([0x0, 0x1, 0x2, 0x3, 0x4]), - Data([0x5, 0x6, 0x7, 0x8, 0x9]), - Data([0xA, 0xB, 0xC, 0xD, 0xE]), - Data([0xF]) - ] - ) - } - - func testSequenceCanBeIteratorMultipleTimes() async { - let buffer = AsyncBufferedCollection(bytes: [ - 0x0, 0x1, 0x2 - ]) - - await AsyncAssertEqual( - await buffer.collectBuffers(ofLength: 5), - [Data([0x0, 0x1, 0x2])] - ) - - await AsyncAssertEqual( - await buffer.collectBuffers(ofLength: 5), - [Data([0x0, 0x1, 0x2])] - ) - } -} - -private extension AsyncBufferedSequence { - - func collectBuffers(ofLength count: Int) async -> [AsyncIterator.Buffer] { - var collected = [AsyncIterator.Buffer]() - var iterator = makeAsyncIterator() - - while let buffer = try? await iterator.nextBuffer(suggested: count) { - collected.append(buffer) - } - return collected - } -} diff --git a/FlyingSocks/XCTests/AsyncBufferedEmptySequenceTests.swift b/FlyingSocks/XCTests/AsyncBufferedEmptySequenceTests.swift deleted file mode 100644 index 20e6707c..00000000 --- a/FlyingSocks/XCTests/AsyncBufferedEmptySequenceTests.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// AsyncBufferedEmptySequenceTests.swift -// FlyingFox -// -// Created by Simon Whitty on 06/08/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import Foundation -import XCTest - -final class AsyncBufferedEmptySequenceTests: XCTestCase { - - func testCompletesImmediatley() async { - var iterator = AsyncBufferedEmptySequence(completeImmediately: true) - .makeAsyncIterator() - - await AsyncAssertNil( - await iterator.nextBuffer(suggested: 1) - ) - } - - func testCancels_AfterWaiting() async { - let task = Task { - await AsyncBufferedEmptySequence(completeImmediately: false) - .first { _ in true } - } - - try? await Task.sleep(seconds: 0.05) - task.cancel() - await AsyncAssertNil( - await task.value - ) - } - - func testCancels_Immediatley() async { - let task = Task { - try? await Task.sleep(seconds: 0.05) - var iterator = AsyncBufferedEmptySequence(completeImmediately: false) - .makeAsyncIterator() - return await iterator.nextBuffer(suggested: 1) - } - - task.cancel() - await AsyncAssertNil( - await task.value - ) - } -} diff --git a/FlyingSocks/XCTests/AsyncBufferedFileSequenceTests.swift b/FlyingSocks/XCTests/AsyncBufferedFileSequenceTests.swift deleted file mode 100644 index da60f1f9..00000000 --- a/FlyingSocks/XCTests/AsyncBufferedFileSequenceTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// AsyncBufferedFileSequenceTests.swift -// FlyingFox -// -// Created by Simon Whitty on 06/08/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import Foundation -import XCTest - -final class AsyncBufferedFileSequenceTests: XCTestCase { - - func testFileSize() { - XCTAssertEqual( - try AsyncBufferedFileSequence.fileSize(at: .jackOfHeartsRecital), - 299 - ) - XCTAssertThrowsError( - try AsyncBufferedFileSequence.fileSize(at: URL(fileURLWithPath: "missing")) - ) - - XCTAssertThrowsError( - try AsyncBufferedFileSequence.fileSize(from: [:]) - ) - } - - func testFileHandleRead() throws { - let handle = try FileHandle(forReadingFrom: .jackOfHeartsRecital) - XCTAssertEqual( - try handle.read(suggestedCount: 14, forceLegacy: false), - "Two doors down".data(using: .utf8) - ) - XCTAssertEqual( - try handle.read(suggestedCount: 9, forceLegacy: true), - " the boys".data(using: .utf8) - ) - } - - func testReadsEntireFile() async throws { - let sequence = try AsyncBufferedFileSequence(contentsOf: .jackOfHeartsRecital) - - await AsyncAssertEqual( - try await sequence.getAllData(), - try Data(contentsOf: .jackOfHeartsRecital) - ) - } -} - -private extension URL { - static var jackOfHeartsRecital: URL { - Bundle.module.url(forResource: "Resources", withExtension: nil)! - .appendingPathComponent("JackOfHeartsRecital.txt") - } -} diff --git a/FlyingSocks/XCTests/AsyncSharedReplaySequenceTests.swift b/FlyingSocks/XCTests/AsyncSharedReplaySequenceTests.swift deleted file mode 100644 index 9f9234d9..00000000 --- a/FlyingSocks/XCTests/AsyncSharedReplaySequenceTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// AsyncSharedReplaySequenceTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/06/2024. -// Copyright © 2024 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import FlyingSocks -import Foundation -import XCTest - -final class AsyncSharedReplaySequenceTests: XCTestCase { - - func testSequenceCanBeIteratorMultipleTimes() async { - let sequence = AsyncSharedReplaySequence.make(string: "The quick brown fox") - var iterator = sequence.makeAsyncIterator() - - await AsyncAssertEqual( - try await iterator.collectBufferStrings(ofLength: 5), - [ - "The q", - "uick ", - "brown", - " fox" - ] - ) - - iterator = sequence.makeAsyncIterator() - await AsyncAssertEqual( - try await iterator.collectBufferStrings(ofLength: 10), - [ - "The quick ", - "brown fox" - ] - ) - } - - func testSequenceCanBeIteratorMultipleTimesA() async { - - let sequence = AsyncSharedReplaySequence.make(string: "The quick brown fox") - var iterator = sequence.makeAsyncIterator() - - await AsyncAssertEqual( - try await iterator.next(), - Character("T").asciiValue - ) - - iterator = sequence.makeAsyncIterator() - await AsyncAssertEqual( - try await iterator.next(), - Character("T").asciiValue - ) - } - - func testCanBeCancelled() async { - let task = Task { - var iterator = AsyncSharedReplaySequence - .makeEmpty() - .makeAsyncIterator() - return try await iterator.nextBuffer(suggested: 1) - } - - try? await Task.sleep(seconds: 0.05) - task.cancel() - - await AsyncAssertNil( - try await task.value - ) - } -} - -private extension AsyncSharedReplaySequence where Base == ConsumingAsyncSequence> { - - static func make(bytes: Data, count: Int?) -> Self { - AsyncSharedReplaySequence( - base: ConsumingAsyncSequence(bytes), - count: count - ) - } - - static func make(string: String) -> Self { - let data = string.data(using: .utf8)! - return make(bytes: data, count: data.count) - } -} - -private extension AsyncSharedReplaySequence where Base == AsyncBufferedEmptySequence { - - static func makeEmpty(count: Int = 100) -> Self { - AsyncSharedReplaySequence( - base: AsyncBufferedEmptySequence(completeImmediately: false), - count: count - ) - } -} - -private extension AsyncSharedReplaySequence.AsyncIterator { - - mutating func collectBuffers(ofLength count: Int) async throws -> [Buffer] { - var collected = [Buffer]() - while let buffer = try await nextBuffer(suggested: count) { - collected.append(buffer) - } - return collected - } -} - -private extension AsyncSharedReplaySequence.AsyncIterator where Element == UInt8 { - - mutating func collectBufferStrings(ofLength count: Int) async throws -> [String] { - try await collectBuffers(ofLength: count) - .map { - String(data: Data($0), encoding: .utf8)! - } - } -} diff --git a/FlyingSocks/XCTests/AsyncSocketTests.swift b/FlyingSocks/XCTests/AsyncSocketTests.swift deleted file mode 100644 index 01120571..00000000 --- a/FlyingSocks/XCTests/AsyncSocketTests.swift +++ /dev/null @@ -1,208 +0,0 @@ -// -// AsyncSocketTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest - -final class AsyncSocketTests: XCTestCase { - - func testSocketReadsByte_WhenAvailable() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - - async let d2 = s2.read() - try await s1.write(Data([10])) - let v2 = try await d2 - XCTAssertEqual(v2, 10) - - try s1.close() - try s2.close() - } - - func testSocketReadsChunk_WhenAvailable() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - - async let d2 = s2.readString(length: 12) - Task { - try await s1.writeString("Fish & Chips") - } - - let text = try await d2 - XCTAssertEqual(text, "Fish & Chips") - } - - func testSocketWrite_WaitsWhenBufferIsFull() async throws { - let (s1, s2) = try await AsyncSocket.makePair() - try s1.socket.setValue(1024, for: .sendBufferSize) - - let task = Task { - try await s1.write(Data(repeating: 0x01, count: 8192)) - } - - _ = try await s2.read(bytes: 8192) - try await task.value - } - - func testSocketReadByte_ThrowsDisconnected_WhenSocketIsClosed() async throws { - let s1 = try await AsyncSocket.make() - try s1.close() - - await AsyncAssertThrowsError(try await s1.read(), of: SocketError.self) { - XCTAssertEqual($0, .disconnected) - } - } - - func testSocketRead0Byte_ReturnsEmptyArray() async throws { - let s1 = try await AsyncSocket.make() - - let bytes = try await s1.read(bytes: 0) - XCTAssertEqual(bytes, []) - } - - func testSocketReadByte_Throws_WhenSocketIsNotOpen() async throws { - let s1 = try await AsyncSocket.make() - - await AsyncAssertThrowsError(try await s1.read(), of: SocketError.self) - } - - func testSocketReadChunk_ThrowsDisconnected_WhenSocketIsClosed() async throws { - let s1 = try await AsyncSocket.make() - try s1.close() - - await AsyncAssertThrowsError(try await s1.read(bytes: 5), of: SocketError.self) { - XCTAssertEqual($0, .disconnected) - } - } - - func testSocketReadChunk_Throws_WhenSocketIsNotOpen() async throws { - let s1 = try await AsyncSocket.make() - - await AsyncAssertThrowsError(try await s1.read(bytes: 5), of: SocketError.self) - } - - func disabled_testSocketBytesReadChunk_Throws_WhenSocketIsClosed() async throws { - let s1 = try await AsyncSocket.make() - try s1.close() - - let bytes = s1.bytes - await AsyncAssertThrowsError(try await bytes.nextBuffer(suggested: 1), of: SocketError.self) - } - - func testSocketBytesReadChunk_Throws_WhenSocketIsNotOpen() async throws { - let s1 = try await AsyncSocket.make() - - let bytes = s1.bytes - await AsyncAssertThrowsError(try await bytes.nextBuffer(suggested: 1), of: SocketError.self) - } - - func testSocketWrite_ThrowsDisconnected_WhenSocketIsClosed() async throws { - let s1 = try await AsyncSocket.make() - try s1.close() - - await AsyncAssertThrowsError(try await s1.writeString("Fish"), of: SocketError.self) { - XCTAssertEqual($0, .disconnected) - } - } - - func testSocketWrite_Throws_WhenSocketIsNotConnected() async throws { - let s1 = try await AsyncSocket.make() - await AsyncAssertThrowsError(try await s1.writeString("Fish"), of: SocketError.self) - } - - func testSocketAccept_Throws_WhenSocketIsClosed() async throws { - let s1 = try await AsyncSocket.make() - - await AsyncAssertThrowsError(try await s1.accept(), of: SocketError.self) - } - - func disabled_testSocket_Throws_WhenAlreadyCLosed() async throws { - let s1 = try await AsyncSocket.make() - - try s1.close() - await AsyncAssertThrowsError(try s1.close(), of: SocketError.self) - } - - func testSocketSequence_Ends_WhenDisconnected() async throws { - let s1 = try AsyncSocket.makeListening(pool: DisconnectedPool()) - var sockets = s1.sockets - await AsyncAssertNil( - try await sockets.next() - ) - } -} - -extension AsyncSocket { - - static func make() async throws -> AsyncSocket { - try await make(pool: .client) - } - - static func makeListening(pool: some AsyncSocketPool) throws -> AsyncSocket { - let address = sockaddr_un.unix(path: #function) - try? Socket.unlink(address) - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - try socket.setValue(true, for: .localAddressReuse) - try socket.bind(to: address) - try socket.listen() - return try AsyncSocket(socket: socket, pool: pool) - } - - static func make(pool: some AsyncSocketPool) throws -> AsyncSocket { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - return try AsyncSocket(socket: socket, pool: pool) - } - - static func makePair() async throws -> (AsyncSocket, AsyncSocket) { - try await makePair(pool: .client) - } - - func writeString(_ string: String) async throws { - try await write(string.data(using: .utf8)!) - } - - func readString(length: Int) async throws -> String { - let bytes = try await read(bytes: length) - guard let string = String(data: Data(bytes), encoding: .utf8) else { - throw SocketError.makeFailed("Read") - } - return string - } -} - -struct DisconnectedPool: AsyncSocketPool { - - func prepare() async throws { } - - func run() async throws { } - - func suspendSocket(_ socket: FlyingSocks.Socket, untilReadyFor events: FlyingSocks.Socket.Events) async throws { - throw SocketError.disconnected - } -} diff --git a/FlyingSocks/XCTests/IdentifiableContinuationTests.swift b/FlyingSocks/XCTests/IdentifiableContinuationTests.swift deleted file mode 100644 index cf2849ba..00000000 --- a/FlyingSocks/XCTests/IdentifiableContinuationTests.swift +++ /dev/null @@ -1,255 +0,0 @@ -// -// IdentifiableContinuationTests.swift -// IdentifiableContinuation -// -// Created by Simon Whitty on 20/05/2023. -// Copyright 2023 Simon Whitty -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/IdentifiableContinuation -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import FlyingSocks -import XCTest - -final class IdentifiableContinuationAsyncTests: XCTestCase { - - func testResumesWithValue() async { - let waiter = Waiter() - let val = await waiter.identifiableContinuation { - $0.resume(returning: "Fish") - } - - XCTAssertEqual(val, "Fish") - } - - func testResumesWithVoid() async { - let waiter = Waiter() - await waiter.identifiableContinuation { - $0.resume() - } - } - - func testResumesWithResult() async { - let waiter = Waiter() - let val = await waiter.identifiableContinuation { - $0.resume(with: .success("Chips")) - } - - XCTAssertEqual(val, "Chips") - } - - func testCancels_After_Created() async { - let waiter = Waiter() - - let task = await waiter.makeTask(onCancel: nil) - try? await Task.sleep(seconds: 0.1) - var isEmpty = await waiter.isEmpty - XCTAssertFalse(isEmpty) - task.cancel() - - let val = await task.value - XCTAssertNil(val) - - isEmpty = await waiter.isEmpty - XCTAssertTrue(isEmpty) - } - - func testCancels_Before_Created() async { - let waiter = Waiter() - - let task = await waiter.makeTask(delay: 1.0, onCancel: nil) - try? await Task.sleep(seconds: 0.1) - let isEmpty = await waiter.isEmpty - XCTAssertTrue(isEmpty) - task.cancel() - - let val = await task.value - XCTAssertNil(val) - } - - func testThrowingResumesWithValue() async { - let waiter = Waiter() - let task = Task { - try await waiter.throwingIdentifiableContinuation { - $0.resume(returning: "Fish") - } - } - - let result = await task.result - XCTAssertEqual(try result.get(), "Fish") - } - - func testThrowingResumesWithError() async { - let waiter = Waiter() - let task = Task { - try await waiter.throwingIdentifiableContinuation { - $0.resume(throwing: CancellationError()) - } - } - - let result = await task.result - XCTAssertThrowsError(try result.get()) - } - - func testThrowingResumesWithResult() async { - let waiter = Waiter() - let task = Task { - try await waiter.throwingIdentifiableContinuation { - $0.resume(with: .success("Fish")) - } - } - - let result = await task.result - XCTAssertEqual(try result.get(), "Fish") - } - - func testThrowingCancels_After_Created() async { - let waiter = Waiter() - - let task = await waiter.makeTask(onCancel: .failure(CancellationError())) - try? await Task.sleep(seconds: 0.1) - var isEmpty = await waiter.isEmpty - XCTAssertFalse(isEmpty) - task.cancel() - - let result = await task.result - XCTAssertThrowsError(try result.get()) - - isEmpty = await waiter.isEmpty - XCTAssertTrue(isEmpty) - } - - func testThrowingCancels_Before_Created() async { - let waiter = Waiter() - - let task = await waiter.makeTask(delay: 1.0, onCancel: .failure(CancellationError())) - try? await Task.sleep(seconds: 0.1) - let isEmpty = await waiter.isEmpty - XCTAssertTrue(isEmpty) - task.cancel() - - let result = await task.result - XCTAssertThrowsError(try result.get()) - } -} - -private actor Waiter { - typealias Continuation = IdentifiableContinuation - - private var waiting = [Continuation.ID: Continuation]() - - var isEmpty: Bool { - waiting.isEmpty - } - - func makeTask(delay: TimeInterval = 0, onCancel: T) -> Task where E == Never { - Task { - try? await Task.sleep(seconds: delay) -#if compiler(>=6.0) - return await withIdentifiableContinuation { - addContinuation($0) - } onCancel: { id in - Task { await self.resumeID(id, returning: onCancel) } - } -#else - return await withIdentifiableContinuation(isolation: self) { - addContinuation($0) - } onCancel: { id in - Task { await self.resumeID(id, returning: onCancel) } - } -#endif - } - } - - func makeTask(delay: TimeInterval = 0, onCancel: Result) -> Task where E == any Error { - Task { - try? await Task.sleep(seconds: delay) -#if compiler(>=6.0) - return try await withIdentifiableThrowingContinuation { - addContinuation($0) - } onCancel: { id in - Task { await self.resumeID(id, with: onCancel) } - } -#else - return try await withIdentifiableThrowingContinuation(isolation: self) { - addContinuation($0) - } onCancel: { id in - Task { await self.resumeID(id, with: onCancel) } - } -#endif - } - } - - private func addContinuation(_ continuation: Continuation) { - assertIsolated() - waiting[continuation.id] = continuation - } - - private func resumeID(_ id: Continuation.ID, returning value: T) { - assertIsolated() - if let continuation = waiting.removeValue(forKey: id) { - continuation.resume(returning: value) - } - } - - private func resumeID(_ id: Continuation.ID, throwing error: E) { - assertIsolated() - if let continuation = waiting.removeValue(forKey: id) { - continuation.resume(throwing: error) - } - } - - private func resumeID(_ id: Continuation.ID, with result: Result) { - assertIsolated() - if let continuation = waiting.removeValue(forKey: id) { - continuation.resume(with: result) - } - } -} - -private extension Actor { - - func identifiableContinuation( - body: @Sendable (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } - ) async -> T { -#if compiler(>=6.0) - await withIdentifiableContinuation(body: body, onCancel: handler) -#else - await withIdentifiableContinuation(isolation: self, body: body, onCancel: handler) -#endif - } - - func throwingIdentifiableContinuation( - body: @Sendable (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } - ) async throws -> T { -#if compiler(>=6.0) - try await withIdentifiableThrowingContinuation(body: body, onCancel: handler) -#else - try await withIdentifiableThrowingContinuation(isolation: self, body: body, onCancel: handler) -#endif - - } -} diff --git a/FlyingSocks/XCTests/Logging+OSLogTests.swift b/FlyingSocks/XCTests/Logging+OSLogTests.swift deleted file mode 100644 index 4f55977e..00000000 --- a/FlyingSocks/XCTests/Logging+OSLogTests.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// Logging+OSLogTests.swift -// FlyingFox -// -// Created by Andre Jacobs on 06/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// -#if canImport(OSLog) -@testable import FlyingSocks -import Foundation -import OSLog -import XCTest - -final class LoggingOSLogTests: XCTestCase { - - func testInfo() { - guard #available(macOS 11.0, iOS 14.0, tvOS 14.0, *) else { return } - // NOTE: For now this test is only used to verify the output by manual confirmation (e.g. Console.app or log tool) - // Run log tool in the terminal first and then run this unit-test: - // log stream --level debug --predicate 'category == "FlyingFox"' - let logger = OSLogLogger.oslog(category: "Fox") - - logger.logDebug("alpha") - logger.logInfo("bravo") - logger.logWarning("charlie") - logger.logError("delta") - logger.logCritical("echo") - } -} - -#endif // canImport(OSLog) diff --git a/FlyingSocks/XCTests/Logging+PrintTests.swift b/FlyingSocks/XCTests/Logging+PrintTests.swift deleted file mode 100644 index f821ca45..00000000 --- a/FlyingSocks/XCTests/Logging+PrintTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// Logging+PrintTests.swift -// FlyingFox -// -// Created by Simon Whitty on 23/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import Foundation -import XCTest - -final class LoggingTests: XCTestCase { - - func testPrintLogger_SetsCategory() { - let logger = PrintLogger.print(category: "Fish") - - XCTAssertEqual( - logger.category, - "Fish" - ) - } - - func testPrintLogger_output() { - // NOTE: For now this test is only used to verify the output by manual confirmation - // until Swift.print can be unit-tested or we are able to inject a mock. - let logger = PrintLogger.print(category: "Fox") - - logger.logDebug("alpha") - logger.logInfo("bravo") - logger.logWarning("charlie") - logger.logError("delta") - logger.logCritical("echo") - } -} diff --git a/FlyingSocks/XCTests/MutexXCTests.swift b/FlyingSocks/XCTests/MutexXCTests.swift deleted file mode 100644 index 3387312e..00000000 --- a/FlyingSocks/XCTests/MutexXCTests.swift +++ /dev/null @@ -1,78 +0,0 @@ -// -// MutexXCTests.swift -// swift-mutex -// -// Created by Simon Whitty on 07/09/2024. -// Copyright 2024 Simon Whitty -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/swift-mutex -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -#if !canImport(Testing) -@testable import FlyingSocks -import XCTest - -final class MutexXCTests: XCTestCase { - - func testWithLock_ReturnsValue() { - let mutex = Mutex("fish") - let val = mutex.withLock { - $0 + " & chips" - } - XCTAssertEqual(val, "fish & chips") - } - - func testWithLock_ThrowsError() { - let mutex = Mutex("fish") - XCTAssertThrowsError(try mutex.withLock { _ -> Void in throw CancellationError() }) { - _ = $0 is CancellationError - } - } - - func testLockIfAvailable_ReturnsValue() { - let mutex = Mutex("fish") - mutex.unsafeLock() - XCTAssertNil( - mutex.withLockIfAvailable { _ in "chips" } - ) - mutex.unsafeUnlock() - XCTAssertEqual( - mutex.withLockIfAvailable { _ in "chips" }, - "chips" - ) - } - - func testWithLockIfAvailable_ThrowsError() { - let mutex = Mutex("fish") - XCTAssertThrowsError(try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() }) { - _ = $0 is CancellationError - } - } -} - -extension Mutex { - func unsafeLock() { storage.lock() } - func unsafeUnlock() { storage.unlock() } -} -#endif diff --git a/FlyingSocks/XCTests/Resources/JackOfHeartsRecital.txt b/FlyingSocks/XCTests/Resources/JackOfHeartsRecital.txt deleted file mode 100644 index 369a3f46..00000000 --- a/FlyingSocks/XCTests/Resources/JackOfHeartsRecital.txt +++ /dev/null @@ -1,5 +0,0 @@ -Two doors down the boys finally made it through the wall -And cleaned out the bank safe, it's said they got off with quite a haul. -In the darkness by the riverbed they waited on the ground -For one more member who had business back in town. -But they couldn't go no further without the Jack of Hearts. diff --git a/FlyingSocks/XCTests/SocketAddress+Glibc.swift b/FlyingSocks/XCTests/SocketAddress+Glibc.swift deleted file mode 100644 index d0443c4b..00000000 --- a/FlyingSocks/XCTests/SocketAddress+Glibc.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// SocketAddress+Glibc.swift -// FlyingFox -// -// Created by Simon Whitty on 15/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -#if canImport(Glibc) -// swift on linux fails to import comformance for these Glibc types 🤷🏻‍♂️: -import Glibc -import FlyingSocks - -extension sockaddr_in: SocketAddress { } - -extension sockaddr_in6: SocketAddress { } - -extension sockaddr_un: SocketAddress { } - -#endif diff --git a/FlyingSocks/XCTests/SocketAddressTests.swift b/FlyingSocks/XCTests/SocketAddressTests.swift deleted file mode 100644 index acf6153c..00000000 --- a/FlyingSocks/XCTests/SocketAddressTests.swift +++ /dev/null @@ -1,321 +0,0 @@ -// -// SocketAddressTests.swift -// FlyingFox -// -// Created by Simon Whitty on 15/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest -import Foundation - -final class SocketAddressTests: XCTestCase { - - func testINET_IsCorrectlyDecodedFromStorage() { - let storage = sockaddr_in - .inet(port: 8001) - .makeStorage() - - XCTAssertEqual( - try sockaddr_in.make(from: storage) - .sin_port - .bigEndian, - 8001 - ) - } - - func testINET_ThrowsInvalidAddress_WhenFamilyIncorrect() { - let storage = sockaddr_un - .unix(path: "/var") - .makeStorage() - - XCTAssertThrowsError( - try sockaddr_in.make(from: storage), - of: SocketError.self - ) { - XCTAssertEqual($0, .unsupportedAddress) - } - } - - func testAddress_DecodesIP4() throws { - let addr = try Socket.makeAddressINET(fromIP4: "192.168.0.1", - port: 1080) - - XCTAssertEqual( - try Socket.makeAddress(from: addr.makeStorage()), - .ip4("192.168.0.1", port: 1080) - ) - } - - func testInvalidIP4_ThrowsError() { - XCTAssertThrowsError( - try Socket.makeInAddr(fromIP4: "192.168.0") - ) - XCTAssertThrowsError( - try sockaddr_in.inet(ip4: "::1", port: 80) - ) - } - - func testINET6_IsCorrectlyDecodedFromStorage() throws { - let storage = sockaddr_in6 - .inet6(port: 8080) - .makeStorage() - - XCTAssertEqual( - try sockaddr_in6.make(from: storage) - .sin6_port - .bigEndian, - 8080 - ) - } - - func testINET6_ThrowsInvalidAddress_WhenFamilyIncorrect() throws { - let storage = sockaddr_un - .unix(path: "/var") - .makeStorage() - - XCTAssertThrowsError( - try sockaddr_in6.make(from: storage), - of: SocketError.self - ) { - XCTAssertEqual($0, .unsupportedAddress) - } - } - - func testAddress_DecodesIP6() throws { - var addr = Socket.makeAddressINET6(port: 5010) - addr.sin6_addr = try Socket.makeInAddr(fromIP6: "::1") - - XCTAssertEqual( - try Socket.makeAddress(from: addr.makeStorage()), - .ip6("::1", port: 5010) - ) - } - - func testLoopbackAddress_DecodesIP6() throws { - let loopback = sockaddr_in6.loopback(port: 5060) - - XCTAssertEqual( - try Socket.makeAddress(from: loopback.makeStorage()), - .ip6("::1", port: 5060) - ) - } - - func testInvalidIP6_ThrowsError() { - XCTAssertThrowsError( - try Socket.makeInAddr(fromIP6: "192.168.0") - ) - XCTAssertThrowsError( - try sockaddr_in6.inet6(ip6: ":10:::::", port: 80) - ) - } - - func testUnix_IsCorrectlyDecodedFromStorage() throws { - let storage = sockaddr_un - .unix(path: "/var") - .makeStorage() - - var unix = try sockaddr_un.make(from: storage) - let path = withUnsafePointer(to: &unix.sun_path.0) { - return String(cString: $0) - } - XCTAssertEqual( - path, - "/var" - ) - } - - func testUnix_ThrowsInvalidAddress_WhenFamilyIncorrect() { - let storage = sockaddr_in6 - .inet6(port: 8080) - .makeStorage() - - XCTAssertThrowsError( - try sockaddr_un.make(from: storage), - of: SocketError.self - ) { - XCTAssertEqual($0, .unsupportedAddress) - } - } - - func testUnlinkUnix_Throws_WhenPathIsInvalid() { - XCTAssertThrowsError( - try Socket.unlink(sockaddr_un()), - of: SocketError.self - ) - } - - func testIPX_ThrowsInvalidAddress() { - var storage = sockaddr_storage() - storage.ss_family = sa_family_t(AF_IPX) - - XCTAssertThrowsError( - try Socket.makeAddress(from: storage), - of: SocketError.self - ) { - XCTAssertEqual($0, .unsupportedAddress) - } - } - - func testSockaddrInToStorageConversion() throws { - var addrIn = sockaddr_in() - addrIn.sin_family = sa_family_t(AF_INET) - addrIn.sin_port = UInt16(8080).bigEndian - addrIn.sin_addr = try Socket.makeInAddr(fromIP4: "192.168.1.1") - - let storage = addrIn.makeStorage() - - XCTAssertEqual(storage.ss_family, sa_family_t(AF_INET)) - withUnsafeBytes(of: addrIn) { addrInPtr in - let storagePtr = addrInPtr.bindMemory(to: sockaddr_storage.self) - XCTAssertEqual(storagePtr.baseAddress!.pointee.ss_family, sa_family_t(AF_INET)) - let sockaddrInPtr = addrInPtr.bindMemory(to: sockaddr_in.self) - XCTAssertEqual(sockaddrInPtr.baseAddress!.pointee.sin_port, addrIn.sin_port) - XCTAssertEqual(sockaddrInPtr.baseAddress!.pointee.sin_addr.s_addr, addrIn.sin_addr.s_addr) - } - } - - func testSockaddrIn6ToStorageConversion() throws { - var addrIn6 = sockaddr_in6() - addrIn6.sin6_family = sa_family_t(AF_INET6) - addrIn6.sin6_port = UInt16(9090).bigEndian - addrIn6.sin6_addr = try Socket.makeInAddr(fromIP6: "fe80::1") - - let storage = addrIn6.makeStorage() - - XCTAssertEqual(storage.ss_family, sa_family_t(AF_INET6)) - XCTAssertEqual(storage.ss_family, addrIn6.sin6_family) - - withUnsafeBytes(of: addrIn6) { addrIn6Ptr in - let storagePtr = addrIn6Ptr.bindMemory(to: sockaddr_storage.self) - XCTAssertEqual(storagePtr.baseAddress!.pointee.ss_family, sa_family_t(AF_INET6)) - let sockaddrIn6Ptr = addrIn6Ptr.bindMemory(to: sockaddr_in6.self) - XCTAssertEqual(sockaddrIn6Ptr.baseAddress!.pointee.sin6_port, addrIn6.sin6_port) - let addrArray = withUnsafeBytes(of: sockaddrIn6Ptr.baseAddress!.pointee.sin6_addr) { - Array($0.bindMemory(to: UInt8.self)) - } - let expectedArray = withUnsafeBytes(of: addrIn6.sin6_addr) { - Array($0.bindMemory(to: UInt8.self)) - } - XCTAssertEqual(addrArray, expectedArray) - } - } - - func testSockaddrUnToStorageConversion() { - var addrUn = sockaddr_un() - addrUn.sun_family = sa_family_t(AF_UNIX) - let path = "/tmp/socket" - _ = path.withCString { pathPtr in - memcpy(&addrUn.sun_path, pathPtr, path.count + 1) - } - - let storage = addrUn.makeStorage() - - XCTAssertEqual(storage.ss_family, sa_family_t(AF_UNIX)) - let addrUnBytes = withUnsafeBytes(of: addrUn) { Data($0) } - let storageBytes = withUnsafeBytes(of: storage) { Data($0) } - let storageSockaddrUnBytes = storageBytes[0...size] - XCTAssertEqual(addrUnBytes, storageSockaddrUnBytes) - } - - func testInvalidAddressFamily() { - var storage = sockaddr_storage() - storage.ss_family = sa_family_t(AF_APPLETALK) // Invalid for our purpose - - XCTAssertThrowsError(try sockaddr_in.make(from: storage)) { error in - XCTAssertEqual(error as? SocketError, SocketError.unsupportedAddress) - } - - XCTAssertThrowsError(try sockaddr_in6.make(from: storage)) { error in - XCTAssertEqual(error as? SocketError, SocketError.unsupportedAddress) - } - - XCTAssertThrowsError(try sockaddr_un.make(from: storage)) { error in - XCTAssertEqual(error as? SocketError, SocketError.unsupportedAddress) - } - } - - func testMaximumPathLengthForUnixDomainSocket() { - var addrUn = sockaddr_un() - addrUn.sun_family = sa_family_t(AF_UNIX) - let maxPathLength = MemoryLayout.size - MemoryLayout.size - 1 - let maxPath = String(repeating: "a", count: maxPathLength) - _ = maxPath.withCString { pathPtr in - memcpy(&addrUn.sun_path, pathPtr, maxPath.count + 1) - } - - let storage = addrUn.makeStorage() - - XCTAssertEqual(storage.ss_family, sa_family_t(AF_UNIX)) - let addrUnBytes = withUnsafeBytes(of: addrUn) { Data($0) } - let storageBytes = withUnsafeBytes(of: storage) { Data($0) } - let storageAddrUnBytes = storageBytes.prefix(MemoryLayout.size) - XCTAssertEqual(addrUnBytes, storageAddrUnBytes) - } - - func testMemoryBoundsInMakeStorage() { - var addrIn = sockaddr_in() - addrIn.sin_family = sa_family_t(AF_INET) - - let storage = addrIn.makeStorage() - - let addrSize = MemoryLayout.size - let storageSize = MemoryLayout.size - XCTAssertLessThanOrEqual(addrSize, storageSize) - - withUnsafeBytes(of: addrIn) { addrInPtr in - let addrInBytes = addrInPtr.bindMemory(to: UInt8.self).baseAddress! - withUnsafeBytes(of: storage) { storagePtr in - let storageBytes = storagePtr.bindMemory(to: UInt8.self).baseAddress! - for i in 0.. sockaddr_storage { - var storage = sockaddr_storage() - var addr = self - let addrSize = MemoryLayout.size - let storageSize = MemoryLayout.size - - withUnsafePointer(to: &addr) { addrPtr in - let addrRawPtr = UnsafeRawPointer(addrPtr) - withUnsafeMutablePointer(to: &storage) { storagePtr in - let storageRawPtr = UnsafeMutableRawPointer(storagePtr) - let copySize = min(addrSize, storageSize) - storageRawPtr.copyMemory(from: addrRawPtr, byteCount: copySize) - } - } - return storage - } -} diff --git a/FlyingSocks/XCTests/SocketErrorTests.swift b/FlyingSocks/XCTests/SocketErrorTests.swift deleted file mode 100644 index bb8b4baf..00000000 --- a/FlyingSocks/XCTests/SocketErrorTests.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// SocketErrorTests.swift -// FlyingFox -// -// Created by Andre Jacobs on 07/03/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest - -final class SocketErrorTests: XCTestCase { - - func testSocketError_errorDescription() { - - let failedType = "failed" - let failedErrno: Int32 = 42 - let failedMessage = "failure is an option" - XCTAssertEqual( - SocketError.failed(type: failedType, errno: failedErrno, message: failedMessage).errorDescription, - "SocketError. \(failedType)(\(failedErrno)): \(failedMessage)" - ) - - XCTAssertEqual(SocketError.blocked.errorDescription, "SocketError. Blocked") - XCTAssertEqual(SocketError.disconnected.errorDescription, "SocketError. Disconnected") - XCTAssertEqual(SocketError.unsupportedAddress.errorDescription, "SocketError. UnsupportedAddress") - } - - func testSocketError_makeFailed() { - errno = EIO - let socketError = SocketError.makeFailed("unit-test") - switch socketError { - case let .failed(type: type, errno: socketErrno, message: message): - XCTAssertEqual(type, "unit-test") - XCTAssertEqual(socketErrno, EIO) - XCTAssertEqual(message, "Input/output error") - default: - XCTFail() - } - } -} diff --git a/FlyingSocks/XCTests/SocketPool+PollTests.swift b/FlyingSocks/XCTests/SocketPool+PollTests.swift deleted file mode 100644 index 582fca70..00000000 --- a/FlyingSocks/XCTests/SocketPool+PollTests.swift +++ /dev/null @@ -1,250 +0,0 @@ -// -// SocketPool+PollTests.swift -// FlyingFox -// -// Created by Simon Whitty on 25/09/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest - -final class PollTests: XCTestCase { - - func testInterval() { - XCTAssertEqual( - Poll.Interval.immediate.milliseconds, - 0 - ) - - XCTAssertEqual( - Poll.Interval.seconds(1).milliseconds, - 1000 - ) - } - - func testAddingAndRemovingEvents() throws { - var queue = Poll.make() - - XCTAssertNoThrow(try queue.addEvents(.connection, for: .validMock)) - XCTAssertEqual(queue.entries, [.init(file: .validMock, events: .connection)]) - - XCTAssertNoThrow(try queue.removeEvents(.connection, for: .validMock)) - XCTAssertEqual(queue.entries, []) - } - - func testAddingEventWhenNotOpen_ThrowsError() { - var queue = Poll.make() - queue.stop() - - XCTAssertThrowsError( - try queue.addEvents(.read, for: .validMock) - ) - } - - func testRemovingEventWhenNotOpen_ThrowsError() { - var queue = Poll.make() - queue.stop() - - XCTAssertThrowsError( - try queue.removeEvents(.read, for: .validMock) - ) - } - - func testReadEntry_CreatesPollFD() { - let entry = Poll.Entry(file: .init(rawValue: 10), events: .read) - - XCTAssertEqual(entry.pollfd.fd, 10) - XCTAssertEqual(entry.pollfd.events, Int16(POLLIN)) - XCTAssertEqual(entry.pollfd.revents, 0) - } - - func testWriteEntry_CreatesPollFD() { - let entry = Poll.Entry(file: .init(rawValue: 20), events: .write) - - XCTAssertEqual(entry.pollfd.fd, 20) - XCTAssertEqual(entry.pollfd.events, Int16(POLLOUT)) - XCTAssertEqual(entry.pollfd.revents, 0) - } - - func testConnectionEntry_CreatesPollFD() { - let entry = Poll.Entry(file: .init(rawValue: 30), events: .connection) - - XCTAssertEqual(entry.pollfd.fd, 30) - XCTAssertEqual(entry.pollfd.events, Int16(POLLOUT | POLLIN)) - XCTAssertEqual(entry.pollfd.revents, 0) - } - - func testReadResult_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: pollfd.make( - fd: 10, - events: POLLIN, - revents: POLLIN - )), - EventNotification( - file: .init(rawValue: 10), - events: .read, - errors: [] - ) - ) - } - - func testErrorsIgnored_WhenReadWithDataAvailable() { - XCTAssertEqual( - EventNotification.make(from: pollfd.make( - fd: 10, - events: POLLIN, - revents: POLLIN | POLLHUP - )), - EventNotification( - file: .init(rawValue: 10), - events: .read, - errors: [] - ) - ) - } - - func testWriteResult_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: pollfd.make( - fd: 10, - events: POLLOUT, - revents: POLLOUT - )), - EventNotification( - file: .init(rawValue: 10), - events: .write, - errors: [] - ) - ) - } - - func testWriteHUP_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: pollfd.make( - fd: 10, - events: POLLIN | POLLOUT, - revents: POLLHUP - )), - EventNotification( - file: .init(rawValue: 10), - events: .connection, - errors: [.endOfFile] - ) - ) - } - - func testWriteErrors_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: pollfd.make( - fd: 10, - events: POLLOUT, - revents: POLLERR - )), - EventNotification( - file: .init(rawValue: 10), - events: .write, - errors: [.error] - ) - ) - } - - func testUnmatchedRevents_DoesNotCreateNotification() { - XCTAssertNil( - EventNotification.make(from: pollfd.make( - events: POLLIN, - revents: 0 - )) - ) - XCTAssertNil( - EventNotification.make(from: pollfd.make( - events: POLLIN, - revents: POLLOUT - )) - ) - XCTAssertNil( - EventNotification.make(from: pollfd.make( - events: POLLIN | POLLOUT, - revents: 0 - )) - ) - } - - func testGetEvents_ReturnsEvents() async throws { - var queue = Poll.make() - - let (s1, s2) = try Socket.makeNonBlockingPair() - - try queue.addEvents([.read], for: s2.file) - - let data = Data([10, 20]) - _ = try s1.write(data, from: data.startIndex) - - await AsyncAssertEqual( - try await queue.getEvents(), - [.init(file: s2.file, events: [.read], errors: [])] - ) - } - - func testGetEventsWhenNotReady_ThrowsError() async { - var queue = Poll.make() - queue.stop() - - await AsyncAssertThrowsError( - try await queue.getEvents() - ) - } -} - -private extension Poll { - - static func make() -> Self { - var queue = Poll(interval: .immediate) - queue.open() - return queue - } - - func getEvents() async throws -> [EventNotification] { - let queue = UncheckedSendable(wrappedValue: self) - return try await withCheckedThrowingContinuation { continuation in - DispatchQueue.global().async { - let result = Result { - try queue.wrappedValue.getNotifications() - } - continuation.resume(with: result) - } - } - } -} - -private extension pollfd { - static func make(fd: Int32 = 0, - events: Int32 = POLLIN, - revents: Int32 = POLLIN) -> Self { - .init(fd: fd, events: Int16(events), revents: Int16(revents)) - } -} diff --git a/FlyingSocks/XCTests/SocketPool+kQueueTests.swift b/FlyingSocks/XCTests/SocketPool+kQueueTests.swift deleted file mode 100644 index 84b62bbb..00000000 --- a/FlyingSocks/XCTests/SocketPool+kQueueTests.swift +++ /dev/null @@ -1,241 +0,0 @@ -// -// SocketPool+kQueueTests.swift -// FlyingFox -// -// Created by Simon Whitty on 10/09/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -#if canImport(Darwin) -@testable import FlyingSocks -import XCTest - -final class kQueueTests: XCTestCase { - - func testQueueCloses() throws { - var queue = try kQueue.make() - XCTAssertNoThrow(try queue.stop()) - } - - func testQueueThrowsError_Closes() throws { - XCTAssertThrowsError(try kQueue.closeQueue(file: .validMock)) - } - - func testQueueThrowsError_Make() throws { - XCTAssertThrowsError(try kQueue.makeQueue(file: -1)) - } - - func testAddingEventToInvalidDescriptor_ThrowsError() throws { - let queue = try kQueue.make() - - XCTAssertThrowsError( - try queue.addEvent(.read, for: .validMock) - ) - } - - func testAddingAndRemovingEvents() throws { - var queue = try kQueue.make() - let (s1, _) = try Socket.makeNonBlockingPair() - - XCTAssertNoThrow(try queue.addEvents(.connection, for: s1.file)) - XCTAssertEqual(queue.existing[s1.file], .connection) - - XCTAssertNoThrow(try queue.removeEvents(.connection, for: s1.file)) - XCTAssertNil(queue.existing[s1.file]) - } - - func testRemovingEventToInvalidDescriptor_ThrowsError() throws { - let queue = try kQueue.make() - - XCTAssertThrowsError( - try queue.removeEvent(.read, for: .validMock) - ) - } - - func testFilterEvents() { - XCTAssertEqual( - Socket.Event.read.kqueueFilter, - Int16(EVFILT_READ) - ) - XCTAssertEqual( - Socket.Event.write.kqueueFilter, - Int16(EVFILT_WRITE) - ) - XCTAssertEqual( - Socket.Event.make(from: Int16(EVFILT_READ)), - .read - ) - XCTAssertEqual( - Socket.Event.make(from: Int16(EVFILT_WRITE)), - .write - ) - XCTAssertNil(Socket.Event.make(from: 10100)) - } - - func testReadResult_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: .make( - ident: 10, - filter: EVFILT_READ - )), - EventNotification( - file: .init(rawValue: 10), - events: .read, - errors: [] - ) - ) - } - - func testReadErrors_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: .make( - ident: 10, - filter: EVFILT_READ, - flags: EV_ERROR - )), - EventNotification( - file: .init(rawValue: 10), - events: .read, - errors: [.error] - ) - ) - } - - func testErrorsIgnored_WhenReadWithDataAvailable() { - XCTAssertEqual( - EventNotification.make(from: .make( - ident: 10, - filter: EVFILT_READ, - flags: EV_ERROR, - data: 5 - )), - EventNotification( - file: .init(rawValue: 10), - events: .read, - errors: [] - ) - ) - } - - func testWriteResult_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: .make( - ident: 10, - filter: EVFILT_WRITE - )), - EventNotification( - file: .init(rawValue: 10), - events: .write, - errors: [] - ) - ) - } - - func testWriteErrors_CreatesNotification() { - XCTAssertEqual( - EventNotification.make(from: .make( - ident: 10, - filter: EVFILT_WRITE, - flags: EV_EOF, - data: 10 - )), - EventNotification( - file: .init(rawValue: 10), - events: .write, - errors: [.endOfFile] - ) - ) - } - - func testInvalidFilter_DoesNotCreateNotification() { - XCTAssertNil( - EventNotification.make(from: .make( - filter: 0 - )) - ) - } - - func testQueueReturnsEvents() async throws { - var queue = try kQueue.make() - - let (s1, s2) = try Socket.makeNonBlockingPair() - - try queue.addEvents([.read], for: s2.file) - - let data = Data([10, 20]) - _ = try s1.write(data, from: data.startIndex) - - await AsyncAssertEqual( - try await queue.getEvents(), - [.init(file: s2.file, events: [.read], errors: [])] - ) - } - - func testQueueThrowsErrorIfClosed() async throws { - var queue = try kQueue.make() - let (s1, _) = try Socket.makeNonBlockingPair() - try queue.addEvents([.read], for: s1.file) - - try queue.stop() - await AsyncAssertThrowsError(try await queue.getEvents()) - } -} - -private extension kQueue { - - static func make() throws -> Self { - var queue = kQueue(maxEvents: 20) - try queue.open() - return queue - } - - func getEvents() async throws -> [EventNotification] { - let queue = UncheckedSendable(wrappedValue: self) - return try await withCheckedThrowingContinuation { continuation in - DispatchQueue.global().async { - let result = Result { - try queue.wrappedValue.getNotifications() - } - continuation.resume(with: result) - } - } - } -} - -private extension kevent { - static func make(ident: UInt = 0, - filter: Int32 = EVFILT_READ, - flags: Int32 = 0, - data: Int = 0) -> Self { - .init(ident: ident, - filter: Int16(filter), - flags: UInt16(flags), - fflags: 0, - data: data, - udata: nil) - } -} -#endif diff --git a/FlyingSocks/XCTests/SocketPoolTests.swift b/FlyingSocks/XCTests/SocketPoolTests.swift deleted file mode 100644 index fc57d206..00000000 --- a/FlyingSocks/XCTests/SocketPoolTests.swift +++ /dev/null @@ -1,349 +0,0 @@ -// -// SocketPoolTests.swift -// FlyingFox -// -// Created by Simon Whitty on 12/09/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest - -final class SocketPoolTests: XCTestCase { - - typealias Continuation = IdentifiableContinuation - typealias Waiting = SocketPool.Waiting - -#if canImport(Darwin) - func testKqueuePool() { - let pool = SocketPool.make(maxEvents: 5) - XCTAssertTrue(type(of: pool) == SocketPool.self) - } -#endif - -#if canImport(CSystemLinux) - func testEPollPool() { - let pool = SocketPool.make(maxEvents: 5) - XCTAssertTrue(type(of: pool) == SocketPool.self) - } -#endif - - func testPoll() { - let pool: some AsyncSocketPool = .poll() - XCTAssertTrue(type(of: pool) == SocketPool.self) - } - - func testQueuePrepare() async throws { - let pool = SocketPool.make() - - await AsyncAssertNil(await pool.state) - - try await pool.prepare() - await AsyncAssertEqual(await pool.state, .ready) - } - - func testQueueRun_ThrowsError_WhenNotReady() async throws { - let pool = SocketPool.make() - - await AsyncAssertThrowsError(try await pool.run(), of: (any Error).self) - } - - func testSuspendedSockets_ThrowError_WhenCancelled() async throws { - let pool = SocketPool.make() - try await pool.prepare() - - let task = Task { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - try await pool.suspendSocket(socket, untilReadyFor: .read) - } - - task.cancel() - - await AsyncAssertThrowsError(try await task.value, of: CancellationError.self) - } - - func testCancellingPollingPool_CancelsSuspendedSocket() async throws { - let pool = SocketPool.make() - try await pool.prepare() - - _ = Task(timeout: 0.5) { - try await pool.run() - } - - let (s1, s2) = try Socket.makeNonBlockingPair() - await AsyncAssertThrowsError( - try await pool.suspendSocket(s1, untilReadyFor: .read), - of: CancellationError.self - ) - try s1.close() - try s2.close() - } - - func testCancellingPool_CancelsNewSockets() async throws { - let pool = SocketPool.make() - try await pool.prepare() - - let task = Task(timeout: 0.1) { - try await pool.run() - } - - try? await task.value - - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - await AsyncAssertThrowsError( - try await pool.suspendSocket(socket, untilReadyFor: .read) - ) - } - - func testQueueNotification_ResumesSocket() async throws { - let pool = SocketPool.make() - try await pool.prepare() - let task = Task { try await pool.run() } - defer { task.cancel() } - - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - let suspension = Task { - try await pool.suspendSocket(socket, untilReadyFor: .read) - } - defer { suspension.cancel() } - - await pool.queue.sendResult(returning: [ - .init(file: socket.file, events: .read, errors: []) - ]) - - await AsyncAssertNoThrow( - try await pool.suspendSocket(socket, untilReadyFor: .read) - ) - } - - func testQueueNotificationError_ResumesSocket_WithError() async throws { - let pool = SocketPool.make() - try await pool.prepare() - let task = Task { try await pool.run() } - defer { task.cancel() } - - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - let suspension = Task { - try await pool.suspendSocket(socket, untilReadyFor: .read) - } - defer { suspension.cancel() } - - await pool.queue.sendResult(returning: [ - .init(file: socket.file, events: .read, errors: [.endOfFile]) - ]) - - await AsyncAssertThrowsError( - try await pool.suspendSocket(socket, untilReadyFor: .read), - of: SocketError.self - ) { XCTAssertEqual($0, .disconnected) } - } - - func testWaiting_IsEmpty() async { - let cn = await Continuation.make() - - var waiting = Waiting() - XCTAssertTrue(waiting.isEmpty) - - _ = waiting.appendContinuation(cn, for: .validMock, events: .read) - XCTAssertFalse(waiting.isEmpty) - - _ = waiting.resumeContinuation(id: cn.id, with: .success(()), for: .validMock) - XCTAssertTrue(waiting.isEmpty) - } - - func testWaitingEvents() async { - var waiting = Waiting() - let cnRead = await Continuation.make() - let cnRead1 = await Continuation.make() - let cnWrite = await Continuation.make() - - XCTAssertEqual( - waiting.appendContinuation(cnRead, for: .validMock, events: .read), - [.read] - ) - XCTAssertEqual( - waiting.appendContinuation(cnRead1, for: .validMock, events: .read), - [.read] - ) - XCTAssertEqual( - waiting.appendContinuation(cnWrite, for: .validMock, events: .write), - [.read, .write] - ) - XCTAssertEqual( - waiting.resumeContinuation(id: .init(), with: .success(()), for: .validMock), - [] - ) - XCTAssertEqual( - waiting.resumeContinuation(id: cnWrite.id, with: .success(()), for: .validMock), - [.write] - ) - XCTAssertEqual( - waiting.resumeContinuation(id: cnRead.id, with: .success(()), for: .validMock), - [] - ) - XCTAssertEqual( - waiting.resumeContinuation(id: cnRead1.id, with: .success(()), for: .validMock), - [.read] - ) - } - - func testWaitingContinuations() async { - var waiting = Waiting() - let cnRead = await Continuation.make() - let cnRead1 = await Continuation.make() - let cnWrite = await Continuation.make() - defer { - cnRead.resume() - cnRead1.resume() - cnWrite.resume() - } - - _ = waiting.appendContinuation(cnRead, for: .validMock, events: .read) - _ = waiting.appendContinuation(cnRead1, for: .validMock, events: .read) - _ = waiting.appendContinuation(cnWrite, for: .validMock, events: .write) - - XCTAssertEqual( - Set(waiting.continuationIDs(for: .validMock, events: .read)), - [cnRead1.id, cnRead.id] - ) - XCTAssertEqual( - Set(waiting.continuationIDs(for: .validMock, events: .write)), - [cnWrite.id] - ) - XCTAssertEqual( - Set(waiting.continuationIDs(for: .validMock, events: .connection)), - [cnRead1.id, cnRead.id, cnWrite.id] - ) - XCTAssertEqual( - Set(waiting.continuationIDs(for: .validMock, events: [])), - [] - ) - XCTAssertEqual( - Set(waiting.continuationIDs(for: .invalid, events: .connection)), - [] - ) - } -} - -private extension SocketPool where Queue == MockEventQueue { - static func make() -> Self { - .init(queue: MockEventQueue()) - } -} - -final class MockEventQueue: EventQueue, @unchecked Sendable { - - private var isWaiting: Bool = false - private let semaphore = DispatchSemaphore(value: 0) - private var result: Result<[EventNotification], any Error>? - - private(set) var state: State? - - enum State { - case open - case stopped - case closed - } - - func sendResult(returning success: [EventNotification]) { - result = .success(success) - if isWaiting { - semaphore.signal() - } - } - - func sendResult(throwing error: some Error) { - result = .failure(error) - if isWaiting { - semaphore.signal() - } - } - - func addEvents(_ events: Socket.Events, for socket: Socket.FileDescriptor) throws { - - } - - func removeEvents(_ events: Socket.Events, for socket: Socket.FileDescriptor) throws { - - } - - func open() throws { - guard (state == nil || state == .closed) else { throw InvalidStateError() } - state = .open - } - - func stop() throws { - guard state == .open else { throw InvalidStateError() } - state = .stopped - result = .failure(CancellationError()) - if isWaiting { - semaphore.signal() - } - } - - func close() throws { - guard state == .stopped else { throw InvalidStateError() } - state = .closed - } - - func getNotifications() throws -> [EventNotification] { - defer { - result = nil - } - if let result = result { - return try result.get() - } else { - isWaiting = true - semaphore.wait() - return try result!.get() - } - } - - private struct InvalidStateError: Error { } -} - - -extension IdentifiableContinuation where T: Sendable { - static func make() async -> IdentifiableContinuation { - await Host().makeThrowingContinuation() - } - - private actor Host { - func makeThrowingContinuation() async -> IdentifiableContinuation { - await withCheckedContinuation { outer in - Task { - try? await withIdentifiableThrowingContinuation(isolation: self) { - outer.resume(returning: $0) - } onCancel: { _ in } - } - } - } - } -} - -extension Socket.FileDescriptor { - static let validMock = Socket.FileDescriptor(rawValue: 999) -} diff --git a/FlyingSocks/XCTests/SocketTests.swift b/FlyingSocks/XCTests/SocketTests.swift deleted file mode 100644 index e4962720..00000000 --- a/FlyingSocks/XCTests/SocketTests.swift +++ /dev/null @@ -1,285 +0,0 @@ -// -// SocketTests.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest - -final class SocketTests: XCTestCase { - - func testSocketType_init() { - XCTAssertEqual(try SocketType(rawValue: Socket.stream), .stream) - XCTAssertEqual(try SocketType(rawValue: Socket.datagram), .datagram) - XCTAssertThrowsError(try SocketType(rawValue: -1)) - } - - func funcSocketType_rawValue() { - XCTAssertEqual(SocketType.stream.rawValue, Socket.stream) - XCTAssertEqual(SocketType.datagram.rawValue, Socket.datagram) - } - - func testSocketEvents() { - let events: Set = [.read, .write] - - XCTAssertTrue( - "\(events)".contains("read") - ) - XCTAssertTrue( - "\(events)".contains("write") - ) - } - - func testSocketReads_DataThatIsSent() throws { - let (s1, s2) = try Socket.makeNonBlockingPair() - - let data = Data([10, 20]) - _ = try s1.write(data, from: data.startIndex) - - XCTAssertEqual(try s2.read(), 10) - XCTAssertEqual(try s2.read(), 20) - } - - func testSocketRead_ThrowsBlocked_WhenNoDataIsAvailable() throws { - let (s1, s2) = try Socket.makeNonBlockingPair() - - XCTAssertThrowsError(try s1.read(), of: SocketError.self) { - XCTAssertEqual($0, .blocked) - } - - try s1.close() - try s2.close() - } - - func testSocketRead_ThrowsDisconnected_WhenSocketIsClosed() throws { - let (s1, s2) = try Socket.makeNonBlockingPair() - try s1.close() - try s2.close() - - XCTAssertThrowsError(try s1.read(), of: SocketError.self) { - XCTAssertEqual($0, .disconnected) - } - } - - func testSocketWrite_ThrowsBlocked_WhenBufferIsFull() throws { - let (s1, s2) = try Socket.makeNonBlockingPair() - try s1.setValue(1024, for: .sendBufferSize) - let data = Data(repeating: 0x01, count: 8192) - let sent = try s1.write(data, from: data.startIndex) - XCTAssertThrowsError(try s1.write(data, from: sent), of: SocketError.self) { - XCTAssertEqual($0, .blocked) - } - - try s1.close() - try s2.close() - } - - func testSocketWrite_Throws_WhenSocketIsNotConnected() async throws { - let s1 = try Socket(domain: AF_UNIX, type: Socket.stream) - let data = Data(repeating: 0x01, count: 100) - XCTAssertThrowsError(try s1.write(data, from: data.startIndex)) - try s1.close() - } - - func testSocket_Sets_And_Gets_ReceiveBufferSize() throws { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - - try socket.setValue(2048, for: .receiveBufferSize) -#if canImport(Darwin) - XCTAssertEqual(try socket.getValue(for: .receiveBufferSize), Int32(2048)) -#else - // Linux kernel doubles this value (to allow space for bookkeeping overhead) - XCTAssertGreaterThanOrEqual(try socket.getValue(for: .receiveBufferSize), Int32(4096)) -#endif - } - - func testSocket_Sets_And_Gets_SendBufferSizeOption() throws { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - - try socket.setValue(2048, for: .sendBufferSize) -#if canImport(Darwin) - XCTAssertEqual(try socket.getValue(for: .sendBufferSize), Int32(2048)) -#else - // Linux kernel doubles this value (to allow space for bookkeeping overhead) - XCTAssertGreaterThanOrEqual(try socket.getValue(for: .sendBufferSize), Int32(4096)) -#endif - } - - func testSocket_Sets_And_Gets_BoolOption() throws { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - - try socket.setValue(true, for: .localAddressReuse) - XCTAssertEqual(try socket.getValue(for: .localAddressReuse), true) - - try socket.setValue(false, for: .localAddressReuse) - XCTAssertEqual(try socket.getValue(for: .localAddressReuse), false) - } - - func testSocket_Sets_And_Gets_Flags() throws { - let socket = try Socket(domain: AF_UNIX, type: Socket.stream) - XCTAssertFalse(try socket.flags.contains(.append)) - - try socket.setFlags(.append) - XCTAssertTrue(try socket.flags.contains(.append)) - } - - func testSocketInit_ThrowsError_WhenInvalid() { - XCTAssertThrowsError( - _ = try Socket(domain: -1, type: -1) - ) - } - - func testSocketAccept_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.accept() - ) - } - - func testSocketConnect_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.connect(to: .unix(path: "test")) - ) - } - - func testSocketClose_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.close() - ) - } - - func testSocketListen_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.listen() - ) - } - - func testSocketBind_ToINET() throws { - let socket = try Socket(domain: AF_INET, type: Socket.stream) - try socket.setValue(true, for: .localAddressReuse) - let address = Socket.makeAddressINET(port:5050) - XCTAssertNoThrow( - try socket.bind(to: address) - ) - - try? socket.close() - } - - func testSocketBind_ToINET6_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - let address = Socket.makeAddressINET6(port: 8080) - XCTAssertThrowsError( - try socket.bind(to: address) - ) - } - - func testSocketBind_ToStorage_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - let address = Socket.makeAddressINET6(port: 8080) - XCTAssertThrowsError( - try socket.bind(to: address) - ) - } - - func testSocketGetOption_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - _ = try socket.getValue(for: .localAddressReuse) - ) - } - - func testSocketSetOption_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.setValue(true, for: .localAddressReuse) - ) - } - - func testSocketGetFlags_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - _ = try socket.flags - ) - } - - func testSocketSetFlags_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.setFlags(.nonBlocking) - ) - } - - func testSocketRemotePeer_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.remotePeer() - ) - } - - func testSocket_sockname_ThrowsError_WhenInvalid() { - let socket = Socket(file: -1) - XCTAssertThrowsError( - try socket.sockname() - ) - } - - func test_ntop_ThrowsError_WhenBufferIsTooSmall() { - var addr = Socket.makeAddressINET6(port: 8080) - let maxLength = socklen_t(1) - let buffer = UnsafeMutablePointer.allocate(capacity: Int(maxLength)) - XCTAssertThrowsError(try Socket.inet_ntop(AF_INET6, &addr.sin6_addr, buffer, maxLength)) - } - - func testMakes_datagram_ip4() throws { - let socket = try Socket(domain: Int32(sa_family_t(AF_INET)), type: .datagram) - XCTAssertTrue( - try socket.getValue(for: .packetInfoIP) - ) - } - - func testMakes_datagram_ip6() throws { - let socket = try Socket(domain: Int32(sa_family_t(AF_INET6)), type: .datagram) - XCTAssertTrue( - try socket.getValue(for: .packetInfoIPv6) - ) - } -} - -extension Socket.Flags { - static let append = Socket.Flags(rawValue: O_APPEND) -} - -private extension Socket { - init(file: Int32) { - self.init(file: .init(rawValue: file)) - } -} diff --git a/FlyingSocks/XCTests/Task+TimeoutTests.swift b/FlyingSocks/XCTests/Task+TimeoutTests.swift deleted file mode 100644 index a082a530..00000000 --- a/FlyingSocks/XCTests/Task+TimeoutTests.swift +++ /dev/null @@ -1,269 +0,0 @@ -// -// Task+TimeoutTests.swift -// FlyingFox -// -// Created by Simon Whitty on 15/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -@testable import FlyingSocks -import XCTest - -final class TaskTimeoutTests: XCTestCase { - - func testTimeoutReturnsSuccess_WhenTimeoutDoesNotExpire() async throws { - // given - let value = try await Task(timeout: 0.5) { - "Fish" - }.value - - // then - XCTAssertEqual(value, "Fish") - } - - func testTimeoutThrowsError_WhenTimeoutExpires() async { - // given - let task = Task(timeout: 0.5) { - try await Task.sleep(seconds: 10) - } - - // then - do { - _ = try await task.value - XCTFail("Expected SocketError.timeout") - } catch { - XCTAssertEqual(error as? SocketError, .makeTaskTimeout(seconds: 0.5)) - } - } - - func testTimeoutCancels() async { - // given - let task = Task(timeout: 0.5) { - try await Task.sleep(seconds: 10) - } - - // when - task.cancel() - - // then - do { - _ = try await task.value - XCTFail("Expected CancellationError") - } catch { - XCTAssertTrue(error is CancellationError) - } - } - - func testTaskTimeoutParentThrowsError() async { - let task = Task { - try await Task.sleep(seconds: 10) - } - - let parent = Task { - try await task.getValue(cancelling: .whenParentIsCancelled) - } - - parent.cancel() - - await AsyncAssertThrowsError( - try await parent.value, - of: CancellationError.self - ) - } - - func testTaskTimeoutZeroThrowsError() async { - let task = Task { - try await Task.sleep(seconds: 10) - } - - await AsyncAssertThrowsError( - try await task.getValue(cancelling: .afterTimeout(seconds: 0)), - of: CancellationError.self - ) - } - - func testTaskTimeoutThrowsError() async { - let task = Task { - try await Task.sleep(seconds: 10) - } - - // then - do { - try await task.getValue(cancelling: .afterTimeout(seconds: 0.1)) - XCTFail("Expected SocketError.timeout") - } catch { - XCTAssertEqual(error as? SocketError, .makeTaskTimeout(seconds: 0.1)) - } - } - - func testTaskTimeoutParentReturnsSuccess() async { - let task = Task { "Fish" } - - await AsyncAssertEqual( - try await task.getValue(cancelling: .whenParentIsCancelled), - "Fish" - ) - } - - func testTaskTimeoutZeroReturnsSuccess() async { - let task = Task { "Fish" } - - await AsyncAssertEqual( - try await task.getValue(cancelling: .afterTimeout(seconds: 0)), - "Fish" - ) - } - - func testTaskTimeoutReturnsSuccess() async { - let task = Task { "Fish" } - - await AsyncAssertEqual( - try await task.getValue(cancelling: .afterTimeout(seconds: 0.1)), - "Fish" - ) - } - - @MainActor - func testMainActor_ReturnsValue() async throws { - let val = try await withThrowingTimeout(seconds: 1) { - MainActor.assertIsolated() - try await Task.sleep(nanoseconds: 1_000) - MainActor.assertIsolated() - return "Fish" - } - XCTAssertEqual(val, "Fish") - } - - @MainActor - func testMainActorThrowsError_WhenTimeoutExpires() async { - do { - try await withThrowingTimeout(seconds: 0.05) { - MainActor.assertIsolated() - defer { MainActor.assertIsolated() } - try await Task.sleep(nanoseconds: 60_000_000_000) - } - XCTFail("Expected Error") - } catch { - XCTAssertEqual(error as? SocketError, .makeTaskTimeout(seconds: 0.05)) - } - } - - func testSendable_ReturnsValue() async throws { - let sendable = TestActor() - let value = try await withThrowingTimeout(seconds: 1) { - sendable - } - XCTAssertTrue(value === sendable) - } - - func testNonSendable_ReturnsValue() async throws { - let ns = try await withThrowingTimeout(seconds: 1) { - NonSendable("chips") - } - XCTAssertEqual(ns.value, "chips") - } - - func testActor_ReturnsValue() async throws { - let val = try await TestActor("Fish").returningValue() - XCTAssertEqual(val, "Fish") - } - - func testActorThrowsError_WhenTimeoutExpires() async { - do { - _ = try await TestActor().returningValue( - after: 60, - timeout: 0.05 - ) - XCTFail("Expected Error") - } catch { - XCTAssertEqual(error as? SocketError, .makeTaskTimeout(seconds: 0.05)) - } - } - - func testTimeout_Cancels() async { - let task = Task { - try await withThrowingTimeout(seconds: 1) { - try await Task.sleep(nanoseconds: 1_000_000_000) - } - } - - task.cancel() - - do { - _ = try await task.value - XCTFail("Expected Error") - } catch { - XCTAssertTrue(error is CancellationError) - } - } -} - -extension Task where Success: Sendable, Failure == any Error { - - // Start a new Task with a timeout. - init(priority: TaskPriority? = nil, timeout: TimeInterval, operation: @escaping @Sendable () async throws -> Success) { - self = Task(priority: priority) { - try await withThrowingTimeout(seconds: timeout) { - try await operation() - } - } - } -} - -extension Task where Success == Never, Failure == Never { - static func sleep(seconds: TimeInterval) async throws { - try await sleep(nanoseconds: UInt64(1_000_000_000 * seconds)) - } -} - -public struct NonSendable { - public var value: T - - init(_ value: T) { - self.value = value - } -} - -private final actor TestActor { - - private var value: T - - init(_ value: T) { - self.value = value - } - - init() where T == String { - self.init("fish") - } - - func returningValue(after sleep: TimeInterval = 0, timeout: TimeInterval = 1) async throws -> T { - try await withThrowingTimeout(seconds: timeout) { - try await Task.sleep(nanoseconds: UInt64(sleep * 1_000_000_000)) - self.assertIsolated() - return self.value - } - } -} diff --git a/FlyingSocks/XCTests/XCTest+Extension.swift b/FlyingSocks/XCTests/XCTest+Extension.swift deleted file mode 100644 index fffbbe78..00000000 --- a/FlyingSocks/XCTests/XCTest+Extension.swift +++ /dev/null @@ -1,150 +0,0 @@ -// -// XCTAssert+Extension.swift -// FlyingFox -// -// Created by Simon Whitty on 22/02/2022. -// Copyright © 2022 Simon Whitty. All rights reserved. -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/FlyingFox -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -// - -import XCTest - -func AsyncAssertEqual(_ expression: @autoclosure () async throws -> T, - _ expected: @autoclosure () throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result(catching: expression) - XCTAssertEqual(try result.get(), try expected(), message(), file: file, line: line) -} - -func AsyncAssertAsyncEqual(_ expression: @autoclosure () async throws -> T, - _ expected: @autoclosure () throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result { - try await expression() == expected() - } - XCTAssertTrue(try result.get(), message(), file: file, line: line) -} - -func XCTAssertThrowsError(_ expression: @autoclosure () throws -> T, - of type: E.Type, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - _ errorHandler: (_ error: E) -> Void = { _ in }) { - XCTAssertThrowsError(try expression(), message(), file: file, line: line) { - guard let error = $0 as? E else { - XCTFail(message(), file: file, line: line) - return - } - errorHandler(error) - } -} - -func AsyncAssertThrowsError(_ expression: @autoclosure () async throws -> T, - of type: E.Type, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - _ errorHandler: (_ error: E) -> Void = { _ in }) async { - let result = await Result(catching: expression) - XCTAssertThrowsError(try result.get(), message(), file: file, line: line) { - guard let error = $0 as? E else { - XCTFail(message(), file: file, line: line) - return - } - errorHandler(error) - } -} - -func AsyncAssertThrowsError(_ expression: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line, - _ errorHandler: (_ error: any Error) -> Void = { _ in }) async { - await AsyncAssertThrowsError(try await expression(), - of: (any Error).self, - message(), - file: file, - line: line, - errorHandler) -} - -func AsyncAssertNoThrow(_ expression: @autoclosure () async throws -> T, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result(catching: expression) - XCTAssertNoThrow(try result.get(), message(), file: file, line: line) -} - -func AsyncAssertNil(_ expression: @autoclosure () async throws -> T?, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result(catching: expression) - XCTAssertNil(try result.get(), message(), file: file, line: line) -} - -func AsyncAssertNotNil(_ expression: @autoclosure () async throws -> T?, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result(catching: expression) - XCTAssertNotNil(try result.get(), message(), file: file, line: line) -} - -private extension Result where Failure == any Error { - init(catching body: () async throws -> Success) async { - do { - self = .success(try await body()) - } catch { - self = .failure(error) - } - } -} - -func AsyncAssertTrue(_ expression: @autoclosure () async throws -> Bool, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result(catching: expression) - XCTAssertTrue(try result.get(), message(), file: file, line: line) -} - -func AsyncAssertFalse(_ expression: @autoclosure () async throws -> Bool, - _ message: @autoclosure () -> String = "", - file: StaticString = #filePath, - line: UInt = #line) async { - let result = await Result(catching: expression) - XCTAssertFalse(try result.get(), message(), file: file, line: line) -} - -protocol AsyncEquatable { - static func ==(lhs: Self, rhs: Self) async -> Bool -} diff --git a/Package@swift-5.10.swift b/Package@swift-5.10.swift deleted file mode 100644 index 68b3616d..00000000 --- a/Package@swift-5.10.swift +++ /dev/null @@ -1,85 +0,0 @@ -// swift-tools-version:5.10 - -import PackageDescription - -let package = Package( - name: "FlyingFox", - platforms: [ - .macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v8) - ], - products: [ - .library( - name: "FlyingFox", - targets: ["FlyingFox"] - ), - .library( - name: "FlyingSocks", - targets: ["FlyingSocks"] - ) - ], - targets: [ - .target( - name: "FlyingFox", - dependencies: ["FlyingSocks"], - path: "FlyingFox/Sources", - exclude: .excludeFiles, - swiftSettings: .upcomingFeatures - ), - .testTarget( - name: "FlyingFoxXCTests", - dependencies: ["FlyingFox"], - path: "FlyingFox/XCTests", - resources: [ - .copy("Stubs") - ], - swiftSettings: .upcomingFeatures - ), - .target( - name: "FlyingSocks", - dependencies: [.target(name: "CSystemLinux", condition: .when(platforms: [.linux]))], - path: "FlyingSocks/Sources", - swiftSettings: .upcomingFeatures - ), - .testTarget( - name: "FlyingSocksXCTests", - dependencies: ["FlyingSocks"], - path: "FlyingSocks/XCTests", - resources: [ - .copy("Resources") - ], - swiftSettings: .upcomingFeatures - ), - .target( - name: "CSystemLinux", - path: "CSystemLinux" - ) - ] -) - -extension Array where Element == String { - static var excludeFiles: [String] { - #if os(Linux) - ["JSONPredicatePattern.swift"] - #else - [] - #endif - } -} - -extension Array where Element == SwiftSetting { - - static var upcomingFeatures: [SwiftSetting] { - [ - .enableUpcomingFeature("BareSlashRegexLiterals"), - .enableUpcomingFeature("ConciseMagicFile"), - .enableUpcomingFeature("DeprecateApplicationMain"), - .enableUpcomingFeature("DisableOutwardActorInference"), - .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("ForwardTrailingClosures"), - .enableUpcomingFeature("GlobalConcurrency"), - .enableUpcomingFeature("ImportObjcForwardDeclarations"), - .enableUpcomingFeature("IsolatedDefaultValues"), - //.enableExperimentalFeature("StrictConcurrency") - ] - } -}