feat: implement WebSocket synchronization and HTTP recovery
This commit is contained in:
+6
-2
@@ -1,7 +1,7 @@
|
||||
CC ?= cc
|
||||
CFLAGS ?= -std=c11 -Wall -Wextra -Werror -I../../include
|
||||
|
||||
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api
|
||||
all: test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service
|
||||
|
||||
test_command_queue: test_command_queue.c ../../src/command_queue.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
@@ -13,6 +13,7 @@ run: all
|
||||
./test_game_lifecycle
|
||||
./test_state_presenter
|
||||
./test_http_api
|
||||
./test_sync_service
|
||||
|
||||
test_game_domain: test_game_domain.c ../../src/fleet_generator.c ../../src/game_engine.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
@@ -29,5 +30,8 @@ test_state_presenter: test_state_presenter.c ../../src/state_presenter.c
|
||||
test_http_api: test_http_api.c ../../src/http_api.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
test_sync_service: test_sync_service.c ../../src/sync_service.c ../../src/application.c ../../src/command_queue.c ../../src/session_manager.c ../../src/game_lifecycle.c ../../src/fleet_generator.c ../../src/game_engine.c ../../src/state_presenter.c
|
||||
$(CC) $(CFLAGS) $^ -o $@
|
||||
|
||||
clean:
|
||||
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api
|
||||
rm -f test_command_queue test_game_domain test_bot_player test_game_lifecycle test_state_presenter test_http_api test_sync_service
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "sync_service.h"
|
||||
|
||||
typedef struct { uint32_t value; } test_random_t;
|
||||
typedef struct { int fail_client; uint8_t sends[kSessionCapacity]; char payloads[kSessionCapacity][kStateMessageCapacity]; } capture_t;
|
||||
|
||||
static uint32_t next_random(void *context) {
|
||||
test_random_t *random = context;
|
||||
random->value = random->value * 1664525U + 1013904223U;
|
||||
return random->value;
|
||||
}
|
||||
|
||||
static void token_text(const uint8_t token[kSessionTokenBytes], char output[33]) {
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (uint8_t index = 0; index < kSessionTokenBytes; ++index) {
|
||||
output[index * 2U] = hex[token[index] >> 4U];
|
||||
output[index * 2U + 1U] = hex[token[index] & 15U];
|
||||
}
|
||||
output[32] = '\0';
|
||||
}
|
||||
|
||||
static bool capture_send(void *context, int client_id, const char *payload, size_t length) {
|
||||
capture_t *capture = context;
|
||||
if (client_id == capture->fail_client) return false;
|
||||
assert(client_id >= 0 && client_id < kSessionCapacity);
|
||||
assert(length < kStateMessageCapacity);
|
||||
++capture->sends[client_id];
|
||||
memcpy(capture->payloads[client_id], payload, length);
|
||||
capture->payloads[client_id][length] = '\0';
|
||||
return true;
|
||||
}
|
||||
|
||||
static const char *board_start(const char *json, uint8_t board) {
|
||||
const char *start = strstr(json, "\"boards\":[\"");
|
||||
assert(start != NULL);
|
||||
start += strlen("\"boards\":[\"");
|
||||
return board == 0U ? start : start + kBoardCellCount + 3U;
|
||||
}
|
||||
|
||||
static void hello(sync_service_t *service, int client_id, const uint8_t token[kSessionTokenBytes], uint32_t version) {
|
||||
char token_value[33];
|
||||
char frame[96];
|
||||
char output[kStateMessageCapacity];
|
||||
size_t output_length = 0U;
|
||||
bool changed = false;
|
||||
bool close = false;
|
||||
token_text(token, token_value);
|
||||
snprintf(frame, sizeof(frame), "{\"type\":\"hello\",\"token\":\"%s\",\"version\":%u}", token_value, version);
|
||||
assert(sync_service_receive(service, client_id, frame, strlen(frame), 10U, output, &output_length, &changed, &close));
|
||||
assert(!changed && !close && output_length > 0U && strstr(output, "\"type\":\"state\"") != NULL);
|
||||
}
|
||||
|
||||
static void test_authentication_visibility_and_backpressure(void) {
|
||||
test_random_t random = {.value = 1U};
|
||||
application_t application;
|
||||
application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random});
|
||||
uint8_t player_1 = 0;
|
||||
uint8_t player_2 = 0;
|
||||
uint8_t spectator = 0;
|
||||
assert(application_join(&application, ROLE_PLAYER_1, "Alice", &player_1) == LIFECYCLE_RESULT_OK);
|
||||
assert(application_join(&application, ROLE_PLAYER_2, "Bob", &player_2) == LIFECYCLE_RESULT_OK);
|
||||
assert(application_join(&application, ROLE_SPECTATOR, "Watch", &spectator) == LIFECYCLE_RESULT_OK);
|
||||
sync_service_t service;
|
||||
sync_service_init(&service, &application);
|
||||
assert(sync_service_open(&service, 0, 0U));
|
||||
assert(sync_service_open(&service, 1, 0U));
|
||||
assert(sync_service_open(&service, 2, 0U));
|
||||
for (int client_id = 3; client_id < kSessionCapacity; ++client_id) assert(sync_service_open(&service, client_id, 0U));
|
||||
assert(!sync_service_open(&service, kSessionCapacity, 0U));
|
||||
hello(&service, 0, application.lifecycle.sessions.entries[player_1].token, 0U);
|
||||
hello(&service, 1, application.lifecycle.sessions.entries[player_2].token, 999U);
|
||||
hello(&service, 2, application.lifecycle.sessions.entries[spectator].token, 0U);
|
||||
|
||||
application.lifecycle.game.state.phase = PHASE_IN_PROGRESS;
|
||||
application.lifecycle.game.state.boards[0].cells[0] = CELL_SHIP;
|
||||
application.lifecycle.game.state.boards[1].cells[0] = CELL_SHIP;
|
||||
application.lifecycle.game.state.boards[0].cells[1] = CELL_HIT;
|
||||
application.lifecycle.game.state.boards[1].cells[1] = CELL_MISS;
|
||||
capture_t capture = {.fail_client = -1};
|
||||
sync_service_broadcast(&service, capture_send, &capture);
|
||||
assert(board_start(capture.payloads[0], 0)[0] == '1' && board_start(capture.payloads[0], 1)[0] == '0');
|
||||
assert(board_start(capture.payloads[1], 0)[0] == '0' && board_start(capture.payloads[1], 1)[0] == '1');
|
||||
assert(board_start(capture.payloads[2], 0)[0] == '0' && board_start(capture.payloads[2], 1)[0] == '0');
|
||||
assert(board_start(capture.payloads[2], 0)[1] == '3' && board_start(capture.payloads[2], 1)[1] == '2');
|
||||
|
||||
capture.fail_client = 2;
|
||||
const uint8_t previous_sends = capture.sends[2];
|
||||
sync_service_broadcast(&service, capture_send, &capture);
|
||||
sync_service_broadcast(&service, capture_send, &capture);
|
||||
assert(capture.sends[2] == previous_sends);
|
||||
}
|
||||
|
||||
static void test_timeout_ping_and_command(void) {
|
||||
test_random_t random = {.value = 9U};
|
||||
application_t application;
|
||||
application_init(&application, (random_source_t){.next_u32 = next_random, .context = &random});
|
||||
uint8_t player_1 = 0;
|
||||
assert(application_join(&application, ROLE_PLAYER_1, "Alice", &player_1) == LIFECYCLE_RESULT_OK);
|
||||
sync_service_t service;
|
||||
sync_service_init(&service, &application);
|
||||
assert(sync_service_open(&service, 3, 0U));
|
||||
int expired[kSessionCapacity] = {0};
|
||||
size_t expired_count = 0U;
|
||||
sync_service_expire(&service, kWebSocketHelloTimeoutMs + 1U, expired, &expired_count);
|
||||
assert(expired_count == 1U && expired[0] == 3);
|
||||
|
||||
assert(sync_service_open(&service, 0, 0U));
|
||||
hello(&service, 0, application.lifecycle.sessions.entries[player_1].token, 0U);
|
||||
char output[kStateMessageCapacity];
|
||||
size_t output_length = 0U;
|
||||
bool changed = false;
|
||||
bool close = false;
|
||||
assert(sync_service_receive(&service, 0, "{\"type\":\"ping\"}", 15U, 20U, output, &output_length, &changed, &close));
|
||||
assert(!changed && !close && strcmp(output, "{\"type\":\"pong\"}") == 0);
|
||||
|
||||
char token[33];
|
||||
char frame[128];
|
||||
token_text(application.lifecycle.sessions.entries[player_1].token, token);
|
||||
snprintf(frame, sizeof(frame), "{\"type\":\"config\",\"token\":\"%s\",\"gameId\":1,\"mode\":\"bot\"}", token);
|
||||
assert(sync_service_receive(&service, 0, frame, strlen(frame), 30U, output, &output_length, &changed, &close));
|
||||
assert(changed && !close && output_length == 0U && application.lifecycle.game.state.mode == MODE_BOT);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
test_authentication_visibility_and_backpressure();
|
||||
test_timeout_ping_and_command();
|
||||
puts("sync service tests passed");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user