ADR 0009 (Proposée) et spécification du protocole Core - rendu 0.1. Le Core projette le monde simulé (micro-mode 2D, SIMULÉ) en deltas SEQUA 3D (sequa::core_render) et les pousse après chaque tick qui change la scène (RenderHub, observateur de LiveSession) ; il lance et supervise sequa-render (RenderSupervisor : lié au Core, relance après plantage). sequa-render charge une bibliothèque d'adaptateur par un point d'entrée C versionné (3d/adapter_library.hpp), applique les deltas et rend les images ; codec JSON provisoire partagé dans 3d/wire. Adaptateur Godot en DLL et nouvel adaptateur de débogage (engines/debug/adapter : vue de côté en images PPM ou vidéo via ffmpeg externe). Protocole Studio - Core 0.3 : render/start, render/stop, render/status (Core et Studio host). Mesuré avec Godot : transport tick -> delta reçu p95 ≈ 0,3 ms (critère 4 ms).
524 lines
21 KiB
C++
524 lines
21 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
#include "sequa/core/server/protocol.hpp"
|
|
|
|
#include "sequa/core/server/services.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <string>
|
|
|
|
using namespace sequa::core::server;
|
|
using nlohmann::json;
|
|
namespace three_d = sequa::three_d;
|
|
|
|
namespace {
|
|
|
|
// Services factices : mémorisent les appels, réponses prévisibles.
|
|
class FakeServices final : public CoreServices {
|
|
public:
|
|
CoreStatus core_status() const override
|
|
{
|
|
CoreStatus status;
|
|
status.core_version = "1.2.3";
|
|
status.protocol_version = std::string(protocol_version);
|
|
status.instance_id = "0123456789abcdef0123456789abcdef";
|
|
status.pid = 4242;
|
|
status.linked = linked;
|
|
status.controller_attached = controller.has_value();
|
|
return status;
|
|
}
|
|
AttachResult attach(ConnectionId id, bool force) override
|
|
{
|
|
if (controller && *controller != id && !force) {
|
|
return AttachResult::controller_exists;
|
|
}
|
|
controller = id;
|
|
linked = true;
|
|
return AttachResult::attached;
|
|
}
|
|
DetachResult detach(ConnectionId id) override
|
|
{
|
|
if (controller != id) {
|
|
return DetachResult::not_controller;
|
|
}
|
|
linked = false;
|
|
return DetachResult::detached;
|
|
}
|
|
void request_shutdown() override { shutdown = true; }
|
|
SessionStateView session_start(std::uint64_t new_seed) override
|
|
{
|
|
state.seed = new_seed;
|
|
state.tick = 0;
|
|
return state;
|
|
}
|
|
SessionStateView session_pause() override
|
|
{
|
|
state.running = false;
|
|
return state;
|
|
}
|
|
SessionStateView session_resume() override
|
|
{
|
|
state.running = true;
|
|
return state;
|
|
}
|
|
SessionStateView session_status() const override { return state; }
|
|
std::uint64_t session_input(std::uint32_t b, std::int32_t x, std::int32_t y) override
|
|
{
|
|
buttons = b;
|
|
move_x = x;
|
|
move_y = y;
|
|
return state.tick;
|
|
}
|
|
SessionExportData session_export() const override
|
|
{
|
|
SessionExportData data;
|
|
data.recording = {std::byte{'S'}, std::byte{'Q'}};
|
|
data.tick = 2;
|
|
data.state_hash = 0xABCDEFull;
|
|
data.hashes = {1, 0x0102030405060708ull};
|
|
return data;
|
|
}
|
|
std::optional<SessionImportData> session_import(std::span<const std::byte> recording, bool resume) override
|
|
{
|
|
if (recording.size() != 2) {
|
|
return std::nullopt;
|
|
}
|
|
SessionImportData data;
|
|
data.state = state;
|
|
data.state.running = resume;
|
|
data.replay_ms = 1.5;
|
|
return data;
|
|
}
|
|
std::optional<std::string> render_attach(ConnectionId, PushFn) override
|
|
{
|
|
return std::string(32, 'a');
|
|
}
|
|
void on_client_connected() override {}
|
|
void on_client_disconnected(ConnectionId) override {}
|
|
|
|
// --- Rendu (protocole 0.3) ---
|
|
RenderStartResult render_start(const RenderStartRequest& request) override
|
|
{
|
|
last_render_start_request = request;
|
|
if (render_running) {
|
|
return RenderStartResult{std::nullopt, "render-running", "Un rendu est déjà en cours."};
|
|
}
|
|
if (render_unavailable) {
|
|
return RenderStartResult{std::nullopt, "render-unavailable", "Rendu non disponible."};
|
|
}
|
|
render_running = true;
|
|
RenderStatusView view;
|
|
view.state = RenderRunState::starting;
|
|
view.adapter = request.adapter;
|
|
view.restarts = 0;
|
|
view.world_version = last_world_version;
|
|
return RenderStartResult{view, "", ""};
|
|
}
|
|
RenderStatusView render_stop() override
|
|
{
|
|
render_running = false;
|
|
RenderStatusView view;
|
|
view.state = RenderRunState::stopped;
|
|
view.restarts = last_restarts;
|
|
view.world_version = last_world_version;
|
|
view.last_failure = last_failure.empty() ? std::nullopt : std::optional<std::string>(last_failure);
|
|
return view;
|
|
}
|
|
RenderStatusView render_status() const override
|
|
{
|
|
RenderStatusView view;
|
|
if (!render_running) {
|
|
view.state = RenderRunState::stopped;
|
|
view.restarts = last_restarts;
|
|
view.world_version = last_world_version;
|
|
view.last_failure = last_failure.empty() ? std::nullopt : std::optional<std::string>(last_failure);
|
|
return view;
|
|
}
|
|
view.state = RenderRunState::running;
|
|
view.adapter = last_adapter;
|
|
view.pid = 1234;
|
|
view.restarts = last_restarts;
|
|
view.engine = three_d::EngineCapabilities{"debug", "0.1", three_d::BufferSet{three_d::FrameBuffer::color}, true, false};
|
|
view.width = 1280;
|
|
view.height = 720;
|
|
view.measurements = RenderFrames{180, 0x1D5, 6.1, 6.1, 340, 340, 5900, 5900};
|
|
view.world_version = last_world_version;
|
|
view.last_failure = last_failure.empty() ? std::nullopt : std::optional<std::string>(last_failure);
|
|
return view;
|
|
}
|
|
|
|
// État simulé pour les tests.
|
|
bool render_running = false;
|
|
bool render_unavailable = false;
|
|
std::string last_adapter;
|
|
std::uint32_t last_restarts = 0;
|
|
std::uint64_t last_world_version = 0x1D5;
|
|
std::string last_failure;
|
|
RenderStartRequest last_render_start_request;
|
|
|
|
SessionStateView state{0x2Aull, 60, 10, 0xFFull, true, 3};
|
|
std::optional<ConnectionId> controller;
|
|
bool linked = false;
|
|
bool shutdown = false;
|
|
std::uint32_t buttons = 0;
|
|
std::int32_t move_x = 0;
|
|
std::int32_t move_y = 0;
|
|
};
|
|
|
|
const std::string hello = encode_hello(protocol_version, "sequa-studio-host");
|
|
const std::string render_hello = encode_hello(render_protocol_version, render_client_name);
|
|
|
|
json single(const Reply& reply)
|
|
{
|
|
REQUIRE(reply.messages.size() == 1);
|
|
return json::parse(reply.messages.front());
|
|
}
|
|
|
|
json request(ProtocolSession& session, const std::string& type, json payload = json::object(), std::string id = "r1")
|
|
{
|
|
const json message{{"v", 1}, {"type", type}, {"id", id}, {"payload", std::move(payload)}};
|
|
const Reply reply = session.on_text(message.dump());
|
|
CHECK_FALSE(reply.close);
|
|
return single(reply);
|
|
}
|
|
|
|
ProtocolSession ready_session(FakeServices& services, ConnectionId id = 1)
|
|
{
|
|
ProtocolSession session(services, id);
|
|
const json welcome = single(session.on_text(hello));
|
|
REQUIRE(welcome["type"] == "welcome");
|
|
return session;
|
|
}
|
|
|
|
ProtocolSession render_session(FakeServices& services, ConnectionId id = 1)
|
|
{
|
|
ProtocolSession session(services, id, [](std::string) {}); // envoi spontané ignoré
|
|
const json welcome = single(session.on_text(render_hello));
|
|
REQUIRE(welcome["type"] == "welcome");
|
|
return session;
|
|
}
|
|
|
|
void check_rejected(ProtocolSession& session, const std::string& text, const std::string& expected_code)
|
|
{
|
|
const Reply reply = session.on_text(text);
|
|
CHECK(single(reply)["payload"]["code"] == expected_code);
|
|
CHECK(reply.close);
|
|
CHECK(reply.close_reason.size() <= 123);
|
|
CHECK(session.state() == ProtocolSession::State::closed);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Protocole : compatibilite des versions 0.3")
|
|
{
|
|
CHECK(is_compatible_protocol_version("0.3.0"));
|
|
CHECK(is_compatible_protocol_version("0.3.7-rc.1"));
|
|
CHECK_FALSE(is_compatible_protocol_version("0.1.0"));
|
|
CHECK_FALSE(is_compatible_protocol_version("0.2.0"));
|
|
CHECK_FALSE(is_compatible_protocol_version("1.2.0"));
|
|
CHECK_FALSE(is_compatible_protocol_version("0.3"));
|
|
CHECK_FALSE(is_compatible_protocol_version("0.3.0x"));
|
|
}
|
|
|
|
TEST_CASE("Protocole : welcome porte instanceId, pid et linked")
|
|
{
|
|
FakeServices services;
|
|
ProtocolSession session(services, 1);
|
|
const json welcome = single(session.on_text(encode_hello(protocol_version, "sequa-studio")));
|
|
CHECK(welcome["payload"]["protocolVersion"] == "0.3.0");
|
|
CHECK(welcome["payload"]["coreVersion"] == "1.2.3");
|
|
CHECK(welcome["payload"]["instanceId"] == "0123456789abcdef0123456789abcdef");
|
|
CHECK(welcome["payload"]["pid"] == 4242);
|
|
CHECK(welcome["payload"]["linked"] == false);
|
|
}
|
|
|
|
TEST_CASE("Protocole : poignee de main refusee")
|
|
{
|
|
FakeServices services;
|
|
ProtocolSession session(services, 1);
|
|
SUBCASE("JSON invalide") { check_rejected(session, "{pas du json", "invalid-json"); }
|
|
SUBCASE("autre message avant hello") { check_rejected(session, R"({"v":1,"type":"core/ping","payload":{}})", "handshake-required"); }
|
|
SUBCASE("client inconnu") { check_rejected(session, encode_hello(protocol_version, "autre"), "invalid-payload"); }
|
|
SUBCASE("version 0.1 incompatible") { check_rejected(session, encode_hello("0.1.0", "sequa-studio"), "incompatible-protocol"); }
|
|
SUBCASE("version 0.2 incompatible") { check_rejected(session, encode_hello("0.2.0", "sequa-studio"), "incompatible-protocol"); }
|
|
SUBCASE("id non textuel") { check_rejected(session, R"({"v":1,"type":"hello","id":5,"payload":{}})", "invalid-envelope"); }
|
|
}
|
|
|
|
TEST_CASE("Protocole : id recopie, reponse du meme type")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
const json pong = request(session, "core/ping", json::object(), "abc-17");
|
|
CHECK(pong["type"] == "core/ping");
|
|
CHECK(pong["id"] == "abc-17");
|
|
// Sans id : réponse sans id.
|
|
const json no_id = single(session.on_text(R"({"v":1,"type":"core/ping","payload":{}})"));
|
|
CHECK_FALSE(no_id.contains("id"));
|
|
}
|
|
|
|
TEST_CASE("Protocole : erreurs apres la poignee de main sans fermeture")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
const json unknown = request(session, "scene/get");
|
|
CHECK(unknown["type"] == "error");
|
|
CHECK(unknown["id"] == "r1");
|
|
CHECK(unknown["payload"]["code"] == "unknown-type");
|
|
CHECK(request(session, "session/start", {{"seed", "xyz"}})["payload"]["code"] == "invalid-payload");
|
|
CHECK(request(session, "session/start", {{"seed", "12345678901234567"}})["payload"]["code"] == "invalid-payload");
|
|
const Reply garbage = session.on_text("pas du json");
|
|
CHECK_FALSE(garbage.close);
|
|
CHECK(single(garbage)["payload"]["code"] == "invalid-json");
|
|
CHECK(session.state() == ProtocolSession::State::ready);
|
|
}
|
|
|
|
TEST_CASE("Protocole : core/status et core/shutdown")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
const json status = request(session, "core/status")["payload"];
|
|
CHECK(status["instanceId"] == "0123456789abcdef0123456789abcdef");
|
|
CHECK(status["controllerAttached"] == false);
|
|
CHECK(request(session, "core/shutdown")["type"] == "core/shutdown");
|
|
CHECK(services.shutdown);
|
|
}
|
|
|
|
TEST_CASE("Protocole : controleur, attach force et detach")
|
|
{
|
|
FakeServices services;
|
|
auto first = ready_session(services, 1);
|
|
auto second = ready_session(services, 2);
|
|
CHECK(request(second, "core/detach")["payload"]["code"] == "not-controller");
|
|
CHECK(request(first, "core/attach")["payload"]["linked"] == true);
|
|
CHECK(request(second, "core/attach")["payload"]["code"] == "controller-exists");
|
|
CHECK(request(second, "core/attach", {{"force", "oui"}})["payload"]["code"] == "invalid-payload");
|
|
CHECK(request(second, "core/attach", {{"force", true}})["payload"]["linked"] == true);
|
|
CHECK(services.controller == 2u);
|
|
CHECK(request(first, "core/detach")["payload"]["code"] == "not-controller");
|
|
CHECK(request(second, "core/detach")["payload"]["linked"] == false);
|
|
}
|
|
|
|
TEST_CASE("Protocole : etat de session en hexadecimal")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
const json state = request(session, "session/status")["payload"];
|
|
CHECK(state["seed"] == "000000000000002a");
|
|
CHECK(state["stateHash"] == "00000000000000ff");
|
|
CHECK(state["tickRateHz"] == 60);
|
|
CHECK(state["tick"] == 10);
|
|
CHECK(state["inputCount"] == 3);
|
|
CHECK(request(session, "session/start", {{"seed", "FF"}})["payload"]["seed"] == "00000000000000ff");
|
|
CHECK(request(session, "session/pause")["payload"]["running"] == false);
|
|
CHECK(request(session, "session/resume")["payload"]["running"] == true);
|
|
}
|
|
|
|
TEST_CASE("Protocole : session/input et ses bornes")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
CHECK(request(session, "session/input", {{"buttons", 4294967295u}, {"moveX", -32767}, {"moveY", 32767}})["payload"]["tick"] == 10);
|
|
CHECK(services.buttons == 4294967295u);
|
|
CHECK(services.move_x == -32767);
|
|
CHECK(request(session, "session/input", {{"buttons", 1}, {"moveX", 32768}, {"moveY", 0}})["payload"]["code"] == "invalid-payload");
|
|
CHECK(request(session, "session/input", {{"buttons", -1}, {"moveX", 0}, {"moveY", 0}})["payload"]["code"] == "invalid-payload");
|
|
CHECK(request(session, "session/input", {{"buttons", 4294967296u}, {"moveX", 0}, {"moveY", 0}})["payload"]["code"] == "invalid-payload");
|
|
CHECK(request(session, "session/input", {{"moveX", 0}, {"moveY", 0}})["payload"]["code"] == "invalid-payload");
|
|
}
|
|
|
|
TEST_CASE("Protocole : session/export et session/import en base64")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
const json exported = request(session, "session/export", {{"includeHashes", true}})["payload"];
|
|
CHECK(exported["recording"] == "U1E=");
|
|
CHECK(exported["tick"] == 2);
|
|
CHECK(exported["stateHash"] == "0000000000abcdef");
|
|
CHECK(exported["hashes"] == "AQAAAAAAAAAIBwYFBAMCAQ=="); // 1 puis 0x0102030405060708, petit-boutiste
|
|
CHECK_FALSE(request(session, "session/export")["payload"].contains("hashes"));
|
|
|
|
const json imported = request(session, "session/import", {{"recording", "U1E="}, {"resume", false}})["payload"];
|
|
CHECK(imported["running"] == false);
|
|
CHECK(imported["replayMs"] == 1.5);
|
|
CHECK(request(session, "session/import", {{"recording", "U1E"}, {"resume", true}})["payload"]["code"] == "invalid-recording");
|
|
CHECK(request(session, "session/import", {{"recording", "AAAA"}, {"resume", true}})["payload"]["code"] == "invalid-recording");
|
|
CHECK(request(session, "session/import", {{"recording", "U1E="}})["payload"]["code"] == "invalid-payload");
|
|
}
|
|
|
|
// --- Tests du rendu (protocole 0.3, §9) ----------------------------------------
|
|
|
|
TEST_CASE("Protocole : render/start valide")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/start",
|
|
{{"adapter", "debug"}, {"width", 1280}, {"height", 720}, {"headless", true}, {"fps", 0}},
|
|
"rs1");
|
|
CHECK(result["type"] == "render/start");
|
|
CHECK(result["id"] == "rs1");
|
|
const json& payload = result["payload"];
|
|
CHECK(payload["state"] == "starting");
|
|
CHECK(payload["adapter"] == "debug");
|
|
CHECK(payload["restarts"] == 0);
|
|
CHECK(payload["worldVersion"] == "00000000000001d5");
|
|
CHECK(services.last_render_start_request.adapter == "debug");
|
|
CHECK(services.last_render_start_request.width == 1280);
|
|
CHECK(services.last_render_start_request.height == 720);
|
|
CHECK(services.last_render_start_request.headless == true);
|
|
CHECK(services.last_render_start_request.fps == 0);
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/start payloads invalides")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
|
|
// adapter absent
|
|
CHECK(request(session, "render/start", {{"width", 1280}}, "id1")["payload"]["code"] == "invalid-payload");
|
|
// adapter inconnu
|
|
CHECK(request(session, "render/start", {{"adapter", "inconnu"}}, "id2")["payload"]["code"] == "invalid-payload");
|
|
// width hors bornes (< 64)
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}, {"width", 32}}, "id3")["payload"]["code"] == "invalid-payload");
|
|
// width hors bornes (> 7680)
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}, {"width", 8000}}, "id4")["payload"]["code"] == "invalid-payload");
|
|
// height hors bornes
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}, {"height", 32}}, "id5")["payload"]["code"] == "invalid-payload");
|
|
// headless non booléen
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}, {"headless", "oui"}}, "id6")["payload"]["code"] == "invalid-payload");
|
|
// fps non entier
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}, {"fps", 3.14}}, "id7")["payload"]["code"] == "invalid-payload");
|
|
// fps hors bornes
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}, {"fps", 250}}, "id8")["payload"]["code"] == "invalid-payload");
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/start erreur render-running")
|
|
{
|
|
FakeServices services;
|
|
services.render_running = true;
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/start", {{"adapter", "debug"}}, "rr1");
|
|
CHECK(result["type"] == "error");
|
|
CHECK(result["id"] == "rr1");
|
|
CHECK(result["payload"]["code"] == "render-running");
|
|
CHECK(result["payload"]["message"] == "Un rendu est déjà en cours.");
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/start erreur render-unavailable")
|
|
{
|
|
FakeServices services;
|
|
services.render_unavailable = true;
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/start", {{"adapter", "debug"}}, "ru1");
|
|
CHECK(result["type"] == "error");
|
|
CHECK(result["id"] == "ru1");
|
|
CHECK(result["payload"]["code"] == "render-unavailable");
|
|
CHECK(result["payload"]["message"] == "Rendu non disponible.");
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/stop")
|
|
{
|
|
FakeServices services;
|
|
services.render_running = true;
|
|
services.last_restarts = 2;
|
|
services.last_world_version = 0xABCD;
|
|
services.last_failure = "adapter-start : timeout";
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/stop", json::object(), "stop1");
|
|
CHECK(result["type"] == "render/stop");
|
|
CHECK(result["id"] == "stop1");
|
|
const json& payload = result["payload"];
|
|
CHECK(payload["state"] == "stopped");
|
|
CHECK(payload["restarts"] == 2);
|
|
CHECK(payload["worldVersion"] == "000000000000abcd");
|
|
CHECK(payload["lastFailure"] == "adapter-start : timeout");
|
|
// adapter, pid, engine, width, height, measurements absents
|
|
CHECK_FALSE(payload.contains("adapter"));
|
|
CHECK_FALSE(payload.contains("pid"));
|
|
CHECK_FALSE(payload.contains("engine"));
|
|
CHECK_FALSE(payload.contains("width"));
|
|
CHECK_FALSE(payload.contains("height"));
|
|
CHECK_FALSE(payload.contains("measurements"));
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/status stopped par defaut")
|
|
{
|
|
FakeServices services;
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/status", json::object(), "st1");
|
|
CHECK(result["type"] == "render/status");
|
|
CHECK(result["id"] == "st1");
|
|
const json& payload = result["payload"];
|
|
CHECK(payload["state"] == "stopped");
|
|
CHECK(payload["restarts"] == 0);
|
|
CHECK(payload["worldVersion"] == "00000000000001d5");
|
|
CHECK_FALSE(payload.contains("adapter"));
|
|
CHECK_FALSE(payload.contains("pid"));
|
|
CHECK_FALSE(payload.contains("engine"));
|
|
CHECK_FALSE(payload.contains("width"));
|
|
CHECK_FALSE(payload.contains("height"));
|
|
CHECK_FALSE(payload.contains("measurements"));
|
|
CHECK_FALSE(payload.contains("lastFailure"));
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/status running avec tous les champs")
|
|
{
|
|
FakeServices services;
|
|
services.render_running = true;
|
|
services.last_adapter = "debug";
|
|
services.last_restarts = 1;
|
|
services.last_world_version = 0x1D5;
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/status", json::object(), "st2");
|
|
CHECK(result["type"] == "render/status");
|
|
CHECK(result["id"] == "st2");
|
|
const json& payload = result["payload"];
|
|
CHECK(payload["state"] == "running");
|
|
CHECK(payload["adapter"] == "debug");
|
|
CHECK(payload["pid"] == 1234);
|
|
CHECK(payload["restarts"] == 1);
|
|
CHECK(payload["engine"]["name"] == "debug");
|
|
CHECK(payload["engine"]["version"] == "0.1");
|
|
CHECK(payload["engine"]["buffers"] == json::array({"color"}));
|
|
CHECK(payload["engine"]["windowed"] == true);
|
|
CHECK(payload["engine"]["headless"] == false);
|
|
CHECK(payload["width"] == 1280);
|
|
CHECK(payload["height"] == 720);
|
|
CHECK(payload["measurements"]["framesPerSecond"] == 180);
|
|
CHECK(payload["measurements"]["cpuMsP95"] == 6.1);
|
|
CHECK(payload["measurements"]["latencyUsP95"] == 340);
|
|
CHECK(payload["measurements"]["applyUsP95"] == 5900);
|
|
CHECK(payload["worldVersion"] == "00000000000001d5");
|
|
CHECK_FALSE(payload.contains("lastFailure"));
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/status running avec lastFailure")
|
|
{
|
|
FakeServices services;
|
|
services.render_running = true;
|
|
services.last_failure = "adapter-start : bibliothèque introuvable";
|
|
auto session = ready_session(services);
|
|
|
|
const json result = request(session, "render/status", json::object(), "st3");
|
|
const json& payload = result["payload"];
|
|
CHECK(payload["lastFailure"] == "adapter-start : bibliothèque introuvable");
|
|
}
|
|
|
|
TEST_CASE("Protocole : render/* sur le canal sequa-render -> unknown-type")
|
|
{
|
|
FakeServices services;
|
|
auto session = render_session(services);
|
|
|
|
CHECK(request(session, "render/start", {{"adapter", "debug"}})["payload"]["code"] == "unknown-type");
|
|
CHECK(request(session, "render/stop")["payload"]["code"] == "unknown-type");
|
|
CHECK(request(session, "render/status")["payload"]["code"] == "unknown-type");
|
|
// Les messages du rendu (render/ready, render/frames, etc.) fonctionnent toujours.
|
|
// Mais core/ping devrait aussi être unknown-type sur le canal de rendu.
|
|
CHECK(request(session, "core/ping")["payload"]["code"] == "unknown-type");
|
|
} |