// 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 #include #include #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; }