diff --git a/demo/build.bat b/demo/build.bat index 9409c40..5fa596f 100644 --- a/demo/build.bat +++ b/demo/build.bat @@ -36,4 +36,10 @@ if errorlevel 1 ( echo built "%DEMO_EXE%" echo built "%TEST_EXE%" + +if /I "%1"=="test" ( + "%TEST_EXE%" + exit /b %ERRORLEVEL% +) + endlocal diff --git a/demo/unit_test.c b/demo/unit_test.c index 6fbd562..5eb0ccb 100644 --- a/demo/unit_test.c +++ b/demo/unit_test.c @@ -1,69 +1,307 @@ #include "ikv.h" +#include #include #include #include +#include -static int fail(const char *message) +#define ANSI_WHITE "\x1b[37m" +#define ANSI_RED "\x1b[31m" +#define ANSI_GREEN "\x1b[32m" +#define ANSI_YELLOW "\x1b[33m" +#define ANSI_RESET "\x1b[0m" + +typedef struct { - fputs(message, stderr); - fputc('\n', stderr); + char message[256]; +} test_context_t; + +typedef int (*test_fn_t)(test_context_t *context); + +typedef struct +{ + const char *name; + test_fn_t fn; +} test_case_t; + +static int fail(test_context_t *context, const char *message) +{ + snprintf(context->message, sizeof(context->message), "%s", message); return 1; } -static int test_text_roundtrip(void) +static void cleanup_file(const char *path) { - const char *src = + if (path) + remove(path); +} + +static int expect_true(test_context_t *context, bool condition, const char *message) +{ + if (!condition) + return fail(context, message); + return 0; +} + +static int expect_string(test_context_t *context, const char *actual, const char *expected, const char *label) +{ + if (strcmp(actual ? actual : "", expected ? expected : "") != 0) + { + snprintf(context->message, sizeof(context->message), "%s: got \"%s\" expected \"%s\"", + label, + actual ? actual : "", + expected ? expected : ""); + return 1; + } + return 0; +} + +static int test_object_metadata_and_scalars(test_context_t *context) +{ + ikv_node_t *root = ikv_create_object("root"); + ikv_node_t *child = NULL; + ikv_node_t *numbers = NULL; + int result = 0; + + if (!root) + return fail(context, "failed to create root object"); + + ikv_object_set_string(root, "title", "demo"); + ikv_object_set_int(root, "count", 42); + child = ikv_object_add_object(root, "child"); + numbers = ikv_object_add_array(root, "numbers", IKV_INT); + + if (!child || !numbers) + result = fail(context, "failed to build object graph"); + else + { + ikv_object_set_bool(child, "alive", true); + ikv_array_add_int(numbers, 7); + ikv_array_add_int(numbers, 8); + + if ((result = expect_string(context, ikv_node_key(root), "root", "root key")) == 0 && + (result = expect_true(context, ikv_node_type(root) == IKV_OBJECT, "root type mismatch")) == 0 && + (result = expect_true(context, ikv_object_size(root) == 4u, "object size mismatch")) == 0 && + (result = expect_true(context, ikv_array_size(numbers) == 2u, "array size mismatch")) == 0 && + (result = expect_true(context, ikv_array_element_type(numbers) == IKV_INT, "array element type mismatch")) == 0 && + (result = expect_string(context, ikv_as_string(ikv_object_get(root, "title")), "demo", "title mismatch")) == 0 && + (result = expect_true(context, ikv_as_int(ikv_object_get(root, "count")) == 42, "count mismatch")) == 0 && + (result = expect_true(context, ikv_as_bool(ikv_object_get(child, "alive")), "bool mismatch")) == 0) + { + result = 0; + } + } + + ikv_free(root); + return result; +} + +static int test_object_replace_and_missing_lookup(test_context_t *context) +{ + ikv_node_t *root = ikv_create_object("root"); + int result = 0; + + if (!root) + return fail(context, "failed to create root object"); + + ikv_object_set_int(root, "value", 1); + ikv_object_set_int(root, "value", 99); + + if ((result = expect_true(context, ikv_object_size(root) == 1u, "object replacement grew size")) == 0 && + (result = expect_true(context, ikv_as_int(ikv_object_get(root, "value")) == 99, "replacement value mismatch")) == 0 && + (result = expect_true(context, ikv_object_get(root, "missing") == NULL, "missing key should be null")) == 0) + { + result = 0; + } + + ikv_free(root); + return result; +} + +static int test_array_type_rules(test_context_t *context) +{ + ikv_node_t *strings = ikv_create_array("strings", IKV_STRING); + ikv_node_t *mixed = ikv_create_array("mixed", IKV_NULL); + int result = 0; + + if (!strings || !mixed) + { + ikv_free(strings); + ikv_free(mixed); + return fail(context, "failed to create arrays"); + } + + ikv_array_add_string(strings, "a"); + ikv_array_add_int(strings, 10); + + ikv_array_add_int(mixed, 1); + ikv_array_add_object(mixed); + + if ((result = expect_true(context, ikv_array_size(strings) == 1u, "typed array accepted wrong type")) == 0 && + (result = expect_true(context, ikv_array_size(mixed) == 2u, "mixed array size mismatch")) == 0 && + (result = expect_true(context, ikv_array_element_type(strings) == IKV_STRING, "typed array element type mismatch")) == 0 && + (result = expect_true(context, ikv_array_element_type(mixed) == IKV_NULL, "mixed array should downgrade to IKV_NULL")) == 0) + { + result = 0; + } + + ikv_free(strings); + ikv_free(mixed); + return result; +} + +static int test_text_parse_variants(test_context_t *context) +{ + const char *v1_src = "ikv1 \"root\"\n" "{\n" " \"name\" \"legacy\"\n" " \"count\" 7\n" "}\n"; - ikv_node_t *root = ikv_parse_string(src); + const char *v2_src = + "ikv2 \"root\"\n" + "{\n" + " \"flag\" true\n" + " \"ratio\" 1.5\n" + "}\n"; + const char *plain_src = + "{\n" + " \"title\" \"plain\"\n" + "}\n"; + ikv_node_t *root = ikv_parse_string(v1_src); + int result = 0; if (!root) - return fail("text parse failed"); - if (ikv_node_type(root) != IKV_OBJECT) - return fail("text root type mismatch"); - if (strcmp(ikv_as_string(ikv_object_get(root, "name")), "legacy") != 0) - return fail("text string value mismatch"); - if (ikv_as_int(ikv_object_get(root, "count")) != 7) - return fail("text int value mismatch"); - + return fail(context, "v1 text parse failed"); + if ((result = expect_string(context, ikv_as_string(ikv_object_get(root, "name")), "legacy", "v1 name mismatch")) != 0 || + (result = expect_true(context, ikv_as_int(ikv_object_get(root, "count")) == 7, "v1 count mismatch")) != 0) + { + ikv_free(root); + return result; + } ikv_free(root); + + root = ikv_parse_string(v2_src); + if (!root) + return fail(context, "v2 text parse failed"); + if ((result = expect_true(context, ikv_as_bool(ikv_object_get(root, "flag")), "v2 flag mismatch")) != 0 || + (result = expect_true(context, ikv_as_float(ikv_object_get(root, "ratio")) > 1.4, "v2 ratio mismatch")) != 0) + { + ikv_free(root); + return result; + } + ikv_free(root); + + root = ikv_parse_string(plain_src); + if (!root) + return fail(context, "plain object parse failed"); + result = expect_string(context, ikv_as_string(ikv_object_get(root, "title")), "plain", "plain title mismatch"); + ikv_free(root); + return result; +} + +static int test_text_invalid_input(test_context_t *context) +{ + if (ikv_parse_string(NULL) != NULL) + return fail(context, "null text input should fail"); + if (ikv_parse_string("ikv1 \"root\" { \"unterminated\" ") != NULL) + return fail(context, "invalid text should fail"); + if (ikv_parse_string("ikv2 root [") != NULL) + return fail(context, "invalid array text should fail"); return 0; } -static int test_binary_v1_roundtrip(void) +static int test_text_file_roundtrip(test_context_t *context) +{ + const char *path = "demo_test_text.ikv"; + ikv_node_t *root = ikv_create_object("save"); + ikv_node_t *loaded = NULL; + int result = 0; + + cleanup_file(path); + if (!root) + return fail(context, "failed to create text file root"); + + ikv_object_set_string(root, "mode", "text"); + if (!ikv_write_file(path, root)) + result = fail(context, "text file write failed"); + else + { + loaded = ikv_parse_file(path); + if (!loaded) + result = fail(context, "text file parse failed"); + else + result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "mode")), "text", "text file value mismatch"); + } + + ikv_free(loaded); + ikv_free(root); + cleanup_file(path); + return result; +} + +static int test_binary_v1_memory_roundtrip(test_context_t *context) { ikv_node_t *root = ikv_create_object("root"); ikv_node_t *loaded = NULL; uint8_t *data = NULL; uint32_t size = 0u; - int status = 0; + int result = 0; if (!root) - return fail("v1 root allocation failed"); + return fail(context, "failed to create v1 root"); ikv_object_set_int(root, "value", 11); + ikv_object_set_string(root, "name", "v1"); + if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_1)) - status = fail("v1 binary write failed"); + result = fail(context, "v1 binary write failed"); else { loaded = ikvb_parse_memory(data, size); if (!loaded) - status = fail("v1 binary parse failed"); - else if (ikv_as_int(ikv_object_get(loaded, "value")) != 11) - status = fail("v1 binary value mismatch"); + result = fail(context, "v1 binary parse failed"); + else if ((result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "value")) == 11, "v1 int mismatch")) == 0) + result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "name")), "v1", "v1 string mismatch"); } ikv_free(loaded); free(data); ikv_free(root); - return status; + return result; } -static int test_binary_v2_lazy_root(void) +static int test_binary_v1_file_roundtrip(test_context_t *context) +{ + const char *path = "demo_test_v1.ikvb"; + ikv_node_t *root = ikv_create_object("root"); + ikv_node_t *loaded = NULL; + int result = 0; + + cleanup_file(path); + if (!root) + return fail(context, "failed to create v1 file root"); + + ikv_object_set_bool(root, "legacy", true); + if (!ikvb_write_file_version(path, root, IKV_VERSION_1)) + result = fail(context, "v1 file write failed"); + else + { + loaded = ikvb_parse_file(path); + if (!loaded) + result = fail(context, "v1 file parse failed"); + else + result = expect_true(context, ikv_as_bool(ikv_object_get(loaded, "legacy")), "v1 file bool mismatch"); + } + + ikv_free(loaded); + ikv_free(root); + cleanup_file(path); + return result; +} + +static int test_binary_v2_memory_lazy_roundtrip(test_context_t *context) { ikv_node_t *root = ikv_create_object("root"); ikv_node_t *nested = NULL; @@ -71,58 +309,287 @@ static int test_binary_v2_lazy_root(void) ikv_node_t *loaded_nested = NULL; uint8_t *data = NULL; uint32_t size = 0u; - int status = 0; + int result = 0; if (!root) - return fail("v2 root allocation failed"); + return fail(context, "failed to create v2 memory root"); ikv_object_set_string(root, "title", "fast"); nested = ikv_object_add_object(root, "nested"); if (!nested) { ikv_free(root); - return fail("v2 nested allocation failed"); + return fail(context, "failed to create nested object"); } ikv_object_set_bool(nested, "flag", true); ikv_object_set_float(nested, "speed", 9.25); if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2)) - status = fail("v2 binary write failed"); + result = fail(context, "v2 binary memory write failed"); else { loaded = ikvb_parse_memory(data, size); if (!loaded) - status = fail("v2 binary parse failed"); - else if (ikv_object_size(loaded) != 2u) - status = fail("v2 root key count mismatch"); - else if (strcmp(ikv_as_string(ikv_object_get(loaded, "title")), "fast") != 0) - status = fail("v2 root lazy lookup failed"); - else + result = fail(context, "v2 binary memory parse failed"); + else if ((result = expect_true(context, ikv_object_size(loaded) == 2u, "v2 root key count mismatch")) == 0 && + (result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "title")), "fast", "v2 title mismatch")) == 0) { loaded_nested = ikv_object_get(loaded, "nested"); if (!loaded_nested) - status = fail("v2 nested lazy lookup failed"); - else if (!ikv_as_bool(ikv_object_get(loaded_nested, "flag"))) - status = fail("v2 nested bool mismatch"); + result = fail(context, "v2 nested lazy lookup failed"); + else if ((result = expect_true(context, ikv_as_bool(ikv_object_get(loaded_nested, "flag")), "v2 nested bool mismatch")) == 0) + result = expect_true(context, ikv_as_float(ikv_object_get(loaded_nested, "speed")) > 9.0, "v2 nested float mismatch"); } } ikv_free(loaded); free(data); ikv_free(root); - return status; + return result; +} + +static int test_binary_v2_file_lazy_roundtrip(test_context_t *context) +{ + const char *path = "demo_test_v2.ikvb"; + ikv_node_t *root = ikv_create_object("root"); + ikv_node_t *inventory = NULL; + ikv_node_t *loaded = NULL; + ikv_node_t *loaded_inventory = NULL; + int result = 0; + + cleanup_file(path); + if (!root) + return fail(context, "failed to create v2 file root"); + + inventory = ikv_object_add_array(root, "inventory", IKV_STRING); + if (!inventory) + { + ikv_free(root); + return fail(context, "failed to create inventory array"); + } + + ikv_object_set_int(root, "count", 3); + ikv_array_add_string(inventory, "a"); + ikv_array_add_string(inventory, "b"); + ikv_array_add_string(inventory, "c"); + + if (!ikvb_write_file(path, root)) + result = fail(context, "v2 file write failed"); + else + { + loaded = ikv_parse_file(path); + if (!loaded) + result = fail(context, "v2 file parse failed"); + else if ((result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 3, "v2 file int mismatch")) == 0) + { + loaded_inventory = ikv_object_get(loaded, "inventory"); + if (!loaded_inventory) + result = fail(context, "v2 file inventory lazy lookup failed"); + else if ((result = expect_true(context, ikv_array_size(loaded_inventory) == 3u, "v2 inventory size mismatch")) == 0) + result = expect_string(context, ikv_as_string(ikv_array_get(loaded_inventory, 1u)), "b", "v2 inventory value mismatch"); + } + } + + ikv_free(loaded); + ikv_free(root); + cleanup_file(path); + return result; +} + +static int test_detection_apis(test_context_t *context) +{ + const char *text_path = "demo_test_detect_text.ikv"; + const char *binary_path = "demo_test_detect_bin.ikvb"; + ikv_node_t *root = ikv_create_object("root"); + int result = 0; + + cleanup_file(text_path); + cleanup_file(binary_path); + if (!root) + return fail(context, "failed to create detection root"); + + ikv_object_set_int(root, "value", 5); + if ((result = expect_true(context, ikv_detect_text_version("ikv1 \"r\" {}") == IKV_VERSION_1, "text v1 detection failed")) != 0 || + (result = expect_true(context, ikv_detect_text_version("ikv2 \"r\" {}") == IKV_VERSION_2, "text v2 detection failed")) != 0 || + (result = expect_true(context, ikv_detect_text_version("{\"x\" 1}") == IKV_VERSION_UNKNOWN, "plain text detection mismatch")) != 0) + { + ikv_free(root); + return result; + } + + if (!ikv_write_file_version(text_path, root, IKV_VERSION_2) || + !ikvb_write_file_version(binary_path, root, IKV_VERSION_1)) + { + ikv_free(root); + cleanup_file(text_path); + cleanup_file(binary_path); + return fail(context, "failed to prepare detection files"); + } + + if ((result = expect_true(context, ikv_detect_file_version(text_path, false) == IKV_VERSION_2, "text file detection failed")) == 0 && + (result = expect_true(context, ikv_detect_file_version(binary_path, true) == IKV_VERSION_1, "binary file detection failed")) == 0) + { + result = 0; + } + + ikv_free(root); + cleanup_file(text_path); + cleanup_file(binary_path); + return result; +} + +static int test_explicit_version_apis(test_context_t *context) +{ + const char *text_path = "demo_test_explicit_text.ikv"; + const char *binary_path = "demo_test_explicit_bin.ikvb"; + ikv_node_t *root = ikv_create_object("root"); + ikv_node_t *loaded = NULL; + int result = 0; + + cleanup_file(text_path); + cleanup_file(binary_path); + if (!root) + return fail(context, "failed to create explicit api root"); + + ikv_object_set_string(root, "kind", "explicit"); + + if (!ikv_write_file_version(text_path, root, IKV_VERSION_1)) + result = fail(context, "explicit text write failed"); + else + { + loaded = ikv_parse_file_version(text_path, IKV_VERSION_1); + if (!loaded) + result = fail(context, "explicit text parse failed"); + else + result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "kind")), "explicit", "explicit text value mismatch"); + } + + ikv_free(loaded); + loaded = NULL; + + if (result == 0) + { + if (!ikvb_write_file_version(binary_path, root, IKV_VERSION_2)) + result = fail(context, "explicit binary write failed"); + else + { + loaded = ikvb_parse_file_version(binary_path, IKV_VERSION_2); + if (!loaded) + result = fail(context, "explicit binary parse failed"); + else + result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "kind")), "explicit", "explicit binary value mismatch"); + } + } + + ikv_free(loaded); + ikv_free(root); + cleanup_file(text_path); + cleanup_file(binary_path); + return result; +} + +static int test_invalid_argument_guards(test_context_t *context) +{ + uint8_t *data = NULL; + uint32_t size = 0u; + + if (!expect_true(context, !ikv_write_file(NULL, NULL), "ikv_write_file should reject nulls") && + !expect_true(context, !ikvb_write_file(NULL, NULL), "ikvb_write_file should reject nulls") && + !expect_true(context, !ikvb_write_memory(NULL, &data, &size), "ikvb_write_memory should reject null root") && + !expect_true(context, ikv_parse_file("definitely_missing_file.ikv") == NULL, "missing text file should fail") && + !expect_true(context, ikvb_parse_file("definitely_missing_file.ikvb") == NULL, "missing binary file should fail") && + !expect_true(context, ikvb_parse_memory("bad", 3u) == NULL, "short binary blob should fail") && + !expect_true(context, ikv_parse_file_version("definitely_missing_file.ikv", IKV_VERSION_2) == NULL, "explicit parse missing file should fail") && + !expect_true(context, ikvb_parse_file_version("definitely_missing_file.ikvb", IKV_VERSION_1) == NULL, "explicit binary parse missing file should fail")) + { + return 0; + } + + return 1; +} + +static const test_case_t test_cases[] = { + {"object metadata and scalars", test_object_metadata_and_scalars}, + {"object replace and missing lookup", test_object_replace_and_missing_lookup}, + {"array type rules", test_array_type_rules}, + {"text parse variants", test_text_parse_variants}, + {"text invalid input", test_text_invalid_input}, + {"text file roundtrip", test_text_file_roundtrip}, + {"binary v1 memory roundtrip", test_binary_v1_memory_roundtrip}, + {"binary v1 file roundtrip", test_binary_v1_file_roundtrip}, + {"binary v2 memory lazy roundtrip", test_binary_v2_memory_lazy_roundtrip}, + {"binary v2 file lazy roundtrip", test_binary_v2_file_lazy_roundtrip}, + {"detection apis", test_detection_apis}, + {"explicit version apis", test_explicit_version_apis}, + {"invalid argument guards", test_invalid_argument_guards}, +}; + +static void log_case(bool passed, unsigned int index, unsigned int total, long elapsed_ms, const char *name, const char *message) +{ + const char *color = passed ? ANSI_GREEN : ANSI_RED; + const char *label = passed ? "PASS" : "FAIL"; + + printf("%s[%s%s%s]%s ( %4ldms ) [%u/%u] %s", + ANSI_WHITE, + color, + label, + ANSI_WHITE, + ANSI_RESET, + elapsed_ms, + index, + total, + name); + + if (!passed && message && message[0] != 0) + printf(" %s- %s%s", ANSI_YELLOW, message, ANSI_RESET); + + putchar('\n'); } int main(void) { - if (test_text_roundtrip() != 0) - return 1; - if (test_binary_v1_roundtrip() != 0) - return 1; - if (test_binary_v2_lazy_root() != 0) - return 1; + const unsigned int total = (unsigned int)(sizeof(test_cases) / sizeof(test_cases[0])); + unsigned int passed = 0u; - puts("all tests passed"); + for (unsigned int i = 0; i < total; ++i) + { + test_context_t context; + clock_t start_time = 0; + clock_t end_time = 0; + long elapsed_ms = 0; + int result = 0; + + memset(&context, 0, sizeof(context)); + start_time = clock(); + result = test_cases[i].fn(&context); + end_time = clock(); + elapsed_ms = (long)(((end_time - start_time) * 1000) / CLOCKS_PER_SEC); + + if (result == 0) + ++passed; + + log_case(result == 0, i + 1u, total, elapsed_ms, test_cases[i].name, context.message); + } + + if (passed != total) + { + printf("%s[%sFAIL%s]%s %u/%u tests passed\n", + ANSI_WHITE, + ANSI_RED, + ANSI_WHITE, + ANSI_RESET, + passed, + total); + return 1; + } + + printf("%s[%sPASS%s]%s %u/%u tests passed\n", + ANSI_WHITE, + ANSI_GREEN, + ANSI_WHITE, + ANSI_RESET, + passed, + total); return 0; }