-
Notifications
You must be signed in to change notification settings - Fork 23
/
MainSerialExecutor.swift
89 lines (84 loc) · 3.6 KB
/
MainSerialExecutor.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#if !os(WASI) && !os(Windows)
import Foundation
/// Perform an operation on the main serial executor.
///
/// Some asynchronous code is [notoriously
/// difficult](https://forums.swift.org/t/reliably-testing-code-that-adopts-swift-concurrency/57304)
/// to test in Swift due to how suspension points are processed by the runtime. This function
/// attempts to run all tasks spawned in the given operation serially and deterministically. It
/// makes asynchronous tests faster and less flakey.
///
/// ```swift
/// await withMainSerialExecutor {
/// // Everything performed in this scope is performed serially...
/// }
/// ```
///
/// See <doc:ReliablyTestingAsync> for more information on why this tool is needed to test
/// async code and how to use it.
///
/// > Warning: This API is only intended to be used from tests to make them more reliable. Please do
/// > not use it from application code.
/// >
/// > We say that it "_attempts_ to run all tasks spawned in an operation serially and
/// > deterministically" because under the hood it relies on a global, mutable variable in the Swift
/// > runtime to do its job, and there are no scoping _guarantees_ should this mutable variable change
/// > during the operation.
///
/// - Parameter operation: An operation to be performed on the main serial executor.
@MainActor
public func withMainSerialExecutor(
@_implicitSelfCapture operation: @MainActor @Sendable () async throws -> Void
) async rethrows {
let didUseMainSerialExecutor = uncheckedUseMainSerialExecutor
defer { uncheckedUseMainSerialExecutor = didUseMainSerialExecutor }
uncheckedUseMainSerialExecutor = true
try await operation()
}
/// Perform an operation on the main serial executor.
///
/// A synchronous version of ``withMainSerialExecutor(operation:)-79jpc`` that can be used in
/// `XCTestCase.invokeTest` to ensure all async tests are performed serially:
///
/// ```swift
/// class BaseTestCase: XCTestCase {
/// override func invokeTest() {
/// withMainSerialExecutor {
/// super.invokeTest()
/// }
/// }
/// }
/// ```
///
/// - Parameter operation: An operation to be performed on the main serial executor.
public func withMainSerialExecutor(
@_implicitSelfCapture operation: () throws -> Void
) rethrows {
let didUseMainSerialExecutor = uncheckedUseMainSerialExecutor
defer { uncheckedUseMainSerialExecutor = didUseMainSerialExecutor }
uncheckedUseMainSerialExecutor = true
try operation()
}
/// Overrides Swift's global executor with the main serial executor in an unchecked fashion.
///
/// > Warning: When set to `true`, all tasks will be enqueued on the main serial executor till set
/// > back to `false`. Consider using ``withMainSerialExecutor(operation:)-79jpc``, instead, which
/// > scopes this work to the duration of a given operation.
public var uncheckedUseMainSerialExecutor: Bool {
get { swift_task_enqueueGlobal_hook != nil }
set {
swift_task_enqueueGlobal_hook =
newValue
? { job, _ in MainActor.shared.enqueue(job) }
: nil
}
}
private typealias Original = @convention(thin) (UnownedJob) -> Void
private typealias Hook = @convention(thin) (UnownedJob, Original) -> Void
private var swift_task_enqueueGlobal_hook: Hook? {
get { _swift_task_enqueueGlobal_hook.pointee }
set { _swift_task_enqueueGlobal_hook.pointee = newValue }
}
private let _swift_task_enqueueGlobal_hook: UnsafeMutablePointer<Hook?> =
dlsym(dlopen(nil, 0), "swift_task_enqueueGlobal_hook").assumingMemoryBound(to: Hook?.self)
#endif