2079 lines
70 KiB
C
2079 lines
70 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[4096];
|
|
} 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 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);
|
|
unsigned int expected_inventory_size = (scale * 8u < 5u) ? 5u : (scale * 8u);
|
|
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 *inventory = 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();
|
|
inventory = ikv_object_get(loaded, "inventory");
|
|
end_time = benchmark_now_us();
|
|
|
|
if ((result = expect_true(context, inventory != NULL, "benchmark read inventory lookup failed")) == 0 &&
|
|
(result = expect_true(context, ikv_array_size(inventory) == expected_inventory_size, "benchmark read inventory size 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)
|
|
{
|
|
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);
|
|
int wrote = snprintf(
|
|
context->benchmark_summary + summary_offset,
|
|
sizeof(context->benchmark_summary) - summary_offset,
|
|
"%s index %s^%.2f [n=%u %.2fus, n=%u %.2fus, n=%u %.2fus, n=%u %.2fus] "
|
|
"read %s^%.2f [n=%u %.2fus, n=%u %.2fus, n=%u %.2fus, n=%u %.2fus] "
|
|
"write %s^%.2f [n=%u %.2fus, n=%u %.2fus, n=%u %.2fus, n=%u %.2fus]%s",
|
|
benchmark_format_name(benchmark_formats[format_index]),
|
|
benchmark_big_o_label(index_exp), index_exp,
|
|
format_samples[0].scale, format_samples[0].avg_index_us,
|
|
format_samples[1].scale, format_samples[1].avg_index_us,
|
|
format_samples[2].scale, format_samples[2].avg_index_us,
|
|
format_samples[3].scale, format_samples[3].avg_index_us,
|
|
benchmark_big_o_label(read_exp), read_exp,
|
|
format_samples[0].scale, format_samples[0].avg_read_us,
|
|
format_samples[1].scale, format_samples[1].avg_read_us,
|
|
format_samples[2].scale, format_samples[2].avg_read_us,
|
|
format_samples[3].scale, format_samples[3].avg_read_us,
|
|
benchmark_big_o_label(write_exp), write_exp,
|
|
format_samples[0].scale, format_samples[0].avg_write_us,
|
|
format_samples[1].scale, format_samples[1].avg_write_us,
|
|
format_samples[2].scale, format_samples[2].avg_write_us,
|
|
format_samples[3].scale, format_samples[3].avg_write_us,
|
|
(format_index + 1u < 4u) ? "\n" : "");
|
|
|
|
if (wrote < 0)
|
|
{
|
|
result = fail(context, "failed to format benchmark summary");
|
|
break;
|
|
}
|
|
|
|
if ((size_t)wrote >= sizeof(context->benchmark_summary) - summary_offset)
|
|
{
|
|
summary_offset = sizeof(context->benchmark_summary) - 1u;
|
|
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},
|
|
{"binary v2 file benchmark", test_binary_v2_file_benchmark},
|
|
{"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
|
|
};
|
|
|
|
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[4096];
|
|
|
|
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
|
|
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: %s\n", 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: %s\n", 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: %s\n", benchmark_summary);
|
|
return 0;
|
|
}
|