Files
SEQUA-Engine/core/tests/server/test_runtime.cpp
T
Ange-Marie MAURIN - A2M Systèmes da0d31385b build: arborescence bin/<plateforme>/<type> et build unifié (ADR 0010)
- platform/layout : racine sequa-root.json, chemins des composants, arguments et chemins en UTF-8.
- Règles install() : bin/windows-x64/{debug,release,asan}/ (core, render, studio + ui, engines).
- Le Core trouve sequa-render et les adaptateurs depuis la racine (--sequa-root, passé par
  Studio host à sa copie fantôme) : plus de render-unavailable depuis le panneau Rendu.
- L'adaptateur Godot cherche Godot et son projet à côté de lui (surcharge par --adapter-options) ;
  template_release pour le type release. Godot hébergé dans les presets debug et release.
- scripts/build-all.ps1 : Godot, C++, interface, arrêt des processus du dossier visé, install ;
  check.ps1 l'appelle (modification de l'ADR 0004).
- docs/installation/ : compiler, lancer, installer et désinstaller par plateforme.
2026-11-07 15:50:37 +01:00

198 lines
6.3 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 : racine dans un dossier temporaire vide (pas de sequa-render).
options.sequa_root = std::filesystem::temp_directory_path() / "sequa_test_empty";
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 sequa_root vide -> render-unavailable")
{
CoreRuntimeOptions opts = fast(false);
opts.sequa_root = ""; // racine inconnue
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);
}