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).
198 lines
6.4 KiB
C++
198 lines
6.4 KiB
C++
// SPDX-License-Identifier: MPL-2.0
|
|
#include "sequa/core/server/runtime.hpp"
|
|
|
|
#include <doctest/doctest.h>
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <thread>
|
|
|
|
using namespace sequa::core::server;
|
|
using namespace std::chrono_literals;
|
|
using Reason = CoreRuntime::ExitReason;
|
|
|
|
namespace {
|
|
|
|
CoreRuntimeOptions fast(bool linked)
|
|
{
|
|
CoreRuntimeOptions options;
|
|
options.linked = linked;
|
|
options.real_time = false;
|
|
options.lease_grace = 50ms;
|
|
options.attach_deadline = 100ms;
|
|
options.shutdown_delay = 20ms;
|
|
options.core_version = "test";
|
|
// Options de rendu pour les tests : core_executable dans un dossier temporaire vide.
|
|
options.core_executable = std::filesystem::temp_directory_path() / "sequa_test_empty" / "sequa-core";
|
|
options.render_core_url = "ws://127.0.0.1:9100/?token=test";
|
|
options.render_log = "";
|
|
options.log = [](std::string_view) {};
|
|
return options;
|
|
}
|
|
|
|
void wait_past(std::chrono::milliseconds duration)
|
|
{
|
|
std::this_thread::sleep_for(duration + 30ms);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("Runtime : instanceId aleatoire de 32 hex, pid renseigne")
|
|
{
|
|
const CoreRuntime a(fast(false));
|
|
const CoreRuntime b(fast(false));
|
|
const auto id = a.core_status().instance_id;
|
|
CHECK(id.size() == 32);
|
|
CHECK(id.find_first_not_of("0123456789abcdef") == std::string::npos);
|
|
CHECK(id != b.core_status().instance_id);
|
|
CHECK(a.core_status().pid > 0);
|
|
}
|
|
|
|
TEST_CASE("Runtime : sans --linked, pas de bail et pas d'arret")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
CHECK_FALSE(runtime.core_status().linked);
|
|
wait_past(100ms);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
}
|
|
|
|
TEST_CASE("Runtime : --linked sans controleur -> bail expire")
|
|
{
|
|
CoreRuntime runtime(fast(true));
|
|
CHECK(runtime.core_status().linked);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
wait_past(100ms);
|
|
CHECK(runtime.exit_reason() == Reason::lease_expired);
|
|
}
|
|
|
|
TEST_CASE("Runtime : perte du controleur -> arret apres le delai de grace, sauf rattachement")
|
|
{
|
|
CoreRuntime runtime(fast(true));
|
|
runtime.on_client_connected();
|
|
REQUIRE(runtime.attach(1, false) == CoreServices::AttachResult::attached);
|
|
wait_past(100ms); // le délai de démarrage ne s'applique plus
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
|
|
SUBCASE("personne ne revient")
|
|
{
|
|
runtime.on_client_disconnected(1);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
wait_past(50ms);
|
|
CHECK(runtime.exit_reason() == Reason::lease_expired);
|
|
}
|
|
SUBCASE("un nouveau controleur s'attache pendant la grace (rechargement)")
|
|
{
|
|
runtime.on_client_disconnected(1);
|
|
runtime.on_client_connected();
|
|
REQUIRE(runtime.attach(2, false) == CoreServices::AttachResult::attached);
|
|
wait_past(50ms);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
}
|
|
}
|
|
|
|
TEST_CASE("Runtime : detach puis deconnexion -> pas d'arret ; attach retablit le bail")
|
|
{
|
|
CoreRuntime runtime(fast(true));
|
|
REQUIRE(runtime.attach(1, false) == CoreServices::AttachResult::attached);
|
|
CHECK(runtime.detach(2) == CoreServices::DetachResult::not_controller);
|
|
REQUIRE(runtime.detach(1) == CoreServices::DetachResult::detached);
|
|
CHECK_FALSE(runtime.core_status().linked);
|
|
runtime.on_client_disconnected(1);
|
|
wait_past(100ms);
|
|
CHECK(runtime.exit_reason() == Reason::none);
|
|
|
|
// Réversible : un nouveau Studio se rattache, le bail revient.
|
|
REQUIRE(runtime.attach(3, false) == CoreServices::AttachResult::attached);
|
|
CHECK(runtime.core_status().linked);
|
|
runtime.on_client_disconnected(3);
|
|
wait_past(50ms);
|
|
CHECK(runtime.exit_reason() == Reason::lease_expired);
|
|
}
|
|
|
|
TEST_CASE("Runtime : core/shutdown -> arret apres un court delai")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
runtime.request_shutdown();
|
|
CHECK(runtime.exit_reason() == Reason::none); // la réponse doit pouvoir partir
|
|
wait_past(20ms);
|
|
CHECK(runtime.exit_reason() == Reason::shutdown);
|
|
}
|
|
|
|
TEST_CASE("Runtime : comptage des clients")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
runtime.on_client_connected();
|
|
runtime.on_client_connected();
|
|
runtime.on_client_disconnected(9);
|
|
CHECK(runtime.core_status().clients == 1);
|
|
}
|
|
|
|
// --- Tests du rendu (protocole 0.3) ---
|
|
|
|
TEST_CASE("Runtime : render_start avec core_executable vide -> render-unavailable")
|
|
{
|
|
CoreRuntimeOptions opts = fast(false);
|
|
opts.core_executable = ""; // invalide
|
|
CoreRuntime runtime(opts);
|
|
|
|
RenderStartRequest req;
|
|
req.adapter = "debug";
|
|
const auto result = runtime.render_start(req);
|
|
CHECK(!result.error_code.empty());
|
|
CHECK(result.error_code == "render-unavailable");
|
|
CHECK_FALSE(result.status.has_value());
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_status stopped par defaut")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
const auto view = runtime.render_status();
|
|
CHECK(view.state == RenderRunState::stopped);
|
|
CHECK(!view.adapter.has_value());
|
|
CHECK(!view.pid.has_value());
|
|
CHECK(view.restarts == 0);
|
|
CHECK(!view.engine.has_value());
|
|
CHECK(!view.width.has_value());
|
|
CHECK(!view.height.has_value());
|
|
CHECK(!view.measurements.has_value());
|
|
CHECK(view.world_version == 0);
|
|
CHECK(!view.last_failure.has_value());
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_stop sans rendu -> stopped")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
const auto view = runtime.render_stop();
|
|
CHECK(view.state == RenderRunState::stopped);
|
|
CHECK(!view.adapter.has_value());
|
|
CHECK(!view.pid.has_value());
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_start adapter inconnu -> render-unavailable")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
RenderStartRequest req;
|
|
req.adapter = "inconnu";
|
|
const auto result = runtime.render_start(req);
|
|
CHECK(result.error_code == "render-unavailable");
|
|
CHECK(result.error_message.find("Adaptateur inconnu") != std::string::npos);
|
|
}
|
|
|
|
TEST_CASE("Runtime : render_start puis render_stop -> stopped")
|
|
{
|
|
CoreRuntime runtime(fast(false));
|
|
|
|
// Sans exécutable réel, render_start échoue avec render-unavailable.
|
|
// On teste juste que l'appel ne plante pas et retourne une erreur structurée.
|
|
RenderStartRequest req;
|
|
req.adapter = "debug";
|
|
const auto start_result = runtime.render_start(req);
|
|
CHECK(start_result.error_code == "render-unavailable");
|
|
|
|
// render_stop doit quand même fonctionner et retourner stopped.
|
|
const auto stop_view = runtime.render_stop();
|
|
CHECK(stop_view.state == RenderRunState::stopped);
|
|
} |