diff --git a/FlyingFox/Sources/URLSession+Async.swift b/FlyingFox/Sources/URLSession+Async.swift index 412307ec..b42b28fd 100644 --- a/FlyingFox/Sources/URLSession+Async.swift +++ b/FlyingFox/Sources/URLSession+Async.swift @@ -39,7 +39,7 @@ extension URLSession { // Ports macOS Foundation method to Linux func data(for request: URLRequest) async throws -> (Data, URLResponse) { - let state = AllocatedLock(initialState: (isCancelled: false, task: URLSessionDataTask?.none)) + 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 diff --git a/FlyingFox/Tests/HTTPLoggingTests.swift b/FlyingFox/Tests/HTTPLoggingTests.swift index 7c77e873..64bfb1ae 100644 --- a/FlyingFox/Tests/HTTPLoggingTests.swift +++ b/FlyingFox/Tests/HTTPLoggingTests.swift @@ -90,7 +90,7 @@ struct HTTPLoggingTests { @Test func disabledLogger_DoesNotExecuteDebugClosure() { let logger = DisabledLogger() - let didLog = AllocatedLock(initialState: false) + let didLog = Mutex(false) logger.logDebug({ didLog.withLock { $0 = true } @@ -103,7 +103,7 @@ struct HTTPLoggingTests { @Test func disabledLogger_DoesNotExecuteInfoClosure() { let logger = DisabledLogger() - let didLog = AllocatedLock(initialState: false) + let didLog = Mutex(false) logger.logInfo({ didLog.withLock { $0 = true } @@ -116,7 +116,7 @@ struct HTTPLoggingTests { @Test func disabledLogger_DoesNotExecuteWarningClosure() { let logger = DisabledLogger() - let didLog = AllocatedLock(initialState: false) + let didLog = Mutex(false) logger.logWarning({ didLog.withLock { $0 = true } @@ -129,7 +129,7 @@ struct HTTPLoggingTests { @Test func disabledLogger_DoesNotExecuteErrorClosure() { let logger = DisabledLogger() - let didLog = AllocatedLock(initialState: false) + let didLog = Mutex(false) logger.logError({ didLog.withLock { $0 = true } @@ -142,7 +142,7 @@ struct HTTPLoggingTests { @Test func disabledLogger_DoesNotExecuteCriticalClosure() { let logger = DisabledLogger() - let didLog = AllocatedLock(initialState: false) + let didLog = Mutex(false) logger.logCritical({ didLog.withLock { $0 = true } diff --git a/FlyingSocks/Sources/AsyncBufferedEmptySequence.swift b/FlyingSocks/Sources/AsyncBufferedEmptySequence.swift index ceafb509..a2bcd015 100644 --- a/FlyingSocks/Sources/AsyncBufferedEmptySequence.swift +++ b/FlyingSocks/Sources/AsyncBufferedEmptySequence.swift @@ -48,7 +48,7 @@ package struct AsyncBufferedEmptySequence: Sendable, AsyncBuf if completeImmediately { return nil } - let state = AllocatedLock(initialState: State()) + let state = Mutex(State()) return await withTaskCancellationHandler { await withCheckedContinuation { (continuation: CheckedContinuation) in let shouldCancel = state.withLock { diff --git a/FlyingSocks/Sources/ConsumingAsyncSequence.swift b/FlyingSocks/Sources/ConsumingAsyncSequence.swift index 6a601368..3088017a 100644 --- a/FlyingSocks/Sources/ConsumingAsyncSequence.swift +++ b/FlyingSocks/Sources/ConsumingAsyncSequence.swift @@ -67,10 +67,10 @@ extension ConsumingAsyncSequence { final class SharedBuffer: @unchecked Sendable { - private(set) var state: AllocatedLock + private(set) var state: Mutex init(_ sequence: Base) { - self.state = AllocatedLock(initialState: .initial(sequence)) + self.state = Mutex(.initial(sequence)) } enum State: @unchecked Sendable { diff --git a/FlyingSocks/Sources/IdentifiableContinuation.swift b/FlyingSocks/Sources/IdentifiableContinuation.swift index 0f997561..5e64f414 100644 --- a/FlyingSocks/Sources/IdentifiableContinuation.swift +++ b/FlyingSocks/Sources/IdentifiableContinuation.swift @@ -44,6 +44,7 @@ /// - 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. +@inlinable package func withIdentifiableContinuation( isolation: isolated (any Actor)? = #isolation, function: String = #function, @@ -51,7 +52,7 @@ package func withIdentifiableContinuation( onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void ) async -> T { let id = IdentifiableContinuation.ID() - let state = AllocatedLock(initialState: (isStarted: false, isCancelled: false)) + let state = Mutex((isStarted: false, isCancelled: false)) nonisolated(unsafe) let body = body return await withTaskCancellationHandler { await withCheckedContinuation(isolation: isolation, function: function) { @@ -90,6 +91,7 @@ package func withIdentifiableContinuation( /// - 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. +@inlinable package func withIdentifiableThrowingContinuation( isolation: isolated (any Actor)? = #isolation, function: String = #function, @@ -97,7 +99,7 @@ package func withIdentifiableThrowingContinuation( onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void ) async throws -> T { let id = IdentifiableContinuation.ID() - let state = AllocatedLock(initialState: (isStarted: false, isCancelled: false)) + let state = Mutex((isStarted: false, isCancelled: false)) nonisolated(unsafe) let body = body return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation(isolation: isolation, function: function) { @@ -138,6 +140,7 @@ package func withIdentifiableThrowingContinuation( /// - 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, @@ -145,7 +148,7 @@ package func withIdentifiableContinuation( onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void ) async -> T { let id = IdentifiableContinuation.ID() - let state = AllocatedLock(initialState: (isStarted: false, isCancelled: false)) + let state = Mutex((isStarted: false, isCancelled: false)) return await withTaskCancellationHandler { await withCheckedContinuation(function: function) { let continuation = IdentifiableContinuation(id: id, continuation: $0) @@ -186,6 +189,7 @@ package func withIdentifiableContinuation( /// - 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, @@ -193,7 +197,7 @@ package func withIdentifiableThrowingContinuation( onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void ) async throws -> T { let id = IdentifiableContinuation.ID() - let state = AllocatedLock(initialState: (isStarted: false, isCancelled: false)) + let state = Mutex((isStarted: false, isCancelled: false)) return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation(function: function) { let continuation = IdentifiableContinuation(id: id, continuation: $0) @@ -219,23 +223,30 @@ package func withIdentifiableThrowingContinuation( } #endif +@usableFromInline package struct IdentifiableContinuation: Sendable, Identifiable where E: Error { + @usableFromInline package let id: ID + @usableFromInline package final class ID: Hashable, Sendable { + @usableFromInline init() { } + @usableFromInline package func hash(into hasher: inout Hasher) { ObjectIdentifier(self).hash(into: &hasher) } + @usableFromInline package static func == (lhs: IdentifiableContinuation.ID, rhs: IdentifiableContinuation.ID) -> Bool { lhs === rhs } } + @usableFromInline init(id: ID, continuation: CheckedContinuation) { self.id = id self.continuation = continuation diff --git a/FlyingSocks/Sources/AllocatedLock.swift b/FlyingSocks/Sources/Mutex.swift similarity index 62% rename from FlyingSocks/Sources/AllocatedLock.swift rename to FlyingSocks/Sources/Mutex.swift index e4142310..f9a57290 100644 --- a/FlyingSocks/Sources/AllocatedLock.swift +++ b/FlyingSocks/Sources/Mutex.swift @@ -1,14 +1,14 @@ // -// AllocatedLock.swift -// AllocatedLock +// Mutex.swift +// swift-mutex // -// Created by Simon Whitty on 10/04/2023. -// Copyright 2023 Simon Whitty +// 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/AllocatedLock +// 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 @@ -29,68 +29,65 @@ // SOFTWARE. // -// Backports the Swift interface around os_unfair_lock_t available in recent Darwin platforms -// -package struct AllocatedLock: @unchecked Sendable { - - @usableFromInline +// Backports the Swift 6.0 Mutex API +@usableFromInline +package struct Mutex: @unchecked Sendable { let storage: Storage +} + +#if compiler(>=6) +package extension Mutex { -#if compiler(>=6.0) - package init(initialState: sending State) { - self.storage = Storage(initialState: initialState) + @usableFromInline + init(_ initialValue: consuming sending Value) { + self.storage = Storage(initialValue) } - @inlinable - package func withLock(_ body: (inout sending State) throws(E) -> sending R) throws(E) -> sending R where E: Error { + @usableFromInline + borrowing func withLock( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result { storage.lock() defer { storage.unlock() } - return try body(&storage.state) - } -#else - package init(initialState: State) { - self.storage = Storage(initialState: initialState) + return try body(&storage.value) } - @inlinable - package func withLock(_ body: @Sendable (inout State) throws -> R) rethrows -> R { - storage.lock() + @usableFromInline + borrowing func withLockIfAvailable( + _ body: (inout sending Value) throws(E) -> sending Result + ) throws(E) -> sending Result? where E: Error { + guard storage.tryLock() else { return nil } defer { storage.unlock() } - return try body(&storage.state) + return try body(&storage.value) } -#endif } +#else +package extension Mutex { -package extension AllocatedLock where State == Void { - - init() { - self.storage = Storage(initialState: ()) - } - - @inlinable @available(*, noasync) - func lock() { - storage.lock() - } - - @inlinable @available(*, noasync) - func unlock() { - storage.unlock() + @usableFromInline + init(_ initialValue: Value) { + self.storage = Storage(initialValue) } - @inlinable - func withLock(_ body: @Sendable () throws -> R) rethrows -> R where R: Sendable { + @usableFromInline + borrowing func withLock( + _ body: (inout Value) throws -> Result + ) rethrows -> Result { storage.lock() defer { storage.unlock() } - return try body() + return try body(&storage.value) } -} -package extension AllocatedLock { - - func copy() -> State where State: Sendable { - withLock { $0 } + @usableFromInline + borrowing func withLockIfAvailable( + _ body: (inout Value) throws -> Result + ) rethrows -> Result? { + guard storage.tryLock() else { return nil } + defer { storage.unlock() } + return try body(&storage.value) } } +#endif #if canImport(Darwin) @@ -98,31 +95,33 @@ 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 + +extension Mutex { -extension AllocatedLock { - @usableFromInline final class Storage { private let _lock: os_unfair_lock_t - @usableFromInline - var state: State + var value: Value - init(initialState: State) { + init(_ initialValue: Value) { self._lock = .allocate(capacity: 1) self._lock.initialize(to: os_unfair_lock()) - self.state = initialState + self.value = initialValue } - @usableFromInline func lock() { os_unfair_lock_lock(_lock) } - @usableFromInline func unlock() { os_unfair_lock_unlock(_lock) } + func tryLock() -> Bool { + os_unfair_lock_trylock(_lock) + } + deinit { self._lock.deinitialize(count: 1) self._lock.deallocate() @@ -134,35 +133,36 @@ extension AllocatedLock { import Glibc -extension AllocatedLock { - @usableFromInline +extension Mutex { + final class Storage { private let _lock: UnsafeMutablePointer - @usableFromInline - var state: State + var value: Value - init(initialState: State) { + init(_ initialValue: 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.state = initialState + self.value = initialValue } - @usableFromInline func lock() { let err = pthread_mutex_lock(_lock) precondition(err == 0, "pthread_mutex_lock error: \(err)") } - @usableFromInline 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)") @@ -176,30 +176,37 @@ extension AllocatedLock { import ucrt import WinSDK -extension AllocatedLock { - @usableFromInline +extension Mutex { + final class Storage { private let _lock: UnsafeMutablePointer - @usableFromInline - var state: State + var value: Value - init(initialState: State) { + init(_ initialValue: Value) { self._lock = .allocate(capacity: 1) InitializeSRWLock(self._lock) - self.state = initialState + self.value = initialValue } - @usableFromInline func lock() { AcquireSRWLockExclusive(_lock) } - @usableFromInline func unlock() { ReleaseSRWLockExclusive(_lock) } + + func tryLock() -> Bool { + TryAcquireSRWLockExclusive(_lock) + } } } #endif + +package extension Mutex where Value: Sendable { + func copy() -> Value { + withLock { $0 } + } +} diff --git a/FlyingSocks/Tests/AllocatedLockTests.swift b/FlyingSocks/Tests/AllocatedLockTests.swift deleted file mode 100644 index 3290f206..00000000 --- a/FlyingSocks/Tests/AllocatedLockTests.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// AllocatedLockTests.swift -// AllocatedLock -// -// Created by Simon Whitty on 10/04/2023. -// Copyright 2023 Simon Whitty -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/AllocatedLock -// -// 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 Testing - -struct AllocatedLockTests { - - @Test - func lockState_IsProtected() async { - let state = AllocatedLock(initialState: 0) - - let total = await withTaskGroup(of: Void.self) { group in - for i in 1...1000 { - group.addTask { - state.withLock { $0 += i } - } - } - await group.waitForAll() - return state.withLock { $0 } - } - - #expect(total == 500500) - } - - @Test - func lock_ReturnsValue() async { - let lock = AllocatedLock() - let value = lock.withLock { true } - #expect(value) - } - - @Test - func lock_Blocks() async { - let lock = AllocatedLock() - await MainActor.run { - lock.unsafeLock() - } - - Task { @MainActor in - try? await Task.sleep(nanoseconds: 200_000) - lock.unsafeUnlock() - } - - let results = await withTaskGroup(of: Bool.self) { group in - group.addTask { - try? await Task.sleep(nanoseconds: 10_000) - return true - } - group.addTask { - lock.unsafeLock() - lock.unsafeUnlock() - return false - } - let first = await group.next()! - let second = await group.next()! - return [first, second] - } - #expect(results == [true, false]) - } -} - -// sidestep warning: unavailable from asynchronous contexts -extension AllocatedLock where State == Void { - func unsafeLock() { lock() } - func unsafeUnlock() { unlock() } -} diff --git a/FlyingSocks/Tests/IdentifiableContinuationTests.swift b/FlyingSocks/Tests/IdentifiableContinuationTests.swift index 4e329a93..e5f49d2f 100644 --- a/FlyingSocks/Tests/IdentifiableContinuationTests.swift +++ b/FlyingSocks/Tests/IdentifiableContinuationTests.swift @@ -259,7 +259,7 @@ private extension Actor { func identifiableContinuation( body: @Sendable (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } + onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } ) async -> T { #if compiler(>=6.0) await withIdentifiableContinuation(body: body, onCancel: handler) @@ -270,7 +270,7 @@ private extension Actor { func throwingIdentifiableContinuation( body: @Sendable (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } + onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } ) async throws -> T { #if compiler(>=6.0) try await withIdentifiableThrowingContinuation(body: body, onCancel: handler) diff --git a/FlyingSocks/Tests/MutexTests.swift b/FlyingSocks/Tests/MutexTests.swift new file mode 100644 index 00000000..070ccdc0 --- /dev/null +++ b/FlyingSocks/Tests/MutexTests.swift @@ -0,0 +1,74 @@ +// +// MutexTests.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. +// + +@testable import FlyingSocks +import Testing + +struct MutexTests { + + @Test + func withLock_ReturnsValue() { + let mutex = Mutex("fish") + let val = mutex.withLock { + $0 + " & chips" + } + #expect(val == "fish & chips") + } + + @Test + func withLock_ThrowsError() { + let mutex = Mutex("fish") + #expect(throws: CancellationError.self) { + try mutex.withLock { _ -> Void in throw CancellationError() } + } + } + + @Test + func lockIfAvailable_ReturnsValue() { + let mutex = Mutex("fish") + mutex.storage.lock() + #expect( + mutex.withLockIfAvailable { _ in "chips" } == nil + ) + mutex.storage.unlock() + #expect( + mutex.withLockIfAvailable { _ in "chips" } == "chips" + ) + } + + @Test + func withLockIfAvailable_ThrowsError() { + let mutex = Mutex("fish") + #expect(throws: CancellationError.self) { + try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() } + } + } +} diff --git a/FlyingSocks/XCTests/AllocatedLockTests.swift b/FlyingSocks/XCTests/AllocatedLockTests.swift deleted file mode 100644 index b859fd6f..00000000 --- a/FlyingSocks/XCTests/AllocatedLockTests.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// AllocatedLockTests.swift -// AllocatedLock -// -// Created by Simon Whitty on 10/04/2023. -// Copyright 2023 Simon Whitty -// -// Distributed under the permissive MIT license -// Get the latest version from here: -// -// https://github.com/swhitty/AllocatedLock -// -// 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 AllocatedLockTests: XCTestCase { - - func testLockState_IsProtected() async { - let state = AllocatedLock(initialState: 0) - - let total = await withTaskGroup(of: Void.self) { group in - for i in 1...1000 { - group.addTask { - state.withLock { $0 += i } - } - } - await group.waitForAll() - return state.withLock { $0 } - } - - XCTAssertEqual(total, 500500) - } - - func testLock_ReturnsValue() async { - let lock = AllocatedLock() - let value = lock.withLock { true } - XCTAssertTrue(value) - } - - func testLock_Blocks() async { - let lock = AllocatedLock() - await MainActor.run { - lock.unsafeLock() - } - - Task { @MainActor in - try? await Task.sleep(nanoseconds: 200_000) - lock.unsafeUnlock() - } - - let results = await withTaskGroup(of: Bool.self) { group in - group.addTask { - try? await Task.sleep(nanoseconds: 10_000) - return true - } - group.addTask { - lock.unsafeLock() - lock.unsafeUnlock() - return false - } - let first = await group.next()! - let second = await group.next()! - return [first, second] - } - XCTAssertEqual(results, [true, false]) - } -} - -// sidestep warning: unavailable from asynchronous contexts -extension AllocatedLock where State == Void { - func unsafeLock() { lock() } - func unsafeUnlock() { unlock() } -} diff --git a/FlyingSocks/XCTests/IdentifiableContinuationTests.swift b/FlyingSocks/XCTests/IdentifiableContinuationTests.swift index fc0e10c8..8d447def 100644 --- a/FlyingSocks/XCTests/IdentifiableContinuationTests.swift +++ b/FlyingSocks/XCTests/IdentifiableContinuationTests.swift @@ -242,7 +242,7 @@ private extension Actor { func identifiableContinuation( body: @Sendable (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } + onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } ) async -> T { #if compiler(>=6.0) await withIdentifiableContinuation(body: body, onCancel: handler) @@ -253,7 +253,7 @@ private extension Actor { func throwingIdentifiableContinuation( body: @Sendable (IdentifiableContinuation) -> Void, - onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } + onCancel handler: @Sendable (IdentifiableContinuation.ID) -> Void = { _ in } ) async throws -> T { #if compiler(>=6.0) try await withIdentifiableThrowingContinuation(body: body, onCancel: handler) diff --git a/FlyingSocks/XCTests/MutexTests.swift b/FlyingSocks/XCTests/MutexTests.swift new file mode 100644 index 00000000..24ec3025 --- /dev/null +++ b/FlyingSocks/XCTests/MutexTests.swift @@ -0,0 +1,71 @@ +// +// MutexTests.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. +// + +@testable import FlyingSocks +import XCTest + +final class MutexTests: 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.storage.lock() + XCTAssertNil( + mutex.withLockIfAvailable { _ in "chips" } + ) + mutex.storage.unlock() + XCTAssertEqual( + mutex.withLockIfAvailable { _ in "chips" }, + "chips" + ) + } + + func testWithLockIfAvailable_ThrowsError() { + let mutex = Mutex("fish") + XCTAssertThrowsError(try mutex.withLockIfAvailable { _ -> Void in throw CancellationError() }) { + _ = $0 is CancellationError + } + } +}