Skip to content

Commit 219583e

Browse files
committed
[chain-pr 2/10] Test infrastructure for typed message bus
1 parent cc46520 commit 219583e

11 files changed

Lines changed: 167 additions & 34 deletions

File tree

DatadogCore/Tests/Datadog/CrashReporting/CrashReporterTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class CrashReporterTests: XCTestCase {
5757
let crashReport: DDCrashReport = .mockRandomWith(context: crashContext)
5858
let rumCrashReceiver = CrashReceiverMock()
5959

60-
let core = PassthroughCoreMock(messageReceiver: rumCrashReceiver)
60+
let core = PassthroughCoreMock()
61+
core.subscribe(receiver: rumCrashReceiver)
6162

6263
let plugin = CrashReportingPluginMock()
6364

DatadogCore/Tests/Datadog/InternalProxyTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class InternalProxyTests: XCTestCase {
1717

1818
override func setUp() {
1919
super.setUp()
20-
core = PassthroughCoreMock(messageReceiver: telemetry)
20+
core = PassthroughCoreMock()
21+
core.subscribe(receiver: telemetry)
2122
}
2223

2324
override func tearDown() {

DatadogCore/Tests/Objc/DDInternalLoggerTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ class DDInternalLoggerTests: XCTestCase {
1717

1818
override func setUp() {
1919
super.setUp()
20-
core = PassthroughCoreMock(messageReceiver: telemetry)
20+
core = PassthroughCoreMock()
21+
core.subscribe(receiver: telemetry)
2122
}
2223

2324
override func tearDown() {

DatadogCrashReporting/Tests/CrashReportSenderTests.swift

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ class CrashReportSenderTests: XCTestCase {
1616
func testItSendsCrashReportWhenTrackingConsentIsGranted() {
1717
// Given
1818
let receiver = CrashReceiverMock()
19-
let core = PassthroughCoreMock(messageReceiver: receiver)
19+
let core = PassthroughCoreMock()
20+
core.subscribe(receiver: receiver)
2021
let sender = MessageBusSender(core: core)
2122
let crashContext: CrashContext = .mockWith(trackingConsent: .granted)
2223
let crashReport: DDCrashReport = .mockAny()
@@ -33,7 +34,8 @@ class CrashReportSenderTests: XCTestCase {
3334
func testItDoesNotSendCrashReportWhenTrackingConsentIsNotGranted() {
3435
// Given
3536
let receiver = CrashReceiverMock()
36-
let core = PassthroughCoreMock(messageReceiver: receiver)
37+
let core = PassthroughCoreMock()
38+
core.subscribe(receiver: receiver)
3739
let sender = MessageBusSender(core: core)
3840
let crashContext: CrashContext = .mockWith(trackingConsent: .notGranted)
3941
let crashReport: DDCrashReport = .mockAny()
@@ -48,7 +50,8 @@ class CrashReportSenderTests: XCTestCase {
4850
func testItDoesNotSendCrashReportWhenTrackingConsentIsPending() {
4951
// Given
5052
let receiver = CrashReceiverMock()
51-
let core = PassthroughCoreMock(messageReceiver: receiver)
53+
let core = PassthroughCoreMock()
54+
core.subscribe(receiver: receiver)
5255
let sender = MessageBusSender(core: core)
5356
let crashContext: CrashContext = .mockWith(trackingConsent: .pending)
5457
let crashReport: DDCrashReport = .mockAny()
@@ -63,7 +66,8 @@ class CrashReportSenderTests: XCTestCase {
6366
func testItSendsCrashReportWithCorrectContext() {
6467
// Given
6568
let receiver = CrashReceiverMock()
66-
let core = PassthroughCoreMock(messageReceiver: receiver)
69+
let core = PassthroughCoreMock()
70+
core.subscribe(receiver: receiver)
6771
let sender = MessageBusSender(core: core)
6872
let crashContext: CrashContext = .mockWith(
6973
service: "test-service",

DatadogLogs/Tests/WebViewLogReceiverTests.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,9 @@ class WebViewLogReceiverTests: XCTestCase {
204204
sessionSampler: .mockRejectAll()
205205
)
206206
]
207-
),
208-
messageReceiver: telemetryReceiver
207+
)
209208
)
209+
core.subscribe(receiver: telemetryReceiver)
210210
core.onEventWriteContext = { _ in expectation.fulfill() }
211211

212212
// When
@@ -236,7 +236,8 @@ class WebViewLogReceiverTests: XCTestCase {
236236
let messageReceiver = WebViewLogReceiver()
237237
let telemetryReceiver = TelemetryReceiverMock()
238238
let expectation = expectation(description: "Send log")
239-
let core = PassthroughCoreMock(messageReceiver: telemetryReceiver)
239+
let core = PassthroughCoreMock()
240+
core.subscribe(receiver: telemetryReceiver)
240241
core.onEventWriteContext = { _ in expectation.fulfill() }
241242

242243
// When

DatadogWebViewTracking/Tests/MessageEmitterTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ class MessageEmitterTests: XCTestCase {
138138

139139
// Given
140140
let telemetry = TelemetryReceiverMock()
141-
let core = PassthroughCoreMock(messageReceiver: telemetry)
141+
let core = PassthroughCoreMock()
142+
core.subscribe(receiver: telemetry)
142143
let bridge = MessageEmitter(logsSampler: .mockAny(), core: core)
143144

144145
// When

TestUtilities/Sources/Mocks/CrashReporting/CrashReportingFeatureMocks.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,15 +108,11 @@ public class CrashReportSenderMock: CrashReportSender {
108108
public func send(launch: DatadogInternal.LaunchReport) {}
109109
}
110110

111-
public class CrashReceiverMock: FeatureMessageReceiver {
111+
public final class CrashReceiverMock: BusMessageReceiver {
112112
public var receivedCrash: Crash?
113113

114-
public func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool {
115-
guard case let .payload(crash as Crash) = message else {
116-
return false
117-
}
118-
receivedCrash = crash
119-
return true
114+
public func receive(message: Crash, from core: DatadogCoreProtocol) {
115+
receivedCrash = message
120116
}
121117

122118
public init() {}

TestUtilities/Sources/Mocks/DatadogInternal/FeatureRegistrationCoreMock.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ public class FeatureRegistrationCoreMock: DatadogCoreProtocol {
5959
// not supported - use different type of core mock if you need this
6060
}
6161

62+
public var messageBus: MessageBus { NOPMessageBus() }
63+
6264
public func mostRecentModifiedFileAt(before: Date) throws -> Date? {
6365
return nil
6466
}

TestUtilities/Sources/Mocks/DatadogInternal/PassthroughCoreMock.swift

Lines changed: 137 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,21 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope, @unchecked Se
3434
/// Current context that will be passed to feature-scopes.
3535
@ReadWriteLock
3636
public var context: DatadogContext {
37-
didSet { send(message: .context(context)) }
37+
didSet {
38+
_messageBus.send(message: context)
39+
_ = messageReceiver.receive(message: .context(context), from: self)
40+
}
3841
}
3942

4043
let writer = FileWriterMock()
4144

42-
/// The message receiver.
45+
/// The legacy `FeatureMessage` receiver.
4346
public var messageReceiver: FeatureMessageReceiver
4447

48+
/// The typed message bus. Owns its own dispatcher table and holds a weak reference
49+
/// to this core so that feature objects storing the bus do not create retain cycles.
50+
private lazy var _messageBus = PassthroughMessageBusMock(core: self)
51+
4552
/// Callback called when `eventWriteContext` closure is executed.
4653
public var onEventWriteContext: ((Bool) -> Void)?
4754

@@ -59,7 +66,7 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope, @unchecked Se
5966
self.dataStore = dataStore
6067
self.messageReceiver = messageReceiver
6168

62-
messageReceiver.receive(message: .context(context), from: self)
69+
messageReceiver.receive(message: .context(context), from: NOPDatadogCore())
6370

6471
PassthroughCoreMock.referenceCount += 1
6572
}
@@ -68,24 +75,65 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope, @unchecked Se
6875
PassthroughCoreMock.referenceCount -= 1
6976
}
7077

78+
/// The typed message bus — delegates to `_messageBus`.
79+
public var messageBus: MessageBus { _messageBus }
80+
81+
/// Subscribes `receiver` to the typed bus and, for `DatadogContext` receivers,
82+
/// delivers the current context immediately (mirrors `DatadogCore.add(messageReceiver:forKey:)`).
83+
public func subscribe<Receiver>(receiver: Receiver) where Receiver: BusMessageReceiver {
84+
_messageBus.subscribe(receiver: receiver)
85+
if let currentContext = context as? Receiver.Message {
86+
receiver.receive(message: currentContext, from: self)
87+
}
88+
}
89+
90+
/// Removes `receiver` from the typed bus.
91+
public func unsubscribe<Receiver>(receiver: Receiver) where Receiver: BusMessageReceiver {
92+
_messageBus.unsubscribe(receiver: receiver)
93+
}
94+
95+
/// Delivers `message` to the typed bus.
96+
public func send<Message>(message: Message, else fallback: @escaping () -> Void) where Message: BusMessage {
97+
_messageBus.send(message: message, else: fallback)
98+
}
99+
71100
/// no-op
72101
public func register<T>(feature: T) throws where T: DatadogFeature { }
73102
/// no-op
74103
public func feature<T>(named name: String, type: T.Type) -> T? { nil }
75104

76-
/// Always returns a feature-scope.
105+
/// Returns a feature-scope backed by a weak reference to avoid retain cycles.
77106
public func scope<T>(for featureType: T.Type) -> FeatureScope where T: DatadogFeature {
78-
self
107+
PassthroughScopeMock(core: self)
79108
}
80109

81110
public func set<Context>(context: @escaping () -> Context?) where Context: AdditionalContext {
82111
self.context.set(additionalContext: context())
83112
}
84113

85-
public func send(message: FeatureMessage, else fallback: () -> Void) {
86-
if !messageReceiver.receive(message: message, from: self) {
87-
fallback()
114+
public func send(message: FeatureMessage, else fallback: @escaping () -> Void) {
115+
var handled = messageReceiver.receive(message: message, from: self)
116+
117+
// Bridge legacy FeatureMessage senders to the typed bus so BusMessageReceiver
118+
// subscribers receive messages during the gradual migration to the typed bus.
119+
switch message {
120+
case let .telemetry(msg):
121+
_messageBus.send(message: msg, else: { })
122+
handled = true
123+
case let .payload(value):
124+
if let crash = value as? Crash {
125+
// Deliver to typed bus; if no subscriber is registered, preserve the
126+
// original fallback so callers can detect the unhandled case.
127+
_messageBus.send(message: crash, else: {
128+
if !handled { fallback() }
129+
})
130+
return
131+
}
132+
default:
133+
break
88134
}
135+
136+
if !handled { fallback() }
89137
}
90138

91139
/// no-op
@@ -137,3 +185,84 @@ open class PassthroughCoreMock: DatadogCoreProtocol, FeatureScope, @unchecked Se
137185
return nil
138186
}
139187
}
188+
189+
/// A `FeatureScope` backed by a **weak** reference to a `PassthroughCoreMock`.
190+
///
191+
/// Returned by `PassthroughCoreMock.scope(for:)` so that receivers holding the scope
192+
/// do not retain the mock core, preventing retain cycles in tests.
193+
public final class PassthroughScopeMock: FeatureScope, @unchecked Sendable {
194+
private weak var core: PassthroughCoreMock?
195+
196+
public init(core: PassthroughCoreMock) {
197+
self.core = core
198+
}
199+
200+
public func eventWriteContext(bypassConsent: Bool, _ block: @escaping (DatadogContext, Writer) -> Void) {
201+
core?.eventWriteContext(bypassConsent: bypassConsent, block)
202+
}
203+
204+
public func context(_ block: @escaping (DatadogContext) -> Void) {
205+
core?.context(block)
206+
}
207+
208+
public var dataStore: DataStore { core?.dataStore ?? NOPDataStore() }
209+
210+
public var telemetry: Telemetry { core?.telemetry ?? NOPTelemetry() }
211+
212+
public func send(message: FeatureMessage, else fallback: @escaping () -> Void) {
213+
if let core = core { core.send(message: message, else: fallback) } else { fallback() }
214+
}
215+
216+
public func set<Context>(context: @escaping () -> Context?) where Context: AdditionalContext {
217+
core?.set(context: context)
218+
}
219+
220+
public func set(anonymousId: String?) {
221+
core?.set(anonymousId: anonymousId)
222+
}
223+
}
224+
225+
/// A standalone `MessageBus` returned by `PassthroughCoreMock.messageBus`.
226+
///
227+
/// Owns its own dispatcher table and holds the core **weakly** so that feature objects
228+
/// (e.g. `FatalErrorContextNotifier`) that store the bus do not create retain cycles
229+
/// back to the mock core.
230+
public final class PassthroughMessageBusMock: MessageBus, @unchecked Sendable {
231+
private typealias Dispatch = (any BusMessage, DatadogCoreProtocol) -> Void
232+
233+
private weak var core: (any DatadogCoreProtocol)?
234+
235+
/// Per-message-key dispatchers, keyed by receiver identity.
236+
private var dispatchers: [String: [ObjectIdentifier: Dispatch]] = [:]
237+
238+
public init(core: DatadogCoreProtocol) {
239+
self.core = core
240+
}
241+
242+
public func subscribe<Receiver>(receiver: Receiver) where Receiver: BusMessageReceiver {
243+
let id = ObjectIdentifier(receiver)
244+
dispatchers[Receiver.Message.key, default: [:]][id] = { message, core in
245+
guard let typed = message as? Receiver.Message else {
246+
return
247+
}
248+
receiver.receive(message: typed, from: core)
249+
}
250+
}
251+
252+
public func unsubscribe<Receiver>(receiver: Receiver) where Receiver: BusMessageReceiver {
253+
let id = ObjectIdentifier(receiver)
254+
dispatchers[Receiver.Message.key]?.removeValue(forKey: id)
255+
}
256+
257+
public func send<Message>(message: Message, else fallback: @escaping () -> Void) where Message: BusMessage {
258+
guard let core = core else {
259+
fallback()
260+
return
261+
}
262+
guard let bucket = dispatchers[Message.key], !bucket.isEmpty else {
263+
fallback()
264+
return
265+
}
266+
bucket.values.forEach { $0(message, core) }
267+
}
268+
}

TestUtilities/Sources/Mocks/DatadogInternal/TelemetryReceiverMock.swift

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,14 @@
77
import Foundation
88
import DatadogInternal
99

10-
/// `FeatureMessageReceiver` that records received telemetry events.
11-
public class TelemetryReceiverMock: FeatureMessageReceiver {
10+
/// `BusMessageReceiver` that records received telemetry events.
11+
public final class TelemetryReceiverMock: BusMessageReceiver {
1212
@ReadWriteLock
1313
public private(set) var messages: [TelemetryMessage] = []
1414

1515
public init() {}
1616

17-
public func receive(message: FeatureMessage, from core: DatadogCoreProtocol) -> Bool {
18-
guard case let .telemetry(message) = message else {
19-
return false
20-
}
21-
17+
public func receive(message: TelemetryMessage, from core: DatadogCoreProtocol) {
2218
messages.append(message)
23-
return true
2419
}
2520
}

0 commit comments

Comments
 (0)