Skip to content

Commit 09af743

Browse files
fatbobmanclaude
andcommitted
Refactor MockLogBackend to support lower iOS versions and Linux
- Replace Synchronization.Mutex with NSLock for cross-platform compatibility - Remove iOS 18+ availability requirements, now supports iOS 13+ - Add @unchecked Sendable conformance for thread safety - Update documentation to reflect broader platform support - Remove platform availability checks from all related tests - Maintain complete API compatibility and thread safety 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 6918e64 commit 09af743

File tree

4 files changed

+58
-86
lines changed

4 files changed

+58
-86
lines changed

Sources/SimpleLogger/Backend/MockBackend.swift

Lines changed: 58 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,41 @@
11
import Foundation
2-
import Synchronization
32

43
#if DEBUG
54

65
/// A mock logger implementation for testing purposes.
76
///
87
/// `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
109
/// and only available in DEBUG builds.
1110
///
1211
/// ## Features
13-
/// - Thread-safe log capture using Swift 6 Synchronization framework
12+
/// - Thread-safe log capture using NSLock for cross-platform compatibility
1413
/// - Comprehensive inspection methods for testing assertions
1514
/// - Async waiting capabilities for testing concurrent logging
1615
/// - 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)
1817
///
1918
/// ## Usage
2019
/// ```swift
2120
/// let mockLogger = MockLogBackend()
2221
/// mockLogger.info("Test message")
2322
///
2423
/// // 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)
2827
/// ```
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 {
3129
/// 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()
3332

3433
/// Returns all captured log calls as an array of (level, message) tuples.
3534
/// This property is thread-safe and returns a snapshot of all logs at the time of access.
3635
public var logCalls: [(level: LogLevel, message: String)] {
37-
_logCalls.withLock { $0 }
36+
lock.lock()
37+
defer { lock.unlock() }
38+
return _logCalls
3839
}
3940

4041
/// Initializes a new MockLogBackend instance.
@@ -50,15 +51,19 @@ import Synchronization
5051
/// - function: The function name (metadata, not stored)
5152
/// - line: The line number (metadata, not stored)
5253
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))
5457
}
5558

5659
// MARK: - Test Helper Methods
5760

5861
/// Clears all captured log entries.
5962
/// This method is thread-safe and removes all previously captured logs.
6063
public func clearLogs() {
61-
_logCalls.withLock { $0.removeAll() }
64+
lock.lock()
65+
defer { lock.unlock() }
66+
_logCalls.removeAll()
6267
}
6368

6469
/// Checks if there's a log entry with the specified level containing the given substring.
@@ -68,54 +73,69 @@ import Synchronization
6873
/// - substring: The substring to search for in log messages
6974
/// - Returns: `true` if a matching log entry is found, `false` otherwise
7075
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) }
7279
}
7380

7481
/// Returns all log messages for a specific log level.
7582
///
7683
/// - Parameter level: The log level to filter by
7784
/// - Returns: An array of log messages matching the specified level
7885
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)
8089
}
8190

8291
/// Returns the most recently captured log entry.
8392
///
8493
/// - Returns: A tuple containing the level and message of the last log, or `nil` if no logs exist
8594
public func getLastLog() -> (level: LogLevel, message: String)? {
86-
_logCalls.withLock { $0.last }
95+
lock.lock()
96+
defer { lock.unlock() }
97+
return _logCalls.last
8798
}
8899
}
89100

90101
// MARK: - Convenience Extensions
91102

92-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
93103
extension MockLogBackend {
94104
// MARK: - Quick Log Level Checks
95105

96106
/// Returns `true` if any error-level logs have been captured.
97107
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 }
99111
}
100112

101113
/// Returns `true` if any warning-level logs have been captured.
102114
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 }
104118
}
105119

106120
/// Returns `true` if any info-level logs have been captured.
107121
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 }
109125
}
110126

111127
/// Returns `true` if any debug-level logs have been captured.
112128
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 }
114132
}
115133

116134
/// Returns all captured log messages as an array of strings, regardless of level.
117135
public var allLogMessages: [String] {
118-
_logCalls.withLock { $0.map(\.message) }
136+
lock.lock()
137+
defer { lock.unlock() }
138+
return _logCalls.map(\.message)
119139
}
120140

121141
// MARK: - Advanced Testing Methods
@@ -125,15 +145,19 @@ import Synchronization
125145
/// - Parameter level: The log level to count
126146
/// - Returns: The number of logs at the specified level
127147
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 })
129151
}
130152

131153
/// Verifies that the captured log sequence exactly matches the expected levels.
132154
///
133155
/// - Parameter expectedLevels: The expected sequence of log levels
134156
/// - Returns: `true` if the actual log sequence matches the expected sequence exactly
135157
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)
137161
return actualLevels == expectedLevels
138162
}
139163

@@ -163,18 +187,21 @@ import Synchronization
163187

164188
/// Returns the total number of captured logs across all levels.
165189
public var totalLogCount: Int {
166-
_logCalls.withLock { $0.count }
190+
lock.lock()
191+
defer { lock.unlock() }
192+
return _logCalls.count
167193
}
168194

169195
/// Verifies that the last N captured logs match the expected level sequence.
170196
///
171197
/// - Parameter expectedLevels: The expected sequence of the most recent log levels
172198
/// - Returns: `true` if the last N logs match the expected sequence, `false` otherwise
173199
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 }
176203

177-
let lastLogs = Array(logs.suffix(expectedLevels.count))
204+
let lastLogs = Array(_logCalls.suffix(expectedLevels.count))
178205
return lastLogs.map(\.level) == expectedLevels
179206
}
180207

@@ -196,7 +223,9 @@ import Synchronization
196223
/// let hasErrorCode = mockLogger.hasLogMatching(level: .error) { $0.contains("ERR_404") }
197224
/// ```
198225
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) }
200229
}
201230
}
202231

Sources/SimpleLogger/LoggerManagerProtocol.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ extension LoggerManagerProtocol where Self == LoggerManager {
120120
}
121121

122122
#if DEBUG
123-
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
124123
extension LoggerManagerProtocol where Self == MockLogBackend {
125124
/// Creates a mock logger for testing purposes.
126125
///

Tests/SimpleLoggerTests/MockFactoryTests.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import Testing
66

77
@Test("MockLogBackend - Factory method")
88
func mockLogBackendFactoryMethod() async throws {
9-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
10-
return // Skip test on older platforms
11-
}
12-
139
// Test the factory method
1410
let mockLogger: LoggerManagerProtocol = .mock()
1511

Tests/SimpleLoggerTests/MockLogBackendTests.swift

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ import Testing
88

99
@Test("MockLogBackend - Basic logging capture")
1010
func mockLogBackendBasicLogging() async throws {
11-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
12-
return // Skip test on older platforms
13-
}
14-
1511
let mockLogger = MockLogBackend()
1612

1713
mockLogger.info("Test info message")
@@ -27,10 +23,6 @@ import Testing
2723

2824
@Test("MockLogBackend - Clear logs function")
2925
func mockLogBackendClearLogs() async throws {
30-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
31-
return // Skip test on older platforms
32-
}
33-
3426
let mockLogger = MockLogBackend()
3527

3628
mockLogger.info("Message 1")
@@ -44,10 +36,6 @@ import Testing
4436

4537
@Test("MockLogBackend - Log level checking")
4638
func mockLogBackendLogLevelChecking() async throws {
47-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
48-
return // Skip test on older platforms
49-
}
50-
5139
let mockLogger = MockLogBackend()
5240

5341
mockLogger.debug("Debug message")
@@ -63,10 +51,6 @@ import Testing
6351

6452
@Test("MockLogBackend - Specific level filtering")
6553
func mockLogBackendSpecificLevelFiltering() async throws {
66-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
67-
return // Skip test on older platforms
68-
}
69-
7054
let mockLogger = MockLogBackend()
7155

7256
mockLogger.info("Info 1")
@@ -92,10 +76,6 @@ import Testing
9276

9377
@Test("MockLogBackend - Content searching")
9478
func mockLogBackendContentSearching() async throws {
95-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
96-
return // Skip test on older platforms
97-
}
98-
9979
let mockLogger = MockLogBackend()
10080

10181
mockLogger.info("User logged in successfully")
@@ -114,10 +94,6 @@ import Testing
11494

11595
@Test("MockLogBackend - Log count by level")
11696
func mockLogBackendLogCountByLevel() async throws {
117-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
118-
return // Skip test on older platforms
119-
}
120-
12197
let mockLogger = MockLogBackend()
12298

12399
mockLogger.debug("Debug 1")
@@ -137,10 +113,6 @@ import Testing
137113

138114
@Test("MockLogBackend - Log sequence verification")
139115
func mockLogBackendLogSequenceVerification() async throws {
140-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
141-
return // Skip test on older platforms
142-
}
143-
144116
let mockLogger = MockLogBackend()
145117

146118
mockLogger.info("First")
@@ -160,10 +132,6 @@ import Testing
160132

161133
@Test("MockLogBackend - Last logs verification")
162134
func mockLogBackendLastLogsVerification() async throws {
163-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
164-
return // Skip test on older platforms
165-
}
166-
167135
let mockLogger = MockLogBackend()
168136

169137
mockLogger.debug("Old debug")
@@ -191,10 +159,6 @@ import Testing
191159

192160
@Test("MockLogBackend - Pattern matching")
193161
func mockLogBackendPatternMatching() async throws {
194-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
195-
return // Skip test on older platforms
196-
}
197-
198162
let mockLogger = MockLogBackend()
199163

200164
mockLogger.info("User ID: 12345")
@@ -215,10 +179,6 @@ import Testing
215179

216180
@Test("MockLogBackend - Async log waiting")
217181
func mockLogBackendAsyncLogWaiting() async throws {
218-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
219-
return // Skip test on older platforms
220-
}
221-
222182
let mockLogger = MockLogBackend()
223183

224184
// Start async task that logs after delay
@@ -238,10 +198,6 @@ import Testing
238198

239199
@Test("MockLogBackend - Empty logger state")
240200
func mockLogBackendEmptyLoggerState() async throws {
241-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
242-
return // Skip test on older platforms
243-
}
244-
245201
let mockLogger = MockLogBackend()
246202

247203
#expect(mockLogger.logCalls.isEmpty)
@@ -262,10 +218,6 @@ import Testing
262218

263219
@Test("MockLogBackend - Large volume logging")
264220
func mockLogBackendLargeVolumeLogging() async throws {
265-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
266-
return // Skip test on older platforms
267-
}
268-
269221
let mockLogger = MockLogBackend()
270222

271223
// Add many logs
@@ -296,10 +248,6 @@ import Testing
296248

297249
@Test("MockLogBackend - Concurrent logging")
298250
func mockLogBackendConcurrentLogging() async throws {
299-
guard #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) else {
300-
return // Skip test on older platforms
301-
}
302-
303251
let mockLogger = MockLogBackend()
304252

305253
// Create multiple concurrent tasks

0 commit comments

Comments
 (0)