Skip to content

Commit 2f6dfb1

Browse files
authored
Make access to Mock thread safe
Merge pull request #21 from tboogh/thread-safe-access
2 parents 44aaddd + 2454dd8 commit 2f6dfb1

File tree

7 files changed

+75
-19
lines changed

7 files changed

+75
-19
lines changed

Sources/MockingKit/Mock.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,13 @@ import Foundation
1616
///
1717
/// Inherit this type instead of implementing the ``Mockable``
1818
/// protocol, to save some code for every mock you create.
19-
open class Mock: Mockable {
19+
open class Mock: Mockable, @unchecked Sendable {
2020

2121
public init() {}
2222

2323
public var mock: Mock { self }
2424

2525
var registeredCalls: [UUID: [AnyCall]] = [:]
2626
var registeredResults: [UUID: Function] = [:]
27+
let registeredCallsLock = NSLock()
2728
}

Sources/MockingKit/Mockable.swift

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Foundation
2222
///
2323
/// Implement this protocol instead of inheriting the ``Mock``
2424
/// base class, to save some code for every mock you create.
25-
public protocol Mockable {
25+
public protocol Mockable: Sendable {
2626

2727
typealias Function = Any
2828

@@ -38,41 +38,55 @@ extension Mockable {
3838
_ call: MockCall<Arguments, Result>,
3939
for ref: MockReference<Arguments, Result>
4040
) {
41-
let calls = mock.registeredCalls[ref.id] ?? []
42-
mock.registeredCalls[ref.id] = calls + [call]
41+
mock.registeredCallsLock.withLock {
42+
let calls = mock.registeredCalls[ref.id] ?? []
43+
mock.registeredCalls[ref.id] = calls + [call]
44+
}
4345
}
4446

4547
func registerCall<Arguments, Result>(
4648
_ call: MockCall<Arguments, Result>,
4749
for ref: AsyncMockReference<Arguments, Result>
4850
) {
49-
let calls = mock.registeredCalls[ref.id] ?? []
50-
mock.registeredCalls[ref.id] = calls + [call]
51+
mock.registeredCallsLock.withLock {
52+
let calls = mock.registeredCalls[ref.id] ?? []
53+
mock.registeredCalls[ref.id] = calls + [call]
54+
}
5155
}
5256

5357
func registeredCalls<Arguments, Result>(
5458
for ref: MockReference<Arguments, Result>
5559
) -> [MockCall<Arguments, Result>] {
56-
let calls = mock.registeredCalls[ref.id]
57-
return (calls as? [MockCall<Arguments, Result>]) ?? []
60+
mock.registeredCallsLock.withLock {
61+
let calls = mock.registeredCalls[ref.id]
62+
return (calls as? [MockCall<Arguments, Result>]) ?? []
63+
}
5864
}
5965

6066
func registeredCalls<Arguments, Result>(
6167
for ref: AsyncMockReference<Arguments, Result>
6268
) -> [MockCall<Arguments, Result>] {
63-
let calls = mock.registeredCalls[ref.id]
64-
return (calls as? [MockCall<Arguments, Result>]) ?? []
69+
mock.registeredCallsLock.withLock {
70+
let calls = mock.registeredCalls[ref.id]
71+
return (calls as? [MockCall<Arguments, Result>]) ?? []
72+
}
6573
}
6674

6775
func registeredResult<Arguments, Result>(
6876
for ref: MockReference<Arguments, Result>
6977
) -> ((Arguments) throws -> Result)? {
70-
mock.registeredResults[ref.id] as? (Arguments) throws -> Result
78+
mock.registeredCallsLock.withLock {
79+
let result = mock.registeredResults[ref.id] as? (Arguments) throws -> Result
80+
return result
81+
}
7182
}
7283

7384
func registeredResult<Arguments, Result>(
7485
for ref: AsyncMockReference<Arguments, Result>
7586
) -> ((Arguments) async throws -> Result)? {
76-
mock.registeredResults[ref.id] as? (Arguments) async throws -> Result
87+
mock.registeredCallsLock.withLock {
88+
let result = mock.registeredResults[ref.id] as? (Arguments) async throws -> Result
89+
return result
90+
}
7791
}
7892
}

Sources/MockingKit/Mocks/MockPasteboard.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import AppKit
3232
This mock only mocks `setValue(_:forKey:)` for now, but you
3333
can subclass this class and mock more functionality.
3434
*/
35-
public class MockPasteboard: NSPasteboard, Mockable {
35+
public class MockPasteboard: NSPasteboard, Mockable, @unchecked Sendable {
3636

3737
public lazy var setValueForKeyRef = MockReference(setValueForKey)
3838

Sources/MockingKit/Mocks/MockUserDefaults.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// This class can be used to mock `UserDefaults`.
12-
open class MockUserDefaults: UserDefaults, Mockable {
12+
open class MockUserDefaults: UserDefaults, Mockable, @unchecked Sendable {
1313

1414
public lazy var boolRef = MockReference(bool)
1515
public lazy var arrayRef = MockReference(array)

Tests/MockingKitTests/GenericTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ final class GenericTests: XCTestCase {
2121
}
2222
}
2323

24-
private class GenericMock<T>: Mock {
24+
private class GenericMock<T>: Mock, @unchecked Sendable {
2525

2626
lazy var doitRef = MockReference(doit)
2727

Tests/MockingKitTests/MockableAsyncTests.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,11 +252,33 @@ class MockableAsyncTests: XCTestCase {
252252
XCTAssertFalse(mock.hasCalled(mock.functionWithIntResultRef))
253253
XCTAssertTrue(mock.hasCalled(\.functionWithStringResultRef))
254254
}
255+
256+
func testMultiThreadedAccess_doesNotCorruptState() async throws {
257+
let expectation = XCTestExpectation()
258+
expectation.expectedFulfillmentCount = 2
259+
let mock = TestClass()
260+
261+
Task {
262+
for index in 0..<100 {
263+
await mock.functionWithVoidResult(arg1: "Test", arg2: index)
264+
}
265+
expectation.fulfill()
266+
}
267+
268+
Task {
269+
for _ in 0..<100 {
270+
_ = mock.hasCalled(\.functionWithIntResultRef)
271+
}
272+
expectation.fulfill()
273+
}
274+
275+
await fulfillment(of: [expectation])
276+
}
255277
}
256278

257-
private class TestClass: AsyncTestProtocol, Mockable {
279+
private final class TestClass: AsyncTestProtocol, Mockable, @unchecked Sendable {
258280

259-
var mock = Mock()
281+
let mock = Mock()
260282

261283
lazy var functionWithIntResultRef = AsyncMockReference(functionWithIntResult)
262284
lazy var functionWithStringResultRef = AsyncMockReference(functionWithStringResult)

Tests/MockingKitTests/MockableTests.swift

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,11 +256,30 @@ class MockableTests: XCTestCase {
256256
XCTAssertFalse(mock.hasCalled(mock.functionWithIntResultRef))
257257
XCTAssertTrue(mock.hasCalled(\.functionWithStringResultRef))
258258
}
259+
260+
func testMultiThreadedAccess_doesNotCorruptState() {
261+
let queueA = DispatchQueue(label: "QueueA")
262+
let queueB = DispatchQueue(label: "QueueB")
263+
264+
let mock = TestClass()
265+
266+
queueA.async {
267+
for index in 0..<100 {
268+
mock.functionWithVoidResult(arg1: "Something", arg2: index)
269+
}
270+
}
271+
272+
queueB.async {
273+
for _ in 0..<100 {
274+
_ = mock.hasCalled(\.functionWithIntResultRef)
275+
}
276+
}
277+
}
259278
}
260279

261-
private class TestClass: AsyncTestProtocol, Mockable {
280+
private final class TestClass: AsyncTestProtocol, Mockable, @unchecked Sendable {
262281

263-
var mock = Mock()
282+
let mock = Mock()
264283

265284
lazy var functionWithIntResultRef = MockReference(functionWithIntResult)
266285
lazy var functionWithStringResultRef = MockReference(functionWithStringResult)

0 commit comments

Comments
 (0)