From d6359422958b30871b4174895ea468280ea55749 Mon Sep 17 00:00:00 2001 From: Scott Marchant Date: Fri, 14 Nov 2025 14:14:14 -0700 Subject: [PATCH] feat: Add testing utilities that make a number of nio-related tests much easier to run. --- Tests/NIOAsyncRuntimeTests/TestUtils.swift | 103 +++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 Tests/NIOAsyncRuntimeTests/TestUtils.swift diff --git a/Tests/NIOAsyncRuntimeTests/TestUtils.swift b/Tests/NIOAsyncRuntimeTests/TestUtils.swift new file mode 100644 index 0000000..274368b --- /dev/null +++ b/Tests/NIOAsyncRuntimeTests/TestUtils.swift @@ -0,0 +1,103 @@ +//===----------------------------------------------------------------------===// +// +// Copyright (c) 2025 PassiveLogic, Inc. +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// swift-format-ignore: AmbiguousTrailingClosureOverload + +import NIOConcurrencyHelpers +import XCTest + +@testable import NIOCore + +func assertNoThrowWithValue( + _ body: @autoclosure () throws -> T, + defaultValue: T? = nil, + message: String? = nil, + file: StaticString = #filePath, + line: UInt = #line +) throws -> T { + do { + return try body() + } catch { + XCTFail( + "\(message.map { $0 + ": " } ?? "")unexpected error \(error) thrown", file: (file), line: line + ) + if let defaultValue = defaultValue { + return defaultValue + } else { + throw error + } + } +} + +func assert( + _ condition: @autoclosure () -> Bool, + within time: TimeAmount, + testInterval: TimeAmount? = nil, + _ message: String = "condition not satisfied in time", + file: StaticString = #filePath, + line: UInt = #line +) { + let testInterval = testInterval ?? TimeAmount.nanoseconds(time.nanoseconds / 5) + let endTime = NIODeadline.now() + time + + repeat { + if condition() { return } + usleep(UInt32(testInterval.nanoseconds / 1000)) + } while NIODeadline.now() < endTime + + if !condition() { + XCTFail(message, file: (file), line: line) + } +} + +func assertSuccess( + _ result: Result, file: StaticString = #filePath, line: UInt = #line +) { + guard case .success = result else { + return XCTFail("Expected result to be successful", file: (file), line: line) + } +} + +func assertFailure( + _ result: Result, file: StaticString = #filePath, line: UInt = #line +) { + guard case .failure = result else { + return XCTFail("Expected result to be a failure", file: (file), line: line) + } +} + +extension EventLoopFuture { + var isFulfilled: Bool { + if self.eventLoop.inEventLoop { + // Easy, we're on the EventLoop. Let's just use our knowledge that we run completed future callbacks + // immediately. + var fulfilled = false + self.assumeIsolated().whenComplete { _ in + fulfilled = true + } + return fulfilled + } else { + let fulfilledBox = NIOLockedValueBox(false) + let group = DispatchGroup() + + group.enter() + self.eventLoop.execute { + let isFulfilled = self.isFulfilled // This will now enter the above branch. + fulfilledBox.withLockedValue { + $0 = isFulfilled + } + group.leave() + } + group.wait() // this is very nasty but this is for tests only, so... + return fulfilledBox.withLockedValue { $0 } + } + } +}