diff --git a/tests/unit/Makefile b/tests/unit/Makefile new file mode 100644 index 0000000..0fe36d8 --- /dev/null +++ b/tests/unit/Makefile @@ -0,0 +1,30 @@ +# Unit Tests Makefile +CC = gcc +CFLAGS = -Wall -Wextra -std=c11 -I../../include +LDFLAGS = -pthread + +# Source files +UTF8_SRC = ../../src/utf8.c +MESSAGE_SRC = ../../src/message.c + +TESTS = test_utf8 test_message + +.PHONY: all clean run + +all: $(TESTS) + +test_utf8: test_utf8.c $(UTF8_SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +test_message: test_message.c $(MESSAGE_SRC) $(UTF8_SRC) + $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + +run: all + @echo "=== Running UTF-8 Tests ===" + ./test_utf8 + @echo "" + @echo "=== Running Message Tests ===" + ./test_message + +clean: + rm -f $(TESTS) *.o test_messages.log diff --git a/tests/unit/test_message b/tests/unit/test_message new file mode 100755 index 0000000..0067319 Binary files /dev/null and b/tests/unit/test_message differ diff --git a/tests/unit/test_message.c b/tests/unit/test_message.c new file mode 100644 index 0000000..4b7ad3e --- /dev/null +++ b/tests/unit/test_message.c @@ -0,0 +1,240 @@ +/* Unit tests for message functions */ +#include "../../include/message.h" +#include +#include +#include +#include +#include + +#define TEST(name) static void test_##name() +#define RUN_TEST(name) do { \ + printf("Running %s... ", #name); \ + test_##name(); \ + printf("✓\n"); \ + tests_passed++; \ +} while(0) + +static int tests_passed = 0; +static const char *test_log = "test_messages.log"; + +/* Helper: Clean up test log file */ +static void cleanup_test_log(void) { + unlink(test_log); +} + +/* Helper: Create test log with N messages */ +static void create_test_log(int count) { + FILE *fp = fopen(test_log, "w"); + assert(fp != NULL); + + for (int i = 0; i < count; i++) { + fprintf(fp, "2026-02-08T10:00:%02d+08:00|user%d|Test message %d\n", + i, i, i); + } + fclose(fp); +} + +/* Test message initialization */ +TEST(message_init) { + message_init(); + /* No assertion needed, just ensure it doesn't crash */ +} + +/* Test loading from empty file */ +TEST(message_load_empty) { + cleanup_test_log(); + + /* Temporarily override LOG_FILE */ + FILE *fp = fopen(test_log, "w"); + fclose(fp); + + message_t *messages = NULL; + /* Can't easily override LOG_FILE constant, so this is a documentation test */ + + cleanup_test_log(); +} + +/* Test message format */ +TEST(message_format_basic) { + message_t msg; + msg.timestamp = 1234567890; + strcpy(msg.username, "testuser"); + strcpy(msg.content, "Hello World"); + + char buffer[512]; + message_format(&msg, buffer, sizeof(buffer), 80); + + /* Should contain timestamp, username, and content */ + assert(strstr(buffer, "testuser") != NULL); + assert(strstr(buffer, "Hello World") != NULL); +} + +TEST(message_format_long_content) { + message_t msg; + msg.timestamp = 1234567890; + strcpy(msg.username, "user"); + + /* Create long message */ + memset(msg.content, 'A', MAX_MESSAGE_LEN - 1); + msg.content[MAX_MESSAGE_LEN - 1] = '\0'; + + char buffer[2048]; + message_format(&msg, buffer, sizeof(buffer), 80); + + /* Should not overflow */ + assert(strlen(buffer) < sizeof(buffer)); +} + +TEST(message_format_unicode) { + message_t msg; + msg.timestamp = 1234567890; + strcpy(msg.username, "用户"); + strcpy(msg.content, "你好世界"); + + char buffer[512]; + message_format(&msg, buffer, sizeof(buffer), 80); + + assert(strstr(buffer, "用户") != NULL); + assert(strstr(buffer, "你好世界") != NULL); +} + +TEST(message_format_width_limits) { + message_t msg; + msg.timestamp = 1234567890; + strcpy(msg.username, "user"); + strcpy(msg.content, "Test"); + + char buffer[512]; + + /* Test various widths */ + message_format(&msg, buffer, sizeof(buffer), 40); + assert(strlen(buffer) < 512); + + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) < 512); + + message_format(&msg, buffer, sizeof(buffer), 120); + assert(strlen(buffer) < 512); +} + +/* Test message save */ +TEST(message_save_basic) { + cleanup_test_log(); + + /* This is harder to test without modifying LOG_FILE constant */ + /* For now, document expected behavior */ + message_t msg; + msg.timestamp = time(NULL); + strcpy(msg.username, "testuser"); + strcpy(msg.content, "Test message"); + + /* Would save to LOG_FILE */ + /* int ret = message_save(&msg); */ + /* assert(ret == 0); */ + + cleanup_test_log(); +} + +/* Test edge cases */ +TEST(message_edge_cases) { + message_t msg; + char buffer[512]; + + /* Empty username */ + msg.timestamp = 1234567890; + msg.username[0] = '\0'; + strcpy(msg.content, "Test"); + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) > 0); + + /* Empty content */ + strcpy(msg.username, "user"); + msg.content[0] = '\0'; + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) > 0); + + /* Maximum length username */ + memset(msg.username, 'A', MAX_USERNAME_LEN - 1); + msg.username[MAX_USERNAME_LEN - 1] = '\0'; + strcpy(msg.content, "Test"); + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) < sizeof(buffer)); + + /* Maximum length content */ + strcpy(msg.username, "user"); + memset(msg.content, 'B', MAX_MESSAGE_LEN - 1); + msg.content[MAX_MESSAGE_LEN - 1] = '\0'; + message_format(&msg, buffer, sizeof(buffer), 80); + /* Should handle gracefully */ +} + +TEST(message_special_characters) { + message_t msg; + char buffer[512]; + + msg.timestamp = 1234567890; + strcpy(msg.username, "user"); + strcpy(msg.content, "Message with\nnewline\tand\ttabs"); + + message_format(&msg, buffer, sizeof(buffer), 80); + + /* Should not crash or overflow */ + assert(strlen(buffer) < sizeof(buffer)); +} + +/* Test buffer safety */ +TEST(message_buffer_safety) { + message_t msg; + char small_buffer[16]; + + msg.timestamp = 1234567890; + strcpy(msg.username, "verylongusername"); + strcpy(msg.content, "Very long message content that exceeds buffer"); + + /* Should not overflow even with small buffer */ + message_format(&msg, small_buffer, sizeof(small_buffer), 80); + assert(strlen(small_buffer) < sizeof(small_buffer)); +} + +/* Test timestamp handling */ +TEST(message_timestamp_formats) { + message_t msg; + char buffer[512]; + + strcpy(msg.username, "user"); + strcpy(msg.content, "Test"); + + /* Test various timestamps */ + msg.timestamp = 0; /* Epoch */ + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) > 0); + + msg.timestamp = time(NULL); /* Current time */ + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) > 0); + + msg.timestamp = 2147483647; /* Max 32-bit timestamp */ + message_format(&msg, buffer, sizeof(buffer), 80); + assert(strlen(buffer) > 0); +} + +int main(void) { + printf("Running message unit tests...\n\n"); + + RUN_TEST(message_init); + RUN_TEST(message_load_empty); + RUN_TEST(message_format_basic); + RUN_TEST(message_format_long_content); + RUN_TEST(message_format_unicode); + RUN_TEST(message_format_width_limits); + RUN_TEST(message_save_basic); + RUN_TEST(message_edge_cases); + RUN_TEST(message_special_characters); + RUN_TEST(message_buffer_safety); + RUN_TEST(message_timestamp_formats); + + cleanup_test_log(); + + printf("\n✓ All %d tests passed!\n", tests_passed); + return 0; +} diff --git a/tests/unit/test_utf8 b/tests/unit/test_utf8 new file mode 100755 index 0000000..f02a121 Binary files /dev/null and b/tests/unit/test_utf8 differ diff --git a/tests/unit/test_utf8.c b/tests/unit/test_utf8.c new file mode 100644 index 0000000..7f42f9e --- /dev/null +++ b/tests/unit/test_utf8.c @@ -0,0 +1,239 @@ +/* Unit tests for UTF-8 functions */ +#include "../../include/utf8.h" +#include +#include +#include + +#define TEST(name) static void test_##name() +#define RUN_TEST(name) do { \ + printf("Running %s... ", #name); \ + test_##name(); \ + printf("✓\n"); \ + tests_passed++; \ +} while(0) + +static int tests_passed = 0; + +/* Test UTF-8 byte length detection */ +TEST(utf8_byte_length_ascii) { + assert(utf8_byte_length('A') == 1); + assert(utf8_byte_length('z') == 1); + assert(utf8_byte_length('0') == 1); +} + +TEST(utf8_byte_length_multibyte) { + assert(utf8_byte_length(0xC3) == 2); /* é first byte */ + assert(utf8_byte_length(0xE4) == 3); /* 中 first byte */ + assert(utf8_byte_length(0xF0) == 4); /* 𝕏 first byte */ +} + +TEST(utf8_byte_length_invalid) { + assert(utf8_byte_length(0xFF) == 1); /* Invalid UTF-8 */ + assert(utf8_byte_length(0x80) == 1); /* Continuation byte */ +} + +/* Test UTF-8 decoding */ +TEST(utf8_decode_ascii) { + int bytes_read; + assert(utf8_decode("A", &bytes_read) == 'A'); + assert(bytes_read == 1); +} + +TEST(utf8_decode_2byte) { + int bytes_read; + /* é = U+00E9 = 0xC3 0xA9 */ + const char *e_acute = "\xC3\xA9"; + uint32_t codepoint = utf8_decode(e_acute, &bytes_read); + assert(codepoint == 0x00E9); + assert(bytes_read == 2); +} + +TEST(utf8_decode_3byte) { + int bytes_read; + /* 中 = U+4E2D = 0xE4 0xB8 0xAD */ + const char *zhong = "\xE4\xB8\xAD"; + uint32_t codepoint = utf8_decode(zhong, &bytes_read); + assert(codepoint == 0x4E2D); + assert(bytes_read == 3); +} + +TEST(utf8_decode_4byte) { + int bytes_read; + /* 𝕏 = U+1D54F = 0xF0 0x9D 0x95 0x8F */ + const char *math_x = "\xF0\x9D\x95\x8F"; + uint32_t codepoint = utf8_decode(math_x, &bytes_read); + assert(codepoint == 0x1D54F); + assert(bytes_read == 4); +} + +/* Test character width calculation */ +TEST(utf8_char_width_ascii) { + assert(utf8_char_width('A') == 1); + assert(utf8_char_width(' ') == 1); + assert(utf8_char_width('0') == 1); +} + +TEST(utf8_char_width_cjk) { + assert(utf8_char_width(0x4E2D) == 2); /* 中 */ + assert(utf8_char_width(0x6587) == 2); /* 文 */ + assert(utf8_char_width(0x5B57) == 2); /* 字 */ +} + +TEST(utf8_char_width_hangul) { + assert(utf8_char_width(0xAC00) == 2); /* 가 */ + assert(utf8_char_width(0xD7A3) == 2); /* 힣 */ +} + +TEST(utf8_char_width_hiragana) { + assert(utf8_char_width(0x3042) == 2); /* あ */ + assert(utf8_char_width(0x3093) == 2); /* ん */ +} + +TEST(utf8_char_width_katakana) { + assert(utf8_char_width(0x30A2) == 2); /* ア */ + assert(utf8_char_width(0x30F3) == 2); /* ン */ +} + +/* Test string width calculation */ +TEST(utf8_string_width_ascii) { + assert(utf8_string_width("Hello") == 5); + assert(utf8_string_width("") == 0); + assert(utf8_string_width("Test123") == 7); +} + +TEST(utf8_string_width_mixed) { + /* "Hello世界" = 5 ASCII + 2*2 CJK = 9 */ + assert(utf8_string_width("Hello世界") == 9); + + /* "测试Test" = 2*2 CJK + 4 ASCII = 8 */ + assert(utf8_string_width("测试Test") == 8); +} + +TEST(utf8_string_width_cjk_only) { + /* "中文字符" = 4 * 2 = 8 */ + assert(utf8_string_width("中文字符") == 8); +} + +/* Test backspace handling */ +TEST(utf8_remove_last_char) { + char buffer[256]; + + /* Test ASCII */ + strcpy(buffer, "Hello"); + utf8_remove_last_char(buffer); + assert(strcmp(buffer, "Hell") == 0); + + /* Test empty string */ + strcpy(buffer, ""); + utf8_remove_last_char(buffer); + assert(strcmp(buffer, "") == 0); + + /* Test single char */ + strcpy(buffer, "A"); + utf8_remove_last_char(buffer); + assert(strcmp(buffer, "") == 0); +} + +TEST(utf8_remove_last_char_multibyte) { + char buffer[256]; + + /* Test 2-byte UTF-8 */ + strcpy(buffer, "café"); + utf8_remove_last_char(buffer); + assert(strcmp(buffer, "caf") == 0); + + /* Test 3-byte UTF-8 (CJK) */ + strcpy(buffer, "你好"); + utf8_remove_last_char(buffer); + assert(strcmp(buffer, "你") == 0); +} + +/* Test word removal (Ctrl+W) */ +TEST(utf8_remove_last_word) { + char buffer[256]; + + /* Test simple case */ + strcpy(buffer, "hello world"); + utf8_remove_last_word(buffer); + assert(strcmp(buffer, "hello ") == 0); + + /* Test multiple words */ + strcpy(buffer, "one two three"); + utf8_remove_last_word(buffer); + assert(strcmp(buffer, "one two ") == 0); + + /* Test trailing spaces */ + strcpy(buffer, "hello "); + utf8_remove_last_word(buffer); + assert(strcmp(buffer, "") == 0); + + /* Test single word */ + strcpy(buffer, "word"); + utf8_remove_last_word(buffer); + assert(strcmp(buffer, "") == 0); + + /* Test empty string */ + strcpy(buffer, ""); + utf8_remove_last_word(buffer); + assert(strcmp(buffer, "") == 0); +} + +/* Test input validation */ +TEST(utf8_is_valid_sequence) { + /* Valid sequences */ + assert(utf8_is_valid_sequence("A", 1) == true); + assert(utf8_is_valid_sequence("\xC3\xA9", 2) == true); /* é */ + assert(utf8_is_valid_sequence("\xE4\xB8\xAD", 3) == true); /* 中 */ + + /* Invalid sequences */ + assert(utf8_is_valid_sequence("\xFF", 1) == false); /* Invalid start */ + assert(utf8_is_valid_sequence("\xC3\xFF", 2) == false); /* Invalid continuation */ + + /* Invalid lengths */ + assert(utf8_is_valid_sequence("", 0) == false); + assert(utf8_is_valid_sequence("ABCDE", 5) == false); /* Too long */ + assert(utf8_is_valid_sequence(NULL, 1) == false); +} + +/* Test boundary cases */ +TEST(utf8_boundary_cases) { + /* Maximum valid codepoints */ + assert(utf8_char_width(0x10FFFF) == 1); /* Max Unicode codepoint */ + + /* BMP boundary */ + assert(utf8_char_width(0xFFFF) == 1); + + /* CJK range boundaries */ + assert(utf8_char_width(0x4DFF) == 1); /* Just before CJK Extension A */ + assert(utf8_char_width(0x4E00) == 2); /* Start of CJK Unified */ + assert(utf8_char_width(0x9FFF) == 2); /* End of CJK Unified */ + assert(utf8_char_width(0xA000) == 1); /* Just after CJK Unified */ +} + +int main(void) { + printf("Running UTF-8 unit tests...\n\n"); + + RUN_TEST(utf8_byte_length_ascii); + RUN_TEST(utf8_byte_length_multibyte); + RUN_TEST(utf8_byte_length_invalid); + RUN_TEST(utf8_decode_ascii); + RUN_TEST(utf8_decode_2byte); + RUN_TEST(utf8_decode_3byte); + RUN_TEST(utf8_decode_4byte); + RUN_TEST(utf8_char_width_ascii); + RUN_TEST(utf8_char_width_cjk); + RUN_TEST(utf8_char_width_hangul); + RUN_TEST(utf8_char_width_hiragana); + RUN_TEST(utf8_char_width_katakana); + RUN_TEST(utf8_string_width_ascii); + RUN_TEST(utf8_string_width_mixed); + RUN_TEST(utf8_string_width_cjk_only); + RUN_TEST(utf8_remove_last_char); + RUN_TEST(utf8_remove_last_char_multibyte); + RUN_TEST(utf8_remove_last_word); + RUN_TEST(utf8_is_valid_sequence); + RUN_TEST(utf8_boundary_cases); + + printf("\n✓ All %d tests passed!\n", tests_passed); + return 0; +}