/* * Cross-cutting properties every wrapper has to have -- TODO.md section 1.9. * * Two of them, and neither is about what any individual function computes: * * Error-pool accounting. libakerror hands out error contexts from a fixed * process-global array of AKERR_MAX_ARRAY_ERROR slots. A wrapper that raises * an error and forgets to release it does not fail visibly -- it fails * AKERR_MAX_ARRAY_ERROR calls later, in whatever unrelated code happens to ask * for a slot next, by which time the leak is nowhere near the symptom. So * every failure path here is driven AKERR_MAX_ARRAY_ERROR + 10 times, which is * past exhaustion several times over, with the pool checked after each round. * * Stack-trace content. An error that reports the wrong origin is worse than * useless when the only debugging you get is a log file after the fact, which * is the stated reason this library exists at all. Each assertion here names * the function the error must say it came from and the source file it must * name, so a FAIL that migrates into a helper is caught. * * AKSL_RUN already checks the pool after every test in every file. This one * checks it inside the loop as well, because a leak of one slot per call needs * more than one call to show up. */ #include "aksl_capture.h" #include #include /* Rounds enough to exhaust the pool several times over. */ #define ROUNDS (AKERR_MAX_ARRAY_ERROR + 10) /* * Assert the last error came from `func` in a file whose name ends in `file`. * The recorded fname is whatever __FILE__ expanded to at the FAIL site, which is * an absolute path under CMake, so this matches on the tail rather than the * whole thing. */ static int came_from(const char *func, const char *file) { size_t flen = strlen(file); size_t rlen = strlen(aksl_last_file); if ( strcmp(aksl_last_function, func) != 0 ) { fprintf(stderr, " CHECK FAILED: error says it came from \"%s\", expected \"%s\"\n", aksl_last_function, func); return 1; } if ( rlen < flen || strcmp(aksl_last_file + (rlen - flen), file) != 0 ) { fprintf(stderr, " CHECK FAILED: error names file \"%s\", expected one ending \"%s\"\n", aksl_last_file, file); return 1; } if ( aksl_last_line <= 0 ) { fprintf(stderr, " CHECK FAILED: error records line %d\n", aksl_last_line); return 1; } return 0; } /* ---------------------------------------------------------------------- */ /* Pool accounting */ /* ---------------------------------------------------------------------- */ static int test_memory_wrappers_do_not_leak_slots(void) { void *ptr = NULL; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_malloc(0, &ptr), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_calloc(0, 1, &ptr), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_realloc(NULL, 8), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_freep(NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_memset(NULL, 0, 1), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_memmove(NULL, "a", 1), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_memcmp(NULL, "a", 1, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_memchr(NULL, 'a', 1, NULL), AKERR_NULLPOINTER); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } static int test_stream_wrappers_do_not_leak_slots(void) { FILE *fp = NULL; size_t moved = 0; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", &fp), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fopen("/nonexistent/aksl/x", "r", &fp), ENOENT); AKSL_CHECK_STATUS(aksl_fread(NULL, 1, 1, stdin, &moved), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fwrite(NULL, 1, 1, stdout, &moved), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_ftell(NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fgetc(NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fputs(NULL, stdout), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_remove("/nonexistent/aksl/x"), ENOENT); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } static int test_format_wrappers_do_not_leak_slots(void) { char buf[16]; int count = 0; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_fprintf(NULL, stdout, "x"), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_snprintf(NULL, buf, sizeof(buf), "x"), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_snprintf(&count, buf, 4, "%s", "far too long"), AKERR_OUTOFBOUNDS); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } static int test_conversion_wrappers_do_not_leak_slots(void) { int iout = 0; long lout = 0; unsigned long ulout = 0; double dout = 0.0; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_atoi(NULL, &iout), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_atoi("junk", &iout), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_atoi("99999999999999999999", &iout), ERANGE); AKSL_CHECK_STATUS(aksl_strtol("junk", NULL, 10, &lout), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_strtoul("-1", NULL, 10, &ulout), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_strtod("junk", NULL, &dout), AKERR_VALUE); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } static int test_string_wrappers_do_not_leak_slots(void) { char buf[8]; char *at = NULL; size_t n = 0; int r = 0; int i = 0; for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"), AKERR_OUTOFBOUNDS); AKSL_CHECK_STATUS(aksl_strcat(buf, 0, "x"), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_strdup(NULL, &at), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strcmp(NULL, "b", &r), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strchr(NULL, 'a', &at), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } static int test_collection_wrappers_do_not_leak_slots(void) { aksl_ListNode node; aksl_ListNode *head = NULL; aksl_List list; aksl_TreeNode tree; aksl_HashMap map; aksl_HashEntry slots[4]; aksl_StrBuf strbuf; size_t n = 0; int i = 0; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); AKSL_CHECK_OK(aksl_list_init(&list)); AKSL_CHECK_OK(aksl_tree_node_init(&tree, NULL)); AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); memset((void *)&strbuf, 0x00, sizeof(strbuf)); for ( i = 0; i < ROUNDS; i++ ) { AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_pop(&head, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_length(&node, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE); AKSL_CHECK_STATUS(aksl_tree_iterate(&tree, NULL, NULL, NULL, AKSL_TREE_SEARCH_DFS_PREORDER, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_tree_count(&tree, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_hashmap_get(&map, "absent", NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strbuf_append(&strbuf, "x"), AKERR_NULLPOINTER); AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, NULL), AKERR_NULLPOINTER); AKSL_CHECK(aksl_slots_in_use() == 0); } (void)n; return 0; } /* * A traversal that fails part-way through has the most opportunities to leak: an * error is raised inside a callback, propagated up through several frames, and * handled or not at the top. Drive that repeatedly too. */ static akerr_ErrorContext AKERR_NOIGNORE *always_fails(aksl_TreeNode *node, void *data) { PREPARE_ERROR(e); (void)node; (void)data; FAIL_RETURN(e, AKERR_VALUE, "callback always fails"); } static akerr_ErrorContext AKERR_NOIGNORE *always_breaks(aksl_TreeNode *node, void *data) { PREPARE_ERROR(e); (void)node; (void)data; FAIL_RETURN(e, AKERR_ITERATOR_BREAK, "callback always breaks"); } static int test_traversal_failures_do_not_leak_slots(void) { aksl_TreeNode tree[3]; int i = 0; for ( i = 0; i < 3; i++ ) { AKSL_CHECK_OK(aksl_tree_node_init(&tree[i], NULL)); } tree[0].left = &tree[1]; tree[0].right = &tree[2]; for ( i = 0; i < ROUNDS; i++ ) { /* Propagated out of the recursion. */ AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL, AKSL_TREE_SEARCH_DFS_PREORDER, NULL), AKERR_VALUE); /* Swallowed at the top -- the released-not-returned path. */ AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL, AKSL_TREE_SEARCH_DFS_PREORDER, NULL)); /* And through the breadth-first walk, which also drains a queue. */ AKSL_CHECK_STATUS(aksl_tree_iterate(&tree[0], &always_fails, NULL, NULL, AKSL_TREE_SEARCH_BFS, NULL), AKERR_VALUE); AKSL_CHECK_OK(aksl_tree_iterate(&tree[0], &always_breaks, NULL, NULL, AKSL_TREE_SEARCH_BFS, NULL)); AKSL_CHECK(aksl_slots_in_use() == 0); } return 0; } /* ---------------------------------------------------------------------- */ /* Stack-trace content */ /* ---------------------------------------------------------------------- */ /* * Each of these asserts that the error names the function that raised it and * the file that function lives in. This is what catches a FAIL that gets moved * into a shared helper during a refactor: the status stays right, the message * stays right, and the origin quietly starts pointing at the wrong place. */ static int test_errors_name_their_origin_in_stdlib(void) { void *ptr = NULL; int count = 0; char resolved[PATH_MAX]; uint32_t h = 0; aksl_ListNode node; AKSL_CHECK_STATUS(aksl_malloc(32, NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_malloc", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_free(NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_free", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_memcpy(NULL, "a", 1), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_memcpy", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_fopen(NULL, "r", (FILE **)&ptr), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_fopen", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_fclose(NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_fclose", "src/stdlib.c") == 0); /* * aksl_printf forwards to aksl_vprintf, so the error legitimately comes * from the latter -- the variadic form is a three-line wrapper with no FAIL * of its own. Asserting the real origin rather than the one a reader might * expect is the point of recording it. */ AKSL_CHECK_STATUS(aksl_printf(NULL, "x"), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_vprintf", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_snprintf(&count, NULL, 8, "x"), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_vsnprintf", "src/stdlib.c") == 0); /* Likewise, the ato* forms are calls into the strto* ones. */ AKSL_CHECK_STATUS(aksl_atol("junk", (long *)&ptr), AKERR_VALUE); AKSL_CHECK(came_from("aksl_strtol", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_realpath(NULL, resolved, sizeof(resolved)), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_realpath", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_strhash_djb2(NULL, 0, &h), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_strhash_djb2", "src/stdlib.c") == 0); AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); AKSL_CHECK_STATUS(aksl_list_append(NULL, &node), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_list_append", "src/stdlib.c") == 0); AKSL_CHECK_STATUS(aksl_tree_iterate(NULL, NULL, NULL, NULL, AKSL_TREE_SEARCH_DFS_PREORDER, NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_tree_iterate", "src/stdlib.c") == 0); return 0; } static int test_errors_name_their_origin_in_string_and_stream(void) { char buf[8]; size_t n = 0; long pos = 0; AKSL_CHECK_STATUS(aksl_strlen(NULL, &n), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_strlen", "src/string.c") == 0); AKSL_CHECK_STATUS(aksl_strcpy(buf, sizeof(buf), "far too long for eight"), AKERR_OUTOFBOUNDS); AKSL_CHECK(came_from("aksl_strcpy", "src/string.c") == 0); AKSL_CHECK_STATUS(aksl_strdup(NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_strdup", "src/string.c") == 0); AKSL_CHECK_STATUS(aksl_strerror(0, buf, 0), AKERR_VALUE); AKSL_CHECK(came_from("aksl_strerror", "src/string.c") == 0); AKSL_CHECK_STATUS(aksl_fseek(NULL, 0, SEEK_SET), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_fseek", "src/stream.c") == 0); AKSL_CHECK_STATUS(aksl_ftell(NULL, &pos), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_ftell", "src/stream.c") == 0); AKSL_CHECK_STATUS(aksl_getline(NULL, NULL, NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_getline", "src/stream.c") == 0); AKSL_CHECK_STATUS(aksl_mkdtemp("no-placeholder"), AKERR_VALUE); AKSL_CHECK(came_from("aksl_mkdtemp", "src/stream.c") == 0); return 0; } static int test_errors_name_their_origin_in_collections(void) { aksl_ListNode node; aksl_List list; aksl_HashMap map; aksl_HashEntry slots[4]; aksl_StrBuf strbuf; char longkey[AKSL_HASHMAP_MAX_KEY + 4]; AKSL_CHECK_OK(aksl_list_node_init(&node, NULL)); AKSL_CHECK_OK(aksl_list_init(&list)); AKSL_CHECK_OK(aksl_hashmap_init(&map, slots, 4)); memset((void *)&strbuf, 0x00, sizeof(strbuf)); memset(longkey, 'k', sizeof(longkey) - 1); longkey[sizeof(longkey) - 1] = '\0'; AKSL_CHECK_STATUS(aksl_list_prepend(NULL, &node), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_list_prepend", "src/collections.c") == 0); AKSL_CHECK_STATUS(aksl_list_remove(&list, &node), AKERR_VALUE); AKSL_CHECK(came_from("aksl_list_remove", "src/collections.c") == 0); AKSL_CHECK_STATUS(aksl_tree_insert(NULL, NULL, NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_tree_insert", "src/collections.c") == 0); AKSL_CHECK_STATUS(aksl_hashmap_put(&map, longkey, NULL), AKERR_OUTOFBOUNDS); AKSL_CHECK(came_from("aksl_hashmap_put", "src/collections.c") == 0); AKSL_CHECK_STATUS(aksl_strhash_fnv1a(NULL, 0, NULL), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_strhash_fnv1a", "src/collections.c") == 0); AKSL_CHECK_STATUS(aksl_strbuf_reset(&strbuf), AKERR_NULLPOINTER); AKSL_CHECK(came_from("aksl_strbuf_reset", "src/collections.c") == 0); return 0; } int main(void) { int failures = 0; akerr_init(); AKSL_RUN(failures, test_memory_wrappers_do_not_leak_slots); AKSL_RUN(failures, test_stream_wrappers_do_not_leak_slots); AKSL_RUN(failures, test_format_wrappers_do_not_leak_slots); AKSL_RUN(failures, test_conversion_wrappers_do_not_leak_slots); AKSL_RUN(failures, test_string_wrappers_do_not_leak_slots); AKSL_RUN(failures, test_collection_wrappers_do_not_leak_slots); AKSL_RUN(failures, test_traversal_failures_do_not_leak_slots); AKSL_RUN(failures, test_errors_name_their_origin_in_stdlib); AKSL_RUN(failures, test_errors_name_their_origin_in_string_and_stream); AKSL_RUN(failures, test_errors_name_their_origin_in_collections); AKSL_REPORT(failures); }