546 lines
18 KiB
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(©, &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(©);
|
|
|
|
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(©, &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(©);
|
|
}
|
|
|
|
|
|
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(©A, &handle);
|
|
CHECK(status == calogOkE, "callable copy A");
|
|
status = calogValueCopy(©B, ©A);
|
|
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(©A);
|
|
CHECK(releaseHookCalls == 0, "release hook not called while references remain");
|
|
calogValueFree(©B);
|
|
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(¤t, 7);
|
|
for (level = 0; level < depth; level++) {
|
|
CalogAggT *wrapper;
|
|
status = calogAggCreate(&wrapper, calogListE);
|
|
CHECK(status == calogOkE, "depth wrapper create");
|
|
status = calogAggPush(wrapper, ¤t);
|
|
CHECK(status == calogOkE, "depth wrapper push");
|
|
calogValueAgg(¤t, wrapper);
|
|
}
|
|
status = calogValueCopy(©, ¤t);
|
|
CHECK(status == calogErrDepthE, "deep copy rejected by depth cap");
|
|
CHECK(copy.type == calogNilE, "rejected copy left nil");
|
|
calogValueFree(¤t);
|
|
calogValueFree(©);
|
|
}
|
|
|
|
|
|
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(©, &container);
|
|
CHECK(status == calogErrDepthE, "map deep copy hits the depth cap");
|
|
CHECK(copy.type == calogNilE, "map deep copy left nil");
|
|
calogValueFree(&container);
|
|
calogValueFree(©);
|
|
}
|
|
|
|
|
|
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(©, &source);
|
|
CHECK(status == calogOkE, "scalar int copy status");
|
|
CHECK(copy.type == calogIntE && copy.as.i == 42, "scalar int copy value");
|
|
calogValueFree(&source);
|
|
calogValueFree(©);
|
|
|
|
calogValueReal(&source, 3.5);
|
|
status = calogValueCopy(©, &source);
|
|
CHECK(status == calogOkE, "scalar real copy status");
|
|
CHECK(copy.type == calogRealE && copy.as.r == 3.5, "scalar real copy value");
|
|
calogValueFree(&source);
|
|
calogValueFree(©);
|
|
|
|
calogValueBool(&source, true);
|
|
status = calogValueCopy(©, &source);
|
|
CHECK(status == calogOkE, "scalar bool copy status");
|
|
CHECK(copy.type == calogBoolE && copy.as.b == true, "scalar bool copy value");
|
|
calogValueFree(&source);
|
|
calogValueFree(©);
|
|
|
|
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(©, &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(©);
|
|
|
|
status = calogValueString(&source, embedded, 3);
|
|
CHECK(status == calogOkE, "embedded-NUL init status");
|
|
CHECK(source.as.s.length == 3, "embedded-NUL length preserved");
|
|
status = calogValueCopy(©, &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(©);
|
|
}
|
|
|
|
|
|
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;
|
|
}
|