diff --git a/.gitignore b/.gitignore index 36a149e..15ba62b 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,8 @@ *.idb *.pdb +*.gc* + # Kernel Module Compile Results *.mod* *.cmd diff --git a/src/Makefile b/src/Makefile index 1670361..d337f7d 100644 --- a/src/Makefile +++ b/src/Makefile @@ -1,4 +1,4 @@ -.PHONY: all clean install uninstall test gcov_report dvi dist run style format +.PHONY: all clean install uninstall test test_coverage gcov_report dvi dist run style format CC ?= gcc CFLAGS ?= -Wall -Wextra -Werror -std=c11 -g -D_POSIX_C_SOURCE=199309L -MMD -MP @@ -8,9 +8,9 @@ CHECK_CFLAGS ?= -I/usr/include/check CHECK_LIBS_AVAILABLE := $(shell ldconfig -p 2>/dev/null | grep -q libsubunit && echo yes || echo no) ifeq ($(CHECK_LIBS_AVAILABLE),yes) - LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses -lsubunit + LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses -lsubunit else - LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses + LDFLAGS ?= -lcheck -lrt -lpthread -lm -lncurses endif BUILDDIR = build @@ -25,6 +25,7 @@ TETRIS_SRC = $(shell find $(TETRISDIR) -name "*.c") TETRIS_OBJ = $(TETRIS_SRC:.c=.o) CLI_SRC = $(shell find $(CLIDIR) -name "*.c") CLI_OBJ = $(CLI_SRC:.c=.o) +TEST_SRC = $(filter-out $(TESTDIR)/test.c, $(shell find $(TESTDIR) -name "*.c")) LIB_TETRIS = $(BUILDDIR)/libtetris.a TARGET = $(BUILDDIR)/tetris_bin.out @@ -36,7 +37,7 @@ BINDIR = $(PREFIX)/bin all: $(TARGET) -run: $(TARGET) +run: clean $(TARGET) ./$(TARGET) install: $(TARGET) @@ -51,24 +52,38 @@ uninstall: clean: rm -rf $(CLI_OBJ) $(TETRIS_OBJ) $(TARGET) $(LIB_TETRIS) $(TEST_TARGET) rm -rf $(TETRISDIR)/*.d $(CLIDIR)/*.d + rm -rf $(TETRISDIR)/*.gcda $(TETRISDIR)/*.gcno $(TETRISDIR)/*.gcov rm -rf $(GCOV_DIR) $(DVI_DIR) -test: $(LIB_TETRIS) - $(CC) $(CFLAGS) $(TESTDIR)/test.c -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(TEST_TARGET) +test: clean $(LIB_TETRIS) + $(CC) $(CFLAGS) $(TEST_SRC) -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(TEST_TARGET) ./$(TEST_TARGET) -gcov_report: CFLAGS += --coverage -gcov_report: LDFLAGS += --coverage -gcov_report: clean test - @mkdir -p $(GCOV_DIR) - gcov $(TETRIS_SRC) -o $(TETRISDIR) - lcov --capture --directory $(TETRISDIR) --output-file $(GCOV_DIR)/coverage.info --ignore-errors unused - lcov --extract $(GCOV_DIR)/coverage.info '*/brick_game/tetris/*' -o $(GCOV_DIR)/coverage.info --ignore-errors unused - genhtml $(GCOV_DIR)/coverage.info --output-directory $(GCOV_DIR) - @mv *.gcov $(GCOV_DIR)/ 2>/dev/null || true +test_coverage: CFLAGS += --coverage +test_coverage: LDFLAGS += --coverage +test_coverage: clean $(LIB_TETRIS) + $(CC) $(CFLAGS) $(TEST_SRC) -L$(BUILDDIR) -ltetris $(LDFLAGS) -o $(TEST_TARGET) + ./$(TEST_TARGET) + +gcov_report: test_coverage + @mkdir -p $(GCOV_DIR)/obj + @find $(TETRISDIR) -name "*.gcda" -exec mv {} $(GCOV_DIR)/obj/ \; + @find $(TETRISDIR) -name "*.gcno" -exec mv {} $(GCOV_DIR)/obj/ \; + @cd $(GCOV_DIR) && for src in $(addprefix ../,$(TETRIS_SRC)); do \ + gcov $$src -o obj/ 2>/dev/null; \ + done + lcov --capture --directory $(GCOV_DIR)/obj \ + --output-file $(GCOV_DIR)/coverage.info \ + --ignore-errors unused + lcov --extract $(GCOV_DIR)/coverage.info '*/brick_game/tetris/*' \ + -o $(GCOV_DIR)/coverage.info \ + --ignore-errors unused + genhtml $(GCOV_DIR)/coverage.info \ + --output-directory $(GCOV_DIR) @echo "Report: $(GCOV_DIR)/index.html" xdg-open $(GCOV_DIR)/index.html + dvi: @mkdir -p $(DVI_DIR) @echo "Generating documentation with Doxygen..." @@ -79,8 +94,7 @@ dvi: echo "Copying doc.md as fallback..."; \ cp doc.md $(DVI_DIR)/; \ fi - xdg-open dvi/html/index.html - + xdg-open $(DVI_DIR)/html/index.html dist: clean tar -czf tetris.tar.gz Makefile $(TETRISDIR) $(CLIDIR) $(TESTDIR) README.md doc.md diff --git a/src/test/test_collision.c b/src/test/test_collision.c index 4a32c05..c25df26 100644 --- a/src/test/test_collision.c +++ b/src/test/test_collision.c @@ -1,4 +1,3 @@ -#include #include "test_helper.h" START_TEST(test_collision_bottom) { diff --git a/src/test/test_figures.c b/src/test/test_figures.c index f51186f..6408d64 100644 --- a/src/test/test_figures.c +++ b/src/test/test_figures.c @@ -1,4 +1,3 @@ -#include #include "test_helper.h" START_TEST(test_generate_figure) { diff --git a/src/test/test_fsm.c b/src/test/test_fsm.c new file mode 100644 index 0000000..a13bb71 --- /dev/null +++ b/src/test/test_fsm.c @@ -0,0 +1,409 @@ +#include +#include "test_helper.h" +#include + +// ============================================================================ +// Тесты FSM и интеграции +// ============================================================================ + +START_TEST(test_game_start) { + userInput(Start, false); + GameInfo_t state = updateCurrentState(); + + // После Start игра должна инициализироваться + ck_assert_int_eq(state.level, 1); + ck_assert_int_eq(state.score, 0); + ck_assert_int_eq(state.pause, 0); +} +END_TEST + +START_TEST(test_pause_toggle) { + userInput(Start, false); + updateCurrentState(); + + // Включаем паузу + userInput(Pause, false); + GameInfo_t state = updateCurrentState(); + ck_assert_int_eq(state.pause, 1); + + // Выключаем паузу + userInput(Pause, false); + state = updateCurrentState(); + ck_assert_int_eq(state.pause, 0); +} +END_TEST + +START_TEST(test_field_initialization) { + userInput(Start, false); + GameInfo_t state = updateCurrentState(); + + // Поле должно быть чистым в начале + int non_zero_count = 0; + for (int i = 0; i < FIELD_HEIGHT; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + if (state.field[i][j] != 0) { + non_zero_count++; + } + } + } + + // Только активная фигура (максимум 4 блока) + ck_assert_int_le(non_zero_count, 4); +} +END_TEST + +START_TEST(test_next_figure_exists) { + userInput(Start, false); + GameInfo_t state = updateCurrentState(); + + // Должна быть следующая фигура + int has_blocks = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (state.next[i][j]) { + has_blocks = 1; + } + } + } + ck_assert_int_eq(has_blocks, 1); +} +END_TEST + +START_TEST(test_movement_left) { + userInput(Start, false); + updateCurrentState(); + + // Двигаем влево (движение происходит мгновенно через Moving) + userInput(Left, false); + GameInfo_t state = updateCurrentState(); + + // Проверяем что состояние изменилось (игра не в паузе) + ck_assert_int_eq(state.pause, 0); +} +END_TEST + +START_TEST(test_movement_right) { + userInput(Start, false); + updateCurrentState(); + + // Двигаем вправо + userInput(Right, false); + GameInfo_t state = updateCurrentState(); + + // Проверяем что состояние валидное + ck_assert_int_eq(state.pause, 0); + ck_assert_int_ge(state.level, 1); +} +END_TEST + +START_TEST(test_rotation) { + userInput(Start, false); + updateCurrentState(); + + // Вращаем фигуру + userInput(Action, false); + GameInfo_t state = updateCurrentState(); + + // Фигура должна остаться на поле + int has_figure = 0; + for (int i = 0; i < FIELD_HEIGHT; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + if (state.field[i][j] == 1) { + has_figure = 1; + } + } + } + ck_assert_int_eq(has_figure, 1); +} +END_TEST + +START_TEST(test_user_input_actions) { + // Просто проверяем что userInput не крашит + userInput(Start, false); + userInput(Left, false); + userInput(Right, false); + userInput(Action, false); + userInput(Pause, false); + userInput(Terminate, false); + + // Если дошли сюда - всё ок + ck_assert_int_eq(1, 1); +} +END_TEST + +START_TEST(test_multiple_updates) { + userInput(Start, false); + + // Многократный вызов updateCurrentState не должен крашить + for (int i = 0; i < 10; i++) { + updateCurrentState(); + } + + GameInfo_t state = updateCurrentState(); + ck_assert_int_eq(state.level, 1); +} +END_TEST + +// ============================================================================ +// Тесты для повышения покрытия +// ============================================================================ + +START_TEST(test_instant_drop_down_key) { + userInput(Start, false); + updateCurrentState(); + + // Устанавливаем флаг что клавиша была отжата + GameState_t* state = get_game_state(); + state->down_key_was_released = 1; + + // Нажимаем Down (instant drop) + userInput(Down, false); + + // Проверяем что флаг сброшен + ck_assert_int_eq(state->down_key_was_released, 0); +} +END_TEST + +START_TEST(test_up_key_release) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + state->down_key_was_released = 0; + + // Нажимаем Up (release) + userInput(Up, false); + updateCurrentState(); + + // Флаг должен установиться + ck_assert_int_eq(state->down_key_was_released, 1); +} +END_TEST + +START_TEST(test_terminate_with_high_score) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + state->info->high_score = 100; + state->info->score = 500; // Больше рекорда + + // Terminate должен сохранить рекорд + userInput(Terminate, false); + + // Проверяем что файл сохранён (загружаем обратно) + int loaded = load_high_score(); + ck_assert_int_ge(loaded, 500); +} +END_TEST + +START_TEST(test_game_over_state) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + + // Заполняем верхние две строки почти полностью + for (int i = 0; i < 2; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + state->field[i][j] = 2; + } + } + + // Вызываем spawn, который должен обнаружить game over + state->state = Spawn; + updateCurrentState(); + + // Состояние должно перейти в GameOver + ck_assert_int_eq(state->state, GameOver); +} +END_TEST + +START_TEST(test_do_gameover_clears_next) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + + // Заполняем поле для trigger GameOver + for (int i = 0; i < 2; i++) { + for (int j = 0; j < FIELD_WIDTH; j++) { + state->field[i][j] = 2; + } + } + + state->state = Spawn; + updateCurrentState(); + + // После GameOver next должна быть пустой + state->state = GameOver; + updateCurrentState(); + + int has_blocks = 0; + for (int i = 0; i < 4; i++) { + for (int j = 0; j < 4; j++) { + if (state->next.mtrx[i][j]) { + has_blocks = 1; + } + } + } + ck_assert_int_eq(has_blocks, 0); +} +END_TEST + +START_TEST(test_place_figure_directly) { + GameState_t* state = get_game_state(); + + // Очищаем поле + for (int i = 0; i < FIELD_HEIGHT; i++) + for (int j = 0; j < FIELD_WIDTH; j++) + state->field[i][j] = 0; + + // Устанавливаем фигуру + state->curr.x = 5; + state->curr.y = 10; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; + state->curr.mtrx[0][0] = 1; + state->curr.mtrx[0][1] = 1; + + // Размещаем + place_figure(); + + // Проверяем что блоки размещены + ck_assert_int_eq(state->field[10][5], 2); + ck_assert_int_eq(state->field[10][6], 2); +} +END_TEST + +START_TEST(test_attach_state_transition) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + + // Принудительно переводим в Attaching + state->state = Attaching; + state->attach_start_time = 0; + state->attach_completed = 0; + + // Первый вызов должен разместить фигуру + updateCurrentState(); + + // Проверяем что таймер запущен + ck_assert_int_gt(state->attach_start_time, 0); +} +END_TEST + +START_TEST(test_moving_to_down) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + + // Устанавливаем фигуру с валидной матрицей + state->curr.y = 5; + state->curr.x = 5; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; + state->curr.mtrx[0][0] = 1; // Один блок + + int initial_y = state->curr.y; + + // Вызываем handle_to_down_move через Moving + state->state = Moving; + state->moving_type = ToDown; + + // ВАЖНО: updateCurrentState вызовет do_moving -> handle_to_down_move + // Защита от бесконечного цикла: ограничиваем время + long long start = get_current_time_ms(); + updateCurrentState(); + long long elapsed = get_current_time_ms() - start; + + // Проверяем что: + // 1. Фигура упала ниже (или перешли в Attaching) + // 2. Не было timeout (< 1 секунды) + ck_assert(state->curr.y > initial_y || state->state == Attaching); + ck_assert_int_lt(elapsed, 1000); +} +END_TEST + + +START_TEST(test_moving_do_nothing) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + + // Вызываем DoNothing + state->state = Moving; + state->moving_type = DoNothing; + updateCurrentState(); + + // Должны перейти в Move + ck_assert_int_eq(state->state, Move); +} +END_TEST + +START_TEST(test_automatic_falling) { + userInput(Start, false); + updateCurrentState(); + + GameState_t* state = get_game_state(); + + // Устанавливаем фигуру + state->curr.y = 5; + state->curr.x = 5; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 4; j++) + state->curr.mtrx[i][j] = 0; + state->curr.mtrx[0][0] = 1; + + // Принудительно переводим в Move с обнулённым таймером + state->state = Move; + state->last_move_time = 0; // Обнуляем время последнего движения + + int initial_y = state->curr.y; + + // Вызываем updateCurrentState, который должен вызвать do_move() + // Поскольку last_move_time = 0, условие should_move сработает + updateCurrentState(); + + // Проверяем что фигура упала или перешла в Attaching + ck_assert(state->curr.y > initial_y || state->state == Attaching); +} +END_TEST + + +Suite* fsm_suite(void) { + Suite* s = suite_create("FSM"); + TCase* tc = tcase_create("Core"); + + // Существующие тесты + tcase_add_test(tc, test_game_start); + tcase_add_test(tc, test_pause_toggle); + tcase_add_test(tc, test_field_initialization); + tcase_add_test(tc, test_next_figure_exists); + tcase_add_test(tc, test_movement_left); + tcase_add_test(tc, test_movement_right); + tcase_add_test(tc, test_rotation); + tcase_add_test(tc, test_user_input_actions); + tcase_add_test(tc, test_multiple_updates); + + tcase_add_test(tc, test_instant_drop_down_key); + tcase_add_test(tc, test_up_key_release); + tcase_add_test(tc, test_terminate_with_high_score); + tcase_add_test(tc, test_game_over_state); + tcase_add_test(tc, test_do_gameover_clears_next); + tcase_add_test(tc, test_place_figure_directly); + tcase_add_test(tc, test_attach_state_transition); + tcase_add_test(tc, test_moving_to_down); + tcase_add_test(tc, test_moving_do_nothing); + tcase_add_test(tc, test_automatic_falling); + + suite_add_tcase(s, tc); + return s; +} + diff --git a/src/test/test_helper.h b/src/test/test_helper.h index 2577eb1..75e1b72 100644 --- a/src/test/test_helper.h +++ b/src/test/test_helper.h @@ -1,6 +1,7 @@ #ifndef TEST_HELPER_H #define TEST_HELPER_H +#include #include "../brick_game/tetris/01_automato.h" // Хелпер для очистки поля diff --git a/src/test/test_lines.c b/src/test/test_lines.c index 946a394..c1d9fc1 100644 --- a/src/test/test_lines.c +++ b/src/test/test_lines.c @@ -1,4 +1,3 @@ -#include #include "test_helper.h" START_TEST(test_clear_one_line) { diff --git a/src/test/test_main.c b/src/test/test_main.c index d0c45a1..545d866 100644 --- a/src/test/test_main.c +++ b/src/test/test_main.c @@ -5,12 +5,14 @@ Suite* collision_suite(void); Suite* lines_suite(void); Suite* figures_suite(void); Suite* score_suite(void); +Suite* fsm_suite(void); // <- Эта строка уже есть? int main(void) { SRunner* sr = srunner_create(collision_suite()); srunner_add_suite(sr, lines_suite()); srunner_add_suite(sr, figures_suite()); srunner_add_suite(sr, score_suite()); + srunner_add_suite(sr, fsm_suite()); // <- Эта строка добавлена? srunner_run_all(sr, CK_VERBOSE); int failed = srunner_ntests_failed(sr); diff --git a/src/test/test_score.c b/src/test/test_score.c index 5b38dfb..05de290 100644 --- a/src/test/test_score.c +++ b/src/test/test_score.c @@ -1,4 +1,3 @@ -#include #include "test_helper.h" START_TEST(test_level_up) {