From 44e3dfa58d8a49dc8bf7e0f915bf95accaea7b18 Mon Sep 17 00:00:00 2001 From: Marco Cetica Date: Thu, 8 Jan 2026 16:29:34 +0100 Subject: [PATCH] Added unit tests for String data type and updated CI --- .github/workflows/clang-build.yml | 4 +- .github/workflows/gcc-build.yml | 4 +- Makefile | 8 +- src/string.c | 9 +- tests/test_string.c | 153 ++++++++++++++++++++++++++++++ 5 files changed, 171 insertions(+), 7 deletions(-) create mode 100644 tests/test_string.c diff --git a/.github/workflows/clang-build.yml b/.github/workflows/clang-build.yml index aea82b1..a404d92 100644 --- a/.github/workflows/clang-build.yml +++ b/.github/workflows/clang-build.yml @@ -16,8 +16,8 @@ jobs: - name: Run unit tests run: | - ./test_vector && ./test_map && ./test_bigint + ./test_vector && ./test_map && ./test_bigint && ./test_string - name: Run benchmarks run: | - ./benchmark_datum \ No newline at end of file + ./benchmark_datum diff --git a/.github/workflows/gcc-build.yml b/.github/workflows/gcc-build.yml index cad9781..c35c938 100644 --- a/.github/workflows/gcc-build.yml +++ b/.github/workflows/gcc-build.yml @@ -13,8 +13,8 @@ jobs: - name: Run unit tests run: | - ./test_vector && ./test_map && ./test_bigint + ./test_vector && ./test_map && ./test_bigint && ./test_string - name: Run benchmarks run: | - ./benchmark_datum \ No newline at end of file + ./benchmark_datum diff --git a/Makefile b/Makefile index 62d8911..c339dbb 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ TARGET = usage TEST_V_TARGET = test_vector TEST_M_TARGET = test_map TEST_B_TARGET = test_bigint +TEST_S_TARGET = test_string BENCH_TARGET = benchmark_datum LIB_OBJS = $(OBJ_DIR)/vector.o $(OBJ_DIR)/map.o $(OBJ_DIR)/bigint.o $(OBJ_DIR)/string.o @@ -24,7 +25,7 @@ PROG_OBJS = $(OBJ_DIR)/usage.o .PHONY: all clean -all: $(TARGET) $(TEST_V_TARGET) $(TEST_M_TARGET) $(TEST_B_TARGET) $(BENCH_TARGET) +all: $(TARGET) $(TEST_V_TARGET) $(TEST_M_TARGET) $(TEST_B_TARGET) $(TEST_S_TARGET) $(BENCH_TARGET) bench: $(BENCH_TARGET) $(TARGET): $(PROG_OBJS) $(LIB_OBJS) @@ -39,6 +40,9 @@ $(TEST_M_TARGET): $(OBJ_DIR)/test_map.o $(OBJ_DIR)/map.o $(TEST_B_TARGET): $(OBJ_DIR)/test_bigint.o $(OBJ_DIR)/bigint.o $(OBJ_DIR)/vector.o $(CC) $(CFLAGS) -o $@ $^ +$(TEST_S_TARGET): $(OBJ_DIR)/test_string.o $(OBJ_DIR)/string.o $(OBJ_DIR)/vector.o + $(CC) $(CFLAGS) -o $@ $^ + $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR) $(CC) $(CFLAGS) -c -o $@ $< @@ -65,4 +69,4 @@ $(BENCH_OBJ_DIR): mkdir -p $(BENCH_OBJ_DIR) clean: - rm -rf $(OBJ_DIR) $(BENCH_OBJ_DIR) $(TARGET) $(TEST_V_TARGET) $(TEST_M_TARGET) $(TEST_B_TARGET) $(BENCH_TARGET) + rm -rf $(OBJ_DIR) $(BENCH_OBJ_DIR) $(TARGET) $(TEST_V_TARGET) $(TEST_M_TARGET) $(TEST_B_TARGET) $(TEST_S_TARGET) $(BENCH_TARGET) diff --git a/src/string.c b/src/string.c index 09d59fe..a4f7b7a 100644 --- a/src/string.c +++ b/src/string.c @@ -451,13 +451,20 @@ string_result_t string_set_at(string_t *str, size_t position, const char *utf8_c int new_len; - if (str == NULL || position >= str->char_count || utf8_is_char_valid(utf8_char, &new_len) == 0) { + if (str == NULL || utf8_is_char_valid(utf8_char, &new_len) == 0) { result.status = STRING_ERR_INVALID; SET_MSG(result, "Invalid index or character"); return result; } + if (position >= str->char_count) { + result.status = STRING_ERR_OVERFLOW; + SET_MSG(result, "Index out of bounds"); + + return result; + } + char *pos = str->data; for (size_t idx = 0; idx < position; idx++) { pos += utf8_char_len((unsigned char)*pos); diff --git a/tests/test_string.c b/tests/test_string.c new file mode 100644 index 0000000..a7d3562 --- /dev/null +++ b/tests/test_string.c @@ -0,0 +1,153 @@ +/* + * Unit tests for String data type +*/ + +#define TEST(NAME) do { \ + printf("Running test_%s...", #NAME); \ + test_##NAME(); \ + printf(" PASSED\n"); \ +} while(0) + +#include +#include +#include + +#include "../src/string.h" + +// Test string creation +void test_string_new(void) { + string_result_t res = string_new("hello"); + + assert(res.status == STRING_OK); + assert(res.value.string != NULL); + assert(strcmp(res.value.string->data, "hello") == 0); + assert(string_len(res.value.string) == 5); + assert(res.value.string->byte_size == 5); + + string_destroy(res.value.string); +} + +// Test empty string +void test_string_new_empty(void) { + string_result_t res = string_new(""); + + assert(res.status == STRING_OK); + assert(string_len(res.value.string) == 0); + assert(res.value.string->byte_size == 0); + assert(res.value.string->data[0] == '\0'); + + string_destroy(res.value.string); +} + +// Test string concatenation +void test_string_concat(void) { + string_t *str1 = string_new("Foo").value.string; + string_t *str2 = string_new(" Bar").value.string; + + string_result_t res = string_concat(str1, str2); + assert(res.status == STRING_OK); + assert(strcmp(res.value.string->data, "Foo Bar") == 0); + assert(string_len(res.value.string) == 7); + + string_destroy(str1); + string_destroy(str2); + string_destroy(res.value.string); +} + +// Test case-insensitive and sensitive comparison +void test_string_eq(void) { + string_t *str1 = string_new("Foo").value.string; + string_t *str2 = string_new("foo").value.string; + + // Case sensitive comparison should be false + assert(string_eq(str1, str2, true).value.is_equ == false); + // Case insensitive comparison should be true + assert(string_eq(str1, str2, false).value.is_equ == true); + + string_destroy(str1); + string_destroy(str2); +} + +// Test string reverse using UTF-8 symbols +void test_string_reverse_utf8(void) { + string_t *str = string_new("A🌍Z").value.string; + + string_result_t res = string_reverse(str); + + assert(res.status == STRING_OK); + assert(string_len(res.value.string) == 3); + assert(strcmp(res.value.string->data, "Z🌍A") == 0); + assert(string_len(res.value.string) == 3); + + string_destroy(str); + string_destroy(res.value.string); +} + +// Test mutation of UTF-8 symbol +void test_string_set_at(void) { + string_t *str = string_new("ABC").value.string; + + // Replace 'B' with emoji + string_result_t res = string_set_at(str, 1, "😆"); + assert(res.status == STRING_OK); + assert(strcmp(str->data, "A😆C") == 0); + assert(string_len(str) == 3); + assert(str->byte_size == 6); // that is: A (1B) + emoji (4B) + C (1B) + + string_destroy(str); +} + +// Test mutation with overflow +void test_string_set_at_overflow(void) { + string_t *str = string_new("ABC").value.string; + + string_result_t res = string_set_at(str, 10, "a"); + assert(res.status == STRING_ERR_OVERFLOW); + + string_destroy(str); +} + +// Test whitespace trimming +void test_string_trim(void) { + string_t *str = string_new(" \t Foo Bar \n ").value.string; + + string_result_t res = string_trim(str); + assert(res.status == STRING_OK); + assert(strcmp(res.value.string->data, "Foo Bar") == 0); + + string_destroy(str); + string_destroy(res.value.string); +} + +// Test string splitting into an array +void test_string_split(void) { + string_t *str = string_new("Red,Green,Blue").value.string; + + string_result_t res = string_split(str, ","); + assert(res.status == STRING_OK); + assert(res.value.split.count == 3); + + assert(strcmp(res.value.split.strings[0]->data, "Red") == 0); + assert(strcmp(res.value.split.strings[1]->data, "Green") == 0); + assert(strcmp(res.value.split.strings[2]->data, "Blue") == 0); + + string_split_destroy(res.value.split.strings, res.value.split.count); + string_destroy(str); +} + +int main(void) { + printf("=== Running Vector unit tests ===\n\n"); + + TEST(string_new); + TEST(string_new_empty); + TEST(string_concat); + TEST(string_eq); + TEST(string_reverse_utf8); + TEST(string_set_at); + TEST(string_set_at_overflow); + TEST(string_trim); + TEST(string_split); + + printf("\n=== All tests passed! ===\n"); + return 0; +}