calog/tests/testBroker.c

546 lines
18 KiB
C

// testBroker.c -- leak-checked unit tests for the broker core.
//
// Built with AddressSanitizer + UndefinedBehaviorSanitizer (see Makefile), so a
// clean run also proves there are no leaks, double-frees, or UB. Every test
// allocates and frees through the public API only.
#include "calogInternal.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define CHECK(cond, msg) checkImpl((cond), (msg), __FILE__, __LINE__)
#define ADD_ARITY 2
#define BULK_REGISTER 100
#define DEPTH_OVERSHOOT 5
#define LIST_ELEMS 3
#define NAME_BUFFER 32
static int32_t testsRun = 0;
static int32_t testsFailed = 0;
static int32_t releaseHookCalls = 0;
static void checkImpl(bool condition, const char *message, const char *file, int32_t line);
static void countingRelease(CalogFnT *callable);
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static int32_t nativeBoom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData);
static void testAggregate(void);
static void testAggregateCopyCleanup(void);
static void testAggregateDuplicateKey(void);
static void testBrokerFailFreesResult(void);
static void testCallable(void);
static void testCallableDead(void);
static void testDepthCap(void);
static void testMapDepthCap(void);
static void testRegistry(void);
static void testScalars(void);
static void testStringIndependence(void);
static void testStringNegativeLength(void);
static void testValueEquals(void);
static void checkImpl(bool condition, const char *message, const char *file, int32_t line) {
testsRun++;
if (!condition) {
testsFailed++;
printf("FAIL %s:%d %s\n", file, line, message);
}
}
static void countingRelease(CalogFnT *callable) {
(void)callable;
releaseHookCalls++;
}
static int32_t nativeAdd(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)userData;
if (argCount != ADD_ARITY || args[0].type != calogIntE || args[1].type != calogIntE) {
return calogFail(result, calogErrArgE, "nativeAdd expects two integers");
}
calogValueInt(result, args[0].as.i + args[1].as.i);
return calogOkE;
}
static int32_t nativeBoom(CalogValueT *args, int32_t argCount, CalogValueT *result, void *userData) {
(void)args;
(void)argCount;
(void)userData;
return calogFail(result, calogErrArgE, "nativeBoom always fails");
}
static void testAggregate(void) {
CalogAggT *list;
CalogAggT *map;
CalogValueT container;
CalogValueT copy;
CalogValueT element;
CalogValueT key;
CalogValueT *found;
int32_t status;
int64_t index;
status = calogAggCreate(&list, calogListE);
CHECK(status == calogOkE, "list create");
for (index = 0; index < LIST_ELEMS; index++) {
calogValueInt(&element, index + 1);
status = calogAggPush(list, &element);
CHECK(status == calogOkE, "list push status");
CHECK(element.type == calogNilE, "push consumes the value");
}
calogValueAgg(&container, list);
status = calogValueCopy(&copy, &container);
CHECK(status == calogOkE, "list deep copy status");
calogValueFree(&container);
CHECK(copy.type == calogAggE, "copy is aggregate");
CHECK(copy.as.agg->arrayCount == LIST_ELEMS, "copy array count");
CHECK(copy.as.agg->array[0].as.i == 1, "copy element 0");
CHECK(copy.as.agg->array[LIST_ELEMS - 1].as.i == LIST_ELEMS, "copy last element");
calogValueFree(&copy);
status = calogAggCreate(&map, calogMapE);
CHECK(status == calogOkE, "map create");
status = calogValueString(&key, "x", 1);
CHECK(status == calogOkE, "map key init");
calogValueBool(&element, true);
status = calogAggSet(map, &key, &element);
CHECK(status == calogOkE, "map set status");
CHECK(key.type == calogNilE && element.type == calogNilE, "set consumes key and value");
calogValueString(&key, "x", 1);
found = calogAggGet(map, &key);
calogValueFree(&key);
CHECK(found != NULL && found->type == calogBoolE && found->as.b == true, "map get hit");
calogValueString(&key, "missing", 7);
found = calogAggGet(map, &key);
calogValueFree(&key);
CHECK(found == NULL, "map get miss");
calogValueAgg(&container, map);
calogValueFree(&container);
}
static void testAggregateCopyCleanup(void) {
CalogFnT *callable;
CalogAggT *list;
CalogValueT container;
CalogValueT copy;
CalogValueT element;
CalogValueT deepValue;
int32_t status;
int32_t level;
releaseHookCalls = 0;
status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
CHECK(status == calogOkE, "cleanup callable create");
status = calogAggCreate(&list, calogListE);
CHECK(status == calogOkE, "cleanup list create");
calogValueString(&element, "sibling", 7);
calogAggPush(list, &element);
calogValueFn(&element, callable);
calogAggPush(list, &element);
calogValueInt(&deepValue, 0);
for (level = 0; level < CALOG_MAX_DEPTH + DEPTH_OVERSHOOT; level++) {
CalogAggT *wrapper;
calogAggCreate(&wrapper, calogListE);
calogAggPush(wrapper, &deepValue);
calogValueAgg(&deepValue, wrapper);
}
calogAggPush(list, &deepValue);
calogValueAgg(&container, list);
status = calogValueCopy(&copy, &container);
CHECK(status == calogErrDepthE, "cleanup copy hits depth cap");
CHECK(copy.type == calogNilE, "cleanup copy left nil");
CHECK(releaseHookCalls == 0, "failed copy released its retained ref, original intact");
calogValueFree(&container);
CHECK(releaseHookCalls == 1, "callable released once when original list freed");
calogValueFree(&copy);
}
static void testAggregateDuplicateKey(void) {
CalogAggT *map;
CalogValueT key;
CalogValueT value;
CalogValueT *found;
int32_t status;
status = calogAggCreate(&map, calogMapE);
CHECK(status == calogOkE, "dup map create");
calogValueString(&key, "k", 1);
calogValueInt(&value, 1);
status = calogAggSet(map, &key, &value);
CHECK(status == calogOkE, "dup first set");
calogValueString(&key, "k", 1);
calogValueInt(&value, 2);
status = calogAggSet(map, &key, &value);
CHECK(status == calogOkE, "dup second set");
CHECK(key.type == calogNilE && value.type == calogNilE, "dup set consumes key and value");
CHECK(map->pairCount == 1, "duplicate key did not add a pair");
calogValueString(&key, "k", 1);
found = calogAggGet(map, &key);
calogValueFree(&key);
CHECK(found != NULL && found->type == calogIntE && found->as.i == 2, "duplicate key replaced value");
calogAggFree(map);
}
static void testBrokerFailFreesResult(void) {
CalogAggT *aggregate;
CalogValueT result;
CalogValueT element;
int32_t status;
status = calogAggCreate(&aggregate, calogListE);
CHECK(status == calogOkE, "fail-frees aggregate create");
calogValueInt(&element, 1);
calogAggPush(aggregate, &element);
calogValueAgg(&result, aggregate);
status = calogFail(&result, calogErrArgE, "boom");
CHECK(status == calogErrArgE, "calogFail returns the status");
CHECK(result.type == calogStringE, "calogFail freed the owning result and set an error string");
calogValueFree(&result);
}
static void testCallable(void) {
CalogFnT *callable;
CalogValueT handle;
CalogValueT copyA;
CalogValueT copyB;
CalogValueT args[ADD_ARITY];
CalogValueT result;
int32_t status;
releaseHookCalls = 0;
status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
CHECK(status == calogOkE, "callable create");
calogValueFn(&handle, callable);
status = calogValueCopy(&copyA, &handle);
CHECK(status == calogOkE, "callable copy A");
status = calogValueCopy(&copyB, &copyA);
CHECK(status == calogOkE, "callable copy B");
calogValueInt(&args[0], 20);
calogValueInt(&args[1], 22);
status = calogFnInvoke(callable, args, ADD_ARITY, &result);
CHECK(status == calogOkE, "callable invoke status");
CHECK(result.type == calogIntE && result.as.i == 42, "callable invoke result");
calogValueFree(&result);
calogValueFree(&args[0]);
calogValueFree(&args[1]);
calogValueFree(&handle);
calogValueFree(&copyA);
CHECK(releaseHookCalls == 0, "release hook not called while references remain");
calogValueFree(&copyB);
CHECK(releaseHookCalls == 1, "release hook called once on last drop");
}
static void testCallableDead(void) {
CalogFnT *callable;
CalogValueT handle;
CalogValueT result;
int32_t status;
releaseHookCalls = 0;
status = calogFnCreate(&callable, NULL, nativeAdd, NULL, countingRelease, 0);
CHECK(status == calogOkE, "dead callable create");
calogValueFn(&handle, callable);
calogFnMarkDead(callable);
status = calogFnInvoke(callable, NULL, 0, &result);
CHECK(status == calogErrNotFoundE, "invoke on dead callable returns not-found");
CHECK(result.type == calogStringE, "dead invoke leaves an error string");
calogValueFree(&result);
calogValueFree(&handle);
CHECK(releaseHookCalls == 1, "dead callable still released on last drop");
}
static void testDepthCap(void) {
CalogValueT current;
CalogValueT copy;
int32_t status;
int32_t level;
int32_t depth;
depth = CALOG_MAX_DEPTH + DEPTH_OVERSHOOT;
calogValueInt(&current, 7);
for (level = 0; level < depth; level++) {
CalogAggT *wrapper;
status = calogAggCreate(&wrapper, calogListE);
CHECK(status == calogOkE, "depth wrapper create");
status = calogAggPush(wrapper, &current);
CHECK(status == calogOkE, "depth wrapper push");
calogValueAgg(&current, wrapper);
}
status = calogValueCopy(&copy, &current);
CHECK(status == calogErrDepthE, "deep copy rejected by depth cap");
CHECK(copy.type == calogNilE, "rejected copy left nil");
calogValueFree(&current);
calogValueFree(&copy);
}
static void testMapDepthCap(void) {
CalogAggT *map;
CalogValueT container;
CalogValueT copy;
CalogValueT key;
CalogValueT value;
CalogValueT deepValue;
int32_t status;
int32_t level;
status = calogAggCreate(&map, calogMapE);
CHECK(status == calogOkE, "map depth create");
calogValueString(&key, "shallow", 7);
calogValueInt(&value, 1);
calogAggSet(map, &key, &value);
calogValueInt(&deepValue, 0);
for (level = 0; level < CALOG_MAX_DEPTH + DEPTH_OVERSHOOT; level++) {
CalogAggT *wrapper;
calogAggCreate(&wrapper, calogListE);
calogAggPush(wrapper, &deepValue);
calogValueAgg(&deepValue, wrapper);
}
calogValueString(&key, "deep", 4);
calogAggSet(map, &key, &deepValue);
calogValueAgg(&container, map);
status = calogValueCopy(&copy, &container);
CHECK(status == calogErrDepthE, "map deep copy hits the depth cap");
CHECK(copy.type == calogNilE, "map deep copy left nil");
calogValueFree(&container);
calogValueFree(&copy);
}
static void testRegistry(void) {
CalogT *broker;
CalogValueT args[ADD_ARITY];
CalogValueT result;
char name[NAME_BUFFER];
int32_t status;
int32_t index;
broker = calogBrokerCreate();
CHECK(broker != NULL, "broker create");
status = calogRegister(broker, "add", nativeAdd, NULL);
CHECK(status == calogOkE, "register add");
status = calogRegister(broker, "boom", nativeBoom, NULL);
CHECK(status == calogOkE, "register boom");
for (index = 0; index < BULK_REGISTER; index++) {
snprintf(name, sizeof(name), "fn%d", index);
status = calogRegister(broker, name, nativeAdd, NULL);
CHECK(status == calogOkE, "register bulk");
}
CHECK(calogLookup(broker, "fn50") != NULL, "bulk lookup hit");
CHECK(calogLookup(broker, "nope") == NULL, "lookup miss");
// Re-registration must replace in place without adding a slot.
status = calogRegister(broker, "add", nativeBoom, NULL);
CHECK(status == calogOkE, "re-register add");
calogValueInt(&args[0], 3);
calogValueInt(&args[1], 4);
status = calogCall(broker, "boom", args, ADD_ARITY, &result);
CHECK(status == calogErrArgE, "re-registered add now boom");
calogValueFree(&result);
status = calogRegister(broker, "add", nativeAdd, NULL);
CHECK(status == calogOkE, "restore add");
status = calogCall(broker, "add", args, ADD_ARITY, &result);
CHECK(status == calogOkE, "call add status");
CHECK(result.type == calogIntE && result.as.i == 7, "call add result");
calogValueFree(&result);
calogValueFree(&args[0]);
calogValueFree(&args[1]);
status = calogCall(broker, "missing", NULL, 0, &result);
CHECK(status == calogErrNotFoundE, "call missing status");
CHECK(result.type == calogStringE, "call missing leaves error string");
calogValueFree(&result);
status = calogCall(broker, "boom", NULL, 0, &result);
CHECK(status == calogErrArgE, "call boom status");
CHECK(result.type == calogStringE, "call boom leaves error string");
calogValueFree(&result);
calogBrokerDestroy(broker);
}
static void testScalars(void) {
CalogValueT source;
CalogValueT copy;
int32_t status;
calogValueInt(&source, 42);
status = calogValueCopy(&copy, &source);
CHECK(status == calogOkE, "scalar int copy status");
CHECK(copy.type == calogIntE && copy.as.i == 42, "scalar int copy value");
calogValueFree(&source);
calogValueFree(&copy);
calogValueReal(&source, 3.5);
status = calogValueCopy(&copy, &source);
CHECK(status == calogOkE, "scalar real copy status");
CHECK(copy.type == calogRealE && copy.as.r == 3.5, "scalar real copy value");
calogValueFree(&source);
calogValueFree(&copy);
calogValueBool(&source, true);
status = calogValueCopy(&copy, &source);
CHECK(status == calogOkE, "scalar bool copy status");
CHECK(copy.type == calogBoolE && copy.as.b == true, "scalar bool copy value");
calogValueFree(&source);
calogValueFree(&copy);
calogValueNil(&source);
CHECK(source.type == calogNilE, "nil init");
calogValueFree(&source);
}
static void testStringIndependence(void) {
CalogValueT source;
CalogValueT copy;
int32_t status;
const char embedded[3] = { 'a', '\0', 'b' };
status = calogValueString(&source, "hello", 5);
CHECK(status == calogOkE, "string init status");
status = calogValueCopy(&copy, &source);
CHECK(status == calogOkE, "string copy status");
CHECK(copy.as.s.length == 5, "string copy length");
CHECK(copy.as.s.bytes != source.as.s.bytes, "string copy is a distinct buffer");
CHECK(memcmp(copy.as.s.bytes, "hello", 5) == 0, "string copy content");
calogValueFree(&source);
CHECK(memcmp(copy.as.s.bytes, "hello", 5) == 0, "string copy survives source free");
calogValueFree(&copy);
status = calogValueString(&source, embedded, 3);
CHECK(status == calogOkE, "embedded-NUL init status");
CHECK(source.as.s.length == 3, "embedded-NUL length preserved");
status = calogValueCopy(&copy, &source);
CHECK(status == calogOkE, "embedded-NUL copy status");
CHECK(copy.as.s.length == 3 && memcmp(copy.as.s.bytes, embedded, 3) == 0, "embedded-NUL copy content");
calogValueFree(&source);
calogValueFree(&copy);
}
static void testStringNegativeLength(void) {
CalogValueT value;
int32_t status;
status = calogValueString(&value, "x", -1);
CHECK(status == calogErrRangeE, "negative length rejected");
CHECK(value.type == calogNilE, "negative length leaves nil");
calogValueFree(&value);
status = calogValueString(&value, NULL, 5);
CHECK(status == calogErrArgE, "null bytes with positive length rejected");
CHECK(value.type == calogNilE, "null bytes leaves nil");
calogValueFree(&value);
status = calogValueString(&value, NULL, 0);
CHECK(status == calogOkE, "empty string from null bytes ok");
CHECK(value.type == calogStringE && value.as.s.length == 0, "empty string created");
calogValueFree(&value);
}
static void testValueEquals(void) {
CalogValueT a;
CalogValueT b;
calogValueInt(&a, 5);
calogValueInt(&b, 5);
CHECK(calogValueEquals(&a, &b), "int equal");
calogValueFree(&a);
calogValueFree(&b);
calogValueInt(&a, 5);
calogValueInt(&b, 6);
CHECK(!calogValueEquals(&a, &b), "int not equal");
calogValueFree(&a);
calogValueFree(&b);
calogValueInt(&a, 5);
calogValueReal(&b, 5.0);
CHECK(!calogValueEquals(&a, &b), "type mismatch not equal");
calogValueFree(&a);
calogValueFree(&b);
calogValueString(&a, "hi", 2);
calogValueString(&b, "hi", 2);
CHECK(calogValueEquals(&a, &b), "string equal");
calogValueFree(&a);
calogValueFree(&b);
calogValueString(&a, "hi", 2);
calogValueString(&b, "ho", 2);
CHECK(!calogValueEquals(&a, &b), "string not equal");
calogValueFree(&a);
calogValueFree(&b);
calogValueNil(&a);
calogValueNil(&b);
CHECK(calogValueEquals(&a, &b), "nil equal");
calogValueFree(&a);
calogValueFree(&b);
}
int main(void) {
testAggregate();
testAggregateCopyCleanup();
testAggregateDuplicateKey();
testBrokerFailFreesResult();
testCallable();
testCallableDead();
testDepthCap();
testMapDepthCap();
testRegistry();
testScalars();
testStringIndependence();
testStringNegativeLength();
testValueEquals();
printf("\n%d checks, %d failed\n", testsRun, testsFailed);
if (testsFailed != 0) {
return 1;
}
return 0;
}