#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 #include #include #include #include #include #include #include #include #include #include #include #include #include namespace renderive::web { namespace { std::uint32_t read_u32_le(const std::string& value, std::size_t offset) { return static_cast(value[offset]) | (static_cast(static_cast(value[offset + 1])) << 8U) | (static_cast(static_cast(value[offset + 2])) << 16U) | (static_cast(static_cast(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 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 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 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(); } 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(*resize)); EXPECT_EQ(std::get(*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)); EXPECT_EQ(std::get(*pointer).button, Mouse_Button::Left); EXPECT_DOUBLE_EQ(std::get(*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(*catalog)); EXPECT_EQ(std::get(*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(*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(*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(*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(pixels), 2, 1, static_cast(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(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(), 180U); EXPECT_GT(catalog.at("coverage").at("manual_action_count").get(), 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 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() + '\n'; for (const auto& action : contract.at("actions").at("data")) api_text += action.at("api").get() + '\n'; } } constexpr std::array 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(accepted.candidate->values.at("current_pen")), "#ff8844"); EXPECT_DOUBLE_EQ(std::get(accepted.candidate->values.at("frequency_point_size")), 1024); EXPECT_DOUBLE_EQ(std::get(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 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 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().empty()); EXPECT_FALSE(control.at("group").get().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(); const auto accepted = apply(alternative); ASSERT_TRUE(accepted.candidate.has_value()); EXPECT_EQ(std::get(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(), 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()) alternative = std::round(alternative); const auto accepted = apply(alternative); ASSERT_TRUE(accepted.candidate.has_value()); EXPECT_DOUBLE_EQ(std::get(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() && 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(), [¤t](const auto& option) { return option.template get() != current; }); ASSERT_NE(alternative, options.end()); const std::string value = alternative->get(); const auto accepted = apply(value); ASSERT_TRUE(accepted.candidate.has_value()); EXPECT_EQ(std::get(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(accepted.candidate->values.at(id)), value); expect_rejected("red"); expect_rejected(7); } else { ASSERT_EQ(input, "text"); std::string value = control.at("value").get() + "_qa"; const auto accepted = apply(value); ASSERT_TRUE(accepted.candidate.has_value()); EXPECT_EQ(std::get(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 cases{ "axis_lab", "spectrum", "afterglow", "sweep_spectrum", "waterfall", "frequency_trace", "selection_overlay", "constellation" }; constexpr std::array 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 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().empty()); EXPECT_FALSE(action.at("group").get().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 argument; if (!action.at("argument_input").get().empty()) argument = action.at("argument_default").get(); const auto result = invoke_action(session, id, argument); EXPECT_EQ(result.at("type"), "case_state"); EXPECT_EQ(result.at("case").at("id").get(), case_id); EXPECT_EQ(result.at("frame_mode").at("id").get(), mode); EXPECT_TRUE(result.at("telemetry").contains("kernel_observer")); } } } EXPECT_GT(dispatched_action_instances, 150U); } TEST(RenderiveWebGallery, EveryPublishedControlAppliesToTheLiveCore2Scene) { constexpr std::array cases{ "axis_lab", "spectrum", "afterglow", "sweep_spectrum", "waterfall", "frequency_trace", "selection_overlay", "constellation" }; constexpr std::array 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(); } 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(), 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()) 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(), [¤t](const auto& option) { return option.template get() != 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() + "_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(), 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(); const auto after = invoke_action(session, "append_time").at("telemetry"); EXPECT_EQ(after.at("axis_lab").at("time_axis_point_count").get(), before + 1); EXPECT_NE(after.at("last_action_result").get().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(); auto telemetry = invoke_action(session, "append_row").at("telemetry"); EXPECT_EQ(telemetry.at("waterfall").at("row_count").get(), before_rows + 1); telemetry = invoke_action(session, "append_tick_row").at("telemetry"); EXPECT_EQ(telemetry.at("waterfall").at("row_count").get(), before_rows + 2); EXPECT_NE(invoke_action(session, "rebind_axes").at("telemetry") .at("last_action_result").get().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(); const auto telemetry = invoke_action(session, "append_spectrum").at("telemetry"); EXPECT_EQ(telemetry.at("afterglow").at("history_frame_count").get(), 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(); const auto telemetry = invoke_action(session, "append_block").at("telemetry"); EXPECT_EQ(telemetry.at("sweep_spectrum").at("stored_block_count").get(), 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(); auto telemetry = invoke_action(session, "append_sample").at("telemetry"); EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get(), before + 1); telemetry = invoke_action(session, "append_tick_sample").at("telemetry"); EXPECT_EQ(telemetry.at("frequency_trace").at("sample_count").get(), 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(); const auto telemetry = invoke_action(session, "append_points").at("telemetry"); EXPECT_EQ(telemetry.at("constellation").at("point_count").get(), before + 24); EXPECT_NE(invoke_action(session, "read_axes").at("telemetry") .at("last_action_result").get().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( 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()); EXPECT_FALSE(telemetry.at("renderable").at("visible").get()); 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()); EXPECT_FALSE(session.handle(Frame_Request{}).has_value()); state = invoke_action(session, "toggle_view"); EXPECT_TRUE(state.at("telemetry").at("view_active").get()); 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 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().find("Selection Overlay"), std::string::npos); } TEST(RenderiveWebGallery, EveryCanvasAcceptsTheCompleteWebEventSetAndStillRendersPixels) { constexpr std::array cases{ "axis_lab", "spectrum", "afterglow", "sweep_spectrum", "waterfall", "frequency_trace", "selection_overlay", "constellation" }; constexpr std::array modes{"manual", "low_latency", "playback"}; constexpr std::array 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()); EXPECT_EQ(telemetry.at("viewport").at("width"), 420); EXPECT_EQ(telemetry.at("viewport").at("height"), 260); } } } TEST(RenderiveWebGallery, InvalidInteractiveEventsAreRejectedBeforeReachingCore2) { constexpr std::array 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 cases{ "axis_lab", "spectrum", "afterglow", "sweep_spectrum", "waterfall", "frequency_trace", "selection_overlay", "constellation" }; constexpr std::array 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(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(), 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 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(), 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(), 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("paint_limited").get() || limit.at("render_limited").get()); EXPECT_FALSE(limit.at("frequency_limited").get()); EXPECT_EQ(limit.at("target_interval_ns"), 1); EXPECT_GT(limit.at("bottleneck_duration_ns").get(), 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 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", "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(), 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(), baseline.at("produced_frame_count").get() + 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(), baseline.at("consumed_frame_count").get() + 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(), baseline.at("dropped_frame_count").get() + 1); const auto failed_before = observer.at("failed_operation_count").get(); 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(), 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(), 4U); EXPECT_GT(telemetry.at("performance").at("successful_render_count").get(), 4U); EXPECT_TRUE(observer.at("limit_state") == "frequency_limited" || observer.at("limit_state") == "paint_limited" || observer.at("limit_state") == "render_limited"); } 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(), 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(); 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(), 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()); EXPECT_GE(performance.at("successful_render_count").get(), 4U); EXPECT_GT(performance.at("measured_fps").get(), 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"); 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> sessions; sessions.reserve(session_count); for (std::size_t index = 0; index < session_count; ++index) { auto session = std::make_unique(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 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(), 2U); EXPECT_GT(telemetry.at("kernel_observer").at("latest_sequence").get(), 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(), 0U); EXPECT_GT(waterfall_telemetry.at("waterfall").at("stored_point_count").get(), 0U); EXPECT_GT(waterfall_telemetry.at("waterfall").at("rendered_cell_count").get(), 0U); } } // namespace } // namespace renderive::web