Skip to content

Commit e981e3e

Browse files
authored
Locked shouldn't be a property wrapper. (#185)
* `Locked` shouldn't be a property wrapper. Property wrappers, when used as static properties, are inherently concurrency-unsafe (because they must be `var`, not `let`.) Discussed with the core Swift folks; this PR removes property wrapper "conformance" from `Locked`, adds `RawRepresentable` to replace it, and modifies call sites as appropriate. Resolves rdar://121054518.
1 parent 8743e4d commit e981e3e

File tree

11 files changed

+84
-78
lines changed

11 files changed

+84
-78
lines changed

Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ extension Event {
6161

6262
/// This event recorder's mutable context about events it has received,
6363
/// which may be used to inform how subsequent events are written.
64-
@Locked private var _context = _Context()
64+
private var _context = Locked(rawValue: _Context())
6565

6666
/// Initialize a new human-readable event recorder.
6767
///
@@ -186,7 +186,7 @@ extension Event.HumanReadableOutputRecorder {
186186

187187
switch event.kind {
188188
case .runStarted:
189-
$_context.withLock { context in
189+
_context.withLock { context in
190190
context.runStartInstant = instant
191191
}
192192
var comments: [Comment] = [
@@ -213,7 +213,7 @@ extension Event.HumanReadableOutputRecorder {
213213

214214
case .testStarted:
215215
let test = test!
216-
$_context.withLock { context in
216+
_context.withLock { context in
217217
context.testData[test.id.keyPathRepresentation] = .init(startInstant: instant)
218218
if test.isSuite {
219219
context.suiteCount += 1
@@ -231,7 +231,7 @@ extension Event.HumanReadableOutputRecorder {
231231
case .testEnded:
232232
let test = test!
233233
let id = test.id
234-
let testDataGraph = _context.testData.subgraph(at: id.keyPathRepresentation)
234+
let testDataGraph = _context.rawValue.testData.subgraph(at: id.keyPathRepresentation)
235235
let testData = testDataGraph?.value ?? .init(startInstant: instant)
236236
let issues = _issueCounts(in: testDataGraph)
237237
let duration = testData.startInstant.descriptionOfDuration(to: instant)
@@ -253,7 +253,7 @@ extension Event.HumanReadableOutputRecorder {
253253

254254
case let .testSkipped(skipInfo):
255255
let test = test!
256-
$_context.withLock { context in
256+
_context.withLock { context in
257257
if test.isSuite {
258258
context.suiteCount += 1
259259
} else {
@@ -278,7 +278,7 @@ extension Event.HumanReadableOutputRecorder {
278278
case let .issueRecorded(issue):
279279
if let test {
280280
let id = test.id.keyPathRepresentation
281-
$_context.withLock { context in
281+
_context.withLock { context in
282282
var testData = context.testData[id] ?? .init(startInstant: instant)
283283
if issue.isKnown {
284284
testData.knownIssueCount += 1
@@ -344,7 +344,7 @@ extension Event.HumanReadableOutputRecorder {
344344
break
345345

346346
case .runEnded:
347-
let context = $_context.wrappedValue
347+
let context = _context.rawValue
348348

349349
let testCount = context.testCount
350350
let issues = _issueCounts(in: context.testData)

Sources/Testing/Events/Recorder/Event.JUnitXMLRecorder.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ extension Event {
5151

5252
/// This event recorder's mutable context about events it has received,
5353
/// which may be used to inform how subsequent events are written.
54-
@Locked private var _context = _Context()
54+
private var _context = Locked(rawValue: _Context())
5555

5656
/// Initialize a new event recorder.
5757
///
@@ -82,7 +82,7 @@ extension Event.JUnitXMLRecorder {
8282

8383
switch event.kind {
8484
case .runStarted:
85-
$_context.withLock { context in
85+
_context.withLock { context in
8686
context.runStartInstant = instant
8787
}
8888
return #"""
@@ -93,14 +93,14 @@ extension Event.JUnitXMLRecorder {
9393
case .testStarted where false == test?.isSuite:
9494
let id = test!.id
9595
let keyPath = id.keyPathRepresentation
96-
$_context.withLock { context in
96+
_context.withLock { context in
9797
context.testData[keyPath] = _Context.TestData(id: id, startInstant: instant)
9898
}
9999
return nil
100100
case .testEnded where false == test?.isSuite:
101101
let id = test!.id
102102
let keyPath = id.keyPathRepresentation
103-
$_context.withLock { context in
103+
_context.withLock { context in
104104
context.testData[keyPath]?.endInstant = instant
105105
}
106106
return nil
@@ -114,12 +114,12 @@ extension Event.JUnitXMLRecorder {
114114
return nil // FIXME: handle issues without known tests
115115
}
116116
let keyPath = id.keyPathRepresentation
117-
$_context.withLock { context in
117+
_context.withLock { context in
118118
context.testData[keyPath]?.issues.append(issue)
119119
}
120120
return nil
121121
case .runEnded:
122-
return $_context.withLock { context in
122+
return _context.withLock { context in
123123
let issueCount = context.testData
124124
.compactMap(\.value?.issues.count)
125125
.reduce(into: 0, +=)

Sources/Testing/Issues/Confirmation.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ public struct Confirmation: Sendable {
1414
///
1515
/// This property is fileprivate because it may be mutated asynchronously and
1616
/// callers may be tempted to use it in ways that result in data races.
17-
@Locked
18-
fileprivate var count = 0
17+
fileprivate var count = Locked(rawValue: 0)
1918

2019
/// Confirm this confirmation.
2120
///
@@ -26,7 +25,7 @@ public struct Confirmation: Sendable {
2625
/// directly.
2726
public func confirm(count: Int = 1) {
2827
precondition(count > 0)
29-
$count.add(count)
28+
self.count.add(count)
3029
}
3130
}
3231

@@ -101,7 +100,7 @@ public func confirmation<R>(
101100
) async rethrows -> R {
102101
let confirmation = Confirmation()
103102
defer {
104-
let actualCount = confirmation.count
103+
let actualCount = confirmation.count.rawValue
105104
if actualCount != expectedCount {
106105
Issue.record(
107106
.confirmationMiscounted(actual: actualCount, expected: expectedCount),

Sources/Testing/Issues/KnownIssue.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ private func _matchError(_ error: any Error, using issueMatcher: KnownIssueMatch
6464
/// - sourceLocation: The source location to which the issue should be
6565
/// attributed.
6666
private func _handleMiscount(by matchCounter: Locked<Int>, comment: Comment?, sourceLocation: SourceLocation) {
67-
if matchCounter.wrappedValue == 0 {
67+
if matchCounter.rawValue == 0 {
6868
Issue.record(
6969
.knownIssueNotRecorded,
7070
comments: Array(comment),
@@ -182,12 +182,12 @@ public func withKnownIssue(
182182
guard precondition() else {
183183
return try body()
184184
}
185-
@Locked var matchCounter = 0
185+
let matchCounter = Locked(rawValue: 0)
186186
let sourceLocation = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column)
187-
let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: $matchCounter)
187+
let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: matchCounter)
188188
defer {
189189
if !isIntermittent {
190-
_handleMiscount(by: $matchCounter, comment: comment, sourceLocation: sourceLocation)
190+
_handleMiscount(by: matchCounter, comment: comment, sourceLocation: sourceLocation)
191191
}
192192
}
193193
try Issue.$currentKnownIssueMatcher.withValue(issueMatcher) {
@@ -297,12 +297,12 @@ public func withKnownIssue(
297297
guard await precondition() else {
298298
return try await body()
299299
}
300-
@Locked var matchCounter = 0
300+
let matchCounter = Locked(rawValue: 0)
301301
let sourceLocation = SourceLocation(fileID: fileID, filePath: filePath, line: line, column: column)
302-
let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: $matchCounter)
302+
let issueMatcher = _combineIssueMatcher(issueMatcher, matchesCountedBy: matchCounter)
303303
defer {
304304
if !isIntermittent {
305-
_handleMiscount(by: $matchCounter, comment: comment, sourceLocation: sourceLocation)
305+
_handleMiscount(by: matchCounter, comment: comment, sourceLocation: sourceLocation)
306306
}
307307
}
308308
try await Issue.$currentKnownIssueMatcher.withValue(issueMatcher) {

Sources/Testing/Running/EntryPoint.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ private import TestingInternals
2121
/// - Warning: This function is used by Swift Package Manager. Do not call it
2222
/// directly.
2323
@_disfavoredOverload public func __swiftPMEntryPoint() async -> CInt {
24-
@Locked var exitCode = EXIT_SUCCESS
24+
let exitCode = Locked(rawValue: EXIT_SUCCESS)
2525

2626
do {
2727
let args = CommandLine.arguments()
@@ -34,7 +34,7 @@ private import TestingInternals
3434
let oldEventHandler = configuration.eventHandler
3535
configuration.eventHandler = { event, context in
3636
if case let .issueRecorded(issue) = event.kind, !issue.isKnown {
37-
$exitCode.withLock { exitCode in
37+
exitCode.withLock { exitCode in
3838
exitCode = EXIT_FAILURE
3939
}
4040
}
@@ -48,12 +48,12 @@ private import TestingInternals
4848
fputs(String(describing: error), stderr)
4949
fflush(stderr)
5050

51-
$exitCode.withLock { exitCode in
51+
exitCode.withLock { exitCode in
5252
exitCode = EXIT_FAILURE
5353
}
5454
}
5555

56-
return exitCode
56+
return exitCode.rawValue
5757
}
5858

5959
/// The entry point to the testing library used by Swift Package Manager.

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ extension Runner {
2727

2828
/// The runtime state related to the runner running on the current task.
2929
@TaskLocal
30-
static var current: Self = .init()
30+
static var current = Self()
3131
}
3232
}
3333

@@ -91,24 +91,23 @@ extension Configuration {
9191
}
9292

9393
/// Mutable storage for ``Configuration/all``.
94-
@Locked
95-
private static var _all = _All()
94+
private static let _all = Locked(rawValue: _All())
9695

9796
/// A collection containing all instances of this type that are currently set
9897
/// as the current configuration for a task.
9998
///
10099
/// This property is used when an event is posted in a context where the value
101100
/// of ``Configuration/current`` is `nil`, such as from a detached task.
102101
static var all: some Collection<Self> {
103-
_all.instances.values
102+
_all.rawValue.instances.values
104103
}
105104

106105
/// Add this instance to ``Configuration/all``.
107106
///
108107
/// - Returns: A unique number identifying `self` that can be
109108
/// passed to `_removeFromAll(identifiedBy:)`` to unregister it.
110109
private func _addToAll() -> UInt64 {
111-
Self.$_all.withLock { all in
110+
Self._all.withLock { all in
112111
let id = all.nextID
113112
all.nextID += 1
114113
all.instances[id] = self
@@ -123,7 +122,7 @@ extension Configuration {
123122
/// `_addToAll()`. If `nil`, this function has no effect.
124123
private func _removeFromAll(identifiedBy id: UInt64?) {
125124
if let id {
126-
Self.$_all.withLock { all in
125+
Self._all.withLock { all in
127126
_ = all.instances.removeValue(forKey: id)
128127
}
129128
}

Sources/Testing/SourceAttribution/Backtrace.swift

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,10 @@ extension Backtrace {
143143
/// same location.)
144144
///
145145
/// Access to this dictionary is guarded by a lock.
146-
@Locked
147-
private static var _errorMappingCache = [ObjectIdentifier: _ErrorMappingCacheEntry]()
146+
private static let _errorMappingCache = Locked<[ObjectIdentifier: _ErrorMappingCacheEntry]>()
148147

149148
/// The previous `swift_willThrow` handler, if any.
150-
@Locked
151-
private static var _oldWillThrowHandler: SWTWillThrowHandler?
149+
private static let _oldWillThrowHandler = Locked<SWTWillThrowHandler?>()
152150

153151
/// Handle a thrown error.
154152
///
@@ -157,14 +155,14 @@ extension Backtrace {
157155
/// refers to an instance of `SwiftError` or (on platforms with
158156
/// Objective-C interop) an instance of `NSError`.
159157
@Sendable private static func _willThrow(_ errorAddress: UnsafeMutableRawPointer) {
160-
_oldWillThrowHandler?(errorAddress)
158+
_oldWillThrowHandler.rawValue?(errorAddress)
161159

162160
let errorObject = unsafeBitCast(errorAddress, to: (any AnyObject & Sendable).self)
163161
let errorID = ObjectIdentifier(errorObject)
164162
let backtrace = Backtrace.current()
165163
let newEntry = _ErrorMappingCacheEntry(errorObject: errorObject, backtrace: backtrace)
166164

167-
Self.$_errorMappingCache.withLock { cache in
165+
_errorMappingCache.withLock { cache in
168166
let oldEntry = cache[errorID]
169167
if oldEntry?.errorObject == nil {
170168
// Either no entry yet, or its weak reference was zeroed.
@@ -176,7 +174,7 @@ extension Backtrace {
176174
/// The implementation of ``Backtrace/startCachingForThrownErrors()``, run
177175
/// only once.
178176
private static let _startCachingForThrownErrors: Void = {
179-
$_oldWillThrowHandler.withLock { oldWillThrowHandler in
177+
_oldWillThrowHandler.withLock { oldWillThrowHandler in
180178
oldWillThrowHandler = swt_setWillThrowHandler { _willThrow($0) }
181179
}
182180
}()
@@ -196,7 +194,7 @@ extension Backtrace {
196194
/// Call this function periodically to ensure that errors do not continue to
197195
/// take up space in the cache after they have been deinitialized.
198196
static func flushThrownErrorCache() {
199-
Self.$_errorMappingCache.withLock { cache in
197+
_errorMappingCache.withLock { cache in
200198
cache = cache.filter { $0.value.errorObject != nil }
201199
}
202200
}
@@ -218,7 +216,7 @@ extension Backtrace {
218216
@inline(never)
219217
init?(forFirstThrowOf error: any Error) {
220218
let errorID = ObjectIdentifier(unsafeBitCast(error, to: AnyObject.self))
221-
let entry = Self.$_errorMappingCache.withLock { cache in
219+
let entry = Self._errorMappingCache.withLock { cache in
222220
cache[errorID]
223221
}
224222
if let entry, entry.errorObject != nil {

Sources/Testing/Support/Environment.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ enum Environment {
2121
///
2222
/// The mechanism by which this dictionary is initially populated depends on
2323
/// platform-specific implementation details.
24-
@Locked
25-
private static var _environment = [String: String]()
24+
private static let _environment = Locked<[String: String]>()
2625
#endif
2726

2827
/// Get the environment variable with the specified name.
@@ -34,7 +33,7 @@ enum Environment {
3433
/// is not set for the current process.
3534
static func variable(named name: String) -> String? {
3635
#if SWT_NO_ENVIRONMENT_VARIABLES
37-
_environment[name]
36+
_environment.rawValue[name]
3837
#elseif SWT_TARGET_OS_APPLE || os(Linux)
3938
getenv(name).flatMap { String(validatingUTF8: $0) }
4039
#elseif os(Windows)

0 commit comments

Comments
 (0)