Files
iKv/demo/unit_test.c

2157 lines
72 KiB
C

#include "ikv.h"
#include <stdbool.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#include <windows.h>
#endif
#ifdef IKV_TESTING
#include "../src/internal/ikv_internal.h"
#endif
#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
{
char message[256];
char benchmark_summary[8192];
} 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 void cleanup_file(const char *path)
{
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 double benchmark_now_us(void)
{
#ifdef _WIN32
static LARGE_INTEGER frequency;
static int initialized = 0;
LARGE_INTEGER counter;
if (!initialized)
{
QueryPerformanceFrequency(&frequency);
initialized = 1;
}
QueryPerformanceCounter(&counter);
return ((double)counter.QuadPart * 1000000.0) / (double)frequency.QuadPart;
#else
struct timespec ts;
timespec_get(&ts, TIME_UTC);
return ((double)ts.tv_sec * 1000000.0) + ((double)ts.tv_nsec / 1000.0);
#endif
}
static ikv_node_t *create_benchmark_root(unsigned int scale)
{
ikv_node_t *root = ikv_create_object("benchmark");
ikv_node_t *inventory = NULL;
ikv_node_t *stats = NULL;
unsigned int item_count = 0u;
unsigned int stat_count = 0u;
if (!root)
return NULL;
ikv_object_set_string(root, "title", "benchmark payload");
ikv_object_set_int(root, "count", 1337);
ikv_object_set_bool(root, "enabled", true);
ikv_object_set_float(root, "ratio", 42.5);
inventory = ikv_object_add_array(root, "inventory", IKV_STRING);
stats = ikv_object_add_object(root, "stats");
if (!inventory || !stats)
{
ikv_free(root);
return NULL;
}
item_count = scale * 8u;
if (item_count < 5u)
item_count = 5u;
for (unsigned int i = 0; i < item_count; ++i)
{
char item_name[64];
snprintf(item_name, sizeof(item_name), "item-%u-payload-%u", scale, i);
ikv_array_add_string(inventory, item_name);
}
stat_count = scale * 16u;
if (stat_count < 4u)
stat_count = 4u;
for (unsigned int i = 0; i < stat_count; ++i)
{
char key[64];
snprintf(key, sizeof(key), "metric_%u", i);
switch (i % 4u)
{
case 0u:
ikv_object_set_int(stats, key, (int64_t)(scale * 100u + i));
break;
case 1u:
ikv_object_set_float(stats, key, (double)scale * 0.5 + (double)i * 0.25);
break;
case 2u:
ikv_object_set_bool(stats, key, (i & 1u) != 0u);
break;
default:
{
char value[64];
snprintf(value, sizeof(value), "value-%u-%u", scale, i);
ikv_object_set_string(stats, key, value);
break;
}
}
}
return root;
}
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";
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(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_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 result = 0;
if (!root)
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))
result = fail(context, "v1 binary write failed");
else
{
loaded = ikvb_parse_memory(data, size);
if (!loaded)
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 result;
}
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;
ikv_node_t *loaded = NULL;
ikv_node_t *loaded_nested = NULL;
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
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(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))
result = fail(context, "v2 binary memory write failed");
else
{
loaded = ikvb_parse_memory(data, size);
if (!loaded)
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)
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 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_binary_v2_refresh_from_path(test_context_t *context)
{
const char *path = "demo_refresh_v2.ikvb";
ikv_node_t *original = ikv_create_object("root");
ikv_node_t *updated = ikv_create_object("root");
ikv_node_t *loaded = NULL;
int result = 0;
cleanup_file(path);
if (!original || !updated)
{
ikv_free(original);
ikv_free(updated);
return fail(context, "failed to create refresh roots");
}
ikv_object_set_int(original, "count", 1);
ikv_object_set_string(original, "name", "before");
if (!ikvb_write_file(path, original))
{
result = fail(context, "failed to write original refresh file");
}
else
{
loaded = ikvb_parse_file(path);
if (!loaded)
result = fail(context, "failed to parse original refresh file");
}
if (result == 0)
{
ikv_node_t *items = ikv_object_add_array(updated, "items", IKV_STRING);
if (!items)
{
result = fail(context, "failed to build updated refresh array");
}
else
{
ikv_object_set_int(updated, "count", 2);
ikv_object_set_string(updated, "name", "after");
ikv_array_add_string(items, "x");
ikv_array_add_string(items, "y");
if (!ikvb_write_file(path, updated))
result = fail(context, "failed to write updated refresh file");
}
}
if (result == 0 && !ikv_refresh_from_path(loaded, path))
result = fail(context, "refresh_from_path failed");
if (result == 0 &&
(result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 2, "refresh count mismatch")) == 0 &&
(result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "name")), "after", "refresh string mismatch")) == 0)
{
ikv_node_t *items = ikv_object_get(loaded, "items");
if (!items)
result = fail(context, "refresh lazy array lookup failed");
else if ((result = expect_true(context, ikv_array_size(items) == 2u, "refresh array size mismatch")) == 0)
result = expect_string(context, ikv_as_string(ikv_array_get(items, 1u)), "y", "refresh array value mismatch");
}
cleanup_file(path);
ikv_free(loaded);
ikv_free(original);
ikv_free(updated);
return result;
}
static int test_refresh_from_path_null_guards(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create null-guard root");
if ((result = expect_true(context, !ikv_refresh_from_path(NULL, "missing.ikv"), "refresh should reject null root")) == 0)
result = expect_true(context, !ikv_refresh_from_path(root, NULL), "refresh should reject null path");
ikv_free(root);
return result;
}
static int test_text_refresh_from_path(test_context_t *context)
{
const char *path = "demo_refresh_text.ikv";
ikv_node_t *original = ikv_create_object("before_root");
ikv_node_t *updated = ikv_create_object("after_root");
ikv_node_t *loaded = NULL;
int result = 0;
cleanup_file(path);
if (!original || !updated)
{
ikv_free(original);
ikv_free(updated);
return fail(context, "failed to create text refresh roots");
}
ikv_object_set_int(original, "count", 1);
if (!ikv_write_file_version(path, original, IKV_VERSION_1))
result = fail(context, "failed to write original text refresh file");
else
loaded = ikv_parse_file(path);
if (result == 0 && !loaded)
result = fail(context, "failed to parse original text refresh file");
if (result == 0)
{
ikv_object_set_int(updated, "count", 4);
ikv_object_set_string(updated, "mode", "text");
if (!ikv_write_file_version(path, updated, IKV_VERSION_2))
result = fail(context, "failed to write updated text refresh file");
}
if (result == 0 && !ikv_refresh_from_path(loaded, path))
result = fail(context, "text refresh_from_path failed");
if (result == 0 &&
(result = expect_string(context, ikv_node_key(loaded), "after_root", "text refresh root key mismatch")) == 0 &&
(result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 4, "text refresh count mismatch")) == 0)
{
result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "mode")), "text", "text refresh mode mismatch");
}
cleanup_file(path);
ikv_free(loaded);
ikv_free(original);
ikv_free(updated);
return result;
}
static int test_binary_v1_refresh_from_path(test_context_t *context)
{
const char *path = "demo_refresh_v1.ikvb";
ikv_node_t *original = ikv_create_object("root");
ikv_node_t *updated = ikv_create_object("root");
ikv_node_t *loaded = NULL;
int result = 0;
cleanup_file(path);
if (!original || !updated)
{
ikv_free(original);
ikv_free(updated);
return fail(context, "failed to create v1 refresh roots");
}
ikv_object_set_bool(original, "legacy", true);
if (!ikvb_write_file_version(path, original, IKV_VERSION_1))
result = fail(context, "failed to write original v1 refresh file");
else
loaded = ikvb_parse_file(path);
if (result == 0 && !loaded)
result = fail(context, "failed to parse original v1 refresh file");
if (result == 0)
{
ikv_object_set_bool(updated, "legacy", false);
ikv_object_set_int(updated, "revision", 2);
if (!ikvb_write_file_version(path, updated, IKV_VERSION_1))
result = fail(context, "failed to write updated v1 refresh file");
}
if (result == 0 && !ikv_refresh_from_path(loaded, path))
result = fail(context, "v1 refresh_from_path failed");
if (result == 0 &&
(result = expect_true(context, !ikv_as_bool(ikv_object_get(loaded, "legacy")), "v1 refresh bool mismatch")) == 0)
{
result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "revision")) == 2, "v1 refresh int mismatch");
}
cleanup_file(path);
ikv_free(loaded);
ikv_free(original);
ikv_free(updated);
return result;
}
static int test_refresh_from_path_failure_preserves_state(test_context_t *context)
{
const char *path = "demo_refresh_preserve.ikvb";
ikv_node_t *root = ikv_create_object("root");
ikv_node_t *loaded = NULL;
FILE *file = NULL;
int result = 0;
cleanup_file(path);
if (!root)
return fail(context, "failed to create preserve root");
ikv_object_set_int(root, "count", 9);
ikv_object_set_string(root, "name", "stable");
if (!ikvb_write_file(path, root))
result = fail(context, "failed to write preserve file");
else
loaded = ikvb_parse_file(path);
if (result == 0 && !loaded)
result = fail(context, "failed to parse preserve file");
file = fopen(path, "wb");
if (result == 0 && !file)
result = fail(context, "failed to open preserve file for corruption");
if (result == 0)
{
static const uint8_t bad_bytes[] = { 'n', 'o', 'p', 'e' };
if (fwrite(bad_bytes, 1u, sizeof(bad_bytes), file) != sizeof(bad_bytes))
result = fail(context, "failed to corrupt preserve file");
fclose(file);
file = NULL;
}
if (result == 0 &&
(result = expect_true(context, !ikv_refresh_from_path(loaded, path), "refresh should fail for invalid file")) == 0 &&
(result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "count")) == 9, "failed refresh should preserve count")) == 0)
{
result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "name")), "stable", "failed refresh should preserve name");
}
if (file)
fclose(file);
cleanup_file(path);
ikv_free(loaded);
ikv_free(root);
return result;
}
static int test_binary_v2_refresh_replaces_keys(test_context_t *context)
{
const char *path = "demo_refresh_replace.ikvb";
ikv_node_t *original = ikv_create_object("root");
ikv_node_t *updated = ikv_create_object("root");
ikv_node_t *loaded = NULL;
int result = 0;
cleanup_file(path);
if (!original || !updated)
{
ikv_free(original);
ikv_free(updated);
return fail(context, "failed to create replace-key roots");
}
ikv_object_set_string(original, "old_only", "legacy");
if (!ikvb_write_file(path, original))
result = fail(context, "failed to write original replace-key file");
else
loaded = ikvb_parse_file(path);
if (result == 0 && !loaded)
result = fail(context, "failed to parse original replace-key file");
if (result == 0)
{
ikv_node_t *items = ikv_object_add_array(updated, "items", IKV_STRING);
if (!items)
{
result = fail(context, "failed to create replace-key array");
}
else
{
ikv_object_set_string(updated, "new_only", "fresh");
ikv_array_add_string(items, "n1");
ikv_array_add_string(items, "n2");
if (!ikvb_write_file(path, updated))
result = fail(context, "failed to write updated replace-key file");
}
}
if (result == 0)
{
if (!ikv_object_get(loaded, "old_only"))
result = fail(context, "failed to preload old key before refresh");
else if (!ikv_refresh_from_path(loaded, path))
result = fail(context, "replace-key refresh_from_path failed");
}
if (result == 0 &&
(result = expect_true(context, ikv_object_get(loaded, "old_only") == NULL, "old key should be removed after refresh")) == 0 &&
(result = expect_string(context, ikv_as_string(ikv_object_get(loaded, "new_only")), "fresh", "new key mismatch after refresh")) == 0)
{
ikv_node_t *items = ikv_object_get(loaded, "items");
if (!items)
result = fail(context, "items key missing after refresh");
else if ((result = expect_true(context, ikv_array_size(items) == 2u, "items size mismatch after refresh")) == 0)
result = expect_string(context, ikv_as_string(ikv_array_get(items, 0u)), "n1", "items value mismatch after refresh");
}
cleanup_file(path);
ikv_free(loaded);
ikv_free(original);
ikv_free(updated);
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 int test_object_rehash_many_keys(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
char key[32];
int result = 0;
if (!root)
return fail(context, "failed to create root");
for (int i = 0; i < 128; ++i)
{
snprintf(key, sizeof(key), "key_%d", i);
ikv_object_set_int(root, key, i);
}
if ((result = expect_true(context, ikv_object_size(root) == 128u, "rehash object size mismatch")) == 0)
{
snprintf(key, sizeof(key), "key_%d", 127);
result = expect_true(context, ikv_as_int(ikv_object_get(root, key)) == 127, "rehash lookup mismatch");
}
ikv_free(root);
return result;
}
static int test_array_bool_roundtrip(test_context_t *context)
{
ikv_node_t *arr = ikv_create_array("flags", IKV_BOOL);
int result = 0;
if (!arr)
return fail(context, "failed to create bool array");
ikv_array_add_bool(arr, true);
ikv_array_add_bool(arr, false);
if ((result = expect_true(context, ikv_array_size(arr) == 2u, "bool array size mismatch")) == 0 &&
(result = expect_true(context, ikv_as_bool(ikv_array_get(arr, 0u)), "bool array first value mismatch")) == 0)
{
result = expect_true(context, !ikv_as_bool(ikv_array_get(arr, 1u)), "bool array second value mismatch");
}
ikv_free(arr);
return result;
}
static int test_array_float_roundtrip(test_context_t *context)
{
ikv_node_t *arr = ikv_create_array("values", IKV_FLOAT);
int result = 0;
if (!arr)
return fail(context, "failed to create float array");
ikv_array_add_float(arr, 1.25);
ikv_array_add_float(arr, 2.5);
if ((result = expect_true(context, ikv_array_size(arr) == 2u, "float array size mismatch")) == 0 &&
(result = expect_true(context, ikv_as_float(ikv_array_get(arr, 0u)) > 1.2, "float array first value mismatch")) == 0)
{
result = expect_true(context, ikv_as_float(ikv_array_get(arr, 1u)) > 2.4, "float array second value mismatch");
}
ikv_free(arr);
return result;
}
static int test_accessor_defaults(test_context_t *context)
{
if (!expect_string(context, ikv_as_string(NULL), "", "null string accessor") &&
!expect_true(context, ikv_as_int(NULL) == 0, "null int accessor") &&
!expect_true(context, ikv_as_float(NULL) == 0.0, "null float accessor") &&
!expect_true(context, !ikv_as_bool(NULL), "null bool accessor"))
{
return 0;
}
return 1;
}
static int test_array_out_of_bounds_access(test_context_t *context)
{
ikv_node_t *arr = ikv_create_array("arr", IKV_INT);
int result = 0;
if (!arr)
return fail(context, "failed to create array");
ikv_array_add_int(arr, 1);
if ((result = expect_true(context, ikv_array_get(arr, 1u) == NULL, "out of bounds array access should be null")) == 0)
result = expect_true(context, ikv_array_get(NULL, 0u) == NULL, "null array access should be null");
ikv_free(arr);
return result;
}
static int test_text_comments_and_plain_root(test_context_t *context)
{
const char *src =
"# comment\n"
"\"name\" \"value\", // comment\n"
"\"count\" 3\n";
ikv_node_t *root = ikv_parse_string(src);
int result = 0;
if (!root)
return fail(context, "failed to parse commented text");
if ((result = expect_string(context, ikv_as_string(ikv_object_get(root, "name")), "value", "comment text name mismatch")) == 0)
result = expect_true(context, ikv_as_int(ikv_object_get(root, "count")) == 3, "comment text count mismatch");
ikv_free(root);
return result;
}
static int test_detect_binary_version_v2(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2))
result = fail(context, "failed to build v2 buffer");
else
result = expect_true(context, ikv_detect_binary_version(data, size) == IKV_VERSION_2, "binary v2 detection mismatch");
free(data);
ikv_free(root);
return result;
}
static int expect_invalid_text(test_context_t *context, const char *src, const char *label)
{
ikv_node_t *node = ikv_parse_string(src);
if (node)
{
ikv_free(node);
return fail(context, label);
}
return 0;
}
static int expect_invalid_binary(test_context_t *context, const uint8_t *data, size_t size, const char *label)
{
ikv_node_t *node = ikvb_parse_memory(data, size);
if (node)
{
ikv_free(node);
return fail(context, label);
}
return 0;
}
#define DEFINE_INVALID_TEXT_CASE(fn_name, src_literal) \
static int fn_name(test_context_t *context) \
{ \
return expect_invalid_text(context, src_literal, #fn_name); \
}
#define DEFINE_INVALID_BINARY_CASE(fn_name, array_name, ...) \
static int fn_name(test_context_t *context) \
{ \
static const uint8_t array_name[] = {__VA_ARGS__}; \
return expect_invalid_binary(context, array_name, sizeof(array_name), #fn_name); \
}
DEFINE_INVALID_TEXT_CASE(test_invalid_text_header_only, "ikv1")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_missing_root_brace, "ikv1 \"root\"")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_wrong_root_token, "ikv1 \"root\" [")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_unclosed_object, "ikv1 \"root\" {")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_unclosed_array, "ikv1 \"root\" { \"a\" [")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_missing_value, "ikv1 \"root\" { \"a\" }")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_bad_plain_pair, "\"a\"")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_bad_string_escape, "ikv1 \"root\" { \"a\\")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_unterminated_string, "ikv1 \"root\" { \"a\" \"unterminated")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_nested_object_truncated, "ikv2 \"root\" { \"a\" { \"b\" 1 ")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_nested_array_truncated, "ikv2 \"root\" { \"a\" [ 1 2 ")
DEFINE_INVALID_TEXT_CASE(test_invalid_text_root_name_missing, "ikv2 {")
static int test_invalid_binary_short_0(test_context_t *context)
{
return expect_invalid_binary(context, NULL, 0u, "test_invalid_binary_short_0");
}
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_short_1, b1, 'i')
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_short_2, b2, 'i', 'K')
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_short_3, b3, 'i', 'K', 'v')
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_bad_magic, b4, 'x', 'K', 'v', '1', 'b', 1, 0, 0, 0)
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_bad_version_digit, b5, 'i', 'K', 'v', '9', 'b', 1, 0, 0, 0)
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_missing_kind, b6, 'i', 'K', 'v', '1', 'x', 1, 0, 0, 0)
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_bad_v1_version, b7, 'i', 'K', 'v', '1', 'b', 2, 0, 0, 0)
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_truncated_root_name, b8, 'i', 'K', 'v', '1', 'b', 1, 0, 0, 0, 4)
DEFINE_INVALID_BINARY_CASE(test_invalid_binary_v2_flags_missing, b9, 'i', 'K', 'v', '2', 'b', 2, 0, 0, 0, 0, 0, 0, 0)
static int test_invalid_binary_v2_truncated_index(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2))
result = fail(context, "failed to build v2 buffer");
else if (size > 4u)
result = expect_invalid_binary(context, data, size - 4u, "truncated v2 buffer should fail");
free(data);
ikv_free(root);
return result;
}
static int test_invalid_binary_v2_payload_offset_oob(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2))
result = fail(context, "failed to build v2 buffer");
else if (size < 24u)
result = fail(context, "unexpected v2 buffer size");
else
{
data[size - 8u] = 0xFFu;
data[size - 7u] = 0xFFu;
data[size - 6u] = 0xFFu;
data[size - 5u] = 0x7Fu;
if (ikvb_parse_memory(data, size) != NULL)
result = fail(context, "oob payload offset should fail indexed parse");
}
free(data);
ikv_free(root);
return result;
}
static int test_invalid_binary_v2_payload_size_oob(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2))
result = fail(context, "failed to build v2 buffer");
else if (size < 4u)
result = fail(context, "unexpected v2 buffer size");
else
{
data[size - 4u] = 0xFFu;
data[size - 3u] = 0xFFu;
data[size - 2u] = 0xFFu;
data[size - 1u] = 0x7Fu;
if (ikvb_parse_memory(data, size) != NULL)
result = fail(context, "oob payload size should fail indexed parse");
}
free(data);
ikv_free(root);
return result;
}
#ifdef IKV_TESTING
static int test_internal_binary_node_memory_roundtrip(test_context_t *context)
{
ikv_node_t *node = ikv_create_object("child");
ikv_node_t *parsed = NULL;
uint8_t *data = NULL;
uint32_t size = 0u;
size_t consumed = 0u;
int result = 0;
if (!node)
return fail(context, "failed to create node");
ikv_object_set_int(node, "value", 7);
if (!ikv__write_binary_node_memory(node, &data, &size))
result = fail(context, "failed to write node memory");
else
{
parsed = ikv__parse_binary_node_memory(data, size, &consumed);
if (!parsed)
result = fail(context, "failed to parse node memory");
else if ((result = expect_true(context, consumed == size, "node memory consumed mismatch")) == 0)
result = expect_true(context, ikv_as_int(ikv_object_get(parsed, "value")) == 7, "node memory value mismatch");
}
ikv_free(parsed);
free(data);
ikv_free(node);
return result;
}
static int test_internal_object_attach_loaded_node(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
ikv_node_t *child = ikv_create_object("child");
int result = 0;
if (!root || !child)
{
ikv_free(root);
ikv_free(child);
return fail(context, "failed to create nodes");
}
ikv__object_attach_loaded_node(root, child);
if ((result = expect_true(context, ikv_object_get(root, "child") == child, "attached child missing")) == 0)
result = expect_true(context, ikv_object_size(root) == 0u, "attach helper should not change advertised size");
ikv_free(root);
return result;
}
static int test_internal_object_reserve_for_count(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
if ((result = expect_true(context, ikv__object_reserve_for_count(root, 512u), "reserve helper failed")) == 0)
result = expect_true(context, root->value.object.bucket_count >= 512u, "reserve bucket count too small");
ikv_free(root);
return result;
}
static int test_internal_parse_binary_node_invalid(test_context_t *context)
{
static const uint8_t bad_node[] = {0xFFu};
return expect_true(context, ikv__parse_binary_node_memory(bad_node, sizeof(bad_node), NULL) == NULL, "invalid node memory should fail");
}
static int test_hook_alloc_fail_create_object_key(test_context_t *context)
{
ikv_test_fail_alloc_after(1);
return expect_true(context, ikv_create_object("root") == NULL, "alloc fail should break create_object key dup");
}
static int test_hook_alloc_fail_create_object_buckets(test_context_t *context)
{
ikv_test_fail_alloc_after(2);
return expect_true(context, ikv_create_object("root") == NULL, "alloc fail should break object buckets");
}
static int test_hook_alloc_fail_create_array(test_context_t *context)
{
ikv_test_fail_alloc_after(1);
return expect_true(context, ikv_create_array("arr", IKV_STRING) == NULL, "alloc fail should break create_array");
}
static int test_hook_alloc_fail_object_set_string(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_test_fail_alloc_after(2);
ikv_object_set_string(root, "name", "value");
result = expect_true(context, ikv_object_get(root, "name") == NULL, "failed set_string should not insert key");
ikv_free(root);
return result;
}
static int test_hook_alloc_fail_array_add_string(test_context_t *context)
{
ikv_node_t *arr = ikv_create_array("arr", IKV_STRING);
int result = 0;
if (!arr)
return fail(context, "failed to create array");
ikv_test_fail_alloc_after(2);
ikv_array_add_string(arr, "value");
result = expect_true(context, ikv_array_size(arr) == 0u, "failed array add should not append");
ikv_free(arr);
return result;
}
static int test_hook_alloc_fail_text_parse(test_context_t *context)
{
ikv_test_fail_alloc_after(1);
return expect_true(context, ikv_parse_string("ikv1 \"root\" { \"a\" 1 }") == NULL, "alloc fail should break text parse");
}
static int test_hook_fopen_fail_parse_file(test_context_t *context)
{
const char *path = "hook_text.ikv";
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikv_write_file(path, root))
result = fail(context, "prep write failed");
else
{
ikv_test_fail_fopen_after(2);
result = expect_true(context, ikv_parse_file(path) == NULL, "fopen fail should break parse_file");
}
cleanup_file(path);
ikv_free(root);
return result;
}
static int test_hook_fseek_fail_parse_file(test_context_t *context)
{
const char *path = "hook_text.ikv";
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikv_write_file(path, root))
result = fail(context, "prep write failed");
else
{
ikv_test_fail_fseek_after(1);
result = expect_true(context, ikv_parse_file(path) == NULL, "fseek fail should break parse_file");
}
cleanup_file(path);
ikv_free(root);
return result;
}
static int test_hook_ftell_fail_parse_file(test_context_t *context)
{
const char *path = "hook_text.ikv";
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikv_write_file(path, root))
result = fail(context, "prep write failed");
else
{
ikv_test_fail_ftell_after(1);
result = expect_true(context, ikv_parse_file(path) == NULL, "ftell fail should break parse_file");
}
cleanup_file(path);
ikv_free(root);
return result;
}
static int test_hook_fread_fail_parse_file(test_context_t *context)
{
const char *path = "hook_text.ikv";
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikv_write_file(path, root))
result = fail(context, "prep write failed");
else
{
ikv_test_fail_fread_after(2);
result = expect_true(context, ikv_parse_file(path) == NULL, "fread fail should break parse_file");
}
cleanup_file(path);
ikv_free(root);
return result;
}
static int test_hook_fopen_fail_binary_write(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_test_fail_fopen_after(1);
result = expect_true(context, !ikvb_write_file("hook_v2.ikvb", root), "fopen fail should break binary write");
cleanup_file("hook_v2.ikvb");
ikv_free(root);
return result;
}
static int test_hook_fopen_fail_detect_file_version(test_context_t *context)
{
const char *path = "hook_detect.ikv";
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikv_write_file_version(path, root, IKV_VERSION_2))
result = fail(context, "prep detect write failed");
else
{
ikv_test_fail_fopen_after(1);
result = expect_true(context, ikv_detect_file_version(path, false) == IKV_VERSION_UNKNOWN, "fopen fail should break detect_file_version");
}
cleanup_file(path);
ikv_free(root);
return result;
}
static int test_hook_fread_fail_detect_file_version(test_context_t *context)
{
const char *path = "hook_detect.ikv";
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikv_write_file_version(path, root, IKV_VERSION_2))
result = fail(context, "prep detect write failed");
else
{
ikv_test_fail_fread_after(1);
result = expect_true(context, ikv_detect_file_version(path, false) == IKV_VERSION_UNKNOWN, "fread fail should break detect_file_version");
}
cleanup_file(path);
ikv_free(root);
return result;
}
static int test_hook_fclose_fail_binary_write(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_test_fail_fclose_after(1);
result = expect_true(context, !ikvb_write_file("hook_v2.ikvb", root), "fclose fail should break binary write");
cleanup_file("hook_v2.ikvb");
ikv_free(root);
return result;
}
static int test_hook_lazy_load_fseek_fail(test_context_t *context)
{
const char *path = "hook_lazy.ikvb";
ikv_node_t *root = ikv_create_object("root");
ikv_node_t *loaded = NULL;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_file(path, root))
result = fail(context, "prep binary write failed");
else
{
loaded = ikvb_parse_file(path);
if (!loaded)
result = fail(context, "prep binary parse failed");
else
{
ikv_test_fail_fseek_after(1);
result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "a")) == 1, "memory-backed lazy load should ignore fseek hooks");
}
}
cleanup_file(path);
ikv_free(loaded);
ikv_free(root);
return result;
}
static int test_hook_lazy_load_fread_fail(test_context_t *context)
{
const char *path = "hook_lazy.ikvb";
ikv_node_t *root = ikv_create_object("root");
ikv_node_t *loaded = NULL;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_file(path, root))
result = fail(context, "prep binary write failed");
else
{
loaded = ikvb_parse_file(path);
if (!loaded)
result = fail(context, "prep binary parse failed");
else
{
ikv_test_fail_fread_after(1);
result = expect_true(context, ikv_as_int(ikv_object_get(loaded, "a")) == 1, "memory-backed lazy load should ignore fread hooks");
}
}
cleanup_file(path);
ikv_free(loaded);
ikv_free(root);
return result;
}
static int test_hook_alloc_fail_internal_v2_parse_index(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2))
result = fail(context, "prep v2 build failed");
else
{
ikv_test_fail_alloc_after(1);
result = expect_true(context, ikvb_parse_memory_version(data, size, IKV_VERSION_2) == NULL, "alloc fail should break v2 parse");
}
free(data);
ikv_free(root);
return result;
}
static int test_hook_alloc_fail_internal_v2_memory_copy(test_context_t *context)
{
ikv_node_t *root = ikv_create_object("root");
uint8_t *data = NULL;
uint32_t size = 0u;
int result = 0;
if (!root)
return fail(context, "failed to create root");
ikv_object_set_int(root, "a", 1);
if (!ikvb_write_memory_version(root, &data, &size, IKV_VERSION_2))
result = fail(context, "prep v2 build failed");
else
{
ikv_test_fail_alloc_after(1);
result = expect_true(context, ikvb_parse_memory(data, size) == NULL, "alloc fail should break v2 memory copy");
}
free(data);
ikv_free(root);
return result;
}
#endif
typedef struct
{
unsigned int scale;
unsigned int iterations;
double avg_index_us;
double avg_read_us;
double avg_write_us;
} benchmark_sample_t;
typedef enum
{
BENCHMARK_FORMAT_TEXT_V1 = 0,
BENCHMARK_FORMAT_TEXT_V2,
BENCHMARK_FORMAT_BINARY_V1,
BENCHMARK_FORMAT_BINARY_V2
} benchmark_format_t;
static const char *benchmark_big_o_label(double exponent)
{
if (exponent < 0.15)
return "O(1)";
if (exponent < 0.50)
return "O(log n)";
if (exponent < 1.20)
return "O(n)";
if (exponent < 1.60)
return "O(n log n)";
if (exponent < 2.40)
return "O(n^2)";
return "O(n^k)";
}
static double benchmark_fit_exponent(const benchmark_sample_t *samples, size_t count, int metric)
{
double sum_x = 0.0;
double sum_y = 0.0;
double sum_xx = 0.0;
double sum_xy = 0.0;
size_t used = 0u;
for (size_t i = 0; i < count; ++i)
{
double y = 0.0;
double x = 0.0;
if (!samples || samples[i].scale == 0u)
continue;
switch (metric)
{
case 0:
y = samples[i].avg_index_us;
break;
case 1:
y = samples[i].avg_read_us;
break;
default:
y = samples[i].avg_write_us;
break;
}
if (y <= 0.0)
continue;
x = log((double)samples[i].scale);
y = log(y);
sum_x += x;
sum_y += y;
sum_xx += x * x;
sum_xy += x * y;
++used;
}
if (used < 2u)
return 0.0;
if (fabs((double)used * sum_xx - sum_x * sum_x) < 1e-9)
return 0.0;
return (((double)used * sum_xy) - (sum_x * sum_y)) / (((double)used * sum_xx) - (sum_x * sum_x));
}
static const char *benchmark_format_name(benchmark_format_t format)
{
switch (format)
{
case BENCHMARK_FORMAT_TEXT_V1:
return "v1-text";
case BENCHMARK_FORMAT_TEXT_V2:
return "v2-text";
case BENCHMARK_FORMAT_BINARY_V1:
return "v1-bin";
case BENCHMARK_FORMAT_BINARY_V2:
return "v2-bin";
default:
return "unknown";
}
}
static const char *benchmark_format_path(benchmark_format_t format)
{
switch (format)
{
case BENCHMARK_FORMAT_TEXT_V1:
return "demo_benchmark_v1.ikv";
case BENCHMARK_FORMAT_TEXT_V2:
return "demo_benchmark_v2.ikv";
case BENCHMARK_FORMAT_BINARY_V1:
return "demo_benchmark_v1.ikvb";
case BENCHMARK_FORMAT_BINARY_V2:
return "demo_benchmark_v2.ikvb";
default:
return "demo_benchmark.ikv";
}
}
static bool benchmark_write_file(benchmark_format_t format, const char *path, const ikv_node_t *root)
{
switch (format)
{
case BENCHMARK_FORMAT_TEXT_V1:
return ikv_write_file_version(path, root, IKV_VERSION_1);
case BENCHMARK_FORMAT_TEXT_V2:
return ikv_write_file_version(path, root, IKV_VERSION_2);
case BENCHMARK_FORMAT_BINARY_V1:
return ikvb_write_file_version(path, root, IKV_VERSION_1);
case BENCHMARK_FORMAT_BINARY_V2:
return ikvb_write_file_version(path, root, IKV_VERSION_2);
default:
return false;
}
}
static ikv_node_t *benchmark_parse_file(benchmark_format_t format, const char *path)
{
switch (format)
{
case BENCHMARK_FORMAT_TEXT_V1:
return ikv_parse_file_version(path, IKV_VERSION_1);
case BENCHMARK_FORMAT_TEXT_V2:
return ikv_parse_file_version(path, IKV_VERSION_2);
case BENCHMARK_FORMAT_BINARY_V1:
return ikvb_parse_file_version(path, IKV_VERSION_1);
case BENCHMARK_FORMAT_BINARY_V2:
return ikvb_parse_file_version(path, IKV_VERSION_2);
default:
return NULL;
}
}
static bool benchmark_append_summary(
test_context_t *context,
size_t *summary_offset,
const char *format,
const char *operation,
double exponent,
const benchmark_sample_t *samples)
{
int wrote = 0;
if (!context || !summary_offset || !format || !operation || !samples)
return false;
wrote = snprintf(
context->benchmark_summary + *summary_offset,
sizeof(context->benchmark_summary) - *summary_offset,
"| %-7s | %-5s | %-13s | %9.2f | %9.2f | %9.2f | %9.2f |\n",
format,
operation,
"",
samples[0].avg_index_us,
samples[1].avg_index_us,
samples[2].avg_index_us,
samples[3].avg_index_us);
if (strcmp(operation, "read") == 0)
{
wrote = snprintf(
context->benchmark_summary + *summary_offset,
sizeof(context->benchmark_summary) - *summary_offset,
"| %-7s | %-5s | %-13s | %9.2f | %9.2f | %9.2f | %9.2f |\n",
format,
operation,
"",
samples[0].avg_read_us,
samples[1].avg_read_us,
samples[2].avg_read_us,
samples[3].avg_read_us);
}
else if (strcmp(operation, "write") == 0)
{
wrote = snprintf(
context->benchmark_summary + *summary_offset,
sizeof(context->benchmark_summary) - *summary_offset,
"| %-7s | %-5s | %-13s | %9.2f | %9.2f | %9.2f | %9.2f |\n",
format,
operation,
"",
samples[0].avg_write_us,
samples[1].avg_write_us,
samples[2].avg_write_us,
samples[3].avg_write_us);
}
if (wrote >= 0 && (size_t)wrote < sizeof(context->benchmark_summary) - *summary_offset)
{
char growth[32];
snprintf(growth, sizeof(growth), "%s^%.2f", benchmark_big_o_label(exponent), exponent);
memcpy(context->benchmark_summary + *summary_offset + 20u, growth, strlen(growth));
}
if (wrote < 0 || (size_t)wrote >= sizeof(context->benchmark_summary) - *summary_offset)
return false;
*summary_offset += (size_t)wrote;
return true;
}
static int test_binary_v2_file_benchmark(test_context_t *context)
{
static const unsigned int benchmark_scales[] = {1u, 4u, 16u, 64u};
static const benchmark_format_t benchmark_formats[] = {
BENCHMARK_FORMAT_TEXT_V1,
BENCHMARK_FORMAT_TEXT_V2,
BENCHMARK_FORMAT_BINARY_V1,
BENCHMARK_FORMAT_BINARY_V2
};
enum { benchmark_scale_count = 4, benchmark_iterations_small = 1000, benchmark_iterations_medium = 400, benchmark_iterations_large = 150 };
benchmark_sample_t samples[4][benchmark_scale_count];
size_t summary_offset = 0u;
int result = 0;
memset(samples, 0, sizeof(samples));
context->benchmark_summary[0] = 0;
for (unsigned int format_index = 0; result == 0 && format_index < 4u; ++format_index)
{
benchmark_format_t format = benchmark_formats[format_index];
const char *path = benchmark_format_path(format);
cleanup_file(path);
for (unsigned int sample_index = 0; result == 0 && sample_index < benchmark_scale_count; ++sample_index)
{
unsigned int scale = benchmark_scales[sample_index];
unsigned int iterations = (scale <= 4u) ? benchmark_iterations_small : ((scale <= 16u) ? benchmark_iterations_medium : benchmark_iterations_large);
int64_t expected_count_value = 1337;
ikv_node_t *root = create_benchmark_root(scale);
double total_index_us = 0.0;
double total_read_us = 0.0;
double total_write_us = 0.0;
if (!root)
{
result = fail(context, "failed to create benchmark root");
break;
}
if (!benchmark_write_file(format, path, root))
{
ikv_free(root);
result = fail(context, "failed to prepare benchmark file");
break;
}
for (unsigned int i = 0; result == 0 && i < iterations; ++i)
{
double start_time = benchmark_now_us();
ikv_node_t *loaded = benchmark_parse_file(format, path);
double end_time = benchmark_now_us();
if (!loaded)
result = fail(context, "benchmark index parse failed");
else
total_index_us += end_time - start_time;
ikv_free(loaded);
}
for (unsigned int i = 0; result == 0 && i < iterations; ++i)
{
ikv_node_t *loaded = benchmark_parse_file(format, path);
ikv_node_t *count = NULL;
double start_time = 0.0;
double end_time = 0.0;
if (!loaded)
{
result = fail(context, "benchmark read parse failed");
break;
}
start_time = benchmark_now_us();
count = ikv_object_get(loaded, "count");
end_time = benchmark_now_us();
if ((result = expect_true(context, count != NULL, "benchmark read count lookup failed")) == 0 &&
(result = expect_true(context, ikv_as_int(count) == expected_count_value, "benchmark read count value mismatch")) == 0)
{
total_read_us += end_time - start_time;
}
ikv_free(loaded);
}
for (unsigned int i = 0; result == 0 && i < iterations; ++i)
{
double start_time = benchmark_now_us();
bool ok = benchmark_write_file(format, path, root);
double end_time = benchmark_now_us();
if (!ok)
result = fail(context, "benchmark write failed");
else
total_write_us += end_time - start_time;
}
if (result == 0)
{
samples[format_index][sample_index].scale = scale;
samples[format_index][sample_index].iterations = iterations;
samples[format_index][sample_index].avg_index_us = total_index_us / (double)iterations;
samples[format_index][sample_index].avg_read_us = total_read_us / (double)iterations;
samples[format_index][sample_index].avg_write_us = total_write_us / (double)iterations;
}
ikv_free(root);
cleanup_file(path);
}
}
if (result == 0)
{
int wrote = snprintf(
context->benchmark_summary,
sizeof(context->benchmark_summary),
"+---------+-------+---------------+-----------+-----------+-----------+-----------+\n"
"| format | op | growth | n=1 us | n=4 us | n=16 us | n=64 us |\n"
"+---------+-------+---------------+-----------+-----------+-----------+-----------+\n");
if (wrote < 0 || (size_t)wrote >= sizeof(context->benchmark_summary))
return fail(context, "failed to format benchmark header");
summary_offset = (size_t)wrote;
for (unsigned int format_index = 0; format_index < 4u; ++format_index)
{
const benchmark_sample_t *format_samples = samples[format_index];
double index_exp = benchmark_fit_exponent(format_samples, benchmark_scale_count, 0);
double read_exp = benchmark_fit_exponent(format_samples, benchmark_scale_count, 1);
double write_exp = benchmark_fit_exponent(format_samples, benchmark_scale_count, 2);
const char *format_name = benchmark_format_name(benchmark_formats[format_index]);
if (!benchmark_append_summary(context, &summary_offset, format_name, "index", index_exp, format_samples) ||
!benchmark_append_summary(context, &summary_offset, format_name, "read", read_exp, format_samples) ||
!benchmark_append_summary(context, &summary_offset, format_name, "write", write_exp, format_samples))
{
result = fail(context, "failed to format benchmark summary");
break;
}
wrote = snprintf(
context->benchmark_summary + summary_offset,
sizeof(context->benchmark_summary) - summary_offset,
"+---------+-------+---------------+-----------+-----------+-----------+-----------+\n");
if (wrote < 0 || (size_t)wrote >= sizeof(context->benchmark_summary) - summary_offset)
{
result = fail(context, "failed to format benchmark divider");
break;
}
summary_offset += (size_t)wrote;
}
}
return result;
}
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},
{"object rehash many keys", test_object_rehash_many_keys},
{"array bool roundtrip", test_array_bool_roundtrip},
{"array float roundtrip", test_array_float_roundtrip},
{"accessor defaults", test_accessor_defaults},
{"array out of bounds access", test_array_out_of_bounds_access},
{"text parse variants", test_text_parse_variants},
{"text comments and plain root", test_text_comments_and_plain_root},
{"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},
{"refresh from path null guards", test_refresh_from_path_null_guards},
{"text refresh from path", test_text_refresh_from_path},
{"binary v1 refresh from path", test_binary_v1_refresh_from_path},
{"binary v2 refresh from path", test_binary_v2_refresh_from_path},
{"refresh from path failure preserves state", test_refresh_from_path_failure_preserves_state},
{"binary v2 refresh replaces keys", test_binary_v2_refresh_replaces_keys},
{"detection apis", test_detection_apis},
{"detect binary version v2", test_detect_binary_version_v2},
{"explicit version apis", test_explicit_version_apis},
{"invalid argument guards", test_invalid_argument_guards},
{"invalid text header only", test_invalid_text_header_only},
{"invalid text missing root brace", test_invalid_text_missing_root_brace},
{"invalid text wrong root token", test_invalid_text_wrong_root_token},
{"invalid text unclosed object", test_invalid_text_unclosed_object},
{"invalid text unclosed array", test_invalid_text_unclosed_array},
{"invalid text missing value", test_invalid_text_missing_value},
{"invalid text bad plain pair", test_invalid_text_bad_plain_pair},
{"invalid text bad string escape", test_invalid_text_bad_string_escape},
{"invalid text unterminated string", test_invalid_text_unterminated_string},
{"invalid text nested object truncated", test_invalid_text_nested_object_truncated},
{"invalid text nested array truncated", test_invalid_text_nested_array_truncated},
{"invalid text root name missing", test_invalid_text_root_name_missing},
{"invalid binary short 0", test_invalid_binary_short_0},
{"invalid binary short 1", test_invalid_binary_short_1},
{"invalid binary short 2", test_invalid_binary_short_2},
{"invalid binary short 3", test_invalid_binary_short_3},
{"invalid binary bad magic", test_invalid_binary_bad_magic},
{"invalid binary bad version digit", test_invalid_binary_bad_version_digit},
{"invalid binary missing kind", test_invalid_binary_missing_kind},
{"invalid binary bad v1 version", test_invalid_binary_bad_v1_version},
{"invalid binary truncated root name", test_invalid_binary_truncated_root_name},
{"invalid binary v2 flags missing", test_invalid_binary_v2_flags_missing},
{"invalid binary v2 truncated index", test_invalid_binary_v2_truncated_index},
{"invalid binary v2 payload offset oob", test_invalid_binary_v2_payload_offset_oob},
{"invalid binary v2 payload size oob", test_invalid_binary_v2_payload_size_oob},
#ifdef IKV_TESTING
{"internal binary node memory roundtrip", test_internal_binary_node_memory_roundtrip},
{"internal object attach loaded node", test_internal_object_attach_loaded_node},
{"internal object reserve for count", test_internal_object_reserve_for_count},
{"internal parse binary node invalid", test_internal_parse_binary_node_invalid},
{"hook alloc fail create object key", test_hook_alloc_fail_create_object_key},
{"hook alloc fail create object buckets", test_hook_alloc_fail_create_object_buckets},
{"hook alloc fail create array", test_hook_alloc_fail_create_array},
{"hook alloc fail object set string", test_hook_alloc_fail_object_set_string},
{"hook alloc fail array add string", test_hook_alloc_fail_array_add_string},
{"hook alloc fail text parse", test_hook_alloc_fail_text_parse},
{"hook fopen fail parse file", test_hook_fopen_fail_parse_file},
{"hook fseek fail parse file", test_hook_fseek_fail_parse_file},
{"hook ftell fail parse file", test_hook_ftell_fail_parse_file},
{"hook fread fail parse file", test_hook_fread_fail_parse_file},
{"hook fopen fail detect file version", test_hook_fopen_fail_detect_file_version},
{"hook fread fail detect file version", test_hook_fread_fail_detect_file_version},
{"hook fopen fail binary write", test_hook_fopen_fail_binary_write},
{"hook fclose fail binary write", test_hook_fclose_fail_binary_write},
{"hook lazy load fseek fail", test_hook_lazy_load_fseek_fail},
{"hook lazy load fread fail", test_hook_lazy_load_fread_fail},
{"hook alloc fail internal v2 parse index", test_hook_alloc_fail_internal_v2_parse_index},
{"hook alloc fail internal v2 memory copy", test_hook_alloc_fail_internal_v2_memory_copy},
#endif
{"binary v2 file benchmark", test_binary_v2_file_benchmark},
};
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');
}
static char benchmark_summary[8192];
static bool is_benchmark_case(const char *name)
{
return name && strstr(name, "benchmark") != NULL;
}
static int run_test_case(unsigned int index, unsigned int total)
{
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));
#ifdef IKV_TESTING
ikv_test_hooks_reset();
#endif
if (is_benchmark_case(test_cases[index].name))
{
printf("%s[%sINFO%s]%s starting benchmark [%u/%u] %s\n",
ANSI_WHITE,
ANSI_YELLOW,
ANSI_WHITE,
ANSI_RESET,
index + 1u,
total,
test_cases[index].name);
}
start_time = clock();
result = test_cases[index].fn(&context);
end_time = clock();
elapsed_ms = (long)(((end_time - start_time) * 1000) / CLOCKS_PER_SEC);
if (context.benchmark_summary[0] != 0)
snprintf(benchmark_summary, sizeof(benchmark_summary), "%s", context.benchmark_summary);
log_case(result == 0, index + 1u, total, elapsed_ms, test_cases[index].name, context.message);
return result;
}
static const test_case_t *find_test_case(const char *name, unsigned int *index_out)
{
const unsigned int total = (unsigned int)(sizeof(test_cases) / sizeof(test_cases[0]));
if (!name)
return NULL;
for (unsigned int i = 0; i < total; ++i)
{
if (strcmp(test_cases[i].name, name) == 0)
{
if (index_out)
*index_out = i;
return &test_cases[i];
}
}
return NULL;
}
int main(int argc, char **argv)
{
const unsigned int total = (unsigned int)(sizeof(test_cases) / sizeof(test_cases[0]));
unsigned int passed = 0u;
benchmark_summary[0] = 0;
if (argc == 3 && strcmp(argv[1], "--case") == 0)
{
unsigned int index = 0u;
if (!find_test_case(argv[2], &index))
{
fprintf(stderr, "unknown test case: %s\n", argv[2]);
return 2;
}
if (run_test_case(index, total) != 0)
return 1;
if (benchmark_summary[0] != 0)
printf("benchmark:\n%s", benchmark_summary);
return 0;
}
if (argc != 1)
{
fprintf(stderr, "usage: %s [--case <test name>]\n", argv[0]);
return 2;
}
for (unsigned int i = 0; i < total; ++i)
{
if (run_test_case(i, total) == 0)
++passed;
}
if (passed != total)
{
printf("%s[%sFAIL%s]%s %u/%u tests passed\n",
ANSI_WHITE,
ANSI_RED,
ANSI_WHITE,
ANSI_RESET,
passed,
total);
if (benchmark_summary[0] != 0)
printf("benchmark:\n%s", benchmark_summary);
return 1;
}
printf("%s[%sPASS%s]%s %u/%u tests passed\n",
ANSI_WHITE,
ANSI_GREEN,
ANSI_WHITE,
ANSI_RESET,
passed,
total);
if (benchmark_summary[0] != 0)
printf("benchmark:\n%s", benchmark_summary);
return 0;
}