Phase 3 refactor in progress
This commit is contained in:
@@ -30,6 +30,35 @@ ShaderParameterDefinition FloatDefinition(const std::string& id, const std::stri
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition Vec2Definition(const std::string& id, const std::string& label)
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = id;
|
||||
definition.label = label;
|
||||
definition.type = ShaderParameterType::Vec2;
|
||||
definition.defaultNumbers = { 0.0, 0.0 };
|
||||
definition.minNumbers = { 0.0, 0.0 };
|
||||
definition.maxNumbers = { 1.0, 1.0 };
|
||||
return definition;
|
||||
}
|
||||
|
||||
ShaderParameterDefinition TriggerDefinition(const std::string& id, const std::string& label)
|
||||
{
|
||||
ShaderParameterDefinition definition;
|
||||
definition.id = id;
|
||||
definition.label = label;
|
||||
definition.type = ShaderParameterType::Trigger;
|
||||
return definition;
|
||||
}
|
||||
|
||||
JsonValue NumberArray(std::initializer_list<double> numbers)
|
||||
{
|
||||
JsonValue value = JsonValue::MakeArray();
|
||||
for (double number : numbers)
|
||||
value.pushBack(JsonValue(number));
|
||||
return value;
|
||||
}
|
||||
|
||||
RuntimeRenderState MakeLayerState()
|
||||
{
|
||||
RuntimeRenderState state;
|
||||
@@ -43,6 +72,16 @@ RuntimeRenderState MakeLayerState()
|
||||
return state;
|
||||
}
|
||||
|
||||
RuntimeRenderState MakeLayerStateWithDefinitions(const std::vector<ShaderParameterDefinition>& definitions)
|
||||
{
|
||||
RuntimeRenderState state;
|
||||
state.layerId = "layer-one";
|
||||
state.shaderId = "test-shader";
|
||||
state.shaderName = "Test Shader";
|
||||
state.parameterDefinitions = definitions;
|
||||
return state;
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateAppliesLatestOscOverlay()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
@@ -71,6 +110,40 @@ void TestRuntimeLiveStateAppliesLatestOscOverlay()
|
||||
"overlay applies the latest target value");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateIgnoresStaleCommitCompletions()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(0.9);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.allowCommit = true;
|
||||
options.smoothing = 0.0;
|
||||
options.commitDelay = std::chrono::milliseconds(0);
|
||||
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
|
||||
liveState.ApplyToLayerStates(states, options, &commitRequests);
|
||||
Expect(commitRequests.size() == 1, "initial commit request is queued");
|
||||
|
||||
liveState.ApplyOscCommitCompletions({ { "other-route", commitRequests[0].generation } });
|
||||
Expect(liveState.OverlayCount() == 1, "completion for another route does not remove overlay");
|
||||
|
||||
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation + 1 } });
|
||||
Expect(liveState.OverlayCount() == 1, "completion for another generation does not remove overlay");
|
||||
|
||||
RuntimeLiveOscUpdate newerUpdate = update;
|
||||
newerUpdate.targetValue = JsonValue(0.2);
|
||||
liveState.ApplyOscUpdates({ newerUpdate });
|
||||
liveState.ApplyOscCommitCompletions({ { commitRequests[0].routeKey, commitRequests[0].generation } });
|
||||
Expect(liveState.OverlayCount() == 1, "stale completion for previous generation is ignored after newer update");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateQueuesAndCompletesCommit()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
@@ -99,6 +172,183 @@ void TestRuntimeLiveStateQueuesAndCompletesCommit()
|
||||
Expect(liveState.OverlayCount() == 0, "matching commit completion removes settled overlay");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateQueuesOneCommitPerGeneration()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(0.8);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.allowCommit = true;
|
||||
options.smoothing = 0.0;
|
||||
options.commitDelay = std::chrono::milliseconds(0);
|
||||
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||
liveState.ApplyToLayerStates(states, options, &commitRequests);
|
||||
Expect(commitRequests.size() == 1, "first apply queues one commit for generation");
|
||||
Expect(commitRequests[0].generation == 1, "first commit uses generation one");
|
||||
|
||||
commitRequests.clear();
|
||||
options.now += std::chrono::milliseconds(1);
|
||||
liveState.ApplyToLayerStates(states, options, &commitRequests);
|
||||
Expect(commitRequests.empty(), "second apply does not duplicate commit for same generation");
|
||||
|
||||
RuntimeLiveOscUpdate newerUpdate = update;
|
||||
newerUpdate.targetValue = JsonValue(0.4);
|
||||
liveState.ApplyOscUpdates({ newerUpdate });
|
||||
|
||||
commitRequests.clear();
|
||||
options.now += std::chrono::milliseconds(1);
|
||||
liveState.ApplyToLayerStates(states, options, &commitRequests);
|
||||
Expect(commitRequests.size() == 1, "newer update allows a new commit request");
|
||||
Expect(commitRequests[0].generation == 2, "new commit uses newer generation");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(1.0);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.smoothing = 0.0;
|
||||
liveState.ApplyToLayerStates(states, options, nullptr);
|
||||
|
||||
const auto valueIt = states[0].parameterValues.find("amount");
|
||||
Expect(valueIt != states[0].parameterValues.end(), "smoothing zero writes amount");
|
||||
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 1.0) < 0.0001,
|
||||
"smoothing zero applies target immediately");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateSmoothingOneConvergesImmediately()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(1.0);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.smoothing = 1.0;
|
||||
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
|
||||
liveState.ApplyToLayerStates(states, options, nullptr);
|
||||
|
||||
const auto valueIt = states[0].parameterValues.find("amount");
|
||||
Expect(valueIt != states[0].parameterValues.end(), "smoothing one writes amount");
|
||||
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 1.0) < 0.0001,
|
||||
"smoothing one converges immediately");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateSmoothingPartiallyConverges()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(1.0);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerState() };
|
||||
ShaderParameterValue amount;
|
||||
amount.numberValues = { 0.0 };
|
||||
states[0].parameterValues["amount"] = amount;
|
||||
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.smoothing = 0.5;
|
||||
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
|
||||
liveState.ApplyToLayerStates(states, options, nullptr);
|
||||
|
||||
const auto valueIt = states[0].parameterValues.find("amount");
|
||||
Expect(valueIt != states[0].parameterValues.end(), "partial smoothing writes amount");
|
||||
Expect(!valueIt->second.numberValues.empty() &&
|
||||
valueIt->second.numberValues[0] > 0.0 &&
|
||||
valueIt->second.numberValues[0] < 1.0,
|
||||
"partial smoothing advances toward target without snapping");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\noffset";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "offset";
|
||||
update.targetValue = NumberArray({ 0.25, 0.75 });
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ Vec2Definition("offset", "Offset") }) };
|
||||
ShaderParameterValue malformedOffset;
|
||||
malformedOffset.numberValues = { 0.1 };
|
||||
states[0].parameterValues["offset"] = malformedOffset;
|
||||
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.smoothing = 0.5;
|
||||
options.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(16);
|
||||
liveState.ApplyToLayerStates(states, options, nullptr);
|
||||
|
||||
const auto valueIt = states[0].parameterValues.find("offset");
|
||||
Expect(valueIt != states[0].parameterValues.end(), "vector mismatch writes offset");
|
||||
Expect(valueIt->second.numberValues.size() == 2, "vector mismatch restores target vector size");
|
||||
Expect(valueIt->second.numberValues.size() == 2 &&
|
||||
std::fabs(valueIt->second.numberValues[0] - 0.25) < 0.0001 &&
|
||||
std::fabs(valueIt->second.numberValues[1] - 0.75) < 0.0001,
|
||||
"vector mismatch snaps to validated target shape");
|
||||
}
|
||||
|
||||
void TestRuntimeLiveStateTriggerOverlayIncrementsAndClears()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\npulse";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "pulse";
|
||||
update.targetValue = JsonValue(true);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> states = { MakeLayerStateWithDefinitions({ TriggerDefinition("pulse", "Pulse") }) };
|
||||
states[0].timeSeconds = 42.0;
|
||||
ShaderParameterValue pulse;
|
||||
pulse.numberValues = { 2.0, 10.0 };
|
||||
states[0].parameterValues["pulse"] = pulse;
|
||||
|
||||
std::vector<RuntimeLiveOscCommitRequest> commitRequests;
|
||||
RuntimeLiveStateApplyOptions options;
|
||||
options.allowCommit = true;
|
||||
options.smoothing = 0.0;
|
||||
options.commitDelay = std::chrono::milliseconds(0);
|
||||
liveState.ApplyToLayerStates(states, options, &commitRequests);
|
||||
|
||||
const auto valueIt = states[0].parameterValues.find("pulse");
|
||||
Expect(valueIt != states[0].parameterValues.end(), "trigger overlay writes pulse");
|
||||
Expect(valueIt->second.numberValues.size() == 2 &&
|
||||
std::fabs(valueIt->second.numberValues[0] - 3.0) < 0.0001 &&
|
||||
std::fabs(valueIt->second.numberValues[1] - 42.0) < 0.0001,
|
||||
"trigger overlay increments count and stamps layer time");
|
||||
Expect(commitRequests.empty(), "trigger overlay does not queue commit");
|
||||
Expect(liveState.OverlayCount() == 0, "trigger overlay clears after apply");
|
||||
}
|
||||
|
||||
void TestRenderStateComposerBuildsFrameState()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
@@ -110,27 +360,102 @@ void TestRenderStateComposerBuildsFrameState()
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
RenderStateCompositionInput input;
|
||||
input.baseLayerStates = { MakeLayerState() };
|
||||
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
|
||||
input.baseLayerStates = &baseLayerStates;
|
||||
input.liveState = &liveState;
|
||||
input.liveStateOptions.allowCommit = false;
|
||||
input.liveStateOptions.smoothing = 0.0;
|
||||
input.allowLiveCommits = false;
|
||||
input.liveSmoothing = 0.0;
|
||||
|
||||
RenderStateComposer composer;
|
||||
RenderStateCompositionResult result = composer.BuildFrameState(input);
|
||||
|
||||
Expect(result.hasLayerStates, "composer reports that it composed base layer states");
|
||||
Expect(result.layerStates.size() == 1, "composer returns composed layer state");
|
||||
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
|
||||
Expect(valueIt != result.layerStates[0].parameterValues.end(), "composer applies live overlay through live state");
|
||||
Expect(!valueIt->second.numberValues.empty() && std::fabs(valueIt->second.numberValues[0] - 0.6) < 0.0001,
|
||||
"composer uses OSC key matching against shader names and labels");
|
||||
const auto baseValueIt = baseLayerStates[0].parameterValues.find("amount");
|
||||
Expect(baseValueIt != baseLayerStates[0].parameterValues.end() &&
|
||||
!baseValueIt->second.numberValues.empty() &&
|
||||
std::fabs(baseValueIt->second.numberValues[0] - 0.25) < 0.0001,
|
||||
"composer leaves base layer states unchanged");
|
||||
}
|
||||
|
||||
void TestRenderStateComposerQueuesCommitRequestsWhenEnabled()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(0.8);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
|
||||
RenderStateCompositionInput input;
|
||||
input.baseLayerStates = &baseLayerStates;
|
||||
input.liveState = &liveState;
|
||||
input.allowLiveCommits = true;
|
||||
input.collectLiveCommitRequests = true;
|
||||
input.liveSmoothing = 0.0;
|
||||
input.liveCommitDelay = std::chrono::milliseconds(0);
|
||||
input.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
|
||||
|
||||
RenderStateComposer composer;
|
||||
RenderStateCompositionResult result = composer.BuildFrameState(input);
|
||||
|
||||
Expect(result.commitRequests.size() == 1, "composer returns live commit requests when collection is enabled");
|
||||
Expect(result.commitRequests[0].routeKey == "layer-one\namount", "composer commit request preserves route");
|
||||
Expect(result.commitRequests[0].generation == 1, "composer commit request preserves generation");
|
||||
}
|
||||
|
||||
void TestRenderStateComposerSuppressesCommitCollection()
|
||||
{
|
||||
RuntimeLiveState liveState;
|
||||
RuntimeLiveOscUpdate update;
|
||||
update.routeKey = "layer-one\namount";
|
||||
update.layerKey = "layer-one";
|
||||
update.parameterKey = "amount";
|
||||
update.targetValue = JsonValue(0.7);
|
||||
liveState.ApplyOscUpdates({ update });
|
||||
|
||||
std::vector<RuntimeRenderState> baseLayerStates = { MakeLayerState() };
|
||||
RenderStateCompositionInput input;
|
||||
input.baseLayerStates = &baseLayerStates;
|
||||
input.liveState = &liveState;
|
||||
input.allowLiveCommits = true;
|
||||
input.collectLiveCommitRequests = false;
|
||||
input.liveSmoothing = 0.0;
|
||||
input.liveCommitDelay = std::chrono::milliseconds(0);
|
||||
input.now = std::chrono::steady_clock::now() + std::chrono::milliseconds(1);
|
||||
|
||||
RenderStateComposer composer;
|
||||
RenderStateCompositionResult result = composer.BuildFrameState(input);
|
||||
|
||||
Expect(result.commitRequests.empty(), "composer can apply overlays without collecting commit requests");
|
||||
const auto valueIt = result.layerStates[0].parameterValues.find("amount");
|
||||
Expect(valueIt != result.layerStates[0].parameterValues.end() &&
|
||||
!valueIt->second.numberValues.empty() &&
|
||||
std::fabs(valueIt->second.numberValues[0] - 0.7) < 0.0001,
|
||||
"composer still applies overlays when commit collection is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
TestRuntimeLiveStateAppliesLatestOscOverlay();
|
||||
TestRuntimeLiveStateIgnoresStaleCommitCompletions();
|
||||
TestRuntimeLiveStateQueuesAndCompletesCommit();
|
||||
TestRuntimeLiveStateQueuesOneCommitPerGeneration();
|
||||
TestRuntimeLiveStateSmoothingZeroAppliesTargetImmediately();
|
||||
TestRuntimeLiveStateSmoothingOneConvergesImmediately();
|
||||
TestRuntimeLiveStateSmoothingPartiallyConverges();
|
||||
TestRuntimeLiveStateSmoothingVectorSizeMismatchUsesTargetShape();
|
||||
TestRuntimeLiveStateTriggerOverlayIncrementsAndClears();
|
||||
TestRenderStateComposerBuildsFrameState();
|
||||
TestRenderStateComposerQueuesCommitRequestsWhenEnabled();
|
||||
TestRenderStateComposerSuppressesCommitCollection();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "LayerStackStore.h"
|
||||
#include "RuntimeCoordinator.h"
|
||||
#include "RuntimeEventDispatcher.h"
|
||||
#include "RuntimeStateJson.h"
|
||||
#include "RuntimeStore.h"
|
||||
#include "ShaderPackageCatalog.h"
|
||||
|
||||
#include <chrono>
|
||||
@@ -8,6 +11,8 @@
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -44,6 +49,31 @@ void WriteShaderPackage(const std::filesystem::path& root, const std::string& di
|
||||
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
|
||||
}
|
||||
|
||||
std::filesystem::path GetCurrentDirectoryPath()
|
||||
{
|
||||
char buffer[MAX_PATH] = {};
|
||||
GetCurrentDirectoryA(MAX_PATH, buffer);
|
||||
return std::filesystem::path(buffer);
|
||||
}
|
||||
|
||||
class ScopedCurrentDirectory
|
||||
{
|
||||
public:
|
||||
explicit ScopedCurrentDirectory(const std::filesystem::path& path) :
|
||||
mPrevious(GetCurrentDirectoryPath())
|
||||
{
|
||||
SetCurrentDirectoryA(path.string().c_str());
|
||||
}
|
||||
|
||||
~ScopedCurrentDirectory()
|
||||
{
|
||||
SetCurrentDirectoryA(mPrevious.string().c_str());
|
||||
}
|
||||
|
||||
private:
|
||||
std::filesystem::path mPrevious;
|
||||
};
|
||||
|
||||
ShaderPackageCatalog BuildCatalog(const std::filesystem::path& root)
|
||||
{
|
||||
ShaderPackageCatalog catalog;
|
||||
@@ -181,6 +211,105 @@ void TestRuntimeStateJsonReadModelSerialization()
|
||||
const JsonValue* value = parameters->asArray()[0].find("value");
|
||||
Expect(value && value->asNumber() == 0.8, "serialized parameter includes current value");
|
||||
}
|
||||
|
||||
void TestRuntimeCoordinatorPersistenceEvents()
|
||||
{
|
||||
const std::filesystem::path root = MakeTestRoot();
|
||||
WriteFile(root / "CMakeLists.txt", "cmake_minimum_required(VERSION 3.24)\n");
|
||||
std::filesystem::create_directories(root / "apps" / "LoopThroughWithOpenGLCompositing");
|
||||
std::filesystem::create_directories(root / "runtime" / "templates");
|
||||
WriteShaderPackage(root / "shaders", "alpha", R"({
|
||||
"id": "alpha",
|
||||
"name": "Alpha",
|
||||
"parameters": [{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 }]
|
||||
})");
|
||||
WriteShaderPackage(root / "shaders", "beta", R"({
|
||||
"id": "beta",
|
||||
"name": "Beta",
|
||||
"parameters": [{ "id": "amount", "label": "Amount", "type": "float", "default": 0.25, "min": 0, "max": 1 }]
|
||||
})");
|
||||
|
||||
{
|
||||
ScopedCurrentDirectory scopedDirectory(root);
|
||||
RuntimeStore store;
|
||||
std::string error;
|
||||
Expect(store.InitializeStore(error), "runtime store initializes in isolated fixture");
|
||||
Expect(error.empty(), "runtime store initialization has no error");
|
||||
|
||||
RuntimeEventDispatcher dispatcher(64);
|
||||
std::vector<RuntimeEvent> seenEvents;
|
||||
dispatcher.SubscribeAll([&seenEvents](const RuntimeEvent& event) {
|
||||
seenEvents.push_back(event);
|
||||
});
|
||||
|
||||
RuntimeCoordinator coordinator(store, dispatcher);
|
||||
auto dispatchAndClear = [&]() {
|
||||
dispatcher.DispatchPending();
|
||||
const std::vector<RuntimeEvent> events = seenEvents;
|
||||
seenEvents.clear();
|
||||
return events;
|
||||
};
|
||||
auto countEvents = [](const std::vector<RuntimeEvent>& events, RuntimeEventType type) {
|
||||
return static_cast<std::size_t>(std::count_if(events.begin(), events.end(),
|
||||
[type](const RuntimeEvent& event) { return event.type == type; }));
|
||||
};
|
||||
auto persistenceReason = [](const std::vector<RuntimeEvent>& events) {
|
||||
for (const RuntimeEvent& event : events)
|
||||
{
|
||||
if (event.type != RuntimeEventType::RuntimePersistenceRequested)
|
||||
continue;
|
||||
const auto* payload = std::get_if<RuntimePersistenceRequestedEvent>(&event.payload);
|
||||
return payload ? payload->reason : std::string();
|
||||
}
|
||||
return std::string();
|
||||
};
|
||||
auto expectAcceptedPersistence = [&](const RuntimeCoordinatorResult& result, const std::string& reason, const char* message) {
|
||||
const std::vector<RuntimeEvent> events = dispatchAndClear();
|
||||
Expect(result.accepted, message);
|
||||
Expect(result.persistenceRequested, "accepted persistent mutation marks coordinator result");
|
||||
Expect(countEvents(events, RuntimeEventType::RuntimeMutationAccepted) == 1, "persistent mutation publishes accepted fact");
|
||||
Expect(countEvents(events, RuntimeEventType::RuntimePersistenceRequested) == 1, "persistent mutation publishes persistence request");
|
||||
Expect(persistenceReason(events) == reason, "persistence request preserves coordinator action reason");
|
||||
};
|
||||
|
||||
std::vector<RuntimeStore::LayerPersistentState> layers = store.CopyLayerStates();
|
||||
Expect(layers.size() == 1, "isolated fixture starts with a default layer");
|
||||
const std::string alphaLayerId = layers.empty() ? std::string() : layers[0].id;
|
||||
|
||||
expectAcceptedPersistence(coordinator.UpdateLayerParameter(alphaLayerId, "gain", JsonValue(0.75)), "UpdateLayerParameter",
|
||||
"parameter changes are accepted");
|
||||
|
||||
expectAcceptedPersistence(coordinator.AddLayer("beta"), "AddLayer", "stack edits are accepted");
|
||||
layers = store.CopyLayerStates();
|
||||
Expect(layers.size() == 2, "stack edit creates a second layer");
|
||||
const std::string betaLayerId = layers.size() > 1 ? layers[1].id : std::string();
|
||||
expectAcceptedPersistence(coordinator.MoveLayer(betaLayerId, -1), "MoveLayer", "layer order edits are accepted");
|
||||
|
||||
expectAcceptedPersistence(coordinator.SaveStackPreset("Look One"), "SaveStackPreset", "preset save is accepted");
|
||||
expectAcceptedPersistence(coordinator.LoadStackPreset("Look One"), "LoadStackPreset", "preset load is accepted");
|
||||
|
||||
RuntimeCoordinatorResult rejected = coordinator.UpdateLayerParameter(alphaLayerId, "missing", JsonValue(0.5));
|
||||
std::vector<RuntimeEvent> rejectedEvents = dispatchAndClear();
|
||||
Expect(!rejected.accepted, "invalid parameter mutation is rejected");
|
||||
Expect(!rejected.persistenceRequested, "rejected mutation does not mark persistence");
|
||||
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimeMutationRejected) == 1, "rejected mutation publishes rejection fact");
|
||||
Expect(countEvents(rejectedEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "rejected mutation publishes no persistence request");
|
||||
|
||||
OscOverlayEvent overlay;
|
||||
overlay.routeKey = "alpha\ngain";
|
||||
overlay.layerKey = "alpha";
|
||||
overlay.parameterKey = "gain";
|
||||
Expect(dispatcher.PublishPayload(overlay, "RuntimeLiveState"), "OSC overlay event publishes");
|
||||
std::vector<RuntimeEvent> overlayEvents = dispatchAndClear();
|
||||
Expect(countEvents(overlayEvents, RuntimeEventType::OscOverlayApplied) == 1, "transient OSC overlay is observable");
|
||||
Expect(countEvents(overlayEvents, RuntimeEventType::RuntimePersistenceRequested) == 0, "transient OSC overlay does not request persistence");
|
||||
|
||||
expectAcceptedPersistence(coordinator.CommitOscParameterByControlKey("alpha", "gain", JsonValue(0.2)), "CommitOscParameterByControlKey",
|
||||
"accepted OSC commit is persistent");
|
||||
}
|
||||
|
||||
std::filesystem::remove_all(root);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -188,6 +317,7 @@ int main()
|
||||
TestLayerDefaultsAndCrud();
|
||||
TestMoveClassificationAndPresetLoad();
|
||||
TestRuntimeStateJsonReadModelSerialization();
|
||||
TestRuntimeCoordinatorPersistenceEvents();
|
||||
|
||||
if (gFailures != 0)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user