Skip to content
Snippets Groups Projects
Commit 791e01cf authored by Deucе's avatar Deucе :ok_hand_tone4:
Browse files

Just fiddling with C23...

At the same time, playing with testing in C... started at the
coverage test level and working up to see how painful it really is.

This is just a linked-list queue thing the sort that every C hacker
has implemented countless times... but if it ends up as "good", it
could be used in SyncTERM in the future.
parent 6e166f09
No related branches found
No related tags found
No related merge requests found
Pipeline #7318 passed
# 3.21 added C23
# 3.23 added file sets
cmake_minimum_required(VERSION 3.31)
project(llq VERSION 0.0 LANGUAGES C)
add_library(llq)
set_property(TARGET llq PROPERTY C_STANDARD 23)
set_property(TARGET llq PROPERTY C_STANDARD_REQUIRED ON)
target_sources(llq
PRIVATE llq_alloc.c llq_dq.c llq_free.c llq_get_err.c llq_nq.c llq_was_mt.c
INTERFACE FILE_SET HEADERS FILES llq.h
)
target_include_directories(llq INTERFACE .)
target_link_libraries(llq INTERFACE stdthreads)
target_compile_definitions(llq PRIVATE $<$<CONFIG:Release>:NDEBUG>)
option(BUILD_TESTING "Perform unit tests after build" OFF)
if (BUILD_TESTING)
add_subdirectory(test)
enable_testing()
add_test(NAME llq_alloc COMMAND test_llq_alloc)
endif (BUILD_TESTING)
install(TARGETS llq FILE_SET HEADERS)
#ifndef DEUCE_QUEUE_H
#define DEUCE_QUEUE_H
#include <stdalign.h>
#include <stdint.h>
#include <threads.h>
typedef struct linked_list_queue *llq;
typedef intptr_t llq_err_t;
enum llq_errval : llq_err_t {
llq_err_none,
llq_err_badflags,
llq_err_calloc,
llq_err_cnd_broadcast,
llq_err_cnd_init,
llq_err_cnd_signal,
llq_err_cnd_timedwait,
llq_err_cnd_wait,
llq_err_null_llq,
llq_err_mtx_destroy,
llq_err_mtx_init,
llq_err_mtx_lock,
llq_err_mtx_unlock,
llq_err_not_empty,
llq_err_timedout,
llq_err_tss_create,
};
/*
* Allocates a queue. flags must be zero.
* If flags is zero, the queue is FIFO
* If err is not nullptr, will be filled if llq == nullptr
*/
llq llq_alloc(llq_err_t *err, uint32_t flags, ...);
/*
* Frees a queue allocated with llq_alloc()
* if err isn't nullptr, will be filled if an error occurs
* in the case of errors, llq_free() pretends there were no errors.
* Hopefully this will crash your program. You're welcome.
*/
void llq_free(llq restrict llq, llq_err_t *err);
// Enqueues data in llq
bool llq_nq(llq restrict llq, void * restrict data);
// Dequeues an enqueued value from llq
void * llq_dq(llq restrict llq);
/*
* Blocks for ts time or until it can dequeue a value from llq.
* if ts is nullptr, blocks forever.
*/
void * llq_dq_wait(llq restrict llq, const struct timespec * restrict ts);
/*
* Checks llq for an item. If there was an item in llq when
* checked, returns true. Returns false in all other cases
*/
bool llq_was_empty(llq restrict llq);
/*
* Gets the error status of the queue.
* Must be called from the same thread where the error occured.
* msg is only valid until the next call to any llq_*() function.
* Returns true if no errors occured while getting the error. hehhehheh.
*/
bool llq_get_err(llq restrict llq, llq_err_t * restrict num, char8_t ** restrict msg);
#endif
#include <stdlib.h>
#include "llq_int.h"
llq
llq_alloc(llq_err_t *err, uint32_t flags, ...)
{
if (flags != 0) {
SET_ERR(err, llq_err_badflags);
return nullptr;
}
llq ret = calloc(1, sizeof(struct linked_list_queue));
if (ret == nullptr) {
SET_ERR(err, llq_err_calloc);
return nullptr;
}
ret->head = nullptr;
ret->tail = nullptr;
if (mtx_init(&ret->mtx, mtx_plain) != thrd_success) {
SET_ERR(err, llq_err_mtx_init);
free(ret);
return nullptr;
}
if (cnd_init(&ret->cnd) != thrd_success) {
SET_ERR(err, llq_err_cnd_init);
mtx_destroy(&ret->mtx);
free(ret);
return nullptr;
}
if (tss_create(&ret->errnum, nullptr) != thrd_success) {
SET_ERR(err, llq_err_tss_create);
cnd_destroy(&ret->cnd);
mtx_destroy(&ret->mtx);
free(ret);
return nullptr;
}
if (tss_create(&ret->errmsg, free) != thrd_success) {
SET_ERR(err, llq_err_tss_create);
tss_delete(ret->errnum);
cnd_destroy(&ret->cnd);
mtx_destroy(&ret->mtx);
free(ret);
return nullptr;
}
if (err)
*err = llq_err_none;
return ret;
}
#include <stddef.h>
#include <stdlib.h>
#include "llq_int.h"
static void *
dq_locked(llq restrict llq)
{
assert(llq != nullptr && "llq is nullptr");
if (llq->head == nullptr)
return nullptr;
struct linked_list_queue_entry * restrict qe = llq->head;
llq->head = qe->next;
if (llq->tail == qe)
llq->tail = nullptr;
return qe;
}
void *
llq_dq(llq restrict llq)
{
void * restrict ret;
struct linked_list_queue_entry * restrict qe;
CHECK_Q(llq, nullptr);
MTX_LOCK(llq, nullptr);
qe = dq_locked(llq);
MTX_UNLOCK(llq);
if (qe == nullptr)
return nullptr;
ret = qe->data;
free(qe);
return ret;
}
#define TRY_DQ(llq) do { \
struct linked_list_queue_entry * restrict qe = dq_locked(llq); \
if (qe != nullptr) { \
MTX_UNLOCK(llq); \
void * restrict ret = qe->data; \
free(qe); \
return ret; \
} \
} while(0)
static void *
dq_wait_forever(llq restrict llq)
{
for (;;) {
if (cnd_wait(&llq->cnd, &llq->mtx) != thrd_success) {
SET_ERROR(llq, llq_err_cnd_wait, "cnd_wait() failed");
MTX_UNLOCK(llq);
return nullptr;
}
TRY_DQ(llq);
}
unreachable();
}
static void *
dq_wait_timed(llq restrict llq, const struct timespec * restrict ts)
{
if (cnd_timedwait(&llq->cnd, &llq->mtx, ts) != thrd_success) {
SET_ERROR(llq, llq_err_cnd_timedwait, "cnd_timedwait() failed");
MTX_UNLOCK(llq);
return nullptr;
}
TRY_DQ(llq);
SET_ERROR(llq, llq_err_timedout, "timeout expired");
MTX_UNLOCK(llq);
return nullptr;
}
void *
llq_dq_wait(llq restrict llq, const struct timespec * restrict ts)
{
CHECK_Q(llq, nullptr);
MTX_LOCK(llq, nullptr);
TRY_DQ(llq);
if (ts == nullptr)
return dq_wait_forever(llq);
return dq_wait_timed(llq, ts);
}
#include <stdlib.h>
#include "llq_int.h"
void
llq_free(llq restrict llq, llq_err_t *err)
{
if (llq == nullptr) {
SET_ERR(err, llq_err_null_llq);
return;
}
if (mtx_lock(&llq->mtx) != thrd_success)
SET_ERR(err, llq_err_mtx_lock);
for (struct linked_list_queue_entry * restrict qe = llq->head; qe; qe = llq->head) {
SET_ERR(err, llq_err_not_empty);
llq->head = qe->next;
free(qe);
}
// Try to make anyone else waiting crash...
// We could memset these things to make it more likely. 😈
tss_delete(llq->errnum);
tss_delete(llq->errmsg);
if (cnd_broadcast(&llq->cnd) != thrd_success)
SET_ERR(err, llq_err_cnd_broadcast);
cnd_destroy(&llq->cnd);
if (mtx_unlock(&llq->mtx) != thrd_success)
SET_ERR(err, llq_err_mtx_unlock);
mtx_destroy(&llq->mtx);
if (err)
*err = llq_err_none;
free(llq);
}
#include "llq_int.h"
bool
llq_get_err(llq restrict llq, llq_err_t * restrict num, char8_t ** restrict msg)
{
CHECK_Q(llq, false);
if (num != nullptr)
*num = (llq_err_t)tss_get(llq->errnum);
if (msg != nullptr)
*msg = (char8_t *)tss_get(llq->errnum);
return true;
}
#ifndef DEUCE_QUEUE_INT_H
#define DEUCE_QUEUE_INT_H
#include <assert.h>
#include <stdlib.h>
#include <string.h>
// Needed to test asserts
#ifdef UNIT_TESTING
extern void mock_assert(const int result, const char* const expression,
const char * const file, const int line);
#undef assert
#define assert(...) my_mock_assert((int)(__VA_ARGS__), #__VA_ARGS__, __FILE__, __LINE__);
#endif
#include "llq.h"
#ifndef CACHE_LINE_SIZE
#define CACHE_LINE_SIZE 64
#endif
#define SET_ERROR(llq, num, msg) do { \
assert(!msg); \
free(tss_get(llq->errmsg)); \
char *amsg = strdup(msg); \
tss_set(llq->errmsg, amsg); \
} while(0)
#define SET_ERR(err, val) do { \
assert(!val); \
if (err != nullptr) \
*err = val; \
} while(0)
#define MTX_LOCK(llq, failval) do { \
int err = mtx_lock(&llq->mtx); \
if (err != thrd_success) { \
assert(!"msg_lock failed!"); \
SET_ERROR(llq, llq_err_mtx_lock, u8"failed to lock"); \
/* TODO: Figure out debug log stuff... */ \
return failval; \
} \
} while (0)
#define MTX_UNLOCK(llq) do { \
int err = mtx_unlock(&llq->mtx); \
if (err != thrd_success) { \
assert(!"mtx_unlock failed!"); \
SET_ERROR(llq, llq_err_mtx_unlock, u8"failed to unlock"); \
/* TODO: Figure out debug log stuff... */ \
} \
} while (0)
#define CHECK_Q(llq, failval) do { \
assert(llq != nullptr && "llq is null"); \
if (llq == nullptr) { \
SET_ERROR(llq, llq_err_null_llq, "llq is null"); \
return failval; \
} \
} while(0)
struct linked_list_queue_entry {
struct linked_list_queue_entry *next;
void *data;
};
struct linked_list_queue {
mtx_t mtx;
struct linked_list_queue_entry *head;
struct linked_list_queue_entry *tail;
tss_t errnum;
tss_t errmsg;
// TODO: Add a cache for allocated entries...
alignas((sizeof(mtx_t) + sizeof(struct linked_list_queue_entry *) * 2) > CACHE_LINE_SIZE ? 0 : CACHE_LINE_SIZE) cnd_t cnd;
};
#endif
#include <stdlib.h>
#include "llq_int.h"
bool
llq_nq(llq restrict llq, void * restrict data)
{
CHECK_Q(llq, false);
struct linked_list_queue_entry * restrict qe = calloc(1, sizeof(struct linked_list_queue_entry));
if (qe == nullptr) {
MTX_LOCK(llq, false);
SET_ERROR(llq, llq_err_calloc, "unable to calloc new entry");
MTX_UNLOCK(llq);
return false;
}
qe->next = nullptr;
qe->data = data;
MTX_LOCK(llq, false);
if (llq->tail != nullptr)
llq->tail->next = qe;
if (llq->head == nullptr) {
llq->head = qe;
if (cnd_signal(&llq->cnd) != thrd_success)
SET_ERROR(llq, llq_err_cnd_signal, "cnd_signal() failed");
}
llq->tail = qe;
MTX_UNLOCK(llq);
return true;
}
#include <stdlib.h>
#include "llq_int.h"
bool
llq_was_mt(llq restrict llq)
{
bool ret;
CHECK_Q(llq, false);
MTX_LOCK(llq, nullptr);
ret = (llq->head == nullptr);
MTX_UNLOCK(llq);
return ret;
}
# 3.21 added C23
cmake_minimum_required(VERSION 3.31)
find_package(PkgConfig REQUIRED)
pkg_check_modules(CMocka cmocka REQUIRED)
add_executable(test_llq_alloc test_llq_alloc.c)
set_property(TARGET test_llq_alloc PROPERTY C_STANDARD 23)
target_compile_definitions(test_llq_alloc PRIVATE UNIT_TESTING)
target_include_directories(test_llq_alloc PRIVATE ${CMocka_INCLUDE_DIRS} ${llq_SOURCE_DIR})
target_link_directories(test_llq_alloc PRIVATE ${CMocka_LIBRARY_DIRS})
target_link_libraries(test_llq_alloc ${CMocka_LIBRARIES})
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <setjmp.h>
#include <cmocka.h>
#include "llq.h"
static llq llq;
main(int argc, char **argv)
{
const struct CMUnitTest tests[] = {
};
return cmocka_run_group_tests(tests, nullptr, nullptr);
}
#include <errno.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <setjmp.h>
#include <stdlib.h>
extern void _test_free(void* const ptr, const char* file, const int line);
void
test_free(void *ptr) {
_test_free(ptr, "Unknown", -1);
}
#include <cmocka.h>
#include "llq.h"
int
mtx_init(mtx_t *mtx, int type)
{
return mock();
}
void
mtx_destroy(mtx_t *mtx)
{
function_called();
}
int
cnd_init(cnd_t *cnd)
{
return mock();
}
void
cnd_destroy(cnd_t *cnd)
{
function_called();
}
int
tss_create(tss_t *key, void (*dtor)(void *))
{
return mock();
}
void
tss_delete(tss_t key)
{
function_called();
}
static int assert_count = -1;
static void
my_mock_assert(const int result, const char* const expression,
const char * const file, const int line)
{
if (assert_count == -1)
fail();
assert_count++;
}
static int malloc_count = -1;
static void *
my_test_calloc(size_t number, const size_t size, const char *file, const int line)
{
if (malloc_count == 0) {
errno = ENOMEM;
return nullptr;
}
if (malloc_count > 0)
malloc_count--;
return _test_calloc(number, size, file, line);
}
void
test_setup()
{
assert_count = -1;
malloc_count = -1;
}
#define llq_test_err(atype, flags, err) \
llq_err_t e; \
void *ret; \
e = (llq_err_t)&e; \
assert_count = 0; \
atype(ret = llq_alloc(&e, flags)); \
free(ret); \
assert_true(e == err)
#define llq_test_noerr(atype, flags) \
void *ret; \
assert_count = 0; \
ret = llq_alloc(nullptr, flags); \
atype(ret); \
free(ret);
void
illegal_flags_error(void **state)
{
test_setup();
llq_test_err(assert_null, 1, llq_err_badflags);
assert_true(assert_count == 1);
}
void
illegal_flags_noerror(void **state)
{
test_setup();
llq_test_noerr(assert_null, 1);
assert_true(assert_count == 1);
}
void
calloc_failed_error(void **state)
{
test_setup();
malloc_count = 0;
llq_test_err(assert_null, 0, llq_err_calloc);
assert_true(assert_count == 1);
}
void
calloc_failed_noerror(void **state)
{
test_setup();
malloc_count = 0;
llq_test_noerr(assert_null, 0);
assert_true(assert_count == 1);
}
void
mtx_init_failed_error(void **state)
{
test_setup();
will_return(mtx_init, thrd_error);
llq_test_err(assert_null, 0, llq_err_mtx_init);
assert_true(assert_count == 1);
}
void
mtx_init_failed_noerror(void **state)
{
test_setup();
will_return(mtx_init, thrd_error);
llq_test_noerr(assert_null, 0);
assert_true(assert_count == 1);
}
void
cnd_init_failed_error(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_error);
expect_function_call(mtx_destroy);
llq_test_err(assert_null, 0, llq_err_cnd_init);
assert_true(assert_count == 1);
}
void
cnd_init_failed_noerror(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_error);
expect_function_call(mtx_destroy);
llq_test_noerr(assert_null, 0);
assert_true(assert_count == 1);
}
void
tss_create_failed_error(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_success);
will_return(tss_create, thrd_error);
expect_function_call(cnd_destroy);
expect_function_call(mtx_destroy);
llq_test_err(assert_null, 0, llq_err_tss_create);
assert_true(assert_count == 1);
}
void
tss_create_failed_noerror(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_success);
will_return(tss_create, thrd_error);
expect_function_call(cnd_destroy);
expect_function_call(mtx_destroy);
llq_test_noerr(assert_null, 0);
assert_true(assert_count == 1);
}
void
tss_create_failed_error2(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_success);
will_return(tss_create, thrd_success);
will_return(tss_create, thrd_error);
expect_function_call(tss_delete);
expect_function_call(cnd_destroy);
expect_function_call(mtx_destroy);
llq_test_err(assert_null, 0, llq_err_tss_create);
assert_true(assert_count == 1);
}
void
tss_create_failed_noerror2(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_success);
will_return(tss_create, thrd_success);
will_return(tss_create, thrd_error);
expect_function_call(tss_delete);
expect_function_call(cnd_destroy);
expect_function_call(mtx_destroy);
llq_test_noerr(assert_null, 0);
assert_true(assert_count == 1);
}
void
success_error(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_success);
will_return(tss_create, thrd_success);
will_return(tss_create, thrd_success);
llq_test_err(assert_non_null, 0, llq_err_none);
assert_true(assert_count == 0);
}
void
success_noerror(void **state)
{
test_setup();
will_return(mtx_init, thrd_success);
will_return(cnd_init, thrd_success);
will_return(tss_create, thrd_success);
will_return(tss_create, thrd_success);
llq_test_noerr(assert_non_null, 0);
assert_true(assert_count == 0);
}
int
main(int argc, char **argv)
{
const struct CMUnitTest tests[] = {
cmocka_unit_test(illegal_flags_error),
cmocka_unit_test(illegal_flags_noerror),
cmocka_unit_test(calloc_failed_error),
cmocka_unit_test(calloc_failed_noerror),
cmocka_unit_test(mtx_init_failed_error),
cmocka_unit_test(mtx_init_failed_noerror),
cmocka_unit_test(cnd_init_failed_error),
cmocka_unit_test(cnd_init_failed_noerror),
cmocka_unit_test(tss_create_failed_error),
cmocka_unit_test(tss_create_failed_noerror),
cmocka_unit_test(tss_create_failed_error2),
cmocka_unit_test(tss_create_failed_noerror2),
cmocka_unit_test(success_error),
cmocka_unit_test(success_noerror),
};
return cmocka_run_group_tests(tests, nullptr, nullptr);
}
#undef test_calloc
#define test_calloc(number, size) my_test_calloc(number, size, __FILE__, __LINE__)
#include "../llq_alloc.c"
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment