// SPDX-License-Identifier: MPL-2.0 #include "sequa/core/server/runtime.hpp" #include #include #include #include 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); }