1
1
import Foundation
2
- import Synchronization
3
2
4
3
#if DEBUG
5
4
6
5
/// A mock logger implementation for testing purposes.
7
6
///
8
7
/// `MockLogBackend` captures all log calls in memory and provides various inspection methods
9
- /// to verify logging behavior in unit tests. It's thread-safe using `Synchronization.Mutex`
8
+ /// to verify logging behavior in unit tests. It's thread-safe using NSLock
10
9
/// and only available in DEBUG builds.
11
10
///
12
11
/// ## Features
13
- /// - Thread-safe log capture using Swift 6 Synchronization framework
12
+ /// - Thread-safe log capture using NSLock for cross-platform compatibility
14
13
/// - Comprehensive inspection methods for testing assertions
15
14
/// - Async waiting capabilities for testing concurrent logging
16
15
/// - Pattern matching and sequence verification
17
- /// - Available on all platforms that support Swift 6
16
+ /// - Available on all platforms (iOS 13+, macOS 10.15+, Linux)
18
17
///
19
18
/// ## Usage
20
19
/// ```swift
21
20
/// let mockLogger = MockLogBackend()
22
21
/// mockLogger.info("Test message")
23
22
///
24
23
/// // Verify logging behavior
25
- /// XCTAssertTrue (mockLogger.hasInfoLogs)
26
- /// XCTAssertEqual (mockLogger.logCount(for: .info), 1)
27
- /// XCTAssertTrue (mockLogger.hasLog(level: .info, containing: "Test"))
24
+ /// #expect (mockLogger.hasInfoLogs == true )
25
+ /// #expect (mockLogger.logCount(for: .info) == 1)
26
+ /// #expect (mockLogger.hasLog(level: .info, containing: "Test") == true )
28
27
/// ```
29
- @available ( macOS 15 . 0 , iOS 18 . 0 , watchOS 11 . 0 , tvOS 18 . 0 , visionOS 2 . 0 , * )
30
- public final class MockLogBackend : LoggerManagerProtocol {
28
+ public final class MockLogBackend : LoggerManagerProtocol , @unchecked Sendable {
31
29
/// Thread-safe storage for captured log calls
32
- private let _logCalls : Mutex < [ ( level: LogLevel , message: String ) ] > = . init( [ ] )
30
+ private var _logCalls : [ ( level: LogLevel , message: String ) ] = [ ]
31
+ private let lock = NSLock ( )
33
32
34
33
/// Returns all captured log calls as an array of (level, message) tuples.
35
34
/// This property is thread-safe and returns a snapshot of all logs at the time of access.
36
35
public var logCalls : [ ( level: LogLevel , message: String ) ] {
37
- _logCalls. withLock { $0 }
36
+ lock. lock ( )
37
+ defer { lock. unlock ( ) }
38
+ return _logCalls
38
39
}
39
40
40
41
/// Initializes a new MockLogBackend instance.
@@ -50,15 +51,19 @@ import Synchronization
50
51
/// - function: The function name (metadata, not stored)
51
52
/// - line: The line number (metadata, not stored)
52
53
public func log( _ message: String , level: LogLevel , file: String , function: String , line: Int ) {
53
- _logCalls. withLock { $0. append ( ( level: level, message: message) ) }
54
+ lock. lock ( )
55
+ defer { lock. unlock ( ) }
56
+ _logCalls. append ( ( level: level, message: message) )
54
57
}
55
58
56
59
// MARK: - Test Helper Methods
57
60
58
61
/// Clears all captured log entries.
59
62
/// This method is thread-safe and removes all previously captured logs.
60
63
public func clearLogs( ) {
61
- _logCalls. withLock { $0. removeAll ( ) }
64
+ lock. lock ( )
65
+ defer { lock. unlock ( ) }
66
+ _logCalls. removeAll ( )
62
67
}
63
68
64
69
/// Checks if there's a log entry with the specified level containing the given substring.
@@ -68,54 +73,69 @@ import Synchronization
68
73
/// - substring: The substring to search for in log messages
69
74
/// - Returns: `true` if a matching log entry is found, `false` otherwise
70
75
public func hasLog( level: LogLevel , containing substring: String ) -> Bool {
71
- _logCalls. withLock { $0. contains { $0. level == level && $0. message. contains ( substring) } }
76
+ lock. lock ( )
77
+ defer { lock. unlock ( ) }
78
+ return _logCalls. contains { $0. level == level && $0. message. contains ( substring) }
72
79
}
73
80
74
81
/// Returns all log messages for a specific log level.
75
82
///
76
83
/// - Parameter level: The log level to filter by
77
84
/// - Returns: An array of log messages matching the specified level
78
85
public func getLogMessages( for level: LogLevel ) -> [ String ] {
79
- _logCalls. withLock { $0. filter { $0. level == level } . map ( \. message) }
86
+ lock. lock ( )
87
+ defer { lock. unlock ( ) }
88
+ return _logCalls. filter { $0. level == level } . map ( \. message)
80
89
}
81
90
82
91
/// Returns the most recently captured log entry.
83
92
///
84
93
/// - Returns: A tuple containing the level and message of the last log, or `nil` if no logs exist
85
94
public func getLastLog( ) -> ( level: LogLevel , message: String ) ? {
86
- _logCalls. withLock { $0. last }
95
+ lock. lock ( )
96
+ defer { lock. unlock ( ) }
97
+ return _logCalls. last
87
98
}
88
99
}
89
100
90
101
// MARK: - Convenience Extensions
91
102
92
- @available ( macOS 15 . 0 , iOS 18 . 0 , watchOS 11 . 0 , tvOS 18 . 0 , visionOS 2 . 0 , * )
93
103
extension MockLogBackend {
94
104
// MARK: - Quick Log Level Checks
95
105
96
106
/// Returns `true` if any error-level logs have been captured.
97
107
public var hasErrorLogs : Bool {
98
- _logCalls. withLock { $0. contains { $0. level == . error } }
108
+ lock. lock ( )
109
+ defer { lock. unlock ( ) }
110
+ return _logCalls. contains { $0. level == . error }
99
111
}
100
112
101
113
/// Returns `true` if any warning-level logs have been captured.
102
114
public var hasWarningLogs : Bool {
103
- _logCalls. withLock { $0. contains { $0. level == . warning } }
115
+ lock. lock ( )
116
+ defer { lock. unlock ( ) }
117
+ return _logCalls. contains { $0. level == . warning }
104
118
}
105
119
106
120
/// Returns `true` if any info-level logs have been captured.
107
121
public var hasInfoLogs : Bool {
108
- _logCalls. withLock { $0. contains { $0. level == . info } }
122
+ lock. lock ( )
123
+ defer { lock. unlock ( ) }
124
+ return _logCalls. contains { $0. level == . info }
109
125
}
110
126
111
127
/// Returns `true` if any debug-level logs have been captured.
112
128
public var hasDebugLogs : Bool {
113
- _logCalls. withLock { $0. contains { $0. level == . debug } }
129
+ lock. lock ( )
130
+ defer { lock. unlock ( ) }
131
+ return _logCalls. contains { $0. level == . debug }
114
132
}
115
133
116
134
/// Returns all captured log messages as an array of strings, regardless of level.
117
135
public var allLogMessages : [ String ] {
118
- _logCalls. withLock { $0. map ( \. message) }
136
+ lock. lock ( )
137
+ defer { lock. unlock ( ) }
138
+ return _logCalls. map ( \. message)
119
139
}
120
140
121
141
// MARK: - Advanced Testing Methods
@@ -125,15 +145,19 @@ import Synchronization
125
145
/// - Parameter level: The log level to count
126
146
/// - Returns: The number of logs at the specified level
127
147
public func logCount( for level: LogLevel ) -> Int {
128
- _logCalls. withLock { $0. count ( where: { $0. level == level } ) }
148
+ lock. lock ( )
149
+ defer { lock. unlock ( ) }
150
+ return _logCalls. count ( where: { $0. level == level } )
129
151
}
130
152
131
153
/// Verifies that the captured log sequence exactly matches the expected levels.
132
154
///
133
155
/// - Parameter expectedLevels: The expected sequence of log levels
134
156
/// - Returns: `true` if the actual log sequence matches the expected sequence exactly
135
157
public func verifyLogSequence( _ expectedLevels: [ LogLevel ] ) -> Bool {
136
- let actualLevels = _logCalls. withLock { $0. map ( \. level) }
158
+ lock. lock ( )
159
+ defer { lock. unlock ( ) }
160
+ let actualLevels = _logCalls. map ( \. level)
137
161
return actualLevels == expectedLevels
138
162
}
139
163
@@ -163,18 +187,21 @@ import Synchronization
163
187
164
188
/// Returns the total number of captured logs across all levels.
165
189
public var totalLogCount : Int {
166
- _logCalls. withLock { $0. count }
190
+ lock. lock ( )
191
+ defer { lock. unlock ( ) }
192
+ return _logCalls. count
167
193
}
168
194
169
195
/// Verifies that the last N captured logs match the expected level sequence.
170
196
///
171
197
/// - Parameter expectedLevels: The expected sequence of the most recent log levels
172
198
/// - Returns: `true` if the last N logs match the expected sequence, `false` otherwise
173
199
public func verifyLastLogs( _ expectedLevels: [ LogLevel ] ) -> Bool {
174
- let logs = _logCalls. withLock { $0 }
175
- guard logs. count >= expectedLevels. count else { return false }
200
+ lock. lock ( )
201
+ defer { lock. unlock ( ) }
202
+ guard _logCalls. count >= expectedLevels. count else { return false }
176
203
177
- let lastLogs = Array ( logs . suffix ( expectedLevels. count) )
204
+ let lastLogs = Array ( _logCalls . suffix ( expectedLevels. count) )
178
205
return lastLogs. map ( \. level) == expectedLevels
179
206
}
180
207
@@ -196,7 +223,9 @@ import Synchronization
196
223
/// let hasErrorCode = mockLogger.hasLogMatching(level: .error) { $0.contains("ERR_404") }
197
224
/// ```
198
225
public func hasLogMatching( level: LogLevel , pattern: ( String ) -> Bool ) -> Bool {
199
- _logCalls. withLock { $0. contains { $0. level == level && pattern ( $0. message) } }
226
+ lock. lock ( )
227
+ defer { lock. unlock ( ) }
228
+ return _logCalls. contains { $0. level == level && pattern ( $0. message) }
200
229
}
201
230
}
202
231
0 commit comments