Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 252 additions & 0 deletions Utils/Tests/UtilsTests/EventBus/EventBusSubscribingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
// generated by polka.codes
import Foundation
import Synchronization
import Testing

@testable import Utils

// TestEvent and AnotherTestEvent are defined in EventBusTests

// A helper actor to safely check if a handler was called and capture values
private actor TestHandlerChecker<T> {
private(set) var callCount = 0
private(set) var capturedValues: [T] = []

func handlerCalled(with value: T) {
callCount += 1
capturedValues.append(value)
}
}

struct EventBusSubscribingTests {
@Test func testBasicSubscription() async throws {
let eventBus = EventBus()
let handlerChecker = TestHandlerChecker<TestEvent>()

let testEvent = TestEvent(id: 1, value: "test")

let token = await eventBus.subscribe(TestEvent.self) { event in
await handlerChecker.handlerCalled(with: event)
}

await eventBus.publish(testEvent)

// Give some time for the event to be processed
try await Task.sleep(for: .milliseconds(100))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Task.sleep with a fixed duration to wait for an asynchronous operation to complete makes the test both slow and unreliable. If the operation takes longer than the sleep duration, the test will fail incorrectly (flakiness). A better approach is to actively wait for the desired condition to become true.

This issue is present in all tests in this file. Please replace the Task.sleep calls with a more deterministic mechanism.

For example, you could introduce a polling helper function:

private func waitUntil(timeout: TimeInterval = 1.0, condition: @escaping () async -> Bool) async throws {
    let deadline = Date.now.addingTimeInterval(timeout)
    while await !condition() {
        if Date.now >= deadline {
            struct TimeoutError: Error {}
            throw TimeoutError()
        }
        try await Task.sleep(for: .milliseconds(10))
    }
}

And then use it in the test like this, removing the Task.sleep:

await eventBus.publish(testEvent)

try await waitUntil { await handlerChecker.callCount == 1 }

let callCount = await handlerChecker.callCount
#expect(callCount == 1)


let callCount = await handlerChecker.callCount
#expect(callCount == 1)
let capturedEvent = await handlerChecker.capturedValues.first
#expect(capturedEvent?.id == testEvent.id)
#expect(capturedEvent?.value == testEvent.value)

await eventBus.unsubscribe(token: token)
}

@Test func testUnsubscribe() async throws {
let eventBus = EventBus()
let handlerChecker = TestHandlerChecker<TestEvent>()

let token = await eventBus.subscribe(TestEvent.self) { event in
await handlerChecker.handlerCalled(with: event)
}

await eventBus.unsubscribe(token: token)

await eventBus.publish(TestEvent(id: 2, value: "after unsubscribe"))

// Give some time for the event to be processed
try await Task.sleep(for: .milliseconds(100))

let callCount = await handlerChecker.callCount
#expect(callCount == 0)
}

@Test func testMultipleSubscribers() async throws {
let eventBus = EventBus()
let handler1Checker = TestHandlerChecker<TestEvent>()
let handler2Checker = TestHandlerChecker<TestEvent>()

let token1 = await eventBus.subscribe(TestEvent.self) { event in
await handler1Checker.handlerCalled(with: event)
}
let token2 = await eventBus.subscribe(TestEvent.self) { event in
await handler2Checker.handlerCalled(with: event)
}

await eventBus.publish(TestEvent(id: 3, value: "multi-subscriber test"))

try await Task.sleep(for: .milliseconds(100))

let callCount1 = await handler1Checker.callCount
let callCount2 = await handler2Checker.callCount
#expect(callCount1 == 1)
#expect(callCount2 == 1)

await eventBus.unsubscribe(token: token1)
await eventBus.unsubscribe(token: token2)
}

@Test func testSubscribingToDifferentEvents() async throws {
let eventBus = EventBus()
let testEventHandler = TestHandlerChecker<TestEvent>()
let anotherTestEventHandler = TestHandlerChecker<AnotherTestEvent>()

let token1 = await eventBus.subscribe(TestEvent.self) { event in
await testEventHandler.handlerCalled(with: event)
}
let token2 = await eventBus.subscribe(AnotherTestEvent.self) { event in
await anotherTestEventHandler.handlerCalled(with: event)
}

await eventBus.publish(TestEvent(id: 4, value: "first event"))
await eventBus.publish(AnotherTestEvent(name: "second event"))

try await Task.sleep(for: .milliseconds(100))

let testEventCallCount = await testEventHandler.callCount
let anotherEventCallCount = await anotherTestEventHandler.callCount
#expect(testEventCallCount == 1)
#expect(anotherEventCallCount == 1)

await eventBus.unsubscribe(token: token1)
await eventBus.unsubscribe(token: token2)
}

@Test func testUnsubscribeOneOfMany() async throws {
let eventBus = EventBus()
let handler1Checker = TestHandlerChecker<TestEvent>()
let handler2Checker = TestHandlerChecker<TestEvent>()

let token1 = await eventBus.subscribe(TestEvent.self) { event in
await handler1Checker.handlerCalled(with: event)
}
let token2 = await eventBus.subscribe(TestEvent.self) { event in
await handler2Checker.handlerCalled(with: event)
}

await eventBus.unsubscribe(token: token1)

await eventBus.publish(TestEvent(id: 5, value: "after one unsubscribe"))

try await Task.sleep(for: .milliseconds(100))

let callCount1 = await handler1Checker.callCount
let callCount2 = await handler2Checker.callCount
#expect(callCount1 == 0)
#expect(callCount2 == 1)

await eventBus.unsubscribe(token: token2)
}

// MARK: - Middleware Tests

private struct EventModifyingMiddleware: MiddlewareProtocol {
func handle<T: Sendable>(_ event: T, next: @escaping MiddlewareHandler<T>) async throws {
if let testEvent = event as? TestEvent {
let modifiedEvent = TestEvent(id: testEvent.id, value: "modified")
try await next(modifiedEvent as! T)
} else {
try await next(event)
}
}
}

@Test func testEventMiddleware() async throws {
let eventBus = EventBus(eventMiddleware: Middleware(EventModifyingMiddleware()))
let handlerChecker = TestHandlerChecker<TestEvent>()

let token = await eventBus.subscribe(TestEvent.self) { event in
await handlerChecker.handlerCalled(with: event)
}

await eventBus.publish(TestEvent(id: 6, value: "original"))

try await Task.sleep(for: .milliseconds(100))

let capturedEvent = await handlerChecker.capturedValues.first
#expect(capturedEvent?.value == "modified")

await eventBus.unsubscribe(token: token)
}

private actor HandlerCountingMiddleware: MiddlewareProtocol {
private(set) var counter = 0
func handle<T: Sendable>(_ event: T, next: @escaping MiddlewareHandler<T>) async throws {
counter += 1
try await next(event)
}
}

@Test func testHandlerMiddleware() async throws {
let countingMiddleware = HandlerCountingMiddleware()
let eventBus = EventBus(handlerMiddleware: Middleware(countingMiddleware))
let handler1Checker = TestHandlerChecker<TestEvent>()
let handler2Checker = TestHandlerChecker<TestEvent>()

let token1 = await eventBus.subscribe(TestEvent.self) { event in
await handler1Checker.handlerCalled(with: event)
}
let token2 = await eventBus.subscribe(TestEvent.self) { event in
await handler2Checker.handlerCalled(with: event)
}

await eventBus.publish(TestEvent(id: 7, value: "handler middleware test"))

try await Task.sleep(for: .milliseconds(100))

let count = await countingMiddleware.counter
#expect(count == 2)
let callCount1 = await handler1Checker.callCount
let callCount2 = await handler2Checker.callCount
#expect(callCount1 == 1)
#expect(callCount2 == 1)

await eventBus.unsubscribe(token: token1)
await eventBus.unsubscribe(token: token2)
}

// MARK: - EventSubscriptions Helper Tests

@Test func testEventSubscriptionsDeinit() async throws {
let eventBus = EventBus()
let handlerChecker = TestHandlerChecker<TestEvent>()

var subscriptions: EventSubscriptions? = EventSubscriptions(eventBus: eventBus)
await subscriptions!.subscribe(TestEvent.self, id: "test") { event in
await handlerChecker.handlerCalled(with: event)
}

// Deinitialize EventSubscriptions
subscriptions = nil

// Allow time for deinit task to run
try await Task.sleep(for: .milliseconds(100))

await eventBus.publish(TestEvent(id: 8, value: "after deinit"))

try await Task.sleep(for: .milliseconds(100))

let callCount = await handlerChecker.callCount
#expect(callCount == 0)
}

@Test func testEventSubscriptionsExplicitUnsubscribe() async throws {
let eventBus = EventBus()
let subscriptions = EventSubscriptions(eventBus: eventBus)
let handlerChecker = TestHandlerChecker<TestEvent>()

let token = await subscriptions.subscribe(TestEvent.self, id: "test") { event in
await handlerChecker.handlerCalled(with: event)
}

await subscriptions.unsubscribe(token: token)

await eventBus.publish(TestEvent(id: 9, value: "after explicit unsubscribe"))

try await Task.sleep(for: .milliseconds(100))

let callCount = await handlerChecker.callCount
#expect(callCount == 0)
}
}