From 19647a60a275f026b0a26bfc6bd92c233fbf1eba Mon Sep 17 00:00:00 2001 From: Justin Bergen Date: Fri, 18 Jul 2025 09:58:42 -0600 Subject: [PATCH] Add capturing log writer test and expose LogSource Re-export Willow.LogSource as a public typealias so consumers implementing the public LogWriter surface can name the type without importing Willow directly, matching the sibling re-exports. Add a CapturingLogWriter test helper and a test that asserts on the exact messages and levels LoggingService forwards to its writers, closing the gap where logging tests exercised the code paths but never verified emitted output at the writer boundary. Drop the unused LoggingServiceMock subclass scaffolding in favor of the established harness pattern. Co-Authored-By: Claude Opus 4.8 --- Sources/ReliaBLE/Logging/LogWriters.swift | 3 + .../ReliaBLETests/ReliaBLEManagerTests.swift | 64 +++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/Sources/ReliaBLE/Logging/LogWriters.swift b/Sources/ReliaBLE/Logging/LogWriters.swift index dede711..cef548f 100644 --- a/Sources/ReliaBLE/Logging/LogWriters.swift +++ b/Sources/ReliaBLE/Logging/LogWriters.swift @@ -29,6 +29,9 @@ import os import Willow + +/// A LogSource represents the position in the source code where a message is logged. +public typealias LogSource = Willow.LogSource /// The LogModifier protocol defines a single method for modifying a log message after it has been constructed. /// This is very flexible allowing any object that conforms to modify messages in any way it wants. public typealias LogModifier = Willow.LogModifier diff --git a/Tests/ReliaBLETests/ReliaBLEManagerTests.swift b/Tests/ReliaBLETests/ReliaBLEManagerTests.swift index b495e9b..0d1853f 100644 --- a/Tests/ReliaBLETests/ReliaBLEManagerTests.swift +++ b/Tests/ReliaBLETests/ReliaBLEManagerTests.swift @@ -490,6 +490,32 @@ struct ReliaBLEManagerTests { #expect(writer.logType(forLogLevel: .event) == .default) } + @Test func capturingWriterRecordsForwardedMessagesAndLevels() { + let queue = DispatchQueue(label: "com.five3apps.relia-ble.tests.capturing") + let writer = CapturingLogWriter() + let service = LoggingService(levels: .all, writers: [writer], queue: queue) + service.enabled = true + + // Each entry point wraps its text in a `LogMessage`, so the structured overload fires for all four. + service.debug(tags: [.category(.scanning)], "debug message") + service.info(tags: [.peripheral("device-1")], "info message") + service.warn(tags: [.category(.connection)], "warn message") + service.error("error message") + + // Writes are dispatched asynchronously onto the serial `queue`; a sync barrier flushes them. + queue.sync {} + + let captured = writer.captured + #expect(captured.map(\.message) == ["debug message", "info message", "warn message", "error message"]) + #expect(captured.map(\.level) == [.debug, .info, .warn, .error]) + + // Disabling the service stops forwarding to the writer entirely. + service.enabled = false + service.error("dropped message") + queue.sync {} + #expect(writer.captured.count == 4) + } + // MARK: - Event Stream Broadcaster @Test func stateStreamReplaysToConcurrentSubscribers() async throws { @@ -530,6 +556,44 @@ struct ReliaBLEManagerTests { } } +// MARK: - Logging Test Support + +/// A ``LogWriter`` that records every forwarded message so tests can assert on exactly what the +/// ``LoggingService`` emitted — message text and level — at the writer boundary. +/// +/// Thread-safe: writes land on the service's logging queue while assertions read from the test +/// thread, so access to the backing store is guarded by a lock. +final class CapturingLogWriter: LogWriter, @unchecked Sendable { + struct Entry { + let message: String + let level: LogLevel + } + + private let lock = NSLock() + private var storage: [Entry] = [] + + /// A snapshot of everything captured so far, in the order it was written. + var captured: [Entry] { + lock.lock() + defer { lock.unlock() } + return storage + } + + func writeMessage(_ message: String, logLevel: LogLevel, logSource: LogSource) { + append(Entry(message: message, level: logLevel)) + } + + func writeMessage(_ message: any Willow.LogMessage, logLevel: LogLevel, logSource: LogSource) { + append(Entry(message: message.name, level: logLevel)) + } + + private func append(_ entry: Entry) { + lock.lock() + storage.append(entry) + lock.unlock() + } +} + // MARK: - Mock Harness /// Helpers for driving the Nordic `CBMCentralManagerMock` simulation under the constraints of the