From 6110fc963c30f7c67e44cffc3688ad97ee5efcbd Mon Sep 17 00:00:00 2001 From: Marco Cetica Date: Fri, 13 Mar 2026 18:27:00 +0100 Subject: [PATCH] Fixed a bug and added unit tests for String type --- .gitea/workflows/clang-build.yml | 4 +- .gitea/workflows/gcc-build.yml | 4 +- Makefile | 8 +- src/string.h | 2 +- tests/test_string.c | 329 +++++++++++++++++++++++++++++++ 5 files changed, 340 insertions(+), 7 deletions(-) create mode 100644 tests/test_string.c diff --git a/.gitea/workflows/clang-build.yml b/.gitea/workflows/clang-build.yml index aea82b1..a404d92 100644 --- a/.gitea/workflows/clang-build.yml +++ b/.gitea/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/.gitea/workflows/gcc-build.yml b/.gitea/workflows/gcc-build.yml index cad9781..c35c938 100644 --- a/.gitea/workflows/gcc-build.yml +++ b/.gitea/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 115bc86..808fbbd 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 + $(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.h b/src/string.h index 66099e8..b4dde4b 100644 --- a/src/string.h +++ b/src/string.h @@ -43,7 +43,7 @@ typedef struct { // Public APIs string_result_t string_new(const char *c_str); -string_result_t string_close(const string_t *str); +string_result_t string_clone(const string_t *str); string_result_t string_concat(const string_t *x, const string_t *y); string_result_t string_contains(const string_t *haystack, const string_t *needle); string_result_t string_slice(const string_t *str, size_t start, size_t end); diff --git a/tests/test_string.c b/tests/test_string.c new file mode 100644 index 0000000..2bf2c1a --- /dev/null +++ b/tests/test_string.c @@ -0,0 +1,329 @@ +/* +* 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 + +#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_size(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_size(res.value.string) == 0); + assert(res.value.string->byte_size == 0); + assert(res.value.string->data[0] == '\0'); + + string_destroy(res.value.string); +} + +// Test cloning an existing string +void test_string_clone(void) { + string_t *original = string_new("Original").value.string; + string_result_t res = string_clone(original); + + assert(res.status == STRING_OK); + assert(res.value.string != original); // Different memory address + assert(strcmp(res.value.string->data, original->data) == 0); + assert(res.value.string->byte_size == original->byte_size); + + string_destroy(original); + 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_size(res.value.string) == 7); + + string_destroy(str1); + string_destroy(str2); + string_destroy(res.value.string); +} + +// Test if string contains substring +void test_string_contains(void) { + string_t *haystack = string_new("Hello 🌍 World").value.string; + string_t *needle_ascii = string_new("World").value.string; + string_t *needle_utf8 = string_new("🌍").value.string; + string_t *needle_none = string_new("not found").value.string; + + // World starts at symbol 8 + string_result_t res1 = string_contains(haystack, needle_ascii); + assert(res1.status == STRING_OK); + assert(res1.value.idx == 8); + + // 🌍 is at position 6 + string_result_t res2 = string_contains(haystack, needle_utf8); + assert(res2.status == STRING_OK); + assert(res2.value.idx == 6); + + // Not found should return -1 + string_result_t res3 = string_contains(haystack, needle_none); + assert(res3.status == STRING_OK); + assert(res3.value.idx == -1); + + string_destroy(haystack); + string_destroy(needle_ascii); + string_destroy(needle_utf8); + string_destroy(needle_none); +} + +// Test string slicing +void test_string_slice(void) { + // ASCII slice + string_t *str1 = string_new("foobar").value.string; + string_result_t res1 = string_slice(str1, 2, 4); + + assert(res1.status == STRING_OK); + assert(strcmp(res1.value.string->data, "oba") == 0); + assert(res1.value.string->char_count == 3); + + // UTF-8 slice + string_t *str2 = string_new("AB😀🌍").value.string; + string_result_t res2 = string_slice(str2, 2, 2); + + assert(res2.status == STRING_OK); + assert(strcmp(res2.value.string->data, "😀") == 0); + assert(res2.value.string->byte_size == 4); // emoji = 4 bytes + + // UTF-8 + ASCII slice + string_result_t res3 = string_slice(str2, 0, 2); + assert(res3.status == STRING_OK); + assert(strcmp(res3.value.string->data, "AB😀") == 0); + + // Invalid bounds + string_result_t res4 = string_slice(str1, 5, 2); + assert(res4.status == STRING_ERR_OVERFLOW); + + res4 = string_slice(str1, 1, 50); + assert(res4.status == STRING_ERR_OVERFLOW); + + string_destroy(str1); + string_destroy(str2); + string_destroy(res1.value.string); + string_destroy(res2.value.string); + string_destroy(res3.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_size(res.value.string) == 3); + assert(strcmp(res.value.string->data, "Z🌍A") == 0); + assert(string_size(res.value.string) == 3); + + string_destroy(str); + string_destroy(res.value.string); +} + +// Test string get_at +void test_string_get_at(void) { + string_t *str = string_new("AB😀🌍").value.string; + + // 😀 is at index 2 + string_result_t res1 = string_get_at(str, 2); + assert(res1.status == STRING_OK); + assert(strcmp((char*)res1.value.symbol, "😀") == 0); + free(res1.value.symbol); + + // 🌍 is at index 3 + string_result_t res2 = string_get_at(str, 3); + assert(res2.status == STRING_OK); + assert(strcmp((char*)res2.value.symbol, "🌍") == 0); + free(res2.value.symbol); + + string_destroy(str); +} + +// Test string get_at with invalid index +void test_string_get_at_overflow(void) { + string_t *str = string_new("ABC").value.string; + + string_result_t res = string_get_at(str, 50); + assert(res.status == STRING_ERR_OVERFLOW); + + string_destroy(str); +} + +// Test mutation of UTF-8 symbol +void test_string_set_at(void) { + string_t *str = string_new("ABC").value.string; + + // Replace 'B' with an emoji + string_result_t res = string_set_at(str, 1, "😀"); + string_t *altered = res.value.string; + + assert(res.status == STRING_OK); + assert(strcmp(altered->data, "A😀C") == 0); + assert(string_size(altered) == 3); + assert(altered->byte_size == 6); // that is: A (1B) + emoji (4B) + C (1B) + + string_destroy(str); + string_destroy(altered); +} + +// Test mutation of invalid UTF-8 symbol +void test_string_set_at_invalid_utf8(void) { + string_t *str = string_new("ABC").value.string; + + const char * const invalid_sym1 = "\xFF"; + const char * const invalid_sym2 = "\x80"; + + string_result_t res1 = string_set_at(str, 1, invalid_sym1); + assert(res1.status == STRING_ERR_INVALID_UTF8); + + string_result_t res2 = string_set_at(str, 1, invalid_sym2); + assert(res2.status == STRING_ERR_INVALID_UTF8); + + 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 string to lowercase +void test_string_to_lower(void) { + string_t *str = string_new("AbC").value.string; + string_result_t res = string_to_lower(str); + + assert(res.status == STRING_OK); + assert(strcmp(res.value.string->data, "abc") == 0); + + string_destroy(str); + string_destroy(res.value.string); +} + +// Test string to uppercase +void test_string_to_upper(void) { + string_t *str = string_new("aBc").value.string; + string_result_t res = string_to_upper(str); + + assert(res.status == STRING_OK); + assert(strcmp(res.value.string->data, "ABC") == 0); + + string_destroy(str); + string_destroy(res.value.string); +} + +// 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); + + const size_t count = res.value.split.count; + string_t **strings = res.value.split.strings; + + const char *expected[] = { "Red", "Green", "Blue" }; + for (size_t idx = 0; idx < count; idx++) { + assert(strcmp(strings[idx]->data, expected[idx]) == 0); + } + + string_split_destroy(strings, count); + string_destroy(str); +} + +// Test string destroy +void test_string_destroy(void) { + string_t *str = string_new("delete me").value.string; + + string_result_t res = string_destroy(str); + assert(res.status == STRING_OK); + + string_result_t res_null = string_destroy(NULL); + assert(res_null.status == STRING_ERR_INVALID); +} + +int main(void) { +printf("=== Running String unit tests ===\n\n"); + + TEST(string_new); + TEST(string_new_empty); + TEST(string_clone); + TEST(string_concat); + TEST(string_contains); + TEST(string_slice); + TEST(string_eq); + TEST(string_reverse_utf8); + TEST(string_get_at); + TEST(string_get_at_overflow); + TEST(string_set_at); + TEST(string_set_at_overflow); + TEST(string_set_at_invalid_utf8); + TEST(string_to_lower); + TEST(string_to_upper); + TEST(string_trim); + TEST(string_split); + TEST(string_destroy); + + printf("\n=== All tests passed! ===\n"); + return 0; +}