diff --git a/.gitignore b/.gitignore index 330d167..08003cc 100644 --- a/.gitignore +++ b/.gitignore @@ -44,7 +44,7 @@ playground.xcworkspace # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project -# .swiftpm +.swiftpm/ .build/ @@ -88,3 +88,6 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + +# Ignore DS Store from Mac +.DS_Store diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a..0000000 --- a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AsyncTimeSequences.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AsyncTimeSequences.xcscheme deleted file mode 100644 index bde1480..0000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/AsyncTimeSequences.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/FormatScript.sh b/FormatScript.sh deleted file mode 100755 index 32104c0..0000000 --- a/FormatScript.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -swift-format format --in-place --recursive Sources --configuration SwiftFormatConfiguration.json - -swift-format format --in-place --recursive Tests --configuration SwiftFormatConfiguration.json - diff --git a/Package.swift b/Package.swift index aa5a408..3af66f8 100644 --- a/Package.swift +++ b/Package.swift @@ -1,47 +1,47 @@ -// swift-tools-version:5.5 +// swift-tools-version:6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( - name: "AsyncTimeSequences", - platforms: [.iOS(.v13), .macOS(.v10_15), .watchOS(.v6), .tvOS(.v13)], - products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library( - name: "AsyncTimeSequences", - targets: ["AsyncTimeSequences"] - ), - .library( - name: "AsyncTimeSequencesSupport", - targets: ["AsyncTimeSequencesSupport"] - ), - ], - dependencies: [ - // Dependencies declare other packages that this package depends on. - ], - targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. - .target( - name: "AsyncTimeSequences", - dependencies: [], - path: "Sources/AsyncTimeSequences" - ), - .target( - name: "AsyncTimeSequencesSupport", - dependencies: [ - "AsyncTimeSequences", - ], - path: "Sources/AsyncTimeSequencesSupport" - ), - .testTarget( - name: "AsyncTimeSequencesTests", - dependencies: [ - "AsyncTimeSequences", - "AsyncTimeSequencesSupport" - ], - path: "Tests" - ), - ] + name: "AsyncTimeSequences", + platforms: [.iOS(.v13), .macOS(.v13), .watchOS(.v6), .tvOS(.v13)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "AsyncTimeSequences", + targets: ["AsyncTimeSequences"] + ), + .library( + name: "AsyncTimeSequencesSupport", + targets: ["AsyncTimeSequencesSupport"] + ), + ], + dependencies: [ + // Dependencies declare other packages that this package depends on. + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "AsyncTimeSequences", + dependencies: [], + path: "Sources/AsyncTimeSequences" + ), + .target( + name: "AsyncTimeSequencesSupport", + dependencies: [ + "AsyncTimeSequences" + ], + path: "Sources/AsyncTimeSequencesSupport" + ), + .testTarget( + name: "AsyncTimeSequencesTests", + dependencies: [ + "AsyncTimeSequences", + "AsyncTimeSequencesSupport", + ], + path: "Tests" + ), + ] ) diff --git a/Sources/AsyncTimeSequences/AsyncScheduler/AsyncScheduler.swift b/Sources/AsyncTimeSequences/AsyncScheduler/AsyncScheduler.swift index d5ebe1b..1fbaf8f 100644 --- a/Sources/AsyncTimeSequences/AsyncScheduler/AsyncScheduler.swift +++ b/Sources/AsyncTimeSequences/AsyncScheduler/AsyncScheduler.swift @@ -7,7 +7,7 @@ import Foundation -public typealias AsyncSchedulerHandler = () async -> Void +public typealias AsyncSchedulerHandler = @Sendable () async -> Void public protocol AsyncScheduler: Actor { var now: TimeInterval { get } @@ -23,6 +23,7 @@ public actor MainAsyncScheduler: AsyncScheduler { lazy var idCounter: UInt = 0 lazy var completedElementIds = Set() lazy var cancelledElementIds = Set() + private var isCompletingElement = false public var now: TimeInterval { Date().timeIntervalSince1970 @@ -34,8 +35,6 @@ public actor MainAsyncScheduler: AsyncScheduler { /// - parameter handler: async closure to be executed when 'after' time elapses /// /// - Returns: reference to a Task which supports cancellation - /// - /// - Complexity: O(log n) where n is the number of elements currently scheduled @discardableResult public func schedule( after: TimeInterval, @@ -52,19 +51,52 @@ public actor MainAsyncScheduler: AsyncScheduler { increaseCounterId() - return Task { - try? await Task.sleep(nanoseconds: UInt64(after * 1_000_000_000)) - await complete(currentId: currentId, cancelled: Task.isCancelled) - } + return createScheduledExecutionTask(currentId: currentId, after: after) } /// Based on the timeIntervalSince1970 from Date, the smallest intervals will need /// to complete before other elements' handlers can be executed. Due to the nature /// of Tasks, there could be some situations where some tasks scheduled to finish /// before others finish first. This could potentially have unwanted behaviors on - /// objects scheduling events. To address this matter, a minimum priority queue - /// is critical to always keep the first element that should be completed in the - /// top of the queue. Once its task completes, a Set will keep track of all + /// objects scheduling events. + /// + /// - parameter currentId: integer variable denoting handler/task id + /// + /// - Returns: reference to a Task which supports cancellation + private func createScheduledExecutionTask( + currentId: UInt, + after: TimeInterval + ) -> Task { + return Task { + try? await Task.sleep(for: .seconds(after)) + + completedElementIds.insert(currentId) + if Task.isCancelled { + cancelledElementIds.insert(currentId) + } + + // Make sure that only one complete method is running at all times. + // The reason why this is important is because there is an inner await in a while loop which + // releases the execution of this actor and it cause race conditions if another scheduled + // task completes within the time this method is executing causing a weird state where two + // while loops might have erroneous values and destroy the serial execution intended from + // this method. + guard !isCompletingElement else { return } + + // Block any other Tasks from calling complete from this point. + isCompletingElement = true + + await complete(currentId: currentId) + + // Allow any future callers of this method to call complete. + isCompletingElement = false + } + } + + /// This method runs the completion handler for a given scheduled item matching the `currentId`. + /// + /// A minimum priority queue is critical to always keep the first element that should be + /// completed in the top of the queue. Once its task completes, a Set will keep track of all /// completed ID tasks that are yet to be executed. If the current top element of /// the queue has already completed, its closure will execute. This will repeat /// until all completed top elements of the queue are executed. @@ -72,16 +104,16 @@ public actor MainAsyncScheduler: AsyncScheduler { /// introduced to some scheduled async-closures. Ideally, this would be in the /// order of micro/nanoseconds depending of the system load. /// - /// - parameter currentId: integer variable denoting handler/task id - /// - parameter cancelled: boolean flag required to determine whether or not to execute the handler + /// This method will execute an inner loop resolving all available completed elements. + /// + /// Note that his actor switches execution and during this `paused` time another scheduled task + /// can complete and call `complete`. It is important to have only one `complete` running at + /// all times (specifically, due the inner while loop). + /// + /// This method should only be called from within `createScheduledExecutionTask`. /// /// - Complexity: O(log n) where n is the number of elements currently scheduled - private func complete(currentId: UInt, cancelled: Bool) async { - completedElementIds.insert(currentId) - if cancelled { - cancelledElementIds.insert(currentId) - } - + private func complete(currentId: UInt) async { while let minElement = queue.peek, completedElementIds.contains(minElement.id) { queue.removeFirst() completedElementIds.remove(minElement.id) diff --git a/Sources/AsyncTimeSequences/AsyncScheduler/AsyncSchedulerHandlerElement.swift b/Sources/AsyncTimeSequences/AsyncScheduler/AsyncSchedulerHandlerElement.swift index 0bdf1b7..68b3b86 100644 --- a/Sources/AsyncTimeSequences/AsyncScheduler/AsyncSchedulerHandlerElement.swift +++ b/Sources/AsyncTimeSequences/AsyncScheduler/AsyncSchedulerHandlerElement.swift @@ -16,7 +16,7 @@ struct AsyncSchedulerHandlerElement { extension AsyncSchedulerHandlerElement: Comparable { static func < (lhs: AsyncSchedulerHandlerElement, rhs: AsyncSchedulerHandlerElement) -> Bool { if lhs.time == rhs.time { - return lhs.id <= rhs.id + return lhs.id < rhs.id } return lhs.time < rhs.time } diff --git a/Sources/AsyncTimeSequences/Debounce/AsyncDebounceSequence.swift b/Sources/AsyncTimeSequences/Debounce/AsyncDebounceSequence.swift index 08d629e..0a6f1b7 100644 --- a/Sources/AsyncTimeSequences/Debounce/AsyncDebounceSequence.swift +++ b/Sources/AsyncTimeSequences/Debounce/AsyncDebounceSequence.swift @@ -8,7 +8,7 @@ import Combine import Foundation -public struct AsyncDebounceSequence { +public struct AsyncDebounceSequence where Base.Element: Sendable { @usableFromInline let base: Base @@ -26,7 +26,7 @@ public struct AsyncDebounceSequence { } } -extension AsyncSequence { +extension AsyncSequence where Element: Sendable { @inlinable public __consuming func debounce( for interval: TimeInterval, @@ -36,7 +36,7 @@ extension AsyncSequence { } } -extension AsyncDebounceSequence: AsyncSequence { +extension AsyncDebounceSequence: AsyncSequence where Base.Element: Sendable { public typealias Element = Base.Element /// The type of iterator that produces elements of the sequence. @@ -107,7 +107,7 @@ extension AsyncDebounceSequence: AsyncSequence { } @usableFromInline - struct Debounce { + struct Debounce: @unchecked Sendable { private var baseIterator: Base.AsyncIterator private let actor: DebounceActor @@ -138,13 +138,13 @@ extension AsyncDebounceSequence: AsyncSequence { @inlinable public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { return AsyncStream { (continuation: AsyncStream.Continuation) in + var debounce = Debounce( + baseIterator: base.makeAsyncIterator(), + continuation: continuation, + interval: interval, + scheduler: scheduler + ) Task { - var debounce = Debounce( - baseIterator: base.makeAsyncIterator(), - continuation: continuation, - interval: interval, - scheduler: scheduler - ) await debounce.start() } }.makeAsyncIterator() diff --git a/Sources/AsyncTimeSequences/Delay/AsyncDelaySequence.swift b/Sources/AsyncTimeSequences/Delay/AsyncDelaySequence.swift index f3d162f..032c4fb 100644 --- a/Sources/AsyncTimeSequences/Delay/AsyncDelaySequence.swift +++ b/Sources/AsyncTimeSequences/Delay/AsyncDelaySequence.swift @@ -8,7 +8,7 @@ import Combine import Foundation -public struct AsyncDelaySequence { +public struct AsyncDelaySequence where Base.Element: Sendable { @usableFromInline let base: Base @@ -26,7 +26,7 @@ public struct AsyncDelaySequence { } } -extension AsyncSequence { +extension AsyncSequence where Element: Sendable { @inlinable public __consuming func delay( for interval: TimeInterval, @@ -36,7 +36,7 @@ extension AsyncSequence { } } -extension AsyncDelaySequence: AsyncSequence { +extension AsyncDelaySequence: AsyncSequence where Base.Element: Sendable { public typealias Element = Base.Element /// The type of iterator that produces elements of the sequence. @@ -97,7 +97,7 @@ extension AsyncDelaySequence: AsyncSequence { } @usableFromInline - struct Delay { + struct Delay: @unchecked Sendable { private var baseIterator: Base.AsyncIterator private let actor: DelayActor @@ -128,13 +128,13 @@ extension AsyncDelaySequence: AsyncSequence { @inlinable public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { return AsyncStream { (continuation: AsyncStream.Continuation) in + var delay = Delay( + baseIterator: base.makeAsyncIterator(), + continuation: continuation, + interval: interval, + scheduler: scheduler + ) Task { - var delay = Delay( - baseIterator: base.makeAsyncIterator(), - continuation: continuation, - interval: interval, - scheduler: scheduler - ) await delay.start() } }.makeAsyncIterator() diff --git a/Sources/AsyncTimeSequences/MeasureInterval/AsyncMeasureIntervalSequence.swift b/Sources/AsyncTimeSequences/MeasureInterval/AsyncMeasureIntervalSequence.swift index f334f4b..8579531 100644 --- a/Sources/AsyncTimeSequences/MeasureInterval/AsyncMeasureIntervalSequence.swift +++ b/Sources/AsyncTimeSequences/MeasureInterval/AsyncMeasureIntervalSequence.swift @@ -8,7 +8,7 @@ import Combine import Foundation -public struct AsyncMeasureIntervalSequence { +public struct AsyncMeasureIntervalSequence where Base.Element: Sendable { @usableFromInline let base: Base @@ -22,7 +22,7 @@ public struct AsyncMeasureIntervalSequence { } } -extension AsyncSequence { +extension AsyncSequence where Element: Sendable { @inlinable public __consuming func measureInterval( using scheduler: AsyncScheduler @@ -31,7 +31,7 @@ extension AsyncSequence { } } -extension AsyncMeasureIntervalSequence: AsyncSequence { +extension AsyncMeasureIntervalSequence: AsyncSequence where Base.Element: Sendable { public typealias Element = TimeInterval /// The type of iterator that produces elements of the sequence. @@ -72,7 +72,7 @@ extension AsyncMeasureIntervalSequence: AsyncSequence { } @usableFromInline - struct MeasureInterval { + struct MeasureInterval: @unchecked Sendable { private var baseIterator: Base.AsyncIterator private let actor: MeasureIntervalActor @@ -101,12 +101,12 @@ extension AsyncMeasureIntervalSequence: AsyncSequence { @inlinable public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { return AsyncStream { (continuation: AsyncStream.Continuation) in + var measureInterval = MeasureInterval( + baseIterator: base.makeAsyncIterator(), + continuation: continuation, + scheduler: scheduler + ) Task { - var measureInterval = MeasureInterval( - baseIterator: base.makeAsyncIterator(), - continuation: continuation, - scheduler: scheduler - ) await measureInterval.start() } }.makeAsyncIterator() diff --git a/Sources/AsyncTimeSequences/Throttle/AsyncThrottleSequence.swift b/Sources/AsyncTimeSequences/Throttle/AsyncThrottleSequence.swift index ca0b120..a4cd036 100644 --- a/Sources/AsyncTimeSequences/Throttle/AsyncThrottleSequence.swift +++ b/Sources/AsyncTimeSequences/Throttle/AsyncThrottleSequence.swift @@ -8,7 +8,7 @@ import Combine import Foundation -extension AsyncSequence { +extension AsyncSequence where Element: Sendable { @inlinable public __consuming func throttle( for interval: TimeInterval, @@ -19,7 +19,7 @@ extension AsyncSequence { } } -public struct AsyncThrottleSequence { +public struct AsyncThrottleSequence where Base.Element: Sendable { @usableFromInline let base: Base @@ -41,7 +41,7 @@ public struct AsyncThrottleSequence { } } -extension AsyncThrottleSequence: AsyncSequence { +extension AsyncThrottleSequence: AsyncSequence where Base.Element: Sendable { public typealias Element = Base.Element /// The type of iterator that produces elements of the sequence. @@ -131,7 +131,7 @@ extension AsyncThrottleSequence: AsyncSequence { } @usableFromInline - struct Throttle { + struct Throttle: @unchecked Sendable { private var baseIterator: Base.AsyncIterator private let actor: ThrottleActor @@ -164,14 +164,14 @@ extension AsyncThrottleSequence: AsyncSequence { @inlinable public __consuming func makeAsyncIterator() -> AsyncStream.Iterator { return AsyncStream { (continuation: AsyncStream.Continuation) in + var throttle = Throttle( + baseIterator: base.makeAsyncIterator(), + continuation: continuation, + interval: interval, + scheduler: scheduler, + latest: latest + ) Task { - var throttle = Throttle( - baseIterator: base.makeAsyncIterator(), - continuation: continuation, - interval: interval, - scheduler: scheduler, - latest: latest - ) await throttle.start() } }.makeAsyncIterator() diff --git a/Sources/AsyncTimeSequences/Timeout/AsyncTimeoutSequence.swift b/Sources/AsyncTimeSequences/Timeout/AsyncTimeoutSequence.swift index 2c5b29a..1a7bea6 100644 --- a/Sources/AsyncTimeSequences/Timeout/AsyncTimeoutSequence.swift +++ b/Sources/AsyncTimeSequences/Timeout/AsyncTimeoutSequence.swift @@ -8,7 +8,7 @@ import Combine import Foundation -public struct AsyncTimeoutSequence { +public struct AsyncTimeoutSequence where Base.Element: Sendable { @usableFromInline let base: Base @@ -26,7 +26,7 @@ public struct AsyncTimeoutSequence { } } -extension AsyncSequence { +extension AsyncSequence where Element: Sendable { @inlinable public __consuming func timeout( for interval: TimeInterval, @@ -41,7 +41,7 @@ public enum AsyncTimeSequenceError: Error { } // TODO: handle continuation.finish being called multiple times -extension AsyncTimeoutSequence: AsyncSequence { +extension AsyncTimeoutSequence: AsyncSequence where Base.Element: Sendable { public typealias Element = Base.Element /// The type of iterator that produces elements of the sequence. @@ -105,7 +105,7 @@ extension AsyncTimeoutSequence: AsyncSequence { } @usableFromInline - struct Timeout { + struct Timeout: @unchecked Sendable { private var baseIterator: Base.AsyncIterator private let actor: TimeoutActor @@ -138,13 +138,13 @@ extension AsyncTimeoutSequence: AsyncSequence { public __consuming func makeAsyncIterator() -> AsyncThrowingStream.Iterator { return AsyncThrowingStream { (continuation: AsyncThrowingStream.Continuation) in + var timeout = Timeout( + baseIterator: base.makeAsyncIterator(), + continuation: continuation, + interval: interval, + scheduler: scheduler + ) Task { - var timeout = Timeout( - baseIterator: base.makeAsyncIterator(), - continuation: continuation, - interval: interval, - scheduler: scheduler - ) await timeout.start() } }.makeAsyncIterator() diff --git a/Sources/AsyncTimeSequencesSupport/ControlledDataSequence.swift b/Sources/AsyncTimeSequencesSupport/ControlledDataSequence.swift index 6642aa2..d953d4f 100644 --- a/Sources/AsyncTimeSequencesSupport/ControlledDataSequence.swift +++ b/Sources/AsyncTimeSequencesSupport/ControlledDataSequence.swift @@ -9,7 +9,7 @@ import Foundation /// This is a really convenient sequence designed to ease the testing of async sequences. /// It provides access to the ControlledDataIterator, which is critical for testing. -public struct ControlledDataSequence: AsyncSequence { +public struct ControlledDataSequence: AsyncSequence { public typealias Element = T public let iterator: ControlledDataIterator @@ -26,7 +26,7 @@ public struct ControlledDataSequence: AsyncSequence { /// This class extends AsyncIteratorProtocol in order to provide an object that returns /// elements on next(). The critical function in this class is waitForItemsToBeSent(count), /// which allows the owner of this iterator to wait until n elements have been dispatched via next(). -public final class ControlledDataIterator: AsyncIteratorProtocol { +public final class ControlledDataIterator: AsyncIteratorProtocol { private let dataActor: ControlledDataActor init(items: [T]) { @@ -42,7 +42,7 @@ public final class ControlledDataIterator: AsyncIteratorProtocol { } } -actor ControlledDataActor { +actor ControlledDataActor { let items: [T] var allowedItemsToBeSentCount = Int.zero var index = Int.zero diff --git a/SwiftFormatConfiguration.json b/SwiftFormatConfiguration.json deleted file mode 100644 index d6854ba..0000000 --- a/SwiftFormatConfiguration.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "blankLineBetweenMembers" : { - "ignoreSingleLineProperties" : true - }, - "indentation" : { - "spaces" : 2 - }, - "indentConditionalCompilationBlocks" : true, - "lineBreakBeforeControlFlowKeywords" : false, - "lineBreakBeforeEachArgument" : false, - "lineLength" : 100, - "maximumBlankLines" : 1, - "respectsExistingLineBreaks" : true, - "rules" : { - "AllPublicDeclarationsHaveDocumentation" : true, - "AlwaysUseLowerCamelCase" : true, - "AmbiguousTrailingClosureOverload" : true, - "BeginDocumentationCommentWithOneLineSummary" : true, - "BlankLineBetweenMembers" : true, - "CaseIndentLevelEqualsSwitch" : true, - "DoNotUseSemicolons" : true, - "DontRepeatTypeInStaticProperties" : true, - "FullyIndirectEnum" : true, - "GroupNumericLiterals" : true, - "IdentifiersMustBeASCII" : true, - "MultiLineTrailingCommas" : true, - "NeverForceUnwrap" : true, - "NeverUseForceTry" : true, - "NeverUseImplicitlyUnwrappedOptionals" : true, - "NoAccessLevelOnExtensionDeclaration" : true, - "NoBlockComments" : true, - "NoCasesWithOnlyFallthrough" : true, - "NoEmptyTrailingClosureParentheses" : true, - "NoLabelsInCasePatterns" : true, - "NoLeadingUnderscores" : true, - "NoParensAroundConditions" : true, - "NoVoidReturnOnFunctionSignature" : true, - "OneCasePerLine" : true, - "OneVariableDeclarationPerLine" : true, - "OnlyOneTrailingClosureArgument" : true, - "OrderedImports" : true, - "ReturnVoidInsteadOfEmptyTuple" : true, - "UseEnumForNamespacing" : true, - "UseLetInEveryBoundCaseVariable" : true, - "UseShorthandTypeNames" : true, - "UseSingleLinePropertyGetter" : true, - "UseSynthesizedInitializer" : true, - "UseTripleSlashForDocumentationComments" : true, - "ValidateDocumentationComments" : true - }, - "tabWidth" : 8, - "version" : 1 -} diff --git a/Tests/AsyncTimeSequencesTests/AsyncDebounceSequence+Tests.swift b/Tests/AsyncTimeSequencesTests/AsyncDebounceSequence+Tests.swift index 9ae6701..483967c 100644 --- a/Tests/AsyncTimeSequencesTests/AsyncDebounceSequence+Tests.swift +++ b/Tests/AsyncTimeSequencesTests/AsyncDebounceSequence+Tests.swift @@ -16,7 +16,7 @@ final class AsyncDebounceSequence_Tests: XCTestCase { func testAsyncDebounceSequence() async { // Given let scheduler = TestAsyncScheduler() - let items = [1, 5, 10, 15, 20] + let items: [Int] = [1, 5, 10, 15, 20] let expectedItems = [20] let baseDelay = 5.0 var receivedItems = [Int]() diff --git a/Tests/AsyncTimeSequencesTests/AsyncSchedulerTests.swift b/Tests/AsyncTimeSequencesTests/AsyncSchedulerTests.swift index 8bf6b79..f7a17d1 100644 --- a/Tests/AsyncTimeSequencesTests/AsyncSchedulerTests.swift +++ b/Tests/AsyncTimeSequencesTests/AsyncSchedulerTests.swift @@ -11,6 +11,18 @@ import XCTest final class AsyncSchedulerTests: XCTestCase { + private actor SafeCounter { + private var setCounter = Set() + + func insert(_ value: Int) { + setCounter.insert(value) + } + + func retrieveSet() -> Set { + return setCounter + } + } + func testMainAsyncSchedulerSortsScheduledClosures() async { // Given let scheduler = MainAsyncScheduler() @@ -32,24 +44,24 @@ final class AsyncSchedulerTests: XCTestCase { let scheduler = MainAsyncScheduler() let firstExpectation = XCTestExpectation(description: "First Expectation") let secondExpectation = XCTestExpectation(description: "Second Expectation") - var setCounter = Set() + let safeCounter = SafeCounter() // When // schedule after 100us await scheduler.schedule(after: 0.0001) { - setCounter.insert(1) + await safeCounter.insert(1) firstExpectation.fulfill() } // schedule after 15 ms let cancellableTask = await scheduler.schedule(after: 0.030) { - setCounter.insert(2) + await safeCounter.insert(2) XCTFail("This should be triggered once cancelled") } // Without cancelling the task, it would execute and fail cancellableTask.cancel() // schedule after 100us await scheduler.schedule(after: 0.0001) { - setCounter.insert(3) + await safeCounter.insert(3) secondExpectation.fulfill() } @@ -62,6 +74,7 @@ final class AsyncSchedulerTests: XCTestCase { let allItemsCompleted = await scheduler.areAllScheduledItemsCompleted() // Then + let setCounter = await safeCounter.retrieveSet() XCTAssertFalse(setCounter.contains(2)) XCTAssertEqual(setCounter, Set([1, 3])) XCTAssertTrue(isQueueEmpty) @@ -101,7 +114,7 @@ final class AsyncSchedulerTests: XCTestCase { let timeInterval: TimeInterval = 0.0001 // 100us let safeActorArray = SafeActorArrayWrapper() var expectedResult = [Int]() - let maxElements = 100 + let maxElements = 1000 // When for index in 0.. { +actor SafeActorArrayWrapper { private var _elements = [T]() private var savedCount = 0 private var savedContinuation: CheckedContinuation?