event dispatcher
This commit is contained in:
334
tests/RuntimeEventTypeTests.cpp
Normal file
334
tests/RuntimeEventTypeTests.cpp
Normal file
@@ -0,0 +1,334 @@
|
||||
#include "RuntimeEvent.h"
|
||||
#include "RuntimeEventCoalescingQueue.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeEventQueue.h"
|
||||
#include "RuntimeEventType.h"
|
||||
#include "RuntimeEventPayloads.h"
|
||||
#include "RuntimeEventTestHarness.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
|
||||
namespace
|
||||
{
|
||||
int gFailures = 0;
|
||||
|
||||
void Expect(bool condition, const char* message)
|
||||
{
|
||||
if (condition)
|
||||
return;
|
||||
|
||||
std::cerr << "FAIL: " << message << "\n";
|
||||
++gFailures;
|
||||
}
|
||||
|
||||
void TestRuntimeEventTypeNames()
|
||||
{
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::Unknown) == "Unknown", "unknown event type has a stable name");
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::OscCommitRequested) == "OscCommitRequested", "control event name is stable");
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::RuntimeMutationAccepted) == "RuntimeMutationAccepted", "runtime event name is stable");
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::ShaderBuildPrepared) == "ShaderBuildPrepared", "shader build event name is stable");
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::RenderSnapshotPublished) == "RenderSnapshotPublished", "render event name is stable");
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::BackendStateChanged) == "BackendStateChanged", "backend event name is stable");
|
||||
Expect(RuntimeEventTypeName(RuntimeEventType::QueueDepthChanged) == "QueueDepthChanged", "telemetry event name is stable");
|
||||
}
|
||||
|
||||
void TestRuntimeEventPayloadTypes()
|
||||
{
|
||||
OscCommitRequestedEvent oscCommit;
|
||||
oscCommit.routeKey = "layer-1\namount";
|
||||
oscCommit.layerKey = "layer-1";
|
||||
oscCommit.parameterKey = "amount";
|
||||
oscCommit.generation = 42;
|
||||
Expect(RuntimeEventPayloadType(oscCommit) == RuntimeEventType::OscCommitRequested, "OSC commit payload maps to OSC commit event type");
|
||||
Expect(oscCommit.generation == 42, "OSC commit payload keeps generation");
|
||||
|
||||
RuntimeMutationEvent acceptedMutation;
|
||||
acceptedMutation.action = "SetLayerShader";
|
||||
acceptedMutation.accepted = true;
|
||||
acceptedMutation.shaderBuildRequested = true;
|
||||
Expect(RuntimeEventPayloadType(acceptedMutation) == RuntimeEventType::RuntimeMutationAccepted, "accepted mutation payload maps to accepted event type");
|
||||
Expect(acceptedMutation.shaderBuildRequested, "mutation payload carries shader build follow-up");
|
||||
|
||||
RuntimeMutationEvent rejectedMutation;
|
||||
rejectedMutation.accepted = false;
|
||||
rejectedMutation.errorMessage = "Unknown layer.";
|
||||
Expect(RuntimeEventPayloadType(rejectedMutation) == RuntimeEventType::RuntimeMutationRejected, "rejected mutation payload maps to rejected event type");
|
||||
Expect(rejectedMutation.errorMessage == "Unknown layer.", "mutation payload carries rejection error");
|
||||
|
||||
ShaderBuildEvent preparedBuild;
|
||||
preparedBuild.phase = RuntimeEventShaderBuildPhase::Prepared;
|
||||
preparedBuild.inputWidth = 1920;
|
||||
preparedBuild.inputHeight = 1080;
|
||||
Expect(RuntimeEventPayloadType(preparedBuild) == RuntimeEventType::ShaderBuildPrepared, "shader build payload maps by phase");
|
||||
Expect(preparedBuild.inputWidth == 1920 && preparedBuild.inputHeight == 1080, "shader build payload carries input dimensions");
|
||||
|
||||
RenderResetEvent appliedReset;
|
||||
appliedReset.scope = RuntimeEventRenderResetScope::TemporalHistoryAndFeedback;
|
||||
appliedReset.applied = true;
|
||||
Expect(RuntimeEventPayloadType(appliedReset) == RuntimeEventType::RenderResetApplied, "render reset payload maps applied state");
|
||||
Expect(appliedReset.scope == RuntimeEventRenderResetScope::TemporalHistoryAndFeedback, "render reset payload carries reset scope");
|
||||
|
||||
SubsystemWarningEvent warning;
|
||||
warning.subsystem = "VideoBackend";
|
||||
warning.warningKey = "late-frame";
|
||||
Expect(RuntimeEventPayloadType(warning) == RuntimeEventType::SubsystemWarningRaised, "warning payload maps to raised event by default");
|
||||
warning.cleared = true;
|
||||
Expect(RuntimeEventPayloadType(warning) == RuntimeEventType::SubsystemWarningCleared, "warning payload maps to cleared event when marked cleared");
|
||||
}
|
||||
|
||||
void TestRuntimeEventEnvelope()
|
||||
{
|
||||
const auto createdAt = std::chrono::steady_clock::now();
|
||||
|
||||
OscCommitRequestedEvent oscCommit;
|
||||
oscCommit.routeKey = "layer-1\namount";
|
||||
oscCommit.layerKey = "layer-1";
|
||||
oscCommit.parameterKey = "amount";
|
||||
oscCommit.generation = 7;
|
||||
|
||||
RuntimeEvent event = MakeRuntimeEvent(oscCommit, "ControlServices", 12, createdAt);
|
||||
Expect(event.type == RuntimeEventType::OscCommitRequested, "runtime event infers type from payload");
|
||||
Expect(event.sequence == 12, "runtime event stores sequence");
|
||||
Expect(event.source == "ControlServices", "runtime event stores source");
|
||||
Expect(event.createdAt == createdAt, "runtime event stores creation time");
|
||||
Expect(event.HasPayload(), "runtime event reports payload presence");
|
||||
Expect(event.PayloadMatchesType(), "runtime event payload matches inferred type");
|
||||
|
||||
const auto* payload = std::get_if<OscCommitRequestedEvent>(&event.payload);
|
||||
Expect(payload && payload->generation == 7, "runtime event stores typed payload in variant");
|
||||
|
||||
event.type = RuntimeEventType::RuntimeMutationAccepted;
|
||||
Expect(!event.PayloadMatchesType(), "runtime event detects mismatched payload and type");
|
||||
|
||||
RuntimeEvent empty;
|
||||
Expect(!empty.HasPayload(), "default runtime event has no payload");
|
||||
Expect(empty.PayloadMatchesType(), "default runtime event unknown type matches empty payload");
|
||||
|
||||
RuntimeMutationEvent acceptedMutation;
|
||||
acceptedMutation.accepted = true;
|
||||
acceptedMutation.action = "SetLayerBypass";
|
||||
RuntimeEvent acceptedEvent = MakeRuntimeEvent(acceptedMutation, "RuntimeCoordinator", 13, createdAt);
|
||||
Expect(acceptedEvent.type == RuntimeEventType::RuntimeMutationAccepted, "runtime event handles payloads with dynamic accepted mapping");
|
||||
|
||||
RuntimeMutationEvent rejectedMutation;
|
||||
rejectedMutation.accepted = false;
|
||||
rejectedMutation.action = "SetLayerBypass";
|
||||
RuntimeEvent rejectedEvent = MakeRuntimeEvent(rejectedMutation, "RuntimeCoordinator", 14, createdAt);
|
||||
Expect(rejectedEvent.type == RuntimeEventType::RuntimeMutationRejected, "runtime event handles payloads with dynamic rejected mapping");
|
||||
}
|
||||
|
||||
void TestRuntimeEventQueue()
|
||||
{
|
||||
RuntimeEventQueue queue(2);
|
||||
const auto createdAt = std::chrono::steady_clock::now() - std::chrono::milliseconds(5);
|
||||
|
||||
RuntimeStateBroadcastRequestedEvent firstPayload;
|
||||
firstPayload.reason = "first";
|
||||
RuntimeStateBroadcastRequestedEvent secondPayload;
|
||||
secondPayload.reason = "second";
|
||||
RuntimeStateBroadcastRequestedEvent thirdPayload;
|
||||
thirdPayload.reason = "third";
|
||||
|
||||
Expect(queue.Push(MakeRuntimeEvent(firstPayload, "test", 1, createdAt)), "queue accepts first event");
|
||||
Expect(queue.Push(MakeRuntimeEvent(secondPayload, "test", 2, createdAt)), "queue accepts second event");
|
||||
Expect(!queue.Push(MakeRuntimeEvent(thirdPayload, "test", 3, createdAt)), "queue rejects event when capacity is full");
|
||||
|
||||
RuntimeEventQueueMetrics fullMetrics = queue.GetMetrics(std::chrono::steady_clock::now());
|
||||
Expect(fullMetrics.depth == 2, "queue metrics report depth");
|
||||
Expect(fullMetrics.capacity == 2, "queue metrics report capacity");
|
||||
Expect(fullMetrics.droppedCount == 1, "queue metrics report dropped count");
|
||||
Expect(fullMetrics.oldestEventAgeMilliseconds >= 0.0, "queue metrics report oldest event age");
|
||||
|
||||
RuntimeEvent event;
|
||||
Expect(queue.TryPop(event), "queue pops first event");
|
||||
Expect(event.sequence == 1, "queue preserves FIFO order");
|
||||
|
||||
std::vector<RuntimeEvent> drained = queue.Drain();
|
||||
Expect(drained.size() == 1, "queue drains remaining event");
|
||||
Expect(drained[0].sequence == 2, "drained event preserves FIFO order");
|
||||
Expect(queue.Depth() == 0, "queue is empty after drain");
|
||||
}
|
||||
|
||||
void TestRuntimeEventDispatcher()
|
||||
{
|
||||
RuntimeEventDispatcher dispatcher(4);
|
||||
int allHandlerCount = 0;
|
||||
int broadcastHandlerCount = 0;
|
||||
int failureHandlerCount = 0;
|
||||
|
||||
dispatcher.SubscribeAll([&](const RuntimeEvent& event) {
|
||||
Expect(event.sequence != 0, "dispatcher assigns sequence before all-handler dispatch");
|
||||
++allHandlerCount;
|
||||
});
|
||||
dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent& event) {
|
||||
Expect(event.type == RuntimeEventType::RuntimeStateBroadcastRequested, "dispatcher invokes type-specific handler for matching event");
|
||||
++broadcastHandlerCount;
|
||||
});
|
||||
dispatcher.Subscribe(RuntimeEventType::RuntimeStateBroadcastRequested, [&](const RuntimeEvent&) {
|
||||
++failureHandlerCount;
|
||||
throw std::runtime_error("test handler failure");
|
||||
});
|
||||
|
||||
RuntimeStateBroadcastRequestedEvent broadcast;
|
||||
broadcast.reason = "test";
|
||||
Expect(dispatcher.PublishPayload(broadcast, "test"), "dispatcher publishes payload");
|
||||
Expect(dispatcher.QueueDepth() == 1, "dispatcher reports queued depth");
|
||||
|
||||
RuntimeEventDispatchResult result = dispatcher.DispatchPending();
|
||||
Expect(result.dispatchedEvents == 1, "dispatcher reports dispatched event count");
|
||||
Expect(result.handlerInvocations == 3, "dispatcher reports handler invocation count");
|
||||
Expect(result.handlerFailures == 1, "dispatcher catches and reports handler failures");
|
||||
Expect(allHandlerCount == 1, "dispatcher invoked all-handler");
|
||||
Expect(broadcastHandlerCount == 1, "dispatcher invoked type-specific handler");
|
||||
Expect(failureHandlerCount == 1, "dispatcher invoked failing handler");
|
||||
Expect(dispatcher.QueueDepth() == 0, "dispatcher queue is empty after dispatch");
|
||||
|
||||
RuntimeEvent mismatched = MakeRuntimeEvent(broadcast, "test");
|
||||
mismatched.type = RuntimeEventType::ShaderBuildRequested;
|
||||
Expect(!dispatcher.Publish(mismatched), "dispatcher rejects mismatched event type and payload");
|
||||
|
||||
RuntimeEventDispatcher tinyDispatcher(1);
|
||||
Expect(tinyDispatcher.PublishPayload(broadcast, "test"), "tiny dispatcher accepts first event");
|
||||
Expect(!tinyDispatcher.PublishPayload(broadcast, "test"), "tiny dispatcher rejects event when queue is full");
|
||||
RuntimeEventQueueMetrics metrics = tinyDispatcher.GetQueueMetrics();
|
||||
Expect(metrics.droppedCount == 1, "dispatcher exposes queue drop metrics");
|
||||
}
|
||||
|
||||
void TestRuntimeEventCoalescingQueue()
|
||||
{
|
||||
RuntimeEventCoalescingQueue queue(2);
|
||||
const auto createdAt = std::chrono::steady_clock::now() - std::chrono::milliseconds(5);
|
||||
|
||||
OscValueReceivedEvent first;
|
||||
first.routeKey = "layer-1\namount";
|
||||
first.layerKey = "layer-1";
|
||||
first.parameterKey = "amount";
|
||||
first.valueJson = "0.1";
|
||||
first.generation = 1;
|
||||
|
||||
OscValueReceivedEvent replacement = first;
|
||||
replacement.valueJson = "0.9";
|
||||
replacement.generation = 2;
|
||||
|
||||
OscValueReceivedEvent otherRoute;
|
||||
otherRoute.routeKey = "layer-2\namount";
|
||||
otherRoute.layerKey = "layer-2";
|
||||
otherRoute.parameterKey = "amount";
|
||||
otherRoute.valueJson = "0.3";
|
||||
otherRoute.generation = 3;
|
||||
|
||||
OscValueReceivedEvent overflow;
|
||||
overflow.routeKey = "layer-3\namount";
|
||||
overflow.layerKey = "layer-3";
|
||||
overflow.parameterKey = "amount";
|
||||
overflow.valueJson = "0.4";
|
||||
overflow.generation = 4;
|
||||
|
||||
Expect(queue.Push(MakeRuntimeEvent(first, "ControlServices", 1, createdAt)), "coalescing queue accepts first keyed event");
|
||||
Expect(queue.Push(MakeRuntimeEvent(replacement, "ControlServices", 2, std::chrono::steady_clock::now())), "coalescing queue replaces matching keyed event");
|
||||
Expect(queue.Push(MakeRuntimeEvent(otherRoute, "ControlServices", 3, createdAt)), "coalescing queue accepts second keyed event");
|
||||
Expect(!queue.Push(MakeRuntimeEvent(overflow, "ControlServices", 4, createdAt)), "coalescing queue rejects new key when full");
|
||||
|
||||
RuntimeEventCoalescingQueueMetrics metrics = queue.GetMetrics(std::chrono::steady_clock::now());
|
||||
Expect(metrics.depth == 2, "coalescing queue metrics report unique-key depth");
|
||||
Expect(metrics.capacity == 2, "coalescing queue metrics report capacity");
|
||||
Expect(metrics.coalescedCount == 1, "coalescing queue metrics report coalesced count");
|
||||
Expect(metrics.droppedCount == 1, "coalescing queue metrics report dropped count");
|
||||
Expect(metrics.oldestEventAgeMilliseconds >= 0.0, "coalescing queue metrics report oldest event age");
|
||||
|
||||
std::vector<RuntimeEvent> drained = queue.Drain();
|
||||
Expect(drained.size() == 2, "coalescing queue drains unique events");
|
||||
Expect(drained[0].sequence == 2, "coalescing queue keeps latest replacement event");
|
||||
Expect(drained[1].sequence == 3, "coalescing queue preserves first-seen key order");
|
||||
|
||||
const auto* latestPayload = std::get_if<OscValueReceivedEvent>(&drained[0].payload);
|
||||
Expect(latestPayload && latestPayload->valueJson == "0.9", "coalescing queue keeps latest payload value");
|
||||
Expect(queue.Depth() == 0, "coalescing queue is empty after drain");
|
||||
}
|
||||
|
||||
void TestRuntimeEventCoalescingCustomKey()
|
||||
{
|
||||
RuntimeEventCoalescingQueue queue(4, [](const RuntimeEvent& event) {
|
||||
return std::string(RuntimeEventTypeName(event.type));
|
||||
});
|
||||
|
||||
RuntimeStateBroadcastRequestedEvent first;
|
||||
first.reason = "parameter";
|
||||
RuntimeStateBroadcastRequestedEvent second;
|
||||
second.reason = "reload";
|
||||
|
||||
Expect(queue.Push(MakeRuntimeEvent(first, "RuntimeCoordinator", 10)), "custom-key coalescing queue accepts first event");
|
||||
Expect(queue.Push(MakeRuntimeEvent(second, "RuntimeCoordinator", 11)), "custom-key coalescing queue coalesces second event by type");
|
||||
|
||||
std::vector<RuntimeEvent> drained = queue.Drain();
|
||||
Expect(drained.size() == 1, "custom-key coalescing queue drains one coalesced event");
|
||||
Expect(drained[0].sequence == 11, "custom-key coalescing queue keeps latest event");
|
||||
|
||||
const auto* payload = std::get_if<RuntimeStateBroadcastRequestedEvent>(&drained[0].payload);
|
||||
Expect(payload && payload->reason == "reload", "custom-key coalescing queue keeps latest typed payload");
|
||||
}
|
||||
|
||||
void TestRuntimeEventTestHarness()
|
||||
{
|
||||
RuntimeEventTestHarness harness;
|
||||
|
||||
RuntimeStateBroadcastRequestedEvent broadcast;
|
||||
broadcast.reason = "parameter";
|
||||
RuntimeEventDispatchResult firstDispatch = harness.PublishAndDispatch(broadcast, "RuntimeCoordinator");
|
||||
Expect(firstDispatch.dispatchedEvents == 1, "test harness publishes and dispatches payloads");
|
||||
Expect(harness.SeenCount() == 1, "test harness records dispatched events");
|
||||
Expect(harness.SeenCount(RuntimeEventType::RuntimeStateBroadcastRequested) == 1, "test harness counts seen events by type");
|
||||
|
||||
const RuntimeEvent* seenBroadcast = harness.LastSeen(RuntimeEventType::RuntimeStateBroadcastRequested);
|
||||
Expect(seenBroadcast && seenBroadcast->source == "RuntimeCoordinator", "test harness returns last seen event by type");
|
||||
|
||||
harness.ClearSeen();
|
||||
Expect(harness.SeenCount() == 0, "test harness clears seen events");
|
||||
|
||||
OscValueReceivedEvent first;
|
||||
first.routeKey = "layer-1\namount";
|
||||
first.layerKey = "layer-1";
|
||||
first.parameterKey = "amount";
|
||||
first.valueJson = "0.1";
|
||||
first.generation = 1;
|
||||
|
||||
OscValueReceivedEvent replacement = first;
|
||||
replacement.valueJson = "0.8";
|
||||
replacement.generation = 2;
|
||||
|
||||
Expect(harness.PublishCoalesced(first, "ControlServices", 20), "test harness accepts first coalesced payload");
|
||||
Expect(harness.PublishCoalesced(replacement, "ControlServices", 21), "test harness accepts replacement coalesced payload");
|
||||
RuntimeEventDispatchResult coalescedDispatch = harness.FlushCoalescedAndDispatch();
|
||||
Expect(coalescedDispatch.dispatchedEvents == 1, "test harness dispatches one coalesced event");
|
||||
Expect(harness.SeenCount(RuntimeEventType::OscValueReceived) == 1, "test harness records coalesced event");
|
||||
|
||||
const RuntimeEvent* seenOsc = harness.LastSeen(RuntimeEventType::OscValueReceived);
|
||||
const auto* seenPayload = seenOsc ? std::get_if<OscValueReceivedEvent>(&seenOsc->payload) : nullptr;
|
||||
Expect(seenPayload && seenPayload->valueJson == "0.8", "test harness keeps latest coalesced payload");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestRuntimeEventTypeNames();
|
||||
TestRuntimeEventPayloadTypes();
|
||||
TestRuntimeEventEnvelope();
|
||||
TestRuntimeEventQueue();
|
||||
TestRuntimeEventDispatcher();
|
||||
TestRuntimeEventCoalescingQueue();
|
||||
TestRuntimeEventCoalescingCustomKey();
|
||||
TestRuntimeEventTestHarness();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
std::cerr << gFailures << " RuntimeEventType test failure(s).\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "RuntimeEventType tests passed.\n";
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user