Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swift 6 Compatibility #12

Merged
merged 8 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ jobs:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziFoundation
artifactname: SpeziFoundation.xcresult
resultBundle: SpeziFoundation.xcresult
packageios_latest:
name: Build and Test Swift Package iOS Latest
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
with:
runsonlabels: '["macOS", "self-hosted"]'
scheme: SpeziFoundation
xcodeversion: latest
swiftVersion: 6
artifactname: SpeziFoundation-Latest.xcresult
resultBundle: SpeziFoundation-Latest.xcresult
packagewatchos:
name: Build and Test Swift Package watchOS
uses: StanfordSpezi/.github/.github/workflows/xcodebuild-or-fastlane.yml@v2
Expand Down
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import PackageDescription


#if swift(<6)
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableExperimentalFeature("StrictConcurrency")
#else
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("SwiftConcurrency")
let swiftConcurrency: SwiftSetting = .enableUpcomingFeature("StrictConcurrency")
#endif


Expand Down
3 changes: 2 additions & 1 deletion Sources/SpeziFoundation/Misc/TimeoutError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ extension TimeoutError: LocalizedError {
/// - Parameters:
/// - timeout: The duration of the timeout.
/// - action: The action to run once the timeout passed.
public func withTimeout(of timeout: Duration, perform action: () async -> Void) async {
@inlinable
public func withTimeout(of timeout: Duration, perform action: @Sendable () async -> Void) async {
try? await Task.sleep(for: timeout)
guard !Task.isCancelled else {
return
Expand Down
93 changes: 46 additions & 47 deletions Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
/// ```
///
/// - Warning: `cancelAll` will trigger a runtime error if it attempts to cancel tasks that are not cancellable.
public final class AsyncSemaphore: @unchecked Sendable {
public final class AsyncSemaphore: Sendable {
private enum Suspension {
case cancelable(UnsafeContinuation<Void, Error>)
case regular(UnsafeContinuation<Void, Never>)
Expand All @@ -72,9 +72,9 @@
}


private var value: Int
private var suspendedTasks: [SuspendedTask] = []
private let nsLock = NSLock()
private nonisolated(unsafe) var value: Int

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / CodeQL / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable

Check warning on line 75 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

stored property 'value' of 'Sendable'-conforming class 'AsyncSemaphore' is mutable
private nonisolated(unsafe) var suspendedTasks: [SuspendedTask] = []
private let nsLock = NSLock() // protects both of the non-isolated unsafe properties above


/// Initializes a new semaphore with a given concurrency limit.
Expand All @@ -90,17 +90,17 @@
///
/// Use this method when access to a resource should be awaited without the possibility of cancellation.
public func wait() async {
lock()
unsafeLock() // this is okay, as the continuation body actually runs sync, so we do no have async code within critical region

value -= 1
if value >= 0 {
unlock()
unsafeUnlock()
return
}

await withUnsafeContinuation { continuation in
suspendedTasks.append(SuspendedTask(id: UUID(), suspension: .regular(continuation)))
unlock()
nsLock.unlock()
}
}

Expand All @@ -112,19 +112,19 @@
public func waitCheckingCancellation() async throws {
try Task.checkCancellation() // check if we are already cancelled

lock()
unsafeLock() // this is okay, as the continuation body actually runs sync, so we do no have async code within critical region

do {
// check if we got cancelled while acquiring the lock
try Task.checkCancellation()
} catch {
unlock()
unsafeUnlock()

Check warning on line 121 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L121

Added line #L121 was not covered by tests
throw error
}

value -= 1 // decrease the value
if value >= 0 {
unlock()
unsafeUnlock()
return
}

Expand All @@ -134,28 +134,27 @@
try await withUnsafeThrowingContinuation { (continuation: UnsafeContinuation<Void, Error>) in
if Task.isCancelled {
value += 1 // restore the value
unlock()
unsafeUnlock()

Check warning on line 137 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L137

Added line #L137 was not covered by tests

continuation.resume(throwing: CancellationError())
} else {
suspendedTasks.append(SuspendedTask(id: id, suspension: .cancelable(continuation)))
unlock()
unsafeUnlock()
}
}
} onCancel: {
self.lock()

value += 1

guard let index = suspendedTasks.firstIndex(where: { $0.id == id }) else {
preconditionFailure("Inconsistent internal state reached")
let task = nsLock.withLock {
value += 1

guard let index = suspendedTasks.firstIndex(where: { $0.id == id }) else {
preconditionFailure("Inconsistent internal state reached")

Check warning on line 150 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L150

Added line #L150 was not covered by tests
}

let task = suspendedTasks[index]
suspendedTasks.remove(at: index)
return task
}

let task = suspendedTasks[index]
suspendedTasks.remove(at: index)

unlock()

switch task.suspension {
case .regular:
preconditionFailure("Tried to cancel a task that was not cancellable!")
Expand All @@ -173,18 +172,20 @@
/// - Returns: `true` if a task was resumed, `false` otherwise.
@discardableResult
public func signal() -> Bool {
lock()
let first: SuspendedTask? = nsLock.withLock {
value += 1

guard let first = suspendedTasks.first else {
return nil
}

value += 1
suspendedTasks.removeFirst()
return first
}

guard let first = suspendedTasks.first else {
unlock()
guard let first else {
return false
}

suspendedTasks.removeFirst()
unlock()

first.suspension.resume()
return true
}
Expand All @@ -193,14 +194,13 @@
///
/// This method resumes all `Task`s that are currently waiting for access.
public func signalAll() {
lock()

value += suspendedTasks.count
let tasks = nsLock.withLock {
value += suspendedTasks.count

Check warning on line 198 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L197-L198

Added lines #L197 - L198 were not covered by tests

let tasks = suspendedTasks
self.suspendedTasks.removeAll()

unlock()
let tasks = suspendedTasks
self.suspendedTasks.removeAll()
return tasks
}

Check warning on line 203 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L200-L203

Added lines #L200 - L203 were not covered by tests

for task in tasks {
task.suspension.resume()
Expand All @@ -213,14 +213,13 @@
///
/// - Warning: Will trigger a runtime error if it attempts to cancel `Task`s that are not cancellable.
public func cancelAll() {
lock()

value += suspendedTasks.count
let tasks = nsLock.withLock {
value += suspendedTasks.count

Check warning on line 217 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L216-L217

Added lines #L216 - L217 were not covered by tests

let tasks = suspendedTasks
self.suspendedTasks.removeAll()

unlock()
let tasks = suspendedTasks
self.suspendedTasks.removeAll()
return tasks
}

Check warning on line 222 in Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift

View check run for this annotation

Codecov / codecov/patch

Sources/SpeziFoundation/Semaphore/AsyncSemaphore.swift#L219-L222

Added lines #L219 - L222 were not covered by tests

for task in tasks {
switch task.suspension {
Expand All @@ -232,11 +231,11 @@
}
}

private func lock() {
private func unsafeLock() { // silences a warning, just make sure that you don't have an await in between lock/unlock!
nsLock.lock()
}

private func unlock() {
private func unsafeUnlock() {
nsLock.unlock()
}
}
21 changes: 16 additions & 5 deletions Tests/SpeziFoundationTests/SharedRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ private protocol AnyTestInstance {

func testKeyLikeKnowledgeSource()

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicy()

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicyReadOnly()

@MainActor
func testComputedKnowledgeSourceStorePolicy()
}

Expand Down Expand Up @@ -66,8 +69,8 @@ final class SharedRepositoryTests: XCTestCase {
}

struct DefaultedTestStruct: DefaultProvidingKnowledgeSource, TestTypes {
var value: Int
static let defaultValue = DefaultedTestStruct(value: 0)
var value: Int
}

struct ComputedTestStruct<Policy: ComputedKnowledgeSourceStoragePolicy>: ComputedKnowledgeSource {
Expand All @@ -76,7 +79,9 @@ final class SharedRepositoryTests: XCTestCase {
typealias StoragePolicy = Policy

static func compute<Repository: SharedRepository<Anchor>>(from repository: Repository) -> Int {
computedValue
MainActor.assumeIsolated {
computedValue
}
}
}

Expand All @@ -86,7 +91,9 @@ final class SharedRepositoryTests: XCTestCase {
typealias StoragePolicy = Policy

static func compute<Repository: SharedRepository<Anchor>>(from repository: Repository) -> Int? {
optionalComputedValue
MainActor.assumeIsolated {
optionalComputedValue
}
}
}

Expand Down Expand Up @@ -254,11 +261,12 @@ final class SharedRepositoryTests: XCTestCase {
}
}

static var computedValue: Int = 3
static var optionalComputedValue: Int?
@MainActor static var computedValue: Int = 3
@MainActor static var optionalComputedValue: Int?

private var repos: [AnyTestInstance] = []

@MainActor
override func setUp() {
repos = [TestInstance(HeapRepository<TestAnchor>()), TestInstance(ValueRepository<TestAnchor>())]
Self.computedValue = 3
Expand Down Expand Up @@ -314,14 +322,17 @@ final class SharedRepositoryTests: XCTestCase {
repos.forEach { $0.testKeyLikeKnowledgeSource() }
}

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicy() {
repos.forEach { $0.testComputedKnowledgeSourceComputedOnlyPolicy() }
}

@MainActor
func testComputedKnowledgeSourceComputedOnlyPolicyReadOnly() {
repos.forEach { $0.testComputedKnowledgeSourceComputedOnlyPolicyReadOnly() }
}

@MainActor
func testComputedKnowledgeSourceStorePolicy() {
repos.forEach { $0.testComputedKnowledgeSourceStorePolicy() }
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/SpeziFoundationTests/TimeoutTests.swift
Supereg marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
final class TimeoutTests: XCTestCase {
@MainActor private var continuation: CheckedContinuation<Void, any Error>?

@MainActor
func operation(for duration: Duration) {
Task { @MainActor in
try? await Task.sleep(for: duration)
Expand All @@ -28,7 +29,7 @@
func operationMethod(timeout: Duration, operation: Duration, timeoutExpectation: XCTestExpectation) async throws {
async let _ = withTimeout(of: timeout) { @MainActor in
timeoutExpectation.fulfill()
if let continuation {

Check warning on line 32 in Tests/SpeziFoundationTests/TimeoutTests.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package iOS / Test using xcodebuild or run fastlane

capture of 'self' with non-sendable type 'TimeoutTests' in 'async let' binding

Check warning on line 32 in Tests/SpeziFoundationTests/TimeoutTests.swift

View workflow job for this annotation

GitHub Actions / Build and Test Swift Package watchOS / Test using xcodebuild or run fastlane

capture of 'self' with non-sendable type 'TimeoutTests' in 'async let' binding
continuation.resume(throwing: TimeoutError())
self.continuation = nil
}
Expand All @@ -40,6 +41,7 @@
}
}

@MainActor
func testTimeout() async throws {
let negativeExpectation = XCTestExpectation()
negativeExpectation.isInverted = true
Expand Down
Loading