test(demo): add structured unit test runner

This commit is contained in:
2026-06-14 21:27:20 -05:00
parent c614e0b8e0
commit 88b3a535bf
2 changed files with 517 additions and 44 deletions

View File

@@ -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

View File

@@ -1,69 +1,307 @@
#include "ikv.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
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;
}