Files
Renderive/web_server/tests/Web_Bridge_Tests.cpp
T
2026-08-11 09:24:12 +08:00

1288 lines
68 KiB
C++

#include "web_server/Gallery_Plot_Session.h"
#include "web_server/Gallery_Protocol.h"
#include "web_server/Pixel_Frame.h"
#include "web_server/Web_Event_Adapter.h"
#include "web_server/Web_Plot_Session.h"
#include <gtest/gtest.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstring>
#include <memory>
#include <set>
#include <string>
#include <string_view>
#include <thread>
#include <variant>
#include <vector>
namespace renderive::web {
namespace {
std::uint32_t read_u32_le(const std::string& value, std::size_t offset) {
return static_cast<std::uint8_t>(value[offset]) |
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 1])) << 8U) |
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 2])) << 16U) |
(static_cast<std::uint32_t>(static_cast<std::uint8_t>(value[offset + 3])) << 24U);
}
nlohmann::json parse_json(const std::string& value) {
return nlohmann::json::parse(value);
}
Gallery_Request gallery_request(Gallery_Request_Kind kind, std::string message) {
return {kind, std::move(message)};
}
std::string open_message(std::string_view case_id, std::string_view frame_mode) {
return std::string(R"({"category":"event","type":"gallery_open","case":")") +
std::string(case_id) + R"(","frame_mode":")" + std::string(frame_mode) + R"("})";
}
nlohmann::json response_json(std::optional<Web_Response> response) {
if (!response) {
ADD_FAILURE() << "expected a Web response";
return nlohmann::json::object();
}
EXPECT_EQ(response->type, Web_Response_Type::Json);
return parse_json(response->payload);
}
nlohmann::json observe_telemetry(Gallery_Plot_Session& session) {
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"))).value(
"telemetry", nlohmann::json::object());
}
nlohmann::json invoke_action(Gallery_Plot_Session& session, std::string_view action_id,
std::optional<double> argument = std::nullopt) {
nlohmann::json request{{"category", "event"}, {"type", "gallery_action"},
{"action", action_id}};
if (argument)
request["argument"] = *argument;
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Action, request.dump())));
}
nlohmann::json patch_controls(Gallery_Plot_Session& session, nlohmann::json patch) {
const nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"},
{"patch", std::move(patch)}};
return response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch, request.dump())));
}
template <class Predicate>
bool wait_for_condition(Predicate&& predicate,
std::chrono::milliseconds timeout = std::chrono::milliseconds(750)) {
const auto deadline = std::chrono::steady_clock::now() + timeout;
do {
if (predicate())
return true;
std::this_thread::sleep_for(std::chrono::milliseconds(5));
} while (std::chrono::steady_clock::now() < deadline);
return predicate();
}
void set_gallery_view_active(Gallery_Plot_Session& session, bool active) {
const auto event = Web_Event_Adapter::decode(active ?
R"({"category":"event","type":"show"})" :
R"({"category":"event","type":"hide"})");
ASSERT_TRUE(event.has_value());
EXPECT_FALSE(session.handle(*event).has_value());
}
std::uint64_t successful_render_count(Gallery_Plot_Session& session) {
return observe_telemetry(session).at("performance").at("successful_render_count")
.get<std::uint64_t>();
}
TEST(RenderiveWebBridge, DecodesOnlyEventMessages) {
const auto resize = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":800,"height":450})");
ASSERT_TRUE(resize.has_value());
ASSERT_TRUE(std::holds_alternative<Viewport_Resize>(*resize));
EXPECT_EQ(std::get<Viewport_Resize>(*resize).size, (Size{800, 450}));
const auto pointer = Web_Event_Adapter::decode(
R"({"category":"event","type":"pointer_press","x":22.5,"y":18,"button":"left","buttons":1,"modifiers":3})");
ASSERT_TRUE(pointer.has_value());
ASSERT_TRUE(std::holds_alternative<Pointer_Event>(*pointer));
EXPECT_EQ(std::get<Pointer_Event>(*pointer).button, Mouse_Button::Left);
EXPECT_DOUBLE_EQ(std::get<Pointer_Event>(*pointer).position.x, 22.5);
EXPECT_FALSE(Web_Event_Adapter::decode(R"({"category":"command","type":"frame"})"));
EXPECT_FALSE(Web_Event_Adapter::decode("not-json"));
const auto catalog = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_catalog"})");
ASSERT_TRUE(catalog.has_value());
ASSERT_TRUE(std::holds_alternative<Gallery_Request>(*catalog));
EXPECT_EQ(std::get<Gallery_Request>(*catalog).kind, Gallery_Request_Kind::Catalog);
const auto open = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_open","case":"spectrum","frame_mode":"manual"})");
ASSERT_TRUE(open.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*open).kind, Gallery_Request_Kind::Open);
const auto observe = Web_Event_Adapter::decode(
R"({"category":"event","type":"gallery_observe"})");
ASSERT_TRUE(observe.has_value());
EXPECT_EQ(std::get<Gallery_Request>(*observe).kind, Gallery_Request_Kind::Observe);
const auto bounded = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":1e100,"height":-1e100})");
ASSERT_TRUE(bounded.has_value());
EXPECT_EQ(std::get<Viewport_Resize>(*bounded).size, (Size{1920, 180}));
}
TEST(RenderiveWebBridge, EncodesRgbaPixelFrame) {
const Pixel pixels[] = {pack_rgba(255, 0, 0), pack_rgba(0, 255, 0)};
const Image_View image{reinterpret_cast<const std::byte*>(pixels), 2, 1,
static_cast<int>(sizeof(pixels)), Pixel_Format::Premultiplied_32};
const std::string frame = encode_pixel_frame(image);
ASSERT_EQ(frame.size(), pixel_frame_header_size + 8);
EXPECT_EQ(frame.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame, 4), 2U);
EXPECT_EQ(read_u32_le(frame, 8), 1U);
EXPECT_EQ(read_u32_le(frame, 12), 8U);
const auto* rgba = reinterpret_cast<const std::uint8_t*>(frame.data() + pixel_frame_header_size);
EXPECT_EQ(rgba[0], 255);
EXPECT_EQ(rgba[1], 0);
EXPECT_EQ(rgba[2], 0);
EXPECT_EQ(rgba[3], 255);
EXPECT_EQ(rgba[4], 0);
EXPECT_EQ(rgba[5], 255);
}
TEST(RenderiveWebBridge, LegacyReceiverStillRendersPixels) {
Web_Plot_Session session;
EXPECT_FALSE(session.handle(Viewport_Resize{{640, 360}}).has_value());
EXPECT_FALSE(session.handle(Set_Demo_Mode{Demo_Mode::Usb}).has_value());
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value());
EXPECT_EQ(frame->type, Web_Response_Type::Pixels);
ASSERT_GE(frame->payload.size(), pixel_frame_header_size);
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame->payload, 4), 640U);
EXPECT_EQ(read_u32_le(frame->payload, 8), 360U);
}
TEST(RenderiveWebGallery, AdminiveCatalogCoversEveryControlCaseAndThreeModes) {
const nlohmann::json catalog = parse_json(Gallery_Protocol::catalog_json());
EXPECT_EQ(catalog.at("category"), "gallery");
EXPECT_EQ(catalog.at("type"), "catalog");
EXPECT_EQ(catalog.at("protocol"), "renderive.control-gallery");
EXPECT_EQ(catalog.at("case_descriptor").at("protocol"), "adminive.resource");
EXPECT_EQ(catalog.at("cases").size(), 8U);
EXPECT_EQ(catalog.at("frame_modes").size(), 3U);
EXPECT_EQ(catalog.at("coverage").at("page_count"), 3);
EXPECT_EQ(catalog.at("coverage").at("canvas_count"), 24);
EXPECT_GT(catalog.at("coverage").at("manual_control_count").get<std::size_t>(), 180U);
EXPECT_GT(catalog.at("coverage").at("manual_action_count").get<std::size_t>(), 50U);
EXPECT_EQ(catalog.at("coverage").at("frequency_modes"),
nlohmann::json::array({"spectrum", "afterglow", "sweep_spectrum"}));
EXPECT_EQ(catalog.at("coverage").at("image_interpolation_modes"),
nlohmann::json::array({"Nearest", "Bilinear", "Bicubic"}));
}
TEST(RenderiveWebGallery, BackendMenuMapsRetainedControlApis) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
std::string api_text;
constexpr Gallery_Frame_Mode modes[]{Gallery_Frame_Mode::Manual,
Gallery_Frame_Mode::Low_Latency,
Gallery_Frame_Mode::Playback};
for (const auto case_id : cases) {
for (const auto mode : modes) {
const auto contract = parse_json(Gallery_Protocol::case_json(
case_id, Gallery_Protocol::default_state(case_id, mode), "{}", {}, mode));
for (const auto& control : contract.at("controls").at("data"))
api_text += control.at("api").get<std::string>() + '\n';
for (const auto& action : contract.at("actions").at("data"))
api_text += action.at("api").get<std::string>() + '\n';
}
}
constexpr std::array<std::string_view, 42> required{
"Plot_Core::set_background_color", "Plot_Core::set_max_render_fps",
"Plot_Core::activate_view", "Plot_Core::remove_renderable",
"Renderable::set_visible", "Renderable::set_cache_mode", "Renderable::set_object_name",
"Abs_Axis::set_x", "Abs_Axis::set_y", "Abs_Axis::set_orientation",
"Abs_Axis::set_pixel_length", "Abs_Axis::set_locale", "Abs_Axis::set_unit_text_font",
"Axis::set_coord_range", "Axis::set_coord_start", "Axis::set_coord_length",
"Axis::set_use_wheel", "Axis::set_use_drag", "Time_Axis::set_time_format",
"Time_Axis::append_time", "Spectrum::set_frequency_axis", "Spectrum::set_frequency_point_size",
"Spectrum::set_interpolation_mode", "Spectrum::set_current_pen", "Spectrum::set_max_brush",
"Spectrum::update_samples", "Spectrum::power_at", "Spectrum::add_custom_marker",
"Spectrum::set_marker_frequency", "Waterfall::set_frequency_range",
"Waterfall::set_interpolation_mode", "Waterfall::append_row", "Color_Map::set_colors",
"Afterglow::set_attenuation_rate", "Afterglow::append_spectrum",
"Sweep_Spectrum::set_bins_per_block", "Sweep_Spectrum::append_block",
"Frequency_Trace::Builder::set_pen", "Selection_Rectangle_Overlay::set_selection_brush",
"Selection_Rectangle_Overlay::clear_selected_regions",
"Constellation_Diagram::Builder::set_type", "Constellation_Diagram::fit_square_to_axes"
};
for (const auto api : required)
EXPECT_NE(api_text.find(api), std::string::npos) << api;
EXPECT_NE(api_text.find("Plot_Core::refresh_manual_frame"), std::string::npos);
EXPECT_NE(api_text.find("Plot_Core::prepare_frame"), std::string::npos);
EXPECT_NE(api_text.find("Plot_Core::render_prepared_frame"), std::string::npos);
EXPECT_NE(api_text.find("Flow_Refresh_Strategy"), std::string::npos);
}
TEST(RenderiveWebGallery, BackendControlSchemaValidatesEveryPatch) {
const Gallery_State current = Gallery_Protocol::default_state("spectrum");
const auto accepted = Gallery_Protocol::apply_patch(
"spectrum", current,
R"({"category":"event","type":"gallery_patch","patch":{"current_pen":"#ff8844","frequency_point_size":1024,"line_interpolation":"Cubic_Value","max_render_fps":1000}})");
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_EQ(std::get<std::string>(accepted.candidate->values.at("current_pen")), "#ff8844");
EXPECT_DOUBLE_EQ(std::get<double>(accepted.candidate->values.at("frequency_point_size")), 1024);
EXPECT_DOUBLE_EQ(std::get<double>(accepted.candidate->values.at("max_render_fps")), 1000);
const auto excessive_fps = Gallery_Protocol::apply_patch(
"spectrum", current,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000001}})");
EXPECT_FALSE(excessive_fps.candidate.has_value());
EXPECT_TRUE(parse_json(excessive_fps.response_json).at("field_errors").contains("max_render_fps"));
const auto invalid_color = Gallery_Protocol::apply_patch(
"spectrum", current,
R"({"category":"event","type":"gallery_patch","patch":{"current_pen":"red"}})");
EXPECT_FALSE(invalid_color.candidate.has_value());
EXPECT_TRUE(parse_json(invalid_color.response_json).at("field_errors").contains("current_pen"));
const auto foreign_control = Gallery_Protocol::apply_patch(
"spectrum", current,
R"({"category":"event","type":"gallery_patch","patch":{"image_interpolation":"Bicubic"}})");
EXPECT_FALSE(foreign_control.candidate.has_value());
EXPECT_TRUE(parse_json(foreign_control.response_json).at("field_errors").contains("image_interpolation"));
}
TEST(RenderiveWebGallery, EveryPublishedControlValidatesItsCompleteInputContract) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
struct Mode {
std::string_view id;
Gallery_Frame_Mode value;
};
constexpr std::array modes{
Mode{"manual", Gallery_Frame_Mode::Manual},
Mode{"low_latency", Gallery_Frame_Mode::Low_Latency},
Mode{"playback", Gallery_Frame_Mode::Playback}
};
std::size_t validated_control_instances{};
for (const auto case_id : cases) {
for (const auto mode : modes) {
const auto defaults = Gallery_Protocol::default_state(case_id, mode.value);
const auto contract = parse_json(Gallery_Protocol::case_json(
case_id, defaults, "{}", {}, mode.value));
const auto& controls = contract.at("controls").at("data");
EXPECT_EQ(controls.size(), defaults.values.size()) << case_id << '/' << mode.id;
std::set<std::string> ids;
for (const auto& control : controls) {
++validated_control_instances;
const std::string id = control.at("id");
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode.id) + "/" + id);
EXPECT_TRUE(ids.insert(id).second);
EXPECT_FALSE(control.at("api").get<std::string>().empty());
EXPECT_FALSE(control.at("group").get<std::string>().empty());
const std::string input = control.at("input");
const auto apply = [&](const nlohmann::json& value) {
nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"},
{"patch", {{id, value}}}};
return Gallery_Protocol::apply_patch(case_id, defaults, request.dump(),
mode.value);
};
const auto expect_rejected = [&](const nlohmann::json& value) {
const auto result = apply(value);
EXPECT_FALSE(result.candidate.has_value());
EXPECT_TRUE(parse_json(result.response_json).at("field_errors").contains(id));
};
if (input == "boolean") {
const bool alternative = !control.at("value").get<bool>();
const auto accepted = apply(alternative);
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_EQ(std::get<bool>(accepted.candidate->values.at(id)), alternative);
expect_rejected(1);
} else if (input == "number") {
const double minimum = control.at("minimum");
const double maximum = control.at("maximum");
const double current = control.at("value");
const double step = std::max(control.at("step").get<double>(), 1e-9);
double alternative = current + step;
if (alternative > maximum)
alternative = current - step;
if (alternative < minimum || alternative == current)
alternative = current == minimum ? maximum : minimum;
if (control.at("integer").get<bool>())
alternative = std::round(alternative);
const auto accepted = apply(alternative);
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_DOUBLE_EQ(std::get<double>(accepted.candidate->values.at(id)),
alternative);
EXPECT_TRUE(apply(minimum).candidate.has_value());
EXPECT_TRUE(apply(maximum).candidate.has_value());
expect_rejected(minimum - std::max(1.0, std::abs(minimum) * 0.1 + 1.0));
expect_rejected(maximum + std::max(1.0, std::abs(maximum) * 0.1 + 1.0));
expect_rejected("not-a-number");
if (control.at("integer").get<bool>() && maximum - minimum >= 1.0)
expect_rejected(std::clamp(std::floor(current) + 0.5,
minimum + 0.5, maximum - 0.5));
} else if (input == "select") {
const auto& options = control.at("options");
ASSERT_FALSE(options.empty());
const std::string current = control.at("value");
const auto alternative = std::find_if(
options.begin(), options.end(), [&current](const auto& option) {
return option.template get<std::string>() != current;
});
ASSERT_NE(alternative, options.end());
const std::string value = alternative->get<std::string>();
const auto accepted = apply(value);
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_EQ(std::get<std::string>(accepted.candidate->values.at(id)), value);
expect_rejected("__invalid_gallery_option__");
expect_rejected(7);
} else if (input == "color") {
const std::string value = control.at("value") == "#123456" ?
"#654321" : "#123456";
const auto accepted = apply(value);
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_EQ(std::get<std::string>(accepted.candidate->values.at(id)), value);
expect_rejected("red");
expect_rejected(7);
} else {
ASSERT_EQ(input, "text");
std::string value = control.at("value").get<std::string>() + "_qa";
const auto accepted = apply(value);
ASSERT_TRUE(accepted.candidate.has_value());
EXPECT_EQ(std::get<std::string>(accepted.candidate->values.at(id)), value);
expect_rejected(std::string(161, 'x'));
expect_rejected(7);
}
}
}
}
EXPECT_GT(validated_control_instances, 180U);
}
TEST(RenderiveWebGallery, EveryPublishedActionDispatchesToItsCanvasAndFrameMode) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
std::size_t dispatched_action_instances{};
for (const auto case_id : cases) {
for (const auto mode : modes) {
Gallery_Plot_Session catalog_session;
const auto opened = response_json(catalog_session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode))));
ASSERT_EQ(opened.at("type"), "case_state");
const auto actions = opened.at("actions").at("data");
std::set<std::string> ids;
for (const auto& action : actions) {
++dispatched_action_instances;
const std::string id = action.at("id");
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode) + "/" + id);
EXPECT_TRUE(ids.insert(id).second);
EXPECT_FALSE(action.at("api").get<std::string>().empty());
EXPECT_FALSE(action.at("group").get<std::string>().empty());
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"),
"case_state");
std::optional<double> argument;
if (!action.at("argument_input").get<std::string>().empty())
argument = action.at("argument_default").get<double>();
const auto result = invoke_action(session, id, argument);
EXPECT_EQ(result.at("type"), "case_state");
EXPECT_EQ(result.at("case").at("id").get<std::string>(), case_id);
EXPECT_EQ(result.at("frame_mode").at("id").get<std::string>(), mode);
EXPECT_TRUE(result.at("telemetry").contains("kernel_observer"));
}
}
}
EXPECT_GT(dispatched_action_instances, 150U);
}
TEST(RenderiveWebGallery, EveryPublishedControlAppliesToTheLiveCore2Scene) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
std::size_t applied_control_instances{};
for (const auto case_id : cases) {
for (const auto mode : modes) {
Gallery_Plot_Session session;
auto state = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode))));
ASSERT_EQ(state.at("type"), "case_state");
const auto published_controls = state.at("controls").at("data");
for (const auto& published : published_controls) {
++applied_control_instances;
const std::string id = published.at("id");
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode) + "/" + id);
const std::string input = published.at("input");
nlohmann::json alternative;
if (input == "boolean") {
alternative = !published.at("value").get<bool>();
} else if (input == "number") {
const double minimum = published.at("minimum");
const double maximum = published.at("maximum");
const double current = published.at("value");
const double step = std::max(published.at("step").get<double>(), 1e-9);
double value = current + step;
if (value > maximum)
value = current - step;
if (value < minimum || value == current)
value = current == minimum ? maximum : minimum;
if (published.at("integer").get<bool>())
value = std::round(value);
alternative = value;
} else if (input == "select") {
const auto& options = published.at("options");
const std::string current = published.at("value");
const auto selected = std::find_if(
options.begin(), options.end(), [&current](const auto& option) {
return option.template get<std::string>() != current;
});
ASSERT_NE(selected, options.end());
alternative = *selected;
} else if (input == "color") {
alternative = published.at("value") == "#123456" ? "#654321" : "#123456";
} else {
ASSERT_EQ(input, "text");
alternative = published.at("value").get<std::string>() + "_live";
}
nlohmann::json request{{"category", "event"}, {"type", "gallery_patch"},
{"patch", {{id, alternative}}}};
state = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch, request.dump())));
ASSERT_EQ(state.at("type"), "case_state");
EXPECT_EQ(state.at("frame_mode").at("id").get<std::string>(), mode);
const auto& returned = state.at("controls").at("data");
const auto found = std::find_if(returned.begin(), returned.end(),
[&id](const auto& item) {
return item.at("id") == id;
});
ASSERT_NE(found, returned.end());
EXPECT_EQ(found->at("value"), alternative);
EXPECT_TRUE(state.at("telemetry").contains("kernel_observer"));
}
}
}
EXPECT_GT(applied_control_instances, 180U);
}
TEST(RenderiveWebGallery, CanvasSpecificActionsProduceObservableStateChanges) {
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("axis_lab", "manual"))));
const auto before = opened.at("telemetry").at("axis_lab").at("time_axis_point_count")
.get<std::size_t>();
const auto after = invoke_action(session, "append_time").at("telemetry");
EXPECT_EQ(after.at("axis_lab").at("time_axis_point_count").get<std::size_t>(),
before + 1);
EXPECT_NE(after.at("last_action_result").get<std::string>().find("tick="),
std::string::npos);
}
{
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual")))).at("type"),
"case_state");
auto telemetry = invoke_action(session, "add_line_marker", 96e6).at("telemetry");
EXPECT_EQ(telemetry.at("spectrum").at("selectable_line_markers"), 1);
telemetry = invoke_action(session, "select_marker", 0).at("telemetry");
EXPECT_EQ(telemetry.at("spectrum").at("selected_marker_index"), 0);
telemetry = invoke_action(session, "set_marker_frequency", 99e6).at("telemetry");
EXPECT_DOUBLE_EQ(telemetry.at("spectrum").at("selected_marker_frequency"), 99e6);
telemetry = invoke_action(session, "clear_markers").at("telemetry");
EXPECT_EQ(telemetry.at("spectrum").at("selectable_line_markers"), 0);
EXPECT_EQ(telemetry.at("spectrum").at("selected_marker_index"), -1);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("waterfall", "manual"))));
const auto before_rows = opened.at("telemetry").at("waterfall").at("row_count")
.get<std::size_t>();
auto telemetry = invoke_action(session, "append_row").at("telemetry");
EXPECT_EQ(telemetry.at("waterfall").at("row_count").get<std::size_t>(),
before_rows + 1);
telemetry = invoke_action(session, "append_tick_row").at("telemetry");
EXPECT_EQ(telemetry.at("waterfall").at("row_count").get<std::size_t>(),
before_rows + 2);
EXPECT_NE(invoke_action(session, "rebind_axes").at("telemetry")
.at("last_action_result").get<std::string>().find("valid"),
std::string::npos);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("afterglow", "manual"))));
const auto before = opened.at("telemetry").at("afterglow")
.at("history_frame_count").get<std::size_t>();
const auto telemetry = invoke_action(session, "append_spectrum").at("telemetry");
EXPECT_EQ(telemetry.at("afterglow").at("history_frame_count").get<std::size_t>(),
before + 1);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("sweep_spectrum", "manual"))));
const auto before = opened.at("telemetry").at("sweep_spectrum")
.at("stored_block_count").get<std::size_t>();
const auto telemetry = invoke_action(session, "append_block").at("telemetry");
EXPECT_EQ(telemetry.at("sweep_spectrum").at("stored_block_count").get<std::size_t>(),
before + 1);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("frequency_trace", "manual"))));
const auto before = opened.at("telemetry").at("frequency_trace")
.at("sample_count").get<std::size_t>();
auto telemetry = invoke_action(session, "append_sample").at("telemetry");
EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get<std::size_t>(),
before + 1);
telemetry = invoke_action(session, "append_tick_sample").at("telemetry");
EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get<std::size_t>(),
before + 2);
}
{
Gallery_Plot_Session session;
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("constellation", "manual"))));
const auto before = opened.at("telemetry").at("constellation")
.at("point_count").get<std::size_t>();
const auto telemetry = invoke_action(session, "append_points").at("telemetry");
EXPECT_EQ(telemetry.at("constellation").at("point_count").get<std::size_t>(),
before + 24);
EXPECT_NE(invoke_action(session, "read_axes").at("telemetry")
.at("last_action_result").get<std::string>().find("valid"),
std::string::npos);
}
}
TEST(RenderiveWebGallery, CommonControlsAndViewLifecycleProduceObservableEffects) {
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual")))).at("type"),
"case_state");
const auto background_state = patch_controls(session, {{"background_color", "#123456"}});
EXPECT_EQ(background_state.at("controls").at("data").at(0).at("value"), "#123456");
ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state");
const auto background_frame = session.handle(Frame_Request{});
ASSERT_TRUE(background_frame.has_value());
ASSERT_EQ(background_frame->type, Web_Response_Type::Pixels);
ASSERT_GE(background_frame->payload.size(), pixel_frame_header_size + 4);
const auto* corner = reinterpret_cast<const std::uint8_t*>(
background_frame->payload.data() + pixel_frame_header_size);
EXPECT_EQ(corner[0], 0x12);
EXPECT_EQ(corner[1], 0x34);
EXPECT_EQ(corner[2], 0x56);
EXPECT_EQ(corner[3], 0xff);
auto state = patch_controls(session, {
{"performance_overlay", true},
{"renderable_visible", false},
{"cache_mode", "Direct"},
{"object_name", "Observable_Spectrum"}
});
auto telemetry = state.at("telemetry");
EXPECT_TRUE(telemetry.at("performance_overlay_enabled").get<bool>());
EXPECT_FALSE(telemetry.at("renderable").at("visible").get<bool>());
EXPECT_EQ(telemetry.at("renderable").at("cache_mode"), "Direct");
EXPECT_EQ(telemetry.at("renderable").at("object_name"), "Observable_Spectrum");
state = invoke_action(session, "toggle_view");
EXPECT_FALSE(state.at("telemetry").at("view_active").get<bool>());
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
state = invoke_action(session, "toggle_view");
EXPECT_TRUE(state.at("telemetry").at("view_active").get<bool>());
ASSERT_EQ(invoke_action(session, "mode_cycle").at("type"), "case_state");
EXPECT_TRUE(session.handle(Frame_Request{}).has_value());
}
TEST(RenderiveWebGallery, SelectionOverlayPointerLifecycleCreatesAndClearsRegion) {
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("selection_overlay", "manual")))).at("type"),
"case_state");
const auto resize = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":560,"height":320})");
ASSERT_TRUE(resize.has_value());
EXPECT_FALSE(session.handle(*resize).has_value());
constexpr std::array<std::string_view, 3> drag{
R"({"category":"event","type":"pointer_press","x":120,"y":90,"button":"left","buttons":1,"modifiers":0})",
R"({"category":"event","type":"pointer_move","x":300,"y":210,"button":"none","buttons":1,"modifiers":0})",
R"({"category":"event","type":"pointer_release","x":300,"y":210,"button":"left","buttons":0,"modifiers":0})"
};
for (const auto encoded : drag) {
const auto event = Web_Event_Adapter::decode(encoded);
ASSERT_TRUE(event.has_value());
EXPECT_FALSE(session.handle(*event).has_value());
}
EXPECT_EQ(observe_telemetry(session).at("selection_regions"), 1);
EXPECT_EQ(invoke_action(session, "clear_selection").at("telemetry")
.at("selection_regions"), 0);
const auto rebound = invoke_action(session, "rebind_selection_axes").at("telemetry");
EXPECT_NE(rebound.at("last_action_result").get<std::string>().find("Selection Overlay"),
std::string::npos);
}
TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRendersPixels) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
constexpr std::array<std::string_view, 10> interactive_events{
R"({"category":"event","type":"show"})",
R"({"category":"event","type":"pointer_move","x":100,"y":100,"button":"none","buttons":0,"modifiers":0})",
R"({"category":"event","type":"pointer_press","x":100,"y":100,"button":"left","buttons":1,"modifiers":1})",
R"({"category":"event","type":"pointer_move","x":180,"y":160,"button":"none","buttons":1,"modifiers":1})",
R"({"category":"event","type":"pointer_release","x":180,"y":160,"button":"left","buttons":0,"modifiers":1})",
R"({"category":"event","type":"wheel","x":160,"y":120,"pixelDeltaX":0,"pixelDeltaY":-24,"angleDeltaX":0,"angleDeltaY":192,"buttons":0,"modifiers":2})",
R"({"category":"event","type":"key_press","key":"Escape","nativeKey":27,"repeat":false,"modifiers":0})",
R"({"category":"event","type":"key_release","key":"Escape","nativeKey":27,"repeat":false,"modifiers":0})",
R"({"category":"event","type":"leave"})",
R"({"category":"event","type":"hide"})"
};
for (const auto case_id : cases) {
for (const auto mode : modes) {
SCOPED_TRACE(std::string(case_id) + "/" + std::string(mode));
Gallery_Plot_Session session(mode == "low_latency");
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode)))).at("type"),
"case_state");
const auto resize = Web_Event_Adapter::decode(
R"({"category":"event","type":"resize","width":420,"height":260})");
ASSERT_TRUE(resize.has_value());
EXPECT_FALSE(session.handle(*resize).has_value());
for (const auto encoded : interactive_events) {
const auto event = Web_Event_Adapter::decode(encoded);
ASSERT_TRUE(event.has_value()) << encoded;
EXPECT_FALSE(session.handle(*event).has_value()) << encoded;
}
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
const auto show = Web_Event_Adapter::decode(
R"({"category":"event","type":"show"})");
ASSERT_TRUE(show.has_value());
EXPECT_FALSE(session.handle(*show).has_value());
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value());
ASSERT_EQ(frame->type, Web_Response_Type::Pixels);
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1");
EXPECT_EQ(read_u32_le(frame->payload, 4), 420U);
EXPECT_EQ(read_u32_le(frame->payload, 8), 260U);
const auto telemetry = observe_telemetry(session);
EXPECT_TRUE(telemetry.at("view_active").get<bool>());
EXPECT_EQ(telemetry.at("viewport").at("width"), 420);
EXPECT_EQ(telemetry.at("viewport").at("height"), 260);
}
}
}
TEST(RenderiveWebGallery, InvalidInteractiveEventsAreRejectedBeforeReachingCore2) {
constexpr std::array<std::string_view, 8> invalid{
R"({"category":"command","type":"pointer_move","x":1,"y":2})",
R"({"category":"event","type":"pointer_move","x":1})",
R"({"category":"event","type":"pointer_press","x":"bad","y":2})",
R"({"category":"event","type":"wheel","x":1,"y":2})",
R"({"category":"event","type":"resize","width":"bad","height":200})",
R"({"category":"event","type":"control","control":"mode","value":"INVALID"})",
R"({"category":"event","type":"unknown"})",
"not-json"
};
for (const auto encoded : invalid)
EXPECT_FALSE(Web_Event_Adapter::decode(encoded).has_value()) << encoded;
}
TEST(RenderiveWebGallery, EveryIndependentCore2CanvasBuildsAndRenders) {
constexpr std::array<std::string_view, 8> cases{
"axis_lab", "spectrum", "afterglow", "sweep_spectrum",
"waterfall", "frequency_trace", "selection_overlay", "constellation"
};
constexpr std::array<std::string_view, 3> modes{"manual", "low_latency", "playback"};
for (const std::string_view mode : modes) {
for (const std::string_view case_id : cases) {
Gallery_Plot_Session session(mode == "low_latency");
const auto opened = session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, mode)));
ASSERT_TRUE(opened.has_value()) << case_id;
ASSERT_EQ(opened->type, Web_Response_Type::Json) << case_id;
const auto state = parse_json(opened->payload);
EXPECT_EQ(state.at("type"), "case_state") << case_id;
EXPECT_EQ(state.at("case").at("id").get<std::string>(), std::string(case_id))
<< case_id;
EXPECT_EQ(state.at("frame_mode").at("id"), std::string(mode)) << case_id;
EXPECT_EQ(state.at("controls").at("descriptor").at("protocol"),
"adminive.resource") << case_id;
EXPECT_EQ(state.at("controls").at("view").at("protocol"),
"adminive.view") << case_id;
EXPECT_FALSE(session.handle(Viewport_Resize{{480, 280}}).has_value()) << case_id;
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value()) << case_id;
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << case_id;
EXPECT_EQ(frame->payload.substr(0, 4), "RVP1") << case_id;
EXPECT_EQ(read_u32_le(frame->payload, 4), 480U) << case_id;
EXPECT_EQ(read_u32_le(frame->payload, 8), 280U) << case_id;
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(observed.has_value()) << case_id;
const auto telemetry = parse_json(observed->payload);
EXPECT_EQ(telemetry.at("type"), "observer_state") << case_id;
EXPECT_EQ(telemetry.at("frame_mode").at("id"), std::string(mode)) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("kernel_observer")) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("performance")) << case_id;
EXPECT_TRUE(telemetry.at("telemetry").contains("data_shape")) << case_id;
EXPECT_GT(telemetry.at("telemetry").at("data_shape").at("rendered_elements")
.get<std::size_t>(), 0U) << case_id;
}
}
}
TEST(RenderiveWebGallery, ActionsMutateLiveCore2ObjectsAndReturnTelemetry) {
Gallery_Plot_Session session;
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open,
open_message("spectrum", "low_latency"))));
const auto add = session.handle(gallery_request(
Gallery_Request_Kind::Action,
R"({"category":"event","type":"gallery_action","action":"add_line_marker","argument":97000000})"));
ASSERT_TRUE(add.has_value());
const auto add_state = parse_json(add->payload);
EXPECT_EQ(add_state.at("type"), "case_state");
EXPECT_EQ(add_state.at("telemetry").at("spectrum").at("selectable_line_markers"), 1);
const auto patch = session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"line_interpolation":"Step_Left","max_hold_visible":true}})"));
ASSERT_TRUE(patch.has_value());
EXPECT_EQ(parse_json(patch->payload).at("type"), "case_state");
const auto rebuild = session.handle(gallery_request(
Gallery_Request_Kind::Action,
R"({"category":"event","type":"gallery_action","action":"rebuild_renderable"})"));
ASSERT_TRUE(rebuild.has_value());
EXPECT_EQ(parse_json(rebuild->payload).at("type"), "case_state");
}
TEST(RenderiveWebGallery, ThreeFrameStrategiesExposeTheirRealActionsAndObservers) {
const auto action = [](Gallery_Plot_Session& session, std::string_view id,
std::optional<int> argument = std::nullopt) {
nlohmann::json request{{"category", "event"}, {"type", "gallery_action"},
{"action", id}};
if (argument)
request["argument"] = *argument;
return session.handle(gallery_request(Gallery_Request_Kind::Action, request.dump()));
};
const auto observe = [](Gallery_Plot_Session& session) {
const auto response = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
EXPECT_TRUE(response.has_value());
return parse_json(response->payload).at("telemetry");
};
Gallery_Plot_Session manual;
ASSERT_TRUE(manual.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
ASSERT_TRUE(action(manual, "mode_prepare"));
auto manual_observer = observe(manual).at("kernel_observer");
EXPECT_EQ(manual_observer.at("last_event"), "prepared");
EXPECT_EQ(manual_observer.at("pending_frame_count"), 1);
ASSERT_TRUE(action(manual, "mode_refresh"));
ASSERT_TRUE(action(manual, "mode_render"));
manual_observer = observe(manual).at("kernel_observer");
EXPECT_EQ(manual_observer.at("last_event"), "rendered");
EXPECT_GE(manual_observer.at("consumed_frame_count").get<std::uint64_t>(), 2U);
Gallery_Plot_Session low_latency;
ASSERT_TRUE(low_latency.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
const auto high_frequency = low_latency.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000}})"));
ASSERT_TRUE(high_frequency.has_value());
ASSERT_EQ(parse_json(high_frequency->payload).at("type"), "case_state");
ASSERT_TRUE(low_latency.handle(Frame_Request{}));
const auto low_telemetry = observe(low_latency);
EXPECT_EQ(low_telemetry.at("frame_mode"), "low_latency");
EXPECT_GT(low_telemetry.at("kernel_observer").at("observation_count").get<std::uint64_t>(), 0U);
EXPECT_TRUE(low_telemetry.contains("render_fps"));
ASSERT_TRUE(low_telemetry.contains("low_latency_limit"));
const auto& limit = low_telemetry.at("low_latency_limit");
EXPECT_TRUE(limit.at("consumer_limited").get<bool>());
EXPECT_FALSE(limit.at("frequency_limited").get<bool>());
EXPECT_FALSE(limit.at("paint_limited").get<bool>());
EXPECT_FALSE(limit.at("render_limited").get<bool>());
EXPECT_EQ(limit.at("target_interval_ns"), 1);
EXPECT_EQ(limit.at("consumer_interval_ns"), 33'333'333);
EXPECT_GT(limit.at("bottleneck_duration_ns").get<std::uint64_t>(), 1U);
EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("paint_lease_wait_ns"));
EXPECT_TRUE(low_telemetry.at("kernel_observer").contains("render_finish_state_wait_ns"));
constexpr std::array<std::string_view, 27> observer_fields{
"mode", "last_event", "limit_state", "configured_frequency_hz", "observation_count",
"produced_frame_count", "consumed_frame_count", "dropped_frame_count",
"failed_operation_count", "pending_frame_count", "latest_sequence",
"paint_duration_ns", "render_duration_ns", "target_interval_ns",
"bottleneck_duration_ns", "consumer_interval_ns", "next_refresh_interval_ns", "paint_lease_wait_ns",
"paint_state_wait_ns", "publish_state_wait_ns", "ready_wait_ns",
"frame_age_at_render_ns", "render_lease_wait_ns", "render_state_wait_ns",
"render_finish_state_wait_ns", "queue_wait_ns", "end_to_end_ns"
};
for (const auto field : observer_fields)
EXPECT_TRUE(low_telemetry.at("kernel_observer").contains(field)) << field;
Gallery_Plot_Session playback;
ASSERT_TRUE(playback.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "playback"))));
ASSERT_TRUE(action(playback, "mode_enqueue_burst", 5));
auto playback_observer = observe(playback).at("kernel_observer");
EXPECT_EQ(playback_observer.at("last_event"), "enqueued");
EXPECT_EQ(playback_observer.at("pending_frame_count"), 5);
ASSERT_TRUE(action(playback, "mode_dequeue"));
playback_observer = observe(playback).at("kernel_observer");
EXPECT_EQ(playback_observer.at("last_event"), "rendered");
EXPECT_EQ(playback_observer.at("pending_frame_count"), 4);
EXPECT_GT(playback_observer.at("queue_wait_ns").get<std::uint64_t>(), 0U);
}
TEST(RenderiveWebGallery, ManualStrategyRunsOnlyExplicitPrepareRefreshRenderCycles) {
Gallery_Plot_Session session(true);
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "manual"))));
ASSERT_EQ(opened.at("type"), "case_state");
const auto baseline = opened.at("telemetry").at("kernel_observer");
EXPECT_EQ(baseline.at("mode"), "manual");
EXPECT_EQ(baseline.at("limit_state"), "not_applicable");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
auto observer = observe_telemetry(session).at("kernel_observer");
EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence"));
EXPECT_EQ(observer.at("produced_frame_count"), baseline.at("produced_frame_count"));
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
auto state = invoke_action(session, "mode_prepare");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "prepared");
EXPECT_EQ(observer.at("pending_frame_count"), 1);
EXPECT_EQ(observer.at("produced_frame_count").get<std::uint64_t>(),
baseline.at("produced_frame_count").get<std::uint64_t>() + 1);
state = invoke_action(session, "mode_refresh");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "refresh_succeeded");
EXPECT_EQ(observer.at("pending_frame_count"), 0);
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
state = invoke_action(session, "mode_render");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "rendered");
EXPECT_EQ(observer.at("pending_frame_count"), 0);
EXPECT_EQ(observer.at("consumed_frame_count").get<std::uint64_t>(),
baseline.at("consumed_frame_count").get<std::uint64_t>() + 1);
ASSERT_EQ(invoke_action(session, "mode_prepare").at("type"), "case_state");
state = invoke_action(session, "mode_discard");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "manually_discarded");
EXPECT_EQ(observer.at("pending_frame_count"), 0);
EXPECT_EQ(observer.at("dropped_frame_count").get<std::uint64_t>(),
baseline.at("dropped_frame_count").get<std::uint64_t>() + 1);
const auto failed_before = observer.at("failed_operation_count").get<std::uint64_t>();
state = invoke_action(session, "mode_refresh");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "refresh_failed");
EXPECT_EQ(observer.at("failed_operation_count").get<std::uint64_t>(), failed_before + 1);
const auto sequence_after_actions = observer.at("latest_sequence");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"),
sequence_after_actions);
}
TEST(RenderiveWebGallery, LowLatencyStrategyKeepsForegroundResponsiveUnderAggressiveFrequency) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000,"pixel_stream_fps":60}})"))).at("type"),
"case_state");
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 4;
}));
std::string first_pixels;
std::string last_pixels;
for (int iteration = 0; iteration < 12; ++iteration) {
const auto started = std::chrono::steady_clock::now();
const auto frame = session.handle(Frame_Request{});
ASSERT_TRUE(frame.has_value()) << iteration;
ASSERT_EQ(frame->type, Web_Response_Type::Pixels) << iteration;
const auto telemetry = observe_telemetry(session);
EXPECT_LT(std::chrono::steady_clock::now() - started,
std::chrono::milliseconds(250)) << iteration;
EXPECT_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 1000.0);
if (iteration == 0)
first_pixels = frame->payload;
last_pixels = frame->payload;
std::this_thread::sleep_for(std::chrono::milliseconds(3));
}
EXPECT_NE(first_pixels, last_pixels);
const auto telemetry = observe_telemetry(session);
const auto& observer = telemetry.at("kernel_observer");
EXPECT_EQ(observer.at("target_interval_ns"), 1'000'000);
EXPECT_GT(observer.at("latest_sequence").get<std::uint64_t>(), 4U);
EXPECT_GT(telemetry.at("performance").at("successful_render_count").get<std::uint64_t>(), 4U);
EXPECT_TRUE(observer.at("limit_state") == "frequency_limited" ||
observer.at("limit_state") == "paint_limited" ||
observer.at("limit_state") == "render_limited" ||
observer.at("limit_state") == "consumer_limited");
}
TEST(RenderiveWebGallery, WebAdapterPublishesGenericConsumerFeedbackToKernelScheduler) {
Gallery_Plot_Session session(true);
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency")))).at("type"),
"case_state");
const auto patched = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":1000000000,"pixel_stream_fps":20}})")));
ASSERT_EQ(patched.at("type"), "case_state");
const auto& telemetry = patched.at("telemetry");
const auto& observer = telemetry.at("kernel_observer");
const auto& limit = telemetry.at("low_latency_limit");
EXPECT_EQ(observer.at("configured_frequency_hz"), 1'000'000'000.0);
EXPECT_EQ(observer.at("target_interval_ns"), 1);
EXPECT_EQ(observer.at("consumer_interval_ns"), 50'000'000);
EXPECT_EQ(observer.at("next_refresh_interval_ns"), 50'000'000);
EXPECT_EQ(observer.at("limit_state"), "consumer_limited");
EXPECT_EQ(limit.at("current"), "consumer_limited");
EXPECT_TRUE(limit.at("consumer_limited").get<bool>());
EXPECT_EQ(limit.at("consumer_feedback_source"), "web_pixel_stream");
EXPECT_EQ(limit.at("consumer_interval_ns"), 50'000'000);
EXPECT_EQ(limit.at("scheduler_interval_ns"), 50'000'000);
EXPECT_DOUBLE_EQ(limit.at("scheduler_frequency_hz"), 20.0);
const auto baseline = successful_render_count(session);
ASSERT_TRUE(wait_for_condition([&session, baseline] {
return successful_render_count(session) > baseline;
}));
std::this_thread::sleep_for(std::chrono::milliseconds(250));
EXPECT_LE(successful_render_count(session) - baseline, 9U);
}
TEST(RenderiveWebGallery, PlaybackStrategyQueuesWithoutAutomaticConsumptionAndReportsEmptyQueue) {
Gallery_Plot_Session session(true);
const auto opened = response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "playback"))));
ASSERT_EQ(opened.at("type"), "case_state");
const auto baseline = opened.at("telemetry").at("kernel_observer");
EXPECT_EQ(baseline.at("mode"), "playback");
EXPECT_EQ(baseline.at("limit_state"), "not_applicable");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
auto observer = observe_telemetry(session).at("kernel_observer");
EXPECT_EQ(observer.at("latest_sequence"), baseline.at("latest_sequence"));
EXPECT_EQ(observer.at("consumed_frame_count"), baseline.at("consumed_frame_count"));
auto state = invoke_action(session, "mode_enqueue_burst", 5);
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "enqueued");
EXPECT_EQ(observer.at("pending_frame_count"), 5);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("pending_frame_count"), 5);
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "rendered");
EXPECT_EQ(observer.at("pending_frame_count"), 4);
EXPECT_GT(observer.at("queue_wait_ns").get<std::uint64_t>(), 0U);
state = invoke_action(session, "mode_cycle");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "rendered");
EXPECT_EQ(observer.at("pending_frame_count"), 4);
for (int remaining = 3; remaining >= 0; --remaining) {
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("pending_frame_count"), remaining);
}
const auto failed_before = observer.at("failed_operation_count").get<std::uint64_t>();
state = invoke_action(session, "mode_dequeue");
observer = state.at("telemetry").at("kernel_observer");
EXPECT_EQ(observer.at("last_event"), "queue_empty");
EXPECT_EQ(observer.at("failed_operation_count").get<std::uint64_t>(), failed_before + 1);
EXPECT_EQ(observer.at("pending_frame_count"), 0);
const auto sequence_after_actions = observer.at("latest_sequence");
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(observe_telemetry(session).at("kernel_observer").at("latest_sequence"),
sequence_after_actions);
}
TEST(RenderiveWebGallery, BackendRejectsActionsHiddenByCurrentCaseOrFrameMode) {
struct Rejected_Action {
std::string_view mode;
std::string_view action;
};
constexpr std::array rejected{
Rejected_Action{"manual", "mode_enqueue"},
Rejected_Action{"low_latency", "mode_refresh"},
Rejected_Action{"playback", "mode_discard"},
Rejected_Action{"manual", "append_row"}
};
for (const auto& item : rejected) {
Gallery_Plot_Session session;
ASSERT_EQ(response_json(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", item.mode)))).at("type"),
"case_state") << item.mode;
const auto response = invoke_action(session, item.action);
EXPECT_EQ(response.at("type"), "error") << item.mode << '/' << item.action;
EXPECT_TRUE(response.at("field_errors").contains("action"))
<< item.mode << '/' << item.action;
}
}
TEST(RenderiveWebGallery, ProductionLowLatencySessionRendersWithoutPixelPulls) {
Gallery_Plot_Session session(true);
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})")));
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 4;
}));
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe","client_metrics":{"transport_fps":120,"presentation_fps":60,"websocket_buffered_bytes":0,"changed_pixel_frames":17,"duplicate_pixel_frames":3,"frame_request_timeout_count":2,"last_pixel_receive_age_ms":8.5,"last_pixel_change_age_ms":12.5}})"));
ASSERT_TRUE(observed.has_value());
const auto telemetry = parse_json(observed->payload).at("telemetry");
const auto& performance = telemetry.at("performance");
EXPECT_TRUE(performance.at("automatic_low_latency_scheduler").get<bool>());
EXPECT_GE(performance.at("successful_render_count").get<std::uint64_t>(), 4U);
EXPECT_GT(performance.at("measured_fps").get<double>(), 0.0);
EXPECT_DOUBLE_EQ(telemetry.at("kernel_observer").at("configured_frequency_hz"), 120.0);
EXPECT_EQ(telemetry.at("kernel_observer").at("target_interval_ns"), 8'333'333);
EXPECT_TRUE(telemetry.at("kernel_observer").at("limit_state") == "frequency_limited" ||
telemetry.at("kernel_observer").at("limit_state") == "paint_limited" ||
telemetry.at("kernel_observer").at("limit_state") == "render_limited" ||
telemetry.at("kernel_observer").at("limit_state") == "consumer_limited");
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("transport_fps"), 120.0);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("presentation_fps"), 60.0);
EXPECT_EQ(telemetry.at("client_performance").at("changed_pixel_frames"), 17);
EXPECT_EQ(telemetry.at("client_performance").at("duplicate_pixel_frames"), 3);
EXPECT_EQ(telemetry.at("client_performance").at("frame_request_timeout_count"), 2);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_receive_age_ms"), 8.5);
EXPECT_DOUBLE_EQ(telemetry.at("client_performance").at("last_pixel_change_age_ms"), 12.5);
}
TEST(RenderiveWebGallery, ProductionLowLatencyStopsWhileHiddenAndResumes) {
Gallery_Plot_Session session(true);
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120}})")));
ASSERT_TRUE(wait_for_condition([&session] {
return successful_render_count(session) >= 3;
}));
set_gallery_view_active(session, false);
const auto stopped_count = successful_render_count(session);
EXPECT_FALSE(session.handle(Frame_Request{}).has_value());
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(successful_render_count(session), stopped_count);
set_gallery_view_active(session, true);
EXPECT_TRUE(wait_for_condition([&session, stopped_count] {
return successful_render_count(session) > stopped_count;
}));
EXPECT_TRUE(session.handle(Frame_Request{}).has_value());
}
TEST(RenderiveWebGallery, ProductionLowLatencyOnlyRendersActiveSessions) {
constexpr std::size_t session_count = 8;
std::vector<std::unique_ptr<Gallery_Plot_Session>> sessions;
sessions.reserve(session_count);
for (std::size_t index = 0; index < session_count; ++index) {
auto session = std::make_unique<Gallery_Plot_Session>(true);
ASSERT_TRUE(session->handle(gallery_request(
Gallery_Request_Kind::Open, open_message("spectrum", "low_latency"))));
ASSERT_TRUE(session->handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":60}})")));
if (index >= 2)
set_gallery_view_active(*session, false);
sessions.push_back(std::move(session));
}
for (std::size_t index = 0; index < 2; ++index) {
ASSERT_TRUE(wait_for_condition([&sessions, index] {
return successful_render_count(*sessions[index]) >= 2;
}));
}
std::array<std::uint64_t, session_count> hidden_counts{};
for (std::size_t index = 2; index < session_count; ++index)
hidden_counts[index] = successful_render_count(*sessions[index]);
std::this_thread::sleep_for(std::chrono::milliseconds(80));
for (std::size_t index = 2; index < session_count; ++index)
EXPECT_EQ(successful_render_count(*sessions[index]), hidden_counts[index]) << index;
set_gallery_view_active(*sessions[0], false);
set_gallery_view_active(*sessions[1], false);
const auto first_stopped = successful_render_count(*sessions[0]);
const auto second_stopped = successful_render_count(*sessions[1]);
set_gallery_view_active(*sessions[2], true);
set_gallery_view_active(*sessions[3], true);
EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[2]] {
return successful_render_count(*sessions[2]) > baseline;
}));
EXPECT_TRUE(wait_for_condition([&sessions, baseline = hidden_counts[3]] {
return successful_render_count(*sessions[3]) > baseline;
}));
std::this_thread::sleep_for(std::chrono::milliseconds(80));
EXPECT_EQ(successful_render_count(*sessions[0]), first_stopped);
EXPECT_EQ(successful_render_count(*sessions[1]), second_stopped);
}
void expect_low_latency_canvas_to_change(std::string_view case_id) {
SCOPED_TRACE(std::string(case_id));
Gallery_Plot_Session session(true);
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Open, open_message(case_id, "low_latency"))));
ASSERT_FALSE(session.handle(Viewport_Resize{{240, 180}}).has_value());
ASSERT_TRUE(session.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"max_render_fps":120,"pixel_stream_fps":30}})")));
std::this_thread::sleep_for(std::chrono::milliseconds(90));
const auto first = session.handle(Frame_Request{});
ASSERT_TRUE(first.has_value());
ASSERT_EQ(first->type, Web_Response_Type::Pixels);
std::this_thread::sleep_for(std::chrono::milliseconds(90));
const auto second = session.handle(Frame_Request{});
ASSERT_TRUE(second.has_value());
ASSERT_EQ(second->type, Web_Response_Type::Pixels);
EXPECT_NE(first->payload, second->payload);
const auto observed = session.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(observed.has_value());
const auto telemetry = parse_json(observed->payload).at("telemetry");
EXPECT_GT(telemetry.at("frame_index").get<std::uint64_t>(), 2U);
EXPECT_GT(telemetry.at("kernel_observer").at("latest_sequence").get<std::uint64_t>(), 1U);
}
TEST(RenderiveWebGallery, AxisLabLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("axis_lab");
}
TEST(RenderiveWebGallery, SpectrumLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("spectrum");
}
TEST(RenderiveWebGallery, AfterglowLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("afterglow");
}
TEST(RenderiveWebGallery, SweepSpectrumLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("sweep_spectrum");
}
TEST(RenderiveWebGallery, WaterfallLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("waterfall");
}
TEST(RenderiveWebGallery, FrequencyTraceLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("frequency_trace");
}
TEST(RenderiveWebGallery, SelectionOverlayLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("selection_overlay");
}
TEST(RenderiveWebGallery, ConstellationLowLatencyPixelsChange) {
expect_low_latency_canvas_to_change("constellation");
}
TEST(RenderiveWebGallery, BuilderOnlyControlsRebuildAndAllImageModesRender) {
Gallery_Plot_Session constellation;
ASSERT_TRUE(constellation.handle(gallery_request(
Gallery_Request_Kind::Open,
open_message("constellation", "low_latency"))));
const auto rebuilt = constellation.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"constellation_type":"PSK16","phase_offset":0.75}})"));
ASSERT_TRUE(rebuilt.has_value());
const auto rebuilt_json = parse_json(rebuilt->payload);
const auto& rebuilt_controls = rebuilt_json.at("controls").at("data");
const auto type = std::find_if(rebuilt_controls.begin(), rebuilt_controls.end(), [](const auto& item) {
return item.at("id") == "constellation_type";
});
ASSERT_NE(type, rebuilt_controls.end());
EXPECT_EQ(type->at("value"), "PSK16");
ASSERT_TRUE(constellation.handle(Frame_Request{}).has_value());
Gallery_Plot_Session waterfall;
ASSERT_TRUE(waterfall.handle(gallery_request(
Gallery_Request_Kind::Open,
open_message("waterfall", "low_latency"))));
for (const std::string_view mode : {"Nearest", "Bilinear", "Bicubic"}) {
const std::string patch =
std::string(R"({"category":"event","type":"gallery_patch","patch":{"image_interpolation":")") +
std::string(mode) + R"("}})";
ASSERT_TRUE(waterfall.handle(gallery_request(Gallery_Request_Kind::Patch, patch)).has_value())
<< mode;
ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value()) << mode;
}
const auto palette = waterfall.handle(gallery_request(
Gallery_Request_Kind::Patch,
R"({"category":"event","type":"gallery_patch","patch":{"color_map":"Ember"}})"));
ASSERT_TRUE(palette.has_value());
ASSERT_TRUE(waterfall.handle(Frame_Request{}).has_value());
const auto waterfall_observer = waterfall.handle(gallery_request(
Gallery_Request_Kind::Observe,
R"({"category":"event","type":"gallery_observe"})"));
ASSERT_TRUE(waterfall_observer.has_value());
const auto waterfall_telemetry = parse_json(waterfall_observer->payload).at("telemetry");
EXPECT_GT(waterfall_telemetry.at("waterfall").at("row_count").get<std::size_t>(), 0U);
EXPECT_GT(waterfall_telemetry.at("waterfall").at("stored_point_count").get<std::size_t>(), 0U);
EXPECT_GT(waterfall_telemetry.at("waterfall").at("rendered_cell_count").get<std::size_t>(), 0U);
}
} // namespace
} // namespace renderive::web