241 lines
9.6 KiB
C++
241 lines
9.6 KiB
C++
// Smoke test de la FSM de sala (RoomFsm) extreta de GameScene. Cobreix:
|
|
// T1 loadConfig — fitxer real → tots els camps llegits.
|
|
// T2 loadConfig — fitxer absent → fallbacks compilats; corbes default
|
|
// resoltes per UiConfig::curveByName.
|
|
// T3 loadConfig — claus parcials → camps absents conserven fallback.
|
|
// T4 advance — ROOM_INTRO consumeix les 5 sub-fases en ordre.
|
|
// T5 advance — ROOM_INTRO desemboca a PLAYING quan acaba l'última fase.
|
|
// T6 advance — PLAYING és sumidor: no avança res ni canvia d'estat.
|
|
// T7 advance — ROOM_OUTRO consumeix les 4 sub-fases i recau a ROOM_INTRO.
|
|
// T8 advance — un delta gran salta més d'una sub-fase en un sol pas.
|
|
// T9 formatRoomTitle — substitueix `{n}` quan el Locale té la clau.
|
|
// T10 formatRoomTitle — sense Locale, retorna la pròpia clau (fallback).
|
|
// T11 stateName — strings esperats per a cada State.
|
|
//
|
|
// El render no es testeja: la FSM és pura i no pinta res. Les regles de
|
|
// gating d'input dels jugadors viuen a `GameScene::update` i estan
|
|
// cobertes pel smoke run del binari (xvfb-run) — la part testable és
|
|
// que `advance` retorna l'estat correcte, que és el senyal que activa
|
|
// el gating al callsite.
|
|
//
|
|
// Execució:
|
|
// make build && cd build && ctest --output-on-failure
|
|
// O bé:
|
|
// ./build/test_game_fsm
|
|
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
#include "core/locale/locale.hpp"
|
|
#include "core/ui/ui_config.hpp"
|
|
#include "game/scenes/room_fsm.hpp"
|
|
|
|
namespace {
|
|
|
|
int g_failures = 0;
|
|
|
|
void check(bool cond, const char* msg, const char* file, int line) {
|
|
if (!cond) {
|
|
std::cerr << " FAIL: " << msg << " (" << file << ":" << line << ")\n";
|
|
++g_failures;
|
|
}
|
|
}
|
|
|
|
#define CHECK(cond) check((cond), #cond, __FILE__, __LINE__)
|
|
|
|
auto writeTemp(const std::string& path, const std::string& content) -> std::string {
|
|
std::ofstream file(path);
|
|
file << content;
|
|
return path;
|
|
}
|
|
|
|
void testLoadConfigFull() {
|
|
std::cout << "T1 loadConfig — fitxer real complet\n";
|
|
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_full.yaml").string();
|
|
writeTemp(PATH,
|
|
"room_intro:\n"
|
|
" fade_in_ms: 100\n"
|
|
" text_delay_ms: 200\n"
|
|
" text_enter_ms: 300\n"
|
|
" text_hold_ms: 400\n"
|
|
" text_exit_ms: 500\n"
|
|
"room_outro:\n"
|
|
" text_enter_ms: 600\n"
|
|
" text_hold_ms: 700\n"
|
|
" text_exit_ms: 800\n"
|
|
" fade_out_ms: 900\n"
|
|
"text:\n"
|
|
" enter_curve: outQuad\n"
|
|
" exit_curve: inQuad\n");
|
|
const auto CFG = RoomFsm::loadConfig(PATH);
|
|
CHECK(CFG.intro_fade_in_ms == 100);
|
|
CHECK(CFG.intro_text_delay_ms == 200);
|
|
CHECK(CFG.intro_text_enter_ms == 300);
|
|
CHECK(CFG.intro_text_hold_ms == 400);
|
|
CHECK(CFG.intro_text_exit_ms == 500);
|
|
CHECK(CFG.outro_text_enter_ms == 600);
|
|
CHECK(CFG.outro_text_hold_ms == 700);
|
|
CHECK(CFG.outro_text_exit_ms == 800);
|
|
CHECK(CFG.outro_fade_out_ms == 900);
|
|
CHECK(CFG.text_enter_curve == UiConfig::curveByName("outQuad"));
|
|
CHECK(CFG.text_exit_curve == UiConfig::curveByName("inQuad"));
|
|
std::filesystem::remove(PATH);
|
|
}
|
|
|
|
void testLoadConfigMissing() {
|
|
std::cout << "T2 loadConfig — fitxer absent (fallbacks)\n";
|
|
const auto CFG = RoomFsm::loadConfig("/tmp/aee_2026_does_not_exist.yaml");
|
|
CHECK(CFG.intro_fade_in_ms == 500);
|
|
CHECK(CFG.intro_text_delay_ms == 500);
|
|
CHECK(CFG.intro_text_enter_ms == 500);
|
|
CHECK(CFG.intro_text_hold_ms == 1000);
|
|
CHECK(CFG.intro_text_exit_ms == 500);
|
|
CHECK(CFG.outro_text_enter_ms == 500);
|
|
CHECK(CFG.outro_text_hold_ms == 1000);
|
|
CHECK(CFG.outro_text_exit_ms == 500);
|
|
CHECK(CFG.outro_fade_out_ms == 500);
|
|
CHECK(CFG.text_enter_curve != nullptr);
|
|
CHECK(CFG.text_exit_curve != nullptr);
|
|
}
|
|
|
|
void testLoadConfigPartial() {
|
|
std::cout << "T3 loadConfig — claus parcials (mantenir fallback)\n";
|
|
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_partial.yaml").string();
|
|
writeTemp(PATH,
|
|
"room_intro:\n"
|
|
" fade_in_ms: 42\n");
|
|
const auto CFG = RoomFsm::loadConfig(PATH);
|
|
CHECK(CFG.intro_fade_in_ms == 42);
|
|
CHECK(CFG.intro_text_delay_ms == 500); // fallback
|
|
CHECK(CFG.outro_fade_out_ms == 500); // fallback
|
|
std::filesystem::remove(PATH);
|
|
}
|
|
|
|
auto makeShortConfig() -> RoomFsm::Config {
|
|
// Durades curtes per fer els tests d'`advance` deterministes i
|
|
// independents dels valors per defecte.
|
|
RoomFsm::Config cfg{};
|
|
cfg.intro_fade_in_ms = 10;
|
|
cfg.intro_text_delay_ms = 20;
|
|
cfg.intro_text_enter_ms = 30;
|
|
cfg.intro_text_hold_ms = 40;
|
|
cfg.intro_text_exit_ms = 50;
|
|
cfg.outro_text_enter_ms = 11;
|
|
cfg.outro_text_hold_ms = 22;
|
|
cfg.outro_text_exit_ms = 33;
|
|
cfg.outro_fade_out_ms = 44;
|
|
return cfg;
|
|
}
|
|
|
|
void testAdvanceIntroPhases() {
|
|
std::cout << "T4 advance — ROOM_INTRO consumeix sub-fases en ordre\n";
|
|
const auto CFG = makeShortConfig();
|
|
RoomFsm::Transition t{};
|
|
auto s = RoomFsm::State::ROOM_INTRO;
|
|
|
|
s = RoomFsm::advance(s, t, CFG, 5); // dins fase 0
|
|
CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 0 && t.elapsed_ms == 5);
|
|
|
|
s = RoomFsm::advance(s, t, CFG, 10); // surt de fase 0, entra a fase 1 (5 ms dins)
|
|
CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 1 && t.elapsed_ms == 5);
|
|
|
|
s = RoomFsm::advance(s, t, CFG, 20); // surt de fase 1 (15 dins), entra a fase 2 amb 5 ms (5+20-20=5)
|
|
CHECK(s == RoomFsm::State::ROOM_INTRO && t.phase == 2 && t.elapsed_ms == 5);
|
|
}
|
|
|
|
void testAdvanceIntroToPlaying() {
|
|
std::cout << "T5 advance — ROOM_INTRO acaba a PLAYING\n";
|
|
const auto CFG = makeShortConfig();
|
|
RoomFsm::Transition t{};
|
|
const int TOTAL = CFG.intro_fade_in_ms + CFG.intro_text_delay_ms +
|
|
CFG.intro_text_enter_ms + CFG.intro_text_hold_ms +
|
|
CFG.intro_text_exit_ms;
|
|
const auto S = RoomFsm::advance(RoomFsm::State::ROOM_INTRO, t, CFG, TOTAL);
|
|
CHECK(S == RoomFsm::State::PLAYING);
|
|
}
|
|
|
|
void testAdvancePlayingNoOp() {
|
|
std::cout << "T6 advance — PLAYING és sumidor\n";
|
|
const auto CFG = makeShortConfig();
|
|
RoomFsm::Transition t{42, 999};
|
|
const auto S = RoomFsm::advance(RoomFsm::State::PLAYING, t, CFG, 1000);
|
|
CHECK(S == RoomFsm::State::PLAYING);
|
|
CHECK(t.phase == 42); // intacte
|
|
CHECK(t.elapsed_ms == 999);
|
|
}
|
|
|
|
void testAdvanceOutroToIntro() {
|
|
std::cout << "T7 advance — ROOM_OUTRO recau a ROOM_INTRO\n";
|
|
const auto CFG = makeShortConfig();
|
|
RoomFsm::Transition t{};
|
|
const int TOTAL = CFG.outro_text_enter_ms + CFG.outro_text_hold_ms +
|
|
CFG.outro_text_exit_ms + CFG.outro_fade_out_ms;
|
|
const auto S = RoomFsm::advance(RoomFsm::State::ROOM_OUTRO, t, CFG, TOTAL);
|
|
CHECK(S == RoomFsm::State::ROOM_INTRO);
|
|
}
|
|
|
|
void testAdvanceLargeDeltaJumps() {
|
|
std::cout << "T8 advance — un delta gran salta múltiples sub-fases\n";
|
|
const auto CFG = makeShortConfig();
|
|
RoomFsm::Transition t{};
|
|
// 10+20+5 = 35 ms → consumeix fase 0 i 1, queda a fase 2 amb 5 ms.
|
|
const auto S = RoomFsm::advance(RoomFsm::State::ROOM_INTRO, t, CFG, 35);
|
|
CHECK(S == RoomFsm::State::ROOM_INTRO);
|
|
CHECK(t.phase == 2);
|
|
CHECK(t.elapsed_ms == 5);
|
|
}
|
|
|
|
void testFormatRoomTitleWithLocale() {
|
|
std::cout << "T9 formatRoomTitle — substitució amb Locale\n";
|
|
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale.yaml").string();
|
|
writeTemp(PATH, "game:\n room_intro: \"Sala {n}\"\n");
|
|
Locale::get().load(PATH);
|
|
CHECK(RoomFsm::formatRoomTitle(1) == "Sala 1");
|
|
CHECK(RoomFsm::formatRoomTitle(7) == "Sala 7");
|
|
std::filesystem::remove(PATH);
|
|
}
|
|
|
|
void testFormatRoomTitleNoLocale() {
|
|
std::cout << "T10 formatRoomTitle — fallback sense traducció\n";
|
|
const auto PATH = (std::filesystem::temp_directory_path() / "aee_test_locale_empty.yaml").string();
|
|
writeTemp(PATH, "other: foo\n");
|
|
Locale::get().load(PATH);
|
|
// Sense la clau "game.room_intro", Locale retorna la pròpia clau,
|
|
// i com que no porta `{n}`, la substitució no canvia res.
|
|
CHECK(RoomFsm::formatRoomTitle(3) == "game.room_intro");
|
|
std::filesystem::remove(PATH);
|
|
}
|
|
|
|
void testStateName() {
|
|
std::cout << "T11 stateName — strings esperats\n";
|
|
CHECK(std::string(RoomFsm::stateName(RoomFsm::State::ROOM_INTRO)) == "ROOM_INTRO");
|
|
CHECK(std::string(RoomFsm::stateName(RoomFsm::State::PLAYING)) == "PLAYING");
|
|
CHECK(std::string(RoomFsm::stateName(RoomFsm::State::ROOM_OUTRO)) == "ROOM_OUTRO");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
auto main() -> int {
|
|
testLoadConfigFull();
|
|
testLoadConfigMissing();
|
|
testLoadConfigPartial();
|
|
testAdvanceIntroPhases();
|
|
testAdvanceIntroToPlaying();
|
|
testAdvancePlayingNoOp();
|
|
testAdvanceOutroToIntro();
|
|
testAdvanceLargeDeltaJumps();
|
|
testFormatRoomTitleWithLocale();
|
|
testFormatRoomTitleNoLocale();
|
|
testStateName();
|
|
|
|
if (g_failures == 0) {
|
|
std::cout << "OK: tots els tests de RoomFsm passen\n";
|
|
return 0;
|
|
}
|
|
std::cerr << g_failures << " tests han fallat\n";
|
|
return 1;
|
|
}
|